dev/render-all-scene-cache-and-camera-integration #16
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1475,6 +1475,8 @@ 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::{MotionFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{ParallaxFactor, 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,
|
||||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap { width: 0, height: 0, tiles: Vec::new() },
|
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 motion_factor_x = f32::from_le_bytes([
|
let parallax_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 motion_factor_y = f32::from_le_bytes([
|
let parallax_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 !motion_factor_x.is_finite() || !motion_factor_y.is_finite() {
|
if !parallax_factor_x.is_finite() || !parallax_factor_y.is_finite() {
|
||||||
return Err("Invalid SCENE motion_factor".to_string());
|
return Err("Invalid SCENE parallax_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,
|
||||||
motion_factor: MotionFactor { x: motion_factor_x, y: motion_factor_y },
|
parallax_factor: ParallaxFactor { x: parallax_factor_x, y: parallax_factor_y },
|
||||||
tilemap: TileMap { width, height, tiles },
|
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::{MotionFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{ParallaxFactor, 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, motion_x: f32, motion_y: f32, tile_size: TileSize| SceneLayer {
|
|glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, tile_size: TileSize| SceneLayer {
|
||||||
active: glyph_bank_id != 3,
|
active: glyph_bank_id != 3,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size,
|
tile_size,
|
||||||
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_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.motion_factor.x.to_le_bytes());
|
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
|
||||||
data.extend_from_slice(&layer.motion_factor.y.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.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].motion_factor.x, 0.5);
|
assert_eq!(decoded.layers[1].parallax_factor.x, 0.5);
|
||||||
assert_eq!(decoded.layers[2].tile_size, TileSize::Size32);
|
assert_eq!(decoded.layers[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);
|
||||||
|
|||||||
719
crates/console/prometeu-drivers/src/frame_composer.rs
Normal file
719
crates/console/prometeu-drivers/src/frame_composer.rs
Normal file
@ -0,0 +1,719 @@
|
|||||||
|
use crate::memory_banks::SceneBankPoolAccess;
|
||||||
|
use prometeu_hal::GfxBridge;
|
||||||
|
use prometeu_hal::glyph::Glyph;
|
||||||
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
|
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
||||||
|
use prometeu_hal::scene_viewport_resolver::{CacheRefreshRequest, SceneViewportResolver};
|
||||||
|
use prometeu_hal::sprite::Sprite;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
const EMPTY_SPRITE: Sprite = Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 0, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 0,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub enum SceneStatus {
|
||||||
|
#[default]
|
||||||
|
Unbound,
|
||||||
|
Available {
|
||||||
|
scene_bank_id: usize,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct SpriteController {
|
||||||
|
sprites: [Sprite; 512],
|
||||||
|
sprite_count: usize,
|
||||||
|
frame_counter: u64,
|
||||||
|
dropped_sprites: usize,
|
||||||
|
layer_buckets: [Vec<usize>; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SpriteController {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SpriteController {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
sprites: [EMPTY_SPRITE; 512],
|
||||||
|
sprite_count: 0,
|
||||||
|
frame_counter: 0,
|
||||||
|
dropped_sprites: 0,
|
||||||
|
layer_buckets: std::array::from_fn(|_| Vec::with_capacity(128)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sprites(&self) -> &[Sprite; 512] {
|
||||||
|
&self.sprites
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sprites_mut(&mut self) -> &mut [Sprite; 512] {
|
||||||
|
&mut self.sprites
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(&mut self) {
|
||||||
|
self.frame_counter = self.frame_counter.wrapping_add(1);
|
||||||
|
self.sprite_count = 0;
|
||||||
|
self.dropped_sprites = 0;
|
||||||
|
for bucket in &mut self.layer_buckets {
|
||||||
|
bucket.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit_sprite(&mut self, mut sprite: Sprite) -> bool {
|
||||||
|
let Some(bucket) = self.layer_buckets.get_mut(sprite.layer as usize) else {
|
||||||
|
self.dropped_sprites += 1;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if self.sprite_count >= self.sprites.len() {
|
||||||
|
self.dropped_sprites += 1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sprite.active = true;
|
||||||
|
let index = self.sprite_count;
|
||||||
|
self.sprites[index] = sprite;
|
||||||
|
self.sprite_count += 1;
|
||||||
|
bucket.push(index);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sprite_count(&self) -> usize {
|
||||||
|
self.sprite_count
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn frame_counter(&self) -> u64 {
|
||||||
|
self.frame_counter
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dropped_sprites(&self) -> usize {
|
||||||
|
self.dropped_sprites
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ordered_sprites(&self) -> Vec<Sprite> {
|
||||||
|
let mut ordered = Vec::with_capacity(self.sprite_count);
|
||||||
|
for bucket in &self.layer_buckets {
|
||||||
|
let mut indices = bucket.clone();
|
||||||
|
indices.sort_by_key(|&index| self.sprites[index].priority);
|
||||||
|
for index in indices {
|
||||||
|
ordered.push(self.sprites[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ordered
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FrameComposer {
|
||||||
|
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
|
||||||
|
viewport_width_px: usize,
|
||||||
|
viewport_height_px: usize,
|
||||||
|
active_scene_id: Option<usize>,
|
||||||
|
active_scene: Option<Arc<SceneBank>>,
|
||||||
|
scene_status: SceneStatus,
|
||||||
|
camera_x_px: i32,
|
||||||
|
camera_y_px: i32,
|
||||||
|
cache: Option<SceneViewportCache>,
|
||||||
|
resolver: Option<SceneViewportResolver>,
|
||||||
|
sprite_controller: SpriteController,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameComposer {
|
||||||
|
pub fn new(
|
||||||
|
viewport_width_px: usize,
|
||||||
|
viewport_height_px: usize,
|
||||||
|
scene_bank_pool: Arc<dyn SceneBankPoolAccess>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
scene_bank_pool,
|
||||||
|
viewport_width_px,
|
||||||
|
viewport_height_px,
|
||||||
|
active_scene_id: None,
|
||||||
|
active_scene: None,
|
||||||
|
scene_status: SceneStatus::Unbound,
|
||||||
|
camera_x_px: 0,
|
||||||
|
camera_y_px: 0,
|
||||||
|
cache: None,
|
||||||
|
resolver: None,
|
||||||
|
sprite_controller: SpriteController::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn viewport_size(&self) -> (usize, usize) {
|
||||||
|
(self.viewport_width_px, self.viewport_height_px)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_bank_pool(&self) -> &Arc<dyn SceneBankPoolAccess> {
|
||||||
|
&self.scene_bank_pool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
|
||||||
|
self.scene_bank_pool.scene_bank_slot(slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_bank_slot_count(&self) -> usize {
|
||||||
|
self.scene_bank_pool.scene_bank_slot_count()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_scene_id(&self) -> Option<usize> {
|
||||||
|
self.active_scene_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active_scene(&self) -> Option<&Arc<SceneBank>> {
|
||||||
|
self.active_scene.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn scene_status(&self) -> SceneStatus {
|
||||||
|
self.scene_status
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn camera(&self) -> (i32, i32) {
|
||||||
|
(self.camera_x_px, self.camera_y_px)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bind_scene(&mut self, scene_bank_id: usize) -> bool {
|
||||||
|
let Some(scene) = self.scene_bank_pool.scene_bank_slot(scene_bank_id) else {
|
||||||
|
self.unbind_scene();
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let (cache, resolver) =
|
||||||
|
Self::build_scene_runtime(self.viewport_width_px, self.viewport_height_px, &scene);
|
||||||
|
self.active_scene_id = Some(scene_bank_id);
|
||||||
|
self.active_scene = Some(scene);
|
||||||
|
self.scene_status = SceneStatus::Available { scene_bank_id };
|
||||||
|
self.cache = Some(cache);
|
||||||
|
self.resolver = Some(resolver);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unbind_scene(&mut self) {
|
||||||
|
self.active_scene_id = None;
|
||||||
|
self.active_scene = None;
|
||||||
|
self.scene_status = SceneStatus::Unbound;
|
||||||
|
self.cache = None;
|
||||||
|
self.resolver = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_camera(&mut self, x: i32, y: i32) {
|
||||||
|
self.camera_x_px = x;
|
||||||
|
self.camera_y_px = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cache(&self) -> Option<&SceneViewportCache> {
|
||||||
|
self.cache.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn resolver(&self) -> Option<&SceneViewportResolver> {
|
||||||
|
self.resolver.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sprite_controller(&self) -> &SpriteController {
|
||||||
|
&self.sprite_controller
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sprite_controller_mut(&mut self) -> &mut SpriteController {
|
||||||
|
&mut self.sprite_controller
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn begin_frame(&mut self) {
|
||||||
|
self.sprite_controller.begin_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn emit_sprite(&mut self, sprite: Sprite) -> bool {
|
||||||
|
self.sprite_controller.emit_sprite(sprite)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ordered_sprites(&self) -> Vec<Sprite> {
|
||||||
|
self.sprite_controller.ordered_sprites()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_frame(&mut self, gfx: &mut dyn GfxBridge) {
|
||||||
|
let ordered_sprites = self.ordered_sprites();
|
||||||
|
gfx.load_frame_sprites(&ordered_sprites);
|
||||||
|
|
||||||
|
if let (Some(scene), Some(cache), Some(resolver)) =
|
||||||
|
(self.active_scene.as_deref(), self.cache.as_mut(), self.resolver.as_mut())
|
||||||
|
{
|
||||||
|
let update = resolver.update(scene, self.camera_x_px, self.camera_y_px);
|
||||||
|
Self::apply_refresh_requests(cache, scene, &update.refresh_requests);
|
||||||
|
// `FrameComposer` owns only canonical game-frame composition.
|
||||||
|
// Deferred `gfx.*` primitives are drained later by a separate
|
||||||
|
// overlay/debug stage outside this service boundary.
|
||||||
|
gfx.render_scene_from_cache(cache, &update);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No-scene frames still stop at canonical game composition. Final
|
||||||
|
// overlay/debug work remains outside `FrameComposer`.
|
||||||
|
gfx.render_no_scene_frame();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_scene_runtime(
|
||||||
|
viewport_width_px: usize,
|
||||||
|
viewport_height_px: usize,
|
||||||
|
scene: &SceneBank,
|
||||||
|
) -> (SceneViewportCache, SceneViewportResolver) {
|
||||||
|
let min_tile_px =
|
||||||
|
scene.layers.iter().map(|layer| layer.tile_size as usize).min().unwrap_or(8);
|
||||||
|
let cache_width_tiles = viewport_width_px.div_ceil(min_tile_px) + 5;
|
||||||
|
let cache_height_tiles = viewport_height_px.div_ceil(min_tile_px) + 4;
|
||||||
|
let hysteresis_safe_px = min_tile_px.saturating_sub(4) as i32;
|
||||||
|
let hysteresis_trigger_px = (min_tile_px + 4) as i32;
|
||||||
|
|
||||||
|
(
|
||||||
|
SceneViewportCache::new(scene, cache_width_tiles, cache_height_tiles),
|
||||||
|
SceneViewportResolver::new(
|
||||||
|
viewport_width_px as i32,
|
||||||
|
viewport_height_px as i32,
|
||||||
|
cache_width_tiles,
|
||||||
|
cache_height_tiles,
|
||||||
|
hysteresis_safe_px,
|
||||||
|
hysteresis_trigger_px,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_refresh_requests(
|
||||||
|
cache: &mut SceneViewportCache,
|
||||||
|
scene: &SceneBank,
|
||||||
|
refresh_requests: &[CacheRefreshRequest],
|
||||||
|
) {
|
||||||
|
for request in refresh_requests {
|
||||||
|
match *request {
|
||||||
|
CacheRefreshRequest::InvalidateLayer { layer_index } => {
|
||||||
|
cache.layers[layer_index].invalidate_all();
|
||||||
|
}
|
||||||
|
CacheRefreshRequest::RefreshLine { layer_index, cache_y } => {
|
||||||
|
cache.refresh_layer_line(scene, layer_index, cache_y);
|
||||||
|
}
|
||||||
|
CacheRefreshRequest::RefreshColumn { layer_index, cache_x } => {
|
||||||
|
cache.refresh_layer_column(scene, layer_index, cache_x);
|
||||||
|
}
|
||||||
|
CacheRefreshRequest::RefreshRegion { layer_index, region } => {
|
||||||
|
cache.refresh_layer_region(scene, layer_index, region);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::gfx::Gfx;
|
||||||
|
use crate::memory_banks::{
|
||||||
|
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
|
||||||
|
};
|
||||||
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||||
|
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||||
|
use prometeu_hal::tile::Tile;
|
||||||
|
use prometeu_hal::tilemap::TileMap;
|
||||||
|
|
||||||
|
fn make_scene() -> SceneBank {
|
||||||
|
make_scene_with_palette(1, 1, TileSize::Size8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_scene_with_palette(
|
||||||
|
glyph_bank_id: u8,
|
||||||
|
palette_id: u8,
|
||||||
|
tile_size: TileSize,
|
||||||
|
) -> SceneBank {
|
||||||
|
let layer = SceneLayer {
|
||||||
|
active: true,
|
||||||
|
glyph_bank_id,
|
||||||
|
tile_size,
|
||||||
|
parallax_factor: ParallaxFactor { x: 1.0, y: 0.5 },
|
||||||
|
tilemap: TileMap {
|
||||||
|
width: 2,
|
||||||
|
height: 2,
|
||||||
|
tiles: vec![
|
||||||
|
Tile {
|
||||||
|
active: true,
|
||||||
|
glyph: Glyph { glyph_id: 0, palette_id },
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
};
|
||||||
|
4
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
SceneBank { layers: std::array::from_fn(|_| layer.clone()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
||||||
|
let size = tile_size as usize;
|
||||||
|
let mut bank = GlyphBank::new(tile_size, size, size);
|
||||||
|
bank.palettes[palette_id as usize][1] = color;
|
||||||
|
for pixel in &mut bank.pixel_indices {
|
||||||
|
*pixel = 1;
|
||||||
|
}
|
||||||
|
bank
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_composer_starts_unbound_with_empty_owned_state() {
|
||||||
|
let frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||||
|
|
||||||
|
assert_eq!(frame_composer.viewport_size(), (320, 180));
|
||||||
|
assert_eq!(frame_composer.active_scene_id(), None);
|
||||||
|
assert!(frame_composer.active_scene().is_none());
|
||||||
|
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||||
|
assert_eq!(frame_composer.camera(), (0, 0));
|
||||||
|
assert!(frame_composer.cache().is_none());
|
||||||
|
assert!(frame_composer.resolver().is_none());
|
||||||
|
assert_eq!(frame_composer.sprite_controller().sprites().len(), 512);
|
||||||
|
assert_eq!(frame_composer.sprite_controller().sprite_count(), 0);
|
||||||
|
assert_eq!(frame_composer.sprite_controller().dropped_sprites(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_composer_exposes_shared_scene_bank_access() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_scene_bank(3, Arc::new(make_scene()));
|
||||||
|
|
||||||
|
let frame_composer = FrameComposer::new(320, 180, banks);
|
||||||
|
let scene =
|
||||||
|
frame_composer.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
|
||||||
|
|
||||||
|
assert_eq!(frame_composer.scene_bank_slot_count(), 16);
|
||||||
|
assert_eq!(scene.layers[0].tile_size, TileSize::Size8);
|
||||||
|
assert_eq!(scene.layers[0].parallax_factor.y, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bind_scene_stores_scene_identity_and_shared_reference() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_scene_bank(3, Arc::new(make_scene()));
|
||||||
|
|
||||||
|
let expected_scene =
|
||||||
|
banks.scene_bank_slot(3).expect("scene bank slot 3 should be resident");
|
||||||
|
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||||
|
|
||||||
|
assert!(frame_composer.bind_scene(3));
|
||||||
|
|
||||||
|
assert_eq!(frame_composer.active_scene_id(), Some(3));
|
||||||
|
assert!(Arc::ptr_eq(
|
||||||
|
frame_composer.active_scene().expect("active scene should exist"),
|
||||||
|
&expected_scene,
|
||||||
|
));
|
||||||
|
assert_eq!(frame_composer.scene_status(), SceneStatus::Available { scene_bank_id: 3 });
|
||||||
|
assert!(frame_composer.cache().is_some());
|
||||||
|
assert!(frame_composer.resolver().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unbind_scene_clears_scene_and_cache_state() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_scene_bank(1, Arc::new(make_scene()));
|
||||||
|
|
||||||
|
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||||
|
assert!(frame_composer.bind_scene(1));
|
||||||
|
|
||||||
|
frame_composer.unbind_scene();
|
||||||
|
|
||||||
|
assert_eq!(frame_composer.active_scene_id(), None);
|
||||||
|
assert!(frame_composer.active_scene().is_none());
|
||||||
|
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||||
|
assert!(frame_composer.cache().is_none());
|
||||||
|
assert!(frame_composer.resolver().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn set_camera_stores_top_left_pixel_coordinates() {
|
||||||
|
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||||
|
|
||||||
|
frame_composer.set_camera(-12, 48);
|
||||||
|
|
||||||
|
assert_eq!(frame_composer.camera(), (-12, 48));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bind_scene_derives_cache_and_resolver_from_eight_pixel_layers() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_scene_bank(0, Arc::new(make_scene()));
|
||||||
|
|
||||||
|
let mut frame_composer = FrameComposer::new(320, 180, banks);
|
||||||
|
assert!(frame_composer.bind_scene(0));
|
||||||
|
|
||||||
|
let cache = frame_composer.cache().expect("cache should exist for bound scene");
|
||||||
|
assert_eq!((cache.width(), cache.height()), (45, 27));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_scene_binding_falls_back_to_no_scene_state() {
|
||||||
|
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||||
|
|
||||||
|
assert!(!frame_composer.bind_scene(7));
|
||||||
|
|
||||||
|
assert_eq!(frame_composer.scene_status(), SceneStatus::Unbound);
|
||||||
|
assert!(frame_composer.cache().is_none());
|
||||||
|
assert!(frame_composer.resolver().is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sprite_controller_begin_frame_resets_sprite_count_and_buckets() {
|
||||||
|
let mut controller = SpriteController::new();
|
||||||
|
let emitted = controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 1, palette_id: 2 },
|
||||||
|
x: 4,
|
||||||
|
y: 5,
|
||||||
|
layer: 2,
|
||||||
|
bank_id: 3,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 1,
|
||||||
|
});
|
||||||
|
assert!(emitted);
|
||||||
|
|
||||||
|
controller.begin_frame();
|
||||||
|
|
||||||
|
assert_eq!(controller.frame_counter(), 1);
|
||||||
|
assert_eq!(controller.sprite_count(), 0);
|
||||||
|
assert_eq!(controller.dropped_sprites(), 0);
|
||||||
|
assert!(controller.ordered_sprites().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sprite_controller_orders_by_layer_then_priority_then_fifo() {
|
||||||
|
let mut controller = SpriteController::new();
|
||||||
|
controller.begin_frame();
|
||||||
|
|
||||||
|
assert!(controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 10, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 1,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 2,
|
||||||
|
}));
|
||||||
|
assert!(controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 11, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 0,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 3,
|
||||||
|
}));
|
||||||
|
assert!(controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 12, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 1,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 1,
|
||||||
|
}));
|
||||||
|
assert!(controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 13, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 1,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 2,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let ordered = controller.ordered_sprites();
|
||||||
|
let ordered_ids: Vec<u16> = ordered.iter().map(|sprite| sprite.glyph.glyph_id).collect();
|
||||||
|
|
||||||
|
assert_eq!(ordered_ids, vec![11, 12, 10, 13]);
|
||||||
|
assert!(ordered.iter().all(|sprite| sprite.active));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sprite_controller_drops_overflow_without_panicking() {
|
||||||
|
let mut controller = SpriteController::new();
|
||||||
|
controller.begin_frame();
|
||||||
|
|
||||||
|
for glyph_id in 0..512 {
|
||||||
|
assert!(controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 0,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 0,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let overflowed = controller.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 999, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 0,
|
||||||
|
bank_id: 0,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(!overflowed);
|
||||||
|
assert_eq!(controller.sprite_count(), 512);
|
||||||
|
assert_eq!(controller.dropped_sprites(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn frame_composer_emits_ordered_sprites_for_rendering() {
|
||||||
|
let mut frame_composer = FrameComposer::new(320, 180, Arc::new(MemoryBanks::new()));
|
||||||
|
frame_composer.begin_frame();
|
||||||
|
|
||||||
|
assert!(frame_composer.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 21, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 2,
|
||||||
|
bank_id: 1,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 1,
|
||||||
|
}));
|
||||||
|
assert!(frame_composer.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 20, palette_id: 0 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 1,
|
||||||
|
bank_id: 1,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let ordered = frame_composer.ordered_sprites();
|
||||||
|
|
||||||
|
assert_eq!(ordered.len(), 2);
|
||||||
|
assert_eq!(ordered[0].glyph.glyph_id, 20);
|
||||||
|
assert_eq!(ordered[1].glyph.glyph_id, 21);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_frame_without_scene_uses_sprite_only_path() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
|
||||||
|
|
||||||
|
let mut frame_composer =
|
||||||
|
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||||
|
frame_composer.begin_frame();
|
||||||
|
assert!(frame_composer.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 0, palette_id: 3 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 0,
|
||||||
|
bank_id: 1,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||||
|
gfx.scene_fade_level = 31;
|
||||||
|
gfx.hud_fade_level = 31;
|
||||||
|
|
||||||
|
frame_composer.render_frame(&mut gfx);
|
||||||
|
gfx.present();
|
||||||
|
|
||||||
|
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_frame_with_scene_applies_refreshes_before_composition() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||||
|
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 2, TileSize::Size8)));
|
||||||
|
|
||||||
|
let mut frame_composer =
|
||||||
|
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||||
|
assert!(frame_composer.bind_scene(0));
|
||||||
|
|
||||||
|
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||||
|
gfx.scene_fade_level = 31;
|
||||||
|
gfx.hud_fade_level = 31;
|
||||||
|
|
||||||
|
frame_composer.render_frame(&mut gfx);
|
||||||
|
gfx.present();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
frame_composer
|
||||||
|
.cache()
|
||||||
|
.expect("cache should exist")
|
||||||
|
.layers
|
||||||
|
.iter()
|
||||||
|
.all(|layer| layer.valid)
|
||||||
|
);
|
||||||
|
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn render_frame_survives_scene_transition_through_unbind_and_rebind() {
|
||||||
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
banks.install_glyph_bank(0, Arc::new(make_glyph_bank(TileSize::Size8, 1, Color::RED)));
|
||||||
|
banks.install_glyph_bank(1, Arc::new(make_glyph_bank(TileSize::Size8, 2, Color::BLUE)));
|
||||||
|
banks.install_glyph_bank(2, Arc::new(make_glyph_bank(TileSize::Size8, 3, Color::WHITE)));
|
||||||
|
banks.install_scene_bank(0, Arc::new(make_scene_with_palette(0, 1, TileSize::Size8)));
|
||||||
|
banks.install_scene_bank(1, Arc::new(make_scene_with_palette(1, 2, TileSize::Size8)));
|
||||||
|
|
||||||
|
let mut frame_composer =
|
||||||
|
FrameComposer::new(16, 16, Arc::clone(&banks) as Arc<dyn SceneBankPoolAccess>);
|
||||||
|
let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc<dyn GlyphBankPoolAccess>);
|
||||||
|
gfx.scene_fade_level = 31;
|
||||||
|
gfx.hud_fade_level = 31;
|
||||||
|
|
||||||
|
assert!(frame_composer.bind_scene(0));
|
||||||
|
frame_composer.render_frame(&mut gfx);
|
||||||
|
gfx.present();
|
||||||
|
assert_eq!(gfx.front_buffer()[0], Color::RED.raw());
|
||||||
|
|
||||||
|
frame_composer.unbind_scene();
|
||||||
|
frame_composer.begin_frame();
|
||||||
|
assert!(frame_composer.emit_sprite(Sprite {
|
||||||
|
glyph: Glyph { glyph_id: 0, palette_id: 3 },
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
layer: 0,
|
||||||
|
bank_id: 2,
|
||||||
|
active: false,
|
||||||
|
flip_x: false,
|
||||||
|
flip_y: false,
|
||||||
|
priority: 0,
|
||||||
|
}));
|
||||||
|
frame_composer.render_frame(&mut gfx);
|
||||||
|
gfx.present();
|
||||||
|
assert_eq!(gfx.front_buffer()[0], Color::WHITE.raw());
|
||||||
|
|
||||||
|
frame_composer.begin_frame();
|
||||||
|
assert!(frame_composer.bind_scene(1));
|
||||||
|
frame_composer.render_frame(&mut gfx);
|
||||||
|
gfx.present();
|
||||||
|
assert_eq!(gfx.front_buffer()[0], Color::BLUE.raw());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand};
|
||||||
use crate::memory_banks::GlyphBankPoolAccess;
|
use crate::memory_banks::GlyphBankPoolAccess;
|
||||||
use prometeu_hal::GfxBridge;
|
use prometeu_hal::GfxBridge;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
@ -31,22 +32,11 @@ pub enum BlendMode {
|
|||||||
|
|
||||||
/// PROMETEU Graphics Subsystem (GFX).
|
/// PROMETEU Graphics Subsystem (GFX).
|
||||||
///
|
///
|
||||||
/// Models a specialized graphics chip with a fixed resolution, double buffering,
|
/// `Gfx` owns the framebuffer backend and the canonical game-frame raster path
|
||||||
/// and a multi-layered tile/sprite architecture.
|
/// consumed by `FrameComposer`. That canonical path covers scene composition,
|
||||||
///
|
/// sprite composition, and fades. Public `gfx.*` primitives remain valid, but
|
||||||
/// The GFX system works by composing several "layers" into a single 16-bit
|
/// they do not define the canonical game composition contract; they belong to a
|
||||||
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
|
/// separate final overlay/debug stage.
|
||||||
/// 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,
|
||||||
@ -54,11 +44,16 @@ 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 "Work RAM" where new frames are composed.
|
/// Back buffer: the working buffer where canonical game frames are composed
|
||||||
|
/// before any final overlay/debug drain.
|
||||||
back: Vec<u16>,
|
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],
|
||||||
|
|
||||||
@ -71,12 +66,29 @@ 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 cache used to sort sprites into priority groups to optimize rendering.
|
/// Internal sprite count for the current frame state.
|
||||||
priority_buckets: [Vec<usize>; 5],
|
sprite_count: usize,
|
||||||
|
/// Internal cache used to sort sprites by layer while keeping stable priority order.
|
||||||
|
layer_buckets: [Vec<usize>; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7];
|
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() {
|
||||||
@ -207,12 +219,15 @@ impl GfxBridge for Gfx {
|
|||||||
fn present(&mut self) {
|
fn present(&mut self) {
|
||||||
self.present()
|
self.present()
|
||||||
}
|
}
|
||||||
fn render_all(&mut self) {
|
fn render_no_scene_frame(&mut self) {
|
||||||
self.render_all()
|
self.render_no_scene_frame()
|
||||||
}
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
@ -224,6 +239,7 @@ 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]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -262,6 +278,7 @@ 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,
|
||||||
@ -276,13 +293,15 @@ 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,
|
||||||
priority_buckets: [
|
layer_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),
|
||||||
@ -295,6 +314,42 @@ 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
|
||||||
@ -314,6 +369,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -355,6 +414,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -387,6 +450,10 @@ 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;
|
||||||
}
|
}
|
||||||
@ -455,6 +522,10 @@ 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);
|
||||||
}
|
}
|
||||||
@ -484,6 +555,10 @@ 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);
|
||||||
}
|
}
|
||||||
@ -530,23 +605,33 @@ 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_all(&mut self) {
|
pub fn render_no_scene_frame(&mut self) {
|
||||||
self.populate_priority_buckets();
|
self.populate_layer_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,
|
||||||
&self.priority_buckets[0],
|
bucket,
|
||||||
&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.
|
||||||
@ -563,18 +648,17 @@ 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_priority_buckets();
|
self.populate_layer_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.priority_buckets[0],
|
&self.layer_buckets[layer_index],
|
||||||
&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,
|
||||||
@ -583,31 +667,26 @@ 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_priority_buckets(&mut self) {
|
fn populate_layer_buckets(&mut self) {
|
||||||
for bucket in self.priority_buckets.iter_mut() {
|
for bucket in self.layer_buckets.iter_mut() {
|
||||||
bucket.clear();
|
bucket.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (idx, sprite) in self.sprites.iter().enumerate() {
|
for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() {
|
||||||
if sprite.active && sprite.priority < 5 {
|
if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() {
|
||||||
self.priority_buckets[sprite.priority as usize].push(idx);
|
self.layer_buckets[sprite.layer as usize].push(idx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for bucket in self.layer_buckets.iter_mut() {
|
||||||
|
bucket.sort_by_key(|&idx| self.sprites[idx].priority);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cache_layer_to_buffer(
|
fn draw_cache_layer_to_buffer(
|
||||||
@ -618,6 +697,7 @@ 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;
|
||||||
@ -646,52 +726,43 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self::draw_cached_tile_pixels(
|
Self::draw_cached_tile_pixels(
|
||||||
back,
|
&mut target,
|
||||||
screen_w,
|
CachedTileDraw {
|
||||||
screen_h,
|
x: screen_tile_x,
|
||||||
screen_tile_x,
|
y: screen_tile_y,
|
||||||
screen_tile_y,
|
|
||||||
entry,
|
entry,
|
||||||
&bank,
|
bank: &bank,
|
||||||
request.tile_size,
|
tile_size: request.tile_size,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_cached_tile_pixels(
|
fn draw_cached_tile_pixels(target: &mut RenderTarget<'_>, tile: CachedTileDraw<'_>) {
|
||||||
back: &mut [u16],
|
let size = tile.tile_size as usize;
|
||||||
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 = y + local_y as i32;
|
let world_y = tile.y + local_y as i32;
|
||||||
if world_y < 0 || world_y >= screen_h as i32 {
|
if world_y < 0 || world_y >= target.screen_h as i32 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for local_x in 0..size {
|
for local_x in 0..size {
|
||||||
let world_x = x + local_x as i32;
|
let world_x = tile.x + local_x as i32;
|
||||||
if world_x < 0 || world_x >= screen_w as i32 {
|
if world_x < 0 || world_x >= target.screen_w as i32 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fetch_x = if entry.flip_x() { size - 1 - local_x } else { local_x };
|
let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x };
|
||||||
let fetch_y = if entry.flip_y() { size - 1 - local_y } else { local_y };
|
let fetch_y = if tile.entry.flip_y() { size - 1 - local_y } else { local_y };
|
||||||
let px_index = bank.get_pixel_index(entry.glyph_id, fetch_x, fetch_y);
|
let px_index = tile.bank.get_pixel_index(tile.entry.glyph_id, fetch_x, fetch_y);
|
||||||
if px_index == 0 {
|
if px_index == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let color = bank.resolve_color(entry.palette_id, px_index);
|
let color = tile.bank.resolve_color(tile.entry.palette_id, px_index);
|
||||||
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
target.back[world_y as usize * target.screen_w + world_x as usize] = color.raw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -775,6 +846,10 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
|
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);
|
||||||
@ -819,10 +894,11 @@ impl Gfx {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks};
|
use crate::FrameComposer;
|
||||||
|
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess};
|
||||||
use prometeu_hal::glyph_bank::TileSize;
|
use prometeu_hal::glyph_bank::TileSize;
|
||||||
use prometeu_hal::scene_bank::SceneBank;
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
use prometeu_hal::scene_layer::{MotionFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||||
use prometeu_hal::scene_viewport_cache::SceneViewportCache;
|
use prometeu_hal::scene_viewport_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;
|
||||||
@ -853,7 +929,7 @@ mod tests {
|
|||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
@ -875,7 +951,7 @@ mod tests {
|
|||||||
active: false,
|
active: false,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] },
|
tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -918,9 +994,12 @@ 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]
|
||||||
@ -946,11 +1025,54 @@ 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]
|
||||||
@ -1004,6 +1126,7 @@ 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,
|
||||||
@ -1014,17 +1137,57 @@ 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.
|
||||||
|
|||||||
39
crates/console/prometeu-drivers/src/gfx_overlay.rs
Normal file
39
crates/console/prometeu-drivers/src/gfx_overlay.rs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
use crate::gfx::BlendMode;
|
||||||
|
use prometeu_hal::color::Color;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum OverlayCommand {
|
||||||
|
FillRectBlend { x: i32, y: i32, w: i32, h: i32, color: Color, mode: BlendMode },
|
||||||
|
DrawLine { x0: i32, y0: i32, x1: i32, y1: i32, color: Color },
|
||||||
|
DrawCircle { x: i32, y: i32, r: i32, color: Color },
|
||||||
|
DrawDisc { x: i32, y: i32, r: i32, border_color: Color, fill_color: Color },
|
||||||
|
DrawSquare { x: i32, y: i32, w: i32, h: i32, border_color: Color, fill_color: Color },
|
||||||
|
DrawText { x: i32, y: i32, text: String, color: Color },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct DeferredGfxOverlay {
|
||||||
|
commands: Vec<OverlayCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeferredGfxOverlay {
|
||||||
|
pub fn begin_frame(&mut self) {
|
||||||
|
self.commands.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&mut self, command: OverlayCommand) {
|
||||||
|
self.commands.push(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn commands(&self) -> &[OverlayCommand] {
|
||||||
|
&self.commands
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn command_count(&self) -> usize {
|
||||||
|
self.commands.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_commands(&mut self) -> Vec<OverlayCommand> {
|
||||||
|
std::mem::take(&mut self.commands)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,13 +1,15 @@
|
|||||||
use crate::asset::AssetManager;
|
use crate::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, SceneBankPoolInstaller,
|
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess,
|
||||||
SoundBankPoolAccess, SoundBankPoolInstaller,
|
SceneBankPoolInstaller, 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;
|
||||||
|
|
||||||
@ -26,6 +28,8 @@ 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.
|
||||||
@ -43,6 +47,36 @@ 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
|
||||||
}
|
}
|
||||||
@ -98,6 +132,11 @@ 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(),
|
||||||
@ -122,7 +161,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::{MotionFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{ParallaxFactor, 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;
|
||||||
@ -142,7 +181,7 @@ mod tests {
|
|||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id: 0,
|
glyph_bank_id: 0,
|
||||||
tile_size: TileSize::Size8,
|
tile_size: TileSize::Size8,
|
||||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap {
|
tilemap: TileMap {
|
||||||
width: 4,
|
width: 4,
|
||||||
height: 4,
|
height: 4,
|
||||||
@ -182,4 +221,20 @@ 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,6 +1,8 @@
|
|||||||
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;
|
||||||
@ -8,7 +10,9 @@ 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,
|
||||||
|
|||||||
10
crates/console/prometeu-hal/src/composer_status.rs
Normal file
10
crates/console/prometeu-hal/src/composer_status.rs
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(i32)]
|
||||||
|
pub enum ComposerOpStatus {
|
||||||
|
Ok = 0,
|
||||||
|
SceneUnavailable = 1,
|
||||||
|
ArgRangeInvalid = 2,
|
||||||
|
BankInvalid = 3,
|
||||||
|
LayerInvalid = 4,
|
||||||
|
SpriteOverflow = 5,
|
||||||
|
}
|
||||||
@ -48,8 +48,21 @@ pub trait GfxBridge {
|
|||||||
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color);
|
fn draw_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);
|
||||||
fn render_all(&mut self);
|
/// Render the canonical game frame with no bound scene.
|
||||||
|
///
|
||||||
|
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
|
||||||
|
/// contract and are drained by a separate final overlay stage.
|
||||||
|
fn render_no_scene_frame(&mut self);
|
||||||
|
/// Render the canonical scene-backed game frame from cache/resolver state.
|
||||||
|
///
|
||||||
|
/// Deferred `gfx.*` overlay/debug primitives are intentionally outside this
|
||||||
|
/// contract and are drained by a separate final overlay stage.
|
||||||
fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate);
|
fn 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,9 +2,18 @@ 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,6 +5,7 @@ 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;
|
||||||
@ -34,6 +35,7 @@ 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::MotionFactor;
|
use crate::scene_layer::ParallaxFactor;
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
use crate::tilemap::TileMap;
|
use crate::tilemap::TileMap;
|
||||||
|
|
||||||
fn layer(glyph_bank_id: u8, motion_x: f32, motion_y: f32, glyph_id: u16) -> SceneLayer {
|
fn layer(glyph_bank_id: u8, parallax_x: f32, parallax_y: f32, glyph_id: u16) -> SceneLayer {
|
||||||
SceneLayer {
|
SceneLayer {
|
||||||
active: true,
|
active: true,
|
||||||
glyph_bank_id,
|
glyph_bank_id,
|
||||||
tile_size: TileSize::Size16,
|
tile_size: TileSize::Size16,
|
||||||
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_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 MotionFactor {
|
pub struct ParallaxFactor {
|
||||||
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 motion_factor: MotionFactor,
|
pub parallax_factor: ParallaxFactor,
|
||||||
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_motion_factor_and_tilemap_ownership() {
|
fn scene_layer_preserves_parallax_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,
|
||||||
motion_factor: MotionFactor { x: 0.5, y: 0.75 },
|
parallax_factor: ParallaxFactor { 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.motion_factor.x, 0.5);
|
assert_eq!(layer.parallax_factor.x, 0.5);
|
||||||
assert_eq!(layer.motion_factor.y, 0.75);
|
assert_eq!(layer.parallax_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::MotionFactor;
|
use crate::scene_layer::ParallaxFactor;
|
||||||
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,
|
||||||
motion_factor: MotionFactor { x: 1.0, y: 1.0 },
|
parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 },
|
||||||
tilemap: TileMap { width: 4, height: 4, tiles },
|
tilemap: TileMap { width: 4, height: 4, tiles },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -325,6 +325,34 @@ 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();
|
||||||
@ -415,6 +443,113 @@ 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.motion_factor.x).floor() as i32;
|
let layer_camera_x_px = ((camera_x_px as f32) * layer.parallax_factor.x).floor() as i32;
|
||||||
let layer_camera_y_px = ((camera_y_px as f32) * layer.motion_factor.y).floor() as i32;
|
let layer_camera_y_px = ((camera_y_px as f32) * layer.parallax_factor.y).floor() as i32;
|
||||||
let layer_center_x_px = layer_camera_x_px + self.viewport_width_px / 2;
|
let layer_center_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::{MotionFactor, SceneLayer};
|
use crate::scene_layer::{ParallaxFactor, 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,
|
||||||
motion_x: f32,
|
parallax_x: f32,
|
||||||
motion_y: f32,
|
parallax_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,
|
||||||
motion_factor: MotionFactor { x: motion_x, y: motion_y },
|
parallax_factor: ParallaxFactor { x: parallax_x, y: parallax_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_motion_factor() {
|
fn per_layer_copy_requests_follow_parallax_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,6 +5,7 @@ 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,6 +19,7 @@ pub use resolver::{
|
|||||||
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
/// Each Syscall has a unique 32-bit ID. The IDs are grouped by category:
|
||||||
/// - **0x0xxx**: System & OS Control
|
/// - **0x0xxx**: System & OS Control
|
||||||
/// - **0x1xxx**: Graphics (GFX)
|
/// - **0x1xxx**: 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)
|
||||||
@ -35,9 +36,12 @@ 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,
|
||||||
|
|||||||
22
crates/console/prometeu-hal/src/syscalls/domains/composer.rs
Normal file
22
crates/console/prometeu-hal/src/syscalls/domains/composer.rs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
use crate::syscalls::{Syscall, SyscallRegistryEntry, caps};
|
||||||
|
|
||||||
|
pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
||||||
|
SyscallRegistryEntry::builder(Syscall::ComposerBindScene, "composer", "bind_scene")
|
||||||
|
.args(1)
|
||||||
|
.rets(1)
|
||||||
|
.caps(caps::GFX)
|
||||||
|
.cost(5),
|
||||||
|
SyscallRegistryEntry::builder(Syscall::ComposerUnbindScene, "composer", "unbind_scene")
|
||||||
|
.rets(1)
|
||||||
|
.caps(caps::GFX)
|
||||||
|
.cost(2),
|
||||||
|
SyscallRegistryEntry::builder(Syscall::ComposerSetCamera, "composer", "set_camera")
|
||||||
|
.args(2)
|
||||||
|
.caps(caps::GFX)
|
||||||
|
.cost(2),
|
||||||
|
SyscallRegistryEntry::builder(Syscall::ComposerEmitSprite, "composer", "emit_sprite")
|
||||||
|
.args(9)
|
||||||
|
.rets(1)
|
||||||
|
.caps(caps::GFX)
|
||||||
|
.cost(5),
|
||||||
|
];
|
||||||
@ -25,11 +25,6 @@ pub(crate) const ENTRIES: &[SyscallRegistryEntry] = &[
|
|||||||
.args(6)
|
.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,6 +1,7 @@
|
|||||||
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;
|
||||||
@ -12,6 +13,7 @@ pub(crate) fn all_entries() -> impl Iterator<Item = &'static SyscallRegistryEntr
|
|||||||
system::ENTRIES
|
system::ENTRIES
|
||||||
.iter()
|
.iter()
|
||||||
.chain(gfx::ENTRIES.iter())
|
.chain(gfx::ENTRIES.iter())
|
||||||
|
.chain(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,9 +20,12 @@ 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),
|
||||||
@ -68,9 +71,12 @@ 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,6 +63,18 @@ fn resolver_rejects_unknown_identity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolver_rejects_removed_legacy_gfx_set_sprite_identity() {
|
||||||
|
assert!(resolve_syscall("gfx", "set_sprite", 1).is_none());
|
||||||
|
|
||||||
|
let requested = [SyscallIdentity { module: "gfx", name: "set_sprite", version: 1 }];
|
||||||
|
let err = resolve_program_syscalls(&requested, caps::ALL).unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err,
|
||||||
|
LoadError::UnknownSyscall { module: "gfx".into(), name: "set_sprite".into(), version: 1 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[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 }];
|
||||||
@ -194,10 +206,6 @@ 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);
|
||||||
@ -206,6 +214,22 @@ 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);
|
||||||
@ -231,10 +255,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: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "bind_scene".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 1,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
@ -306,10 +330,24 @@ fn declared_resolver_rejects_legacy_status_first_signatures() {
|
|||||||
fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
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: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "bind_scene".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 1,
|
||||||
|
ret_slots: 1,
|
||||||
|
},
|
||||||
|
prometeu_bytecode::SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "unbind_scene".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 0,
|
||||||
|
ret_slots: 1,
|
||||||
|
},
|
||||||
|
prometeu_bytecode::SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "emit_sprite".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 9,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
prometeu_bytecode::SyscallDecl {
|
prometeu_bytecode::SyscallDecl {
|
||||||
@ -342,8 +380,10 @@ fn declared_resolver_accepts_mixed_status_first_surface_as_a_single_module() {
|
|||||||
assert_eq!(resolved.len(), declared.len());
|
assert_eq!(resolved.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, 2);
|
assert_eq!(resolved[2].meta.ret_slots, 1);
|
||||||
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, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool,
|
AudioOpStatus, ComposerOpStatus, HostContext, HostReturn, NativeInterface, SyscallId,
|
||||||
expect_int,
|
expect_bool, expect_int,
|
||||||
};
|
};
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
@ -56,6 +56,23 @@ 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 {
|
||||||
@ -135,46 +152,6 @@ 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;
|
||||||
@ -191,6 +168,100 @@ 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,17 +5,25 @@ 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::GfxOpStatus;
|
use prometeu_hal::ComposerOpStatus;
|
||||||
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::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::glyph::Glyph;
|
||||||
|
use prometeu_hal::glyph_bank::{GLYPH_BANK_PALETTE_COUNT_V1, GlyphBank, TileSize};
|
||||||
|
use prometeu_hal::scene_bank::SceneBank;
|
||||||
|
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||||
use prometeu_hal::syscalls::caps;
|
use prometeu_hal::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)]
|
||||||
@ -129,6 +137,40 @@ 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);
|
||||||
@ -233,6 +275,174 @@ 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);
|
||||||
@ -364,22 +574,19 @@ fn initialize_vm_failure_clears_previous_identity_and_handles() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
|
fn tick_composer_bind_scene_operational_error_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut 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 99\nHOSTCALL 0\nHALT").expect("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: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "bind_scene".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 1,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -389,26 +596,29 @@ fn tick_gfx_set_sprite_operational_error_returns_status_not_crash() {
|
|||||||
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
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!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::BankInvalid as i64)]);
|
assert_eq!(
|
||||||
|
vm.operand_stack_top(1),
|
||||||
|
vec![Value::Int64(ComposerOpStatus::SceneUnavailable as i64)]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
|
fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut 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 512\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "emit_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 9,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -416,28 +626,57 @@ fn tick_gfx_set_sprite_invalid_index_returns_status_not_crash() {
|
|||||||
|
|
||||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
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 sprite index must not crash");
|
assert!(report.is_none(), "operational error must not crash");
|
||||||
assert!(vm.is_halted());
|
assert!(vm.is_halted());
|
||||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::InvalidSpriteIndex as i64)]);
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut 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 64\nPUSH_BOOL 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 4\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "emit_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 9,
|
||||||
|
ret_slots: 1,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
||||||
|
|
||||||
|
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
||||||
|
let report = runtime.tick(&mut vm, &signals, &mut hardware);
|
||||||
|
assert!(report.is_none(), "invalid layer must not crash");
|
||||||
|
assert!(vm.is_halted());
|
||||||
|
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::LayerInvalid as i64)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tick_composer_emit_sprite_invalid_range_returns_status_not_crash() {
|
||||||
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
|
let mut vm = VirtualMachine::default();
|
||||||
|
let mut hardware = Hardware::new();
|
||||||
|
let signals = InputSignals::default();
|
||||||
|
let code = assemble(
|
||||||
|
"PUSH_I32 0\nPUSH_I32 64\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||||
|
)
|
||||||
|
.expect("assemble");
|
||||||
|
let program = serialized_single_function_module(
|
||||||
|
code,
|
||||||
|
vec![SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "emit_sprite".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 9,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
@ -452,9 +691,12 @@ fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
|||||||
|
|
||||||
runtime.initialize_vm(&mut vm, &cartridge).expect("runtime must initialize");
|
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 gfx parameter range must not crash");
|
assert!(report.is_none(), "invalid composer parameter range must not crash");
|
||||||
assert!(vm.is_halted());
|
assert!(vm.is_halted());
|
||||||
assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(GfxOpStatus::ArgRangeInvalid as i64)]);
|
assert_eq!(
|
||||||
|
vm.operand_stack_top(1),
|
||||||
|
vec![Value::Int64(ComposerOpStatus::ArgRangeInvalid as i64)]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -816,13 +1058,13 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
fn tick_status_first_surface_smoke_across_composer_audio_and_asset() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut 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 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
|
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\n\
|
||||||
PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 255\nPUSH_I32 128\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 1\n\
|
PUSH_I32 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"
|
||||||
@ -832,10 +1074,10 @@ fn tick_status_first_surface_smoke_across_gfx_audio_and_asset() {
|
|||||||
code,
|
code,
|
||||||
vec![
|
vec![
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "emit_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 9,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
@ -866,28 +1108,28 @@ fn tick_status_first_surface_smoke_across_gfx_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(GfxOpStatus::BankInvalid as i64),
|
Value::Int64(ComposerOpStatus::BankInvalid as i64),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn tick_gfx_set_sprite_type_mismatch_surfaces_trap_not_panic() {
|
fn tick_composer_emit_sprite_type_mismatch_surfaces_trap_not_panic() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut 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 1\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
"PUSH_BOOL 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT",
|
||||||
)
|
)
|
||||||
.expect("assemble");
|
.expect("assemble");
|
||||||
let program = serialized_single_function_module(
|
let program = serialized_single_function_module(
|
||||||
code,
|
code,
|
||||||
vec![SyscallDecl {
|
vec![SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "emit_sprite".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 9,
|
||||||
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.gfx_mut().render_all();
|
hw.render_frame();
|
||||||
|
|
||||||
// 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,6 +250,7 @@ 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,11 +2567,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_status_first_syscall_results_count_mismatch_panic() {
|
fn test_status_first_syscall_results_count_mismatch_panic() {
|
||||||
// GfxSetSprite (0x1007) expects 1 result.
|
// ComposerBindScene (0x1101) expects 1 result.
|
||||||
let code = assemble(
|
let code = assemble("PUSH_I32 0\nSYSCALL 0x1101").expect("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 {
|
||||||
@ -2921,10 +2918,24 @@ 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: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "bind_scene".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 1,
|
||||||
|
ret_slots: 1,
|
||||||
|
},
|
||||||
|
SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "unbind_scene".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 0,
|
||||||
|
ret_slots: 1,
|
||||||
|
},
|
||||||
|
SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "emit_sprite".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 9,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
@ -2977,10 +2988,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: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "bind_scene".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 1,
|
||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
|
|||||||
@ -5,4 +5,6 @@ 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,7 +3,25 @@ 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> {
|
||||||
@ -20,13 +38,6 @@ 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(),
|
||||||
@ -42,10 +53,24 @@ pub fn generate() -> Result<()> {
|
|||||||
ret_slots: 0,
|
ret_slots: 0,
|
||||||
},
|
},
|
||||||
SyscallDecl {
|
SyscallDecl {
|
||||||
module: "gfx".into(),
|
module: "composer".into(),
|
||||||
name: "set_sprite".into(),
|
name: "bind_scene".into(),
|
||||||
version: 1,
|
version: 1,
|
||||||
arg_slots: 10,
|
arg_slots: 1,
|
||||||
|
ret_slots: 1,
|
||||||
|
},
|
||||||
|
SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "set_camera".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 2,
|
||||||
|
ret_slots: 0,
|
||||||
|
},
|
||||||
|
SyscallDecl {
|
||||||
|
module: "composer".into(),
|
||||||
|
name: "emit_sprite".into(),
|
||||||
|
version: 1,
|
||||||
|
arg_slots: 9,
|
||||||
ret_slots: 1,
|
ret_slots: 1,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -59,7 +84,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: 16,
|
max_stack_slots: 32,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
let module = BytecodeModule {
|
let module = BytecodeModule {
|
||||||
@ -67,7 +92,8 @@ 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("missing_glyph_bank".into()),
|
ConstantPoolEntry::String("overlay".into()),
|
||||||
|
ConstantPoolEntry::String("composer".into()),
|
||||||
],
|
],
|
||||||
functions,
|
functions,
|
||||||
code: rom,
|
code: rom,
|
||||||
@ -89,129 +115,332 @@ 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)?;
|
||||||
let assets_pa_path = out_dir.join("assets.pa");
|
fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
|
||||||
if assets_pa_path.exists() {
|
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?;
|
||||||
fs::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
|
||||||
// Everything runs here — no coroutines, no SPAWN, no YIELD.
|
// Global 0 = frame counter
|
||||||
//
|
// Global 1 = scene bound flag
|
||||||
// Global 0 = t (frame counter)
|
// Local 0 = sprite row
|
||||||
// Local 0 = scratch
|
// Local 1 = sprite col
|
||||||
// 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"));
|
||||||
|
|
||||||
// --- clear screen ---
|
rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ"));
|
||||||
|
let jif_bind_done_offset = rom.len() + 2;
|
||||||
|
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||||
|
|
||||||
|
rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1"));
|
||||||
|
let bind_done_target = rom.len() as u32;
|
||||||
|
|
||||||
rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
|
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(
|
||||||
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1",
|
"GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4",
|
||||||
));
|
));
|
||||||
|
|
||||||
// --- draw 500 discs ---
|
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0"));
|
||||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
let row_loop_start = rom.len() as u32;
|
||||||
let disc_loop_start = rom.len() as u32;
|
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 16\nLT"));
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 500\nLT"));
|
let jif_row_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"));
|
||||||
|
|
||||||
// x = (t * (i+7) + i * 13) % 320
|
|
||||||
rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 7\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 13\nMUL\nADD\nPUSH_I32 320\nMOD"));
|
|
||||||
// y = (t * (i+11) + i * 17) % 180
|
|
||||||
rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 11\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 17\nMUL\nADD\nPUSH_I32 180\nMOD"));
|
|
||||||
// r = ( (i*13) % 20 ) + 5
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 13\nMUL\nPUSH_I32 20\nMOD\nPUSH_I32 5\nADD"));
|
|
||||||
// border color = (i * 1234) & 0xFFFF
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND"));
|
|
||||||
// fill color = (i * 5678 + t) & 0xFFFF
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND"));
|
|
||||||
// HOSTCALL gfx.draw_disc (x, y, r, border, fill)
|
|
||||||
rom.extend(asm("HOSTCALL 1"));
|
|
||||||
|
|
||||||
// i++
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
|
||||||
let jmp_disc_loop_offset = rom.len() + 2;
|
|
||||||
rom.extend(asm("JMP 0"));
|
|
||||||
let disc_loop_end = rom.len() as u32;
|
|
||||||
|
|
||||||
// --- draw 20 texts ---
|
|
||||||
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
|
||||||
let text_loop_start = rom.len() as u32;
|
let col_loop_start = rom.len() as u32;
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 20\nLT"));
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT"));
|
||||||
let jif_text_end_offset = rom.len() + 2;
|
let jif_col_end_offset = rom.len() + 2;
|
||||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
rom.extend(asm("JMP_IF_FALSE 0"));
|
||||||
|
|
||||||
// x = (t * 3 + i * 40) % 320
|
|
||||||
rom.extend(asm(
|
rom.extend(asm(
|
||||||
"GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD",
|
"PUSH_I32 0\n\
|
||||||
|
GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\
|
||||||
|
GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\
|
||||||
|
GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\
|
||||||
|
GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\
|
||||||
|
PUSH_I32 0\n\
|
||||||
|
GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
|
||||||
|
GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
|
||||||
|
GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\
|
||||||
|
HOSTCALL 5\nPOP_N 1",
|
||||||
));
|
));
|
||||||
// y = (i * 30 + t) % 180
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 30\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 180\nMOD"));
|
|
||||||
// string (toggle between "stress" and "frame")
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ"));
|
|
||||||
let jif_text_alt_offset = rom.len() + 2;
|
|
||||||
rom.extend(asm("JMP_IF_FALSE 0"));
|
|
||||||
rom.extend(asm("PUSH_CONST 0")); // "stress"
|
|
||||||
let jmp_text_join_offset = rom.len() + 2;
|
|
||||||
rom.extend(asm("JMP 0"));
|
|
||||||
let text_alt_target = rom.len() as u32;
|
|
||||||
rom.extend(asm("PUSH_CONST 1")); // "frame"
|
|
||||||
let text_join_target = rom.len() as u32;
|
|
||||||
|
|
||||||
// color = (t * 10 + i * 1000) & 0xFFFF
|
|
||||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND"));
|
|
||||||
// HOSTCALL gfx.draw_text (x, y, str, color)
|
|
||||||
rom.extend(asm("HOSTCALL 2"));
|
|
||||||
|
|
||||||
// i++
|
|
||||||
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
|
||||||
let jmp_text_loop_offset = rom.len() + 2;
|
let jmp_col_loop_offset = rom.len() + 2;
|
||||||
rom.extend(asm("JMP 0"));
|
rom.extend(asm("JMP 0"));
|
||||||
let text_loop_end = rom.len() as u32;
|
let col_loop_end = rom.len() as u32;
|
||||||
|
|
||||||
|
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0"));
|
||||||
|
let jmp_row_loop_offset = rom.len() + 2;
|
||||||
|
rom.extend(asm("JMP 0"));
|
||||||
|
let row_loop_end = rom.len() as u32;
|
||||||
|
|
||||||
|
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 220\nMOD\n\
|
||||||
|
PUSH_I32 8\n\
|
||||||
|
PUSH_CONST 0\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||||
|
HOSTCALL 1"));
|
||||||
|
rom.extend(asm("PUSH_I32 12\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 120\nMOD\nPUSH_I32 24\nADD\n\
|
||||||
|
PUSH_CONST 1\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||||
|
HOSTCALL 1"));
|
||||||
|
rom.extend(asm("PUSH_I32 220\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 5\nMUL\nPUSH_I32 140\nMOD\n\
|
||||||
|
PUSH_CONST 2\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 1237\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||||
|
HOSTCALL 1"));
|
||||||
|
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 4\nMUL\nPUSH_I32 180\nMOD\nPUSH_I32 80\nADD\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 3\nMUL\nPUSH_I32 90\nMOD\nPUSH_I32 70\nADD\n\
|
||||||
|
PUSH_CONST 3\n\
|
||||||
|
GET_GLOBAL 0\nPUSH_I32 3001\nMUL\nPUSH_I32 65535\nBIT_AND\n\
|
||||||
|
HOSTCALL 1"));
|
||||||
|
|
||||||
// --- log every 60 frames ---
|
|
||||||
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
|
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 3"));
|
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2"));
|
||||||
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_disc_end_offset, disc_loop_end);
|
patch(rom, jif_bind_done_offset, bind_done_target);
|
||||||
patch(rom, jmp_disc_loop_offset, disc_loop_start);
|
patch(rom, jif_row_end_offset, row_loop_end);
|
||||||
|
patch(rom, jif_col_end_offset, col_loop_end);
|
||||||
patch(rom, jif_text_end_offset, text_loop_end);
|
patch(rom, jmp_col_loop_offset, col_loop_start);
|
||||||
patch(rom, jif_text_alt_offset, text_alt_target);
|
patch(rom, jmp_row_loop_offset, row_loop_start);
|
||||||
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
29
discussion/.backups/index.ndjson.20260418-162736.bak
Normal file
29
discussion/.backups/index.ndjson.20260418-162736.bak
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":31,"CLSN":1}}
|
||||||
|
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"workflow/agendas/AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"workflow/agendas/AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"workflow/decisions/DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"workflow/plans/PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"workflow/agendas/AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.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-0026","status":"open","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["gfx","runtime","render","camera","scene"],"agendas":[{"id":"AGD-0026","file":"workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15"}],"decisions":[{"id":"DEC-0014","file":"workflow/decisions/DEC-0014-frame-composer-render-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0026"}],"plans":[{"id":"PLN-0017","file":"workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0018","file":"workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0019","file":"workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0020","file":"workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0021","file":"workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]}],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
29
discussion/.backups/index.ndjson.20260418-162925.bak
Normal file
29
discussion/.backups/index.ndjson.20260418-162925.bak
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":32,"CLSN":1}}
|
||||||
|
{"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"workflow/agendas/AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"workflow/agendas/AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0008","status":"done","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0005","file":"workflow/decisions/DEC-0005-perf-push-based-telemetry-model.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0005","file":"workflow/plans/PLN-0005-perf-push-based-telemetry-implementation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0026","file":"lessons/DSC-0008-perf-runtime-telemetry-hot-path/LSN-0026-push-based-telemetry-model.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"workflow/agendas/AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.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-0026","status":"done","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-18","tags":["gfx","runtime","render","camera","scene"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
|
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
{"type":"meta","next_id":{"DSC":26,"AGD":26,"DEC":14,"PLN":17,"LSN":31,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":34,"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"}]}
|
||||||
@ -18,6 +18,9 @@
|
|||||||
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
{"type":"discussion","id":"DSC-0013","status":"done","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-04-10","tags":[],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0027","file":"lessons/DSC-0013-perf-host-debug-overlay-isolation/LSN-0027-host-debug-overlay-isolation.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-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":"done","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-18","tags":["gfx","runtime","render","camera","scene"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0031","file":"lessons/DSC-0026-render-all-scene-cache-and-camera-integration/LSN-0031-frame-composition-belongs-above-the-render-backend.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0027","status":"done","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-18","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0032","file":"lessons/DSC-0027-frame-composer-public-syscall-surface/LSN-0032-public-abi-must-follow-the-canonical-service-boundary.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0028","status":"done","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":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0033","file":"lessons/DSC-0028-deferred-overlay-and-primitive-composition/LSN-0033-debug-primitives-should-be-a-final-overlay-not-part-of-game-composition.md","status":"done","created_at":"2026-04-18","updated_at":"2026-04-18"}]}
|
||||||
{"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"}]}
|
||||||
|
|||||||
@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0031
|
||||||
|
ticket: render-all-scene-cache-and-camera-integration
|
||||||
|
title: Frame Composition Belongs Above the Render Backend
|
||||||
|
created: 2026-04-18
|
||||||
|
tags: [gfx, runtime, render, camera, scene, sprites, frame-composer]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`DSC-0025` split canonical scene ownership from viewport caching and resolver policy, but the runtime still treated `Gfx.render_all()` as the operational frame entrypoint. That left scene binding, camera state, cache refresh, and sprite submission spread across the wrong layer.
|
||||||
|
|
||||||
|
`DSC-0026` completed the integration by making `FrameComposer` the owner of frame orchestration and reducing `Gfx` to a backend that consumes already prepared render state.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Frame Orchestration Must Not Live in the Backend
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
`FrameComposer.render_frame()` became the canonical frame service, while `Gfx.render_all()` was retired from the runtime-facing flow.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
The backend should execute composition, not decide scene binding, camera policy, cache refresh, or sprite lifecycle. Keeping those responsibilities above `Gfx` preserves a cleaner ownership model and avoids re-entangling policy with raster code.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
This adds an explicit orchestration layer between runtime callsites and the renderer, but the resulting boundaries are easier to evolve and test.
|
||||||
|
|
||||||
|
### Scene Binding, Camera, Cache, and Sprites Form One Operational Unit
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
`FrameComposer` owns:
|
||||||
|
- active scene binding by bank id and shared scene reference;
|
||||||
|
- camera coordinates in top-left world pixel space;
|
||||||
|
- `SceneViewportCache` and `SceneViewportResolver`;
|
||||||
|
- a frame-emission `SpriteController`.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
These concerns all define what a frame is. Splitting them across multiple owners would recreate stale-state bugs and make no-scene behavior ambiguous.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
The composer becomes a richer subsystem, but it carries policy in one place instead of leaking it into unrelated APIs.
|
||||||
|
|
||||||
|
### The World Path Must Stay Tile-Size Agnostic
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
The integrated frame path derives cache sizing, resolver math, and world-copy behavior from per-layer scene metadata instead of a hard-coded `16x16` assumption.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
The scene contract already allows canonical `8x8`, `16x16`, and `32x32` tile sizes. The frame service has to consume that contract faithfully or it becomes a hidden compatibility break.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
The integration and tests need to exercise more than the legacy default path, but the renderer no longer bakes in a false invariant.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
- Put frame policy in a dedicated orchestration layer and keep the renderer backend-oriented.
|
||||||
|
- Treat scene binding, camera state, cache lifetime, and sprite submission as one cohesive frame model.
|
||||||
|
- Refresh cache state inside the orchestrator before composition instead of letting the renderer discover refresh policy.
|
||||||
|
- Prefer frame-emission sprite submission with internal ordering over caller-owned sprite slots.
|
||||||
|
- Keep the no-scene path valid so world composition remains optional, not mandatory.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Leaving `render_all()` alive as a canonical path creates a fragile dual-service model.
|
||||||
|
- Letting `Gfx` own cache refresh semantics collapses the boundary between policy and execution.
|
||||||
|
- Requiring a scene for every frame quietly breaks sprite-only or fade-only operation.
|
||||||
|
- Testing only `16x16` scenes hides regressions against valid `8x8` or `32x32` content.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- Frame composition belongs in a subsystem that owns policy, not in the backend that draws pixels.
|
||||||
|
- Scene binding, camera, cache, resolver, and sprite submission should converge under one frame owner.
|
||||||
|
- No-scene rendering is part of the contract and should stay valid throughout integration work.
|
||||||
|
- Tile-size assumptions must be derived from canonical scene metadata, never from renderer habit.
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0032
|
||||||
|
ticket: frame-composer-public-syscall-surface
|
||||||
|
title: Public ABI Must Follow the Canonical Service Boundary
|
||||||
|
created: 2026-04-18
|
||||||
|
tags: [gfx, runtime, syscall, abi, frame-composer, scene, camera, sprites]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
`DSC-0026` finished the internal migration to `FrameComposer` as the canonical frame-orchestration owner, but the public VM-facing ABI still exposed part of that behavior through legacy `gfx`-domain calls. That left the codebase with a mismatch between the real runtime ownership model and the surface visible to cartridges, tooling, and syscall declarations.
|
||||||
|
|
||||||
|
`DSC-0027` closed that gap by introducing the `composer.*` public domain and removing the old public sprite path.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Public Syscalls Must Expose the Real Owner
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
Scene binding, camera control, and sprite emission now live under `composer.*`, and the legacy public `gfx.set_sprite` path is gone.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
Once `FrameComposer` became the canonical orchestration service, keeping public orchestration under `gfx.*` would preserve the wrong mental model and encourage callers to treat the render backend as the owner of frame policy.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
This forces migration across ABI declarations, runtime dispatch, tests, and tooling, but it removes the long-term cost of a misleading public boundary.
|
||||||
|
|
||||||
|
### Domain-Specific Status Types Preserve Architectural Meaning
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
Mutating public composer operations return `ComposerOpStatus` instead of reusing backend-oriented status naming.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
Operational outcomes for scene binding or sprite emission are not backend-domain results. Reusing `GfxOpStatus` would blur the boundary that the migration was trying to make explicit.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
This adds one more status family to maintain, but it keeps the public ABI semantically aligned with the actual service contract.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
- Promote internal ownership changes into the public ABI as part of the same migration thread.
|
||||||
|
- Use syscall domains to encode service boundaries, not just namespace aesthetics.
|
||||||
|
- Remove obsolete public fallbacks completely when they preserve the wrong operational model.
|
||||||
|
- Keep runtime dispatch, bytecode declarations, and tooling aligned so the public path is exercised end to end.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Leaving a legacy public syscall alive after the internal model changes creates a dual-contract system that is harder to remove later.
|
||||||
|
- Migrating runtime dispatch without migrating declarations and tooling can leave hidden ABI drift in tests and generators.
|
||||||
|
- Reusing backend-specific status names in the wrong domain quietly leaks old ownership assumptions into new APIs.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- The public ABI should mirror the canonical service boundary, not historical implementation leftovers.
|
||||||
|
- Namespace changes are architectural when they change who is responsible for a behavior.
|
||||||
|
- Removing a legacy public entrypoint is often safer than preserving a compatibility shim that encodes the wrong model.
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0033
|
||||||
|
ticket: deferred-overlay-and-primitive-composition
|
||||||
|
title: Debug Primitives Should Be a Final Overlay, Not Part of Game Composition
|
||||||
|
created: 2026-04-18
|
||||||
|
tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud, debug]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
After `FrameComposer.render_frame()` became the canonical game-frame entrypoint, immediate `gfx.*` primitive writes were no longer stable. Scene-backed composition could rebuild the framebuffer after `draw_text(...)` or other debug primitives had already written to it.
|
||||||
|
|
||||||
|
`DSC-0028` resolved that conflict by moving `gfx.*` primitives into a deferred overlay/debug stage outside `FrameComposer`, drained only after canonical game composition and fades are complete.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Debug Overlay Must Stay Outside the Canonical Game Pipeline
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
`FrameComposer` keeps ownership of canonical game composition, while debug/text/primitive commands are captured separately and drained later as a final overlay.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
Game composition and debug overlay have different purposes. The first must remain canonical and deterministic; the second must remain opportunistic, screen-space, and independent from scene or sprite semantics.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
The renderer needs a second deferred path, but the game pipeline no longer depends on transient debug state.
|
||||||
|
|
||||||
|
### Final Visual Ordering Matters More Than Immediate Writes
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
Overlay/debug commands are drained after scene composition, sprite composition, and fades, with parity between scene-bound and no-scene frame paths.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
The stable user-visible contract is that debug primitives appear on top. Immediate writes were only an implementation detail, and they stopped preserving that contract once frame composition became deferred and canonical.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
This changes primitive semantics from "write now" to "show at frame end," but it produces the behavior users actually rely on.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
- Separate canonical composition state from debug-overlay state even when both reuse the same raster backend.
|
||||||
|
- Capture primitives as commands first, then drain them at the final stage where visual priority is unambiguous.
|
||||||
|
- Preserve the same overlay semantics whether a scene is bound or not.
|
||||||
|
- Keep implementation reuse internal while maintaining a clear semantic boundary in the public model.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Treating debug primitives as part of HUD or scene composition will eventually couple tooling/debug behavior to gameplay pipeline rules.
|
||||||
|
- Draining overlay before fades or before final frame composition breaks the visible "always on top" contract.
|
||||||
|
- Reusing `FrameComposer` storage for overlay state collapses the ownership split that prevents these bugs.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- Immediate framebuffer writes are not a reliable contract once final composition is orchestrated elsewhere.
|
||||||
|
- Debug primitives work best as a dedicated final overlay layer.
|
||||||
|
- Ownership separation is what keeps debug behavior stable while the canonical render pipeline evolves.
|
||||||
@ -48,10 +48,12 @@ The GFX maintains two buffers:
|
|||||||
|
|
||||||
Per-frame flow:
|
Per-frame flow:
|
||||||
|
|
||||||
1. The system draws to the back buffer
|
1. The system prepares the logical frame
|
||||||
2. Calls `present()`
|
2. Canonical game composition is rendered into the back buffer
|
||||||
3. Buffers are swapped
|
3. Deferred final overlay/debug primitives are drained on top of the completed game frame
|
||||||
4. The host displays the front buffer
|
4. Calls `present()`
|
||||||
|
5. Buffers are swapped
|
||||||
|
6. The host displays the front buffer
|
||||||
|
|
||||||
This guarantees:
|
This guarantees:
|
||||||
|
|
||||||
@ -183,7 +185,7 @@ Access:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 10. Projection to the Back Buffer
|
## 10. Canonical Game Projection to the Back Buffer
|
||||||
|
|
||||||
For each frame:
|
For each frame:
|
||||||
|
|
||||||
@ -198,6 +200,10 @@ 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
|
||||||
@ -214,6 +220,15 @@ Base order:
|
|||||||
4. Tile Layer 3
|
4. Tile Layer 3
|
||||||
5. Sprites (by priority between layers)
|
5. Sprites (by priority between layers)
|
||||||
6. HUD Layer
|
6. HUD Layer
|
||||||
|
7. Scene Fade
|
||||||
|
8. HUD Fade
|
||||||
|
9. Deferred `gfx.*` overlay/debug primitives
|
||||||
|
|
||||||
|
Normative boundary:
|
||||||
|
|
||||||
|
- Items 1 through 8 belong to canonical game-frame composition.
|
||||||
|
- Item 9 is a separate overlay/debug stage.
|
||||||
|
- Deferred `gfx.*` primitives MUST NOT be interpreted as scene, sprite, or canonical HUD content.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -258,8 +273,9 @@ Everything is:
|
|||||||
## 14. Where Blend is Applied
|
## 14. Where Blend is Applied
|
||||||
|
|
||||||
- Blending occurs during drawing
|
- Blending occurs during drawing
|
||||||
- The result goes directly to the back buffer
|
- For canonical game composition, the result goes to the back buffer during composition
|
||||||
- There is no automatic post-composition
|
- For deferred `gfx.*` overlay/debug primitives, the result is applied during the final overlay/debug drain stage
|
||||||
|
- There is no automatic GPU-style post-processing pipeline
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -296,6 +312,8 @@ controls:
|
|||||||
- **Scene Fade**: affects the entire scene (Tile Layers 0–3 + Sprites)
|
- **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.
|
||||||
@ -536,7 +554,12 @@ The system can measure:
|
|||||||
|
|
||||||
## 19. Syscall Return and Fault Policy
|
## 19. Syscall Return and Fault Policy
|
||||||
|
|
||||||
`gfx` follows status-first policy for operations with operational failure modes.
|
Graphics-related public ABI in v1 is split between:
|
||||||
|
|
||||||
|
- `gfx.*` for direct drawing/backend-oriented operations;
|
||||||
|
- `composer.*` for frame orchestration operations.
|
||||||
|
|
||||||
|
Only operations with real operational rejection paths return explicit status values.
|
||||||
|
|
||||||
Fault boundary:
|
Fault boundary:
|
||||||
|
|
||||||
@ -544,50 +567,62 @@ Fault boundary:
|
|||||||
- `status`: operational failure;
|
- `status`: operational failure;
|
||||||
- `Panic`: internal runtime invariant break only.
|
- `Panic`: internal runtime invariant break only.
|
||||||
|
|
||||||
### 19.1 `gfx.set_sprite`
|
### 19.1 Return-shape matrix in v1
|
||||||
|
|
||||||
Return-shape matrix in v1:
|
|
||||||
|
|
||||||
| Syscall | Return | Policy basis |
|
| 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 |
|
||||||
|
|
||||||
Only `gfx.set_sprite` is status-returning in v1.
|
### 19.1.a Deferred overlay/debug semantics for `gfx.*`
|
||||||
All other `gfx` syscalls remain `void` unless a future domain revision introduces a real operational failure path.
|
|
||||||
|
|
||||||
### 19.2 `gfx.set_sprite`
|
The public `gfx.*` primitive family remains valid in v1, but its stable operational meaning is:
|
||||||
|
|
||||||
`gfx.set_sprite` returns `status:int`.
|
- deferred final overlay/debug composition;
|
||||||
|
- screen-space and pipeline-agnostic relative to `composer.*`;
|
||||||
|
- outside `FrameComposer`;
|
||||||
|
- above scene, sprites, and canonical HUD;
|
||||||
|
- drained after `hud_fade`.
|
||||||
|
|
||||||
|
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. `bank_id: int` — index of the tile bank
|
1. `glyph_id: int` — glyph index within the bank
|
||||||
2. `index: int` — sprite index (0..511)
|
2. `palette_id: int` — palette index
|
||||||
3. `x: int` — x coordinate
|
3. `x: int` — x coordinate
|
||||||
4. `y: int` — y coordinate
|
4. `y: int` — y coordinate
|
||||||
5. `tile_id: int` — tile index within the bank
|
5. `layer: int` — composition layer reference
|
||||||
6. `palette_id: int` — palette index (0..63)
|
6. `bank_id: int` — glyph bank index
|
||||||
7. `active: bool` — visibility toggle
|
7. `flip_x: bool` — horizontal flip
|
||||||
8. `flip_x: bool` — horizontal flip
|
8. `flip_y: bool` — vertical flip
|
||||||
9. `flip_y: bool` — vertical flip
|
9. `priority: int` — within-layer ordering priority
|
||||||
10. `priority: int` — layer priority (0..4)
|
|
||||||
|
|
||||||
Minimum status table:
|
Minimum status table:
|
||||||
|
|
||||||
- `0` = `OK`
|
- `0` = `OK`
|
||||||
- `2` = `INVALID_SPRITE_INDEX`
|
- `1` = `SCENE_UNAVAILABLE`
|
||||||
- `3` = `INVALID_ARG_RANGE`
|
- `2` = `INVALID_ARG_RANGE`
|
||||||
- `4` = `BANK_INVALID`
|
- `3` = `BANK_INVALID`
|
||||||
|
- `4` = `LAYER_INVALID`
|
||||||
|
- `5` = `SPRITE_OVERFLOW`
|
||||||
|
|
||||||
Operational notes:
|
Operational notes:
|
||||||
|
|
||||||
- no fallback to default bank when the sprite bank id cannot be resolved;
|
- the canonical public sprite contract is frame-emission based;
|
||||||
- no silent no-op for invalid index/range;
|
- no caller-provided sprite index exists in the v1 canonical ABI;
|
||||||
- `palette_id` and `priority` must be validated against runtime-supported ranges.
|
- no `active` flag exists in the v1 canonical ABI;
|
||||||
|
- overflow remains non-fatal and must not escalate to trap in v1.
|
||||||
|
|||||||
@ -39,6 +39,7 @@ Example:
|
|||||||
```
|
```
|
||||||
("gfx", "present", 1)
|
("gfx", "present", 1)
|
||||||
("audio", "play", 2)
|
("audio", "play", 2)
|
||||||
|
("composer", "emit_sprite", 1)
|
||||||
```
|
```
|
||||||
|
|
||||||
This identity is:
|
This identity is:
|
||||||
@ -198,6 +199,24 @@ 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,6 +85,9 @@ 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
|
||||||
|
|
||||||
|
|||||||
BIN
test-cartridges/stress-console/assets.pa
Normal file
BIN
test-cartridges/stress-console/assets.pa
Normal file
Binary file not shown.
@ -5,5 +5,5 @@
|
|||||||
"title": "Stress Console",
|
"title": "Stress Console",
|
||||||
"app_version": "0.1.0",
|
"app_version": "0.1.0",
|
||||||
"app_mode": "Game",
|
"app_mode": "Game",
|
||||||
"capabilities": ["gfx", "log"]
|
"capabilities": ["gfx", "log", "asset"]
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user