dev/new-scene-plt #15

Merged
bquarkz merged 8 commits from dev/new-scene-plt into master 2026-04-14 04:26:50 +00:00
22 changed files with 1905 additions and 348 deletions
Showing only changes of commit e1afc164ae - Show all commits

View File

@ -1,5 +1,5 @@
#![allow(clippy::collapsible_if)]
use crate::memory_banks::{GlyphBankPoolInstaller, SoundBankPoolInstaller};
use crate::memory_banks::{GlyphBankPoolInstaller, SceneBankPoolInstaller, SoundBankPoolInstaller};
use prometeu_hal::AssetBridge;
use prometeu_hal::asset::{
AssetCodec, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankTelemetry, BankType,
@ -9,6 +9,7 @@ use prometeu_hal::cartridge::AssetsPayloadSource;
use prometeu_hal::color::Color;
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
use prometeu_hal::sample::Sample;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::sound_bank::SoundBank;
use std::collections::HashMap;
use std::io::Read;
@ -151,15 +152,19 @@ pub struct AssetManager {
/// Narrow hardware interfaces
gfx_installer: Arc<dyn GlyphBankPoolInstaller>,
sound_installer: Arc<dyn SoundBankPoolInstaller>,
scene_installer: Arc<dyn SceneBankPoolInstaller>,
/// Track what is installed in each hardware slot (for stats/info).
gfx_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
sound_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
scene_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
/// Residency policy for GFX glyph banks.
gfx_policy: BankPolicy<GlyphBank>,
/// Residency policy for sound banks.
sound_policy: BankPolicy<SoundBank>,
/// Residency policy for scene banks.
scene_policy: BankPolicy<SceneBank>,
// Commits that are ready to be applied at the next frame boundary.
pending_commits: Mutex<Vec<HandleId>>,
@ -275,6 +280,7 @@ impl AssetManager {
match (entry.bank_type, entry.codec) {
(BankType::GLYPH, AssetCodec::None) => Ok(AssetOpMode::StageInMemory),
(BankType::SOUNDS, AssetCodec::None) => Ok(AssetOpMode::DirectFromSlice),
(BankType::SCENE, AssetCodec::None) => Ok(AssetOpMode::DirectFromSlice),
}
}
@ -283,6 +289,7 @@ impl AssetManager {
assets_data: AssetsPayloadSource,
gfx_installer: Arc<dyn GlyphBankPoolInstaller>,
sound_installer: Arc<dyn SoundBankPoolInstaller>,
scene_installer: Arc<dyn SceneBankPoolInstaller>,
) -> Self {
let mut asset_map = HashMap::new();
for entry in assets {
@ -293,10 +300,13 @@ impl AssetManager {
assets: Arc::new(RwLock::new(asset_map)),
gfx_installer,
sound_installer,
scene_installer,
gfx_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
sound_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
scene_slots: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
gfx_policy: BankPolicy::new(),
sound_policy: BankPolicy::new(),
scene_policy: BankPolicy::new(),
handles: Arc::new(RwLock::new(HashMap::new())),
next_handle_id: Mutex::new(1),
assets_data: Arc::new(RwLock::new(assets_data)),
@ -345,17 +355,7 @@ impl AssetManager {
if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id);
}
// println!(
// "[AssetManager] Preloaded tile asset '{}' (id: {}) into slot {}",
// entry.asset_name, entry.asset_id, slot_index
// );
}
// else {
// eprintln!(
// "[AssetManager] Failed to preload tile asset '{}'",
// entry.asset_name
// );
// }
}
BankType::SOUNDS => {
if let Ok(bank) =
@ -372,23 +372,27 @@ impl AssetManager {
if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id);
}
// println!(
// "[AssetManager] Preloaded sound asset '{}' (id: {}) into slot {}",
// entry.asset_name, entry.asset_id, slot_index
// );
}
// else {
// eprintln!(
// "[AssetManager] Failed to preload sound asset '{}'",
// entry.asset_name
// );
// }
}
BankType::SCENE => {
if let Ok(bank) =
Self::perform_load_scene_bank(&entry, self.assets_data.clone())
{
let bank_arc = Arc::new(bank);
self.scene_policy.put_resident(
entry.asset_id,
Arc::clone(&bank_arc),
entry.decoded_size as usize,
);
self.scene_installer.install_scene_bank(slot_index, bank_arc);
let mut slots = self.scene_slots.write().unwrap();
if slot_index < slots.len() {
slots[slot_index] = Some(entry.asset_id);
}
}
}
}
}
// else {
// eprintln!("[AssetManager] Preload failed: asset id '{}' not found in table", item.asset_id);
// }
}
}
@ -403,6 +407,7 @@ impl AssetManager {
let slot = match entry.bank_type {
BankType::GLYPH => SlotRef::gfx(slot_index),
BankType::SOUNDS => SlotRef::audio(slot_index),
BankType::SCENE => SlotRef::scene(slot_index),
};
let mut next_id = self.next_handle_id.lock().unwrap();
@ -427,6 +432,14 @@ impl AssetManager {
false
}
}
BankType::SCENE => {
if let Some(bank) = self.scene_policy.get_resident(asset_id) {
self.scene_policy.stage(handle_id, bank, entry.decoded_size as usize);
true
} else {
false
}
}
};
if already_resident {
@ -450,6 +463,7 @@ impl AssetManager {
// Capture policies for the worker thread
let gfx_policy = self.gfx_policy.clone();
let sound_policy = self.sound_policy.clone();
let scene_policy = self.scene_policy.clone();
thread::spawn(move || {
// Update status to LOADING
@ -510,6 +524,34 @@ impl AssetManager {
entry_clone.decoded_size as usize,
);
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING {
h.status = LoadStatus::READY;
}
}
} else {
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
h.status = LoadStatus::ERROR;
}
}
}
BankType::SCENE => {
let result = Self::perform_load_scene_bank(&entry_clone, assets_data);
if let Ok(scenebank) = result {
let bank_arc = Arc::new(scenebank);
let resident_arc = scene_policy.put_resident(
asset_id,
bank_arc,
entry_clone.decoded_size as usize,
);
scene_policy.stage(
handle_id,
resident_arc,
entry_clone.decoded_size as usize,
);
let mut handles_map = handles.write().unwrap();
if let Some(h) = handles_map.get_mut(&handle_id) {
if h.status == LoadStatus::LOADING {
@ -642,6 +684,17 @@ impl AssetManager {
}
}
fn perform_load_scene_bank(
entry: &AssetEntry,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
) -> Result<SceneBank, String> {
let _ = (entry, assets_data);
// TODO: SCENE payloads must not be decoded from ad-hoc JSON.
// Close and document the binary scene-bank format first, then implement
// the runtime decoder here using the canonical binary contract.
todo!("SCENE binary payload format is still open")
}
fn decode_sound_bank_from_buffer(
entry: &AssetEntry,
buffer: &[u8],
@ -709,6 +762,7 @@ impl AssetManager {
}
self.gfx_policy.take_staging(handle);
self.sound_policy.take_staging(handle);
self.scene_policy.take_staging(handle);
final_status
}
@ -740,6 +794,16 @@ impl AssetManager {
h.status = LoadStatus::COMMITTED;
}
}
BankType::SCENE => {
if let Some((bank, _)) = self.scene_policy.take_staging(handle_id) {
self.scene_installer.install_scene_bank(h.slot.index, bank);
let mut slots = self.scene_slots.write().unwrap();
if h.slot.index < slots.len() {
slots[h.slot.index] = Some(h._asset_id);
}
h.status = LoadStatus::COMMITTED;
}
}
}
}
}
@ -747,7 +811,11 @@ impl AssetManager {
}
pub fn bank_telemetry(&self) -> Vec<BankTelemetry> {
vec![self.bank_telemetry_for(BankType::GLYPH), self.bank_telemetry_for(BankType::SOUNDS)]
vec![
self.bank_telemetry_for(BankType::GLYPH),
self.bank_telemetry_for(BankType::SOUNDS),
self.bank_telemetry_for(BankType::SCENE),
]
}
fn bank_telemetry_for(&self, kind: BankType) -> BankTelemetry {
@ -758,6 +826,9 @@ impl AssetManager {
BankType::SOUNDS => {
self.sound_slots.read().unwrap().iter().filter(|slot| slot.is_some()).count()
}
BankType::SCENE => {
self.scene_slots.read().unwrap().iter().filter(|slot| slot.is_some()).count()
}
};
BankTelemetry { bank_type: kind, used_slots, total_slots: 16 }
@ -805,6 +876,27 @@ impl AssetManager {
(0, None)
};
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
}
BankType::SCENE => {
let slots = self.scene_slots.read().unwrap();
let asset_id = slots.get(slot.index).and_then(|s| *s);
let (bytes, asset_name) = if let Some(id) = &asset_id {
let bytes = self
.scene_policy
.resident
.read()
.unwrap()
.get(id)
.map(|entry| entry.bytes)
.unwrap_or(0);
let name = self.assets.read().unwrap().get(id).map(|e| e.asset_name.clone());
(bytes, name)
} else {
(0, None)
};
SlotStats { asset_id, asset_name, generation: 0, resident_bytes: bytes }
}
}
@ -813,10 +905,12 @@ impl AssetManager {
pub fn shutdown(&self) {
self.gfx_policy.clear();
self.sound_policy.clear();
self.scene_policy.clear();
self.handles.write().unwrap().clear();
self.pending_commits.lock().unwrap().clear();
self.gfx_slots.write().unwrap().fill(None);
self.sound_slots.write().unwrap().fill(None);
self.scene_slots.write().unwrap().fill(None);
}
}
@ -933,11 +1027,28 @@ mod tests {
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::DirectFromSlice));
}
#[test]
fn test_op_mode_for_scene_none_reads_direct_from_slice() {
let entry = AssetEntry {
asset_id: 2,
asset_name: "scene".to_string(),
bank_type: BankType::SCENE,
offset: 0,
size: 32,
decoded_size: 32,
codec: AssetCodec::None,
metadata: serde_json::json!({}),
};
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::DirectFromSlice));
}
#[test]
fn test_asset_loading_flow() {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let data = test_glyph_asset_data();
let asset_entry = test_glyph_asset_entry("test_glyphs", 16, 16);
@ -947,6 +1058,7 @@ mod tests {
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
let handle = am.load(0, 0).expect("Should start loading");
@ -976,6 +1088,7 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let data = test_glyph_asset_data();
let asset_entry = test_glyph_asset_entry("test_glyphs", 16, 16);
@ -985,6 +1098,7 @@ mod tests {
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
let handle1 = am.load(0, 0).unwrap();
@ -1007,6 +1121,7 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
// 100 samples of 16-bit PCM (zeros)
let data = vec![0u8; 200];
@ -1030,6 +1145,7 @@ mod tests {
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
let handle = am.load(1, 0).expect("Should start loading");
@ -1052,6 +1168,7 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let data = vec![0u8; 200];
@ -1071,8 +1188,13 @@ mod tests {
let preload = vec![PreloadEntry { asset_id: 2, slot: 5 }];
let am =
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
scene_installer,
);
// Before init, slot 5 is empty
assert!(banks.sound_bank_slot(5).is_none());
@ -1093,8 +1215,14 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let am =
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
scene_installer,
);
let result = am.load(999, 0);
@ -1106,12 +1234,14 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let data = test_glyph_asset_data();
let am = AssetManager::new(
vec![test_glyph_asset_entry("test_glyphs", 16, 16)],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
let result = am.load(0, 16);
@ -1124,8 +1254,14 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let am =
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let am = AssetManager::new(
vec![],
AssetsPayloadSource::empty(),
gfx_installer,
sound_installer,
scene_installer,
);
assert_eq!(am.status(999), LoadStatus::UnknownHandle);
}
@ -1135,12 +1271,14 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let data = test_glyph_asset_data();
let am = AssetManager::new(
vec![test_glyph_asset_entry("test_glyphs", 16, 16)],
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
assert_eq!(am.commit(999), AssetOpStatus::UnknownHandle);
@ -1162,6 +1300,7 @@ mod tests {
let banks = Arc::new(MemoryBanks::new());
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
let scene_installer = Arc::clone(&banks) as Arc<dyn SceneBankPoolInstaller>;
let width = 16;
let height = 16;
@ -1172,6 +1311,7 @@ mod tests {
AssetsPayloadSource::from_bytes(data),
gfx_installer,
sound_installer,
scene_installer,
);
// Initially zero

View File

@ -2,10 +2,8 @@ use crate::memory_banks::GlyphBankPoolAccess;
use prometeu_hal::GfxBridge;
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
use prometeu_hal::glyph_bank::GlyphBank;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::tile::Tile;
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
use std::sync::Arc;
/// Blending modes inspired by classic 16-bit hardware.
@ -57,10 +55,6 @@ pub struct Gfx {
/// Back buffer: the "Work RAM" where new frames are composed.
back: Vec<u16>,
/// 4 scrollable background layers. Each can have its own scroll (X, Y) and GlyphBank.
pub layers: [ScrollableTileLayer; 4],
/// 1 fixed layer for User Interface (HUD). It doesn't scroll.
pub hud: HudTileLayer,
/// Shared access to graphical memory banks (tiles and palettes).
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
@ -214,12 +208,6 @@ impl GfxBridge for Gfx {
fn render_all(&mut self) {
self.render_all()
}
fn render_layer(&mut self, layer_idx: usize) {
self.render_layer(layer_idx)
}
fn render_hud(&mut self) {
self.render_hud()
}
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
self.draw_text(x, y, text, color)
}
@ -227,18 +215,6 @@ impl GfxBridge for Gfx {
self.draw_char(x, y, c, color)
}
fn layer(&self, index: usize) -> &ScrollableTileLayer {
&self.layers[index]
}
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer {
&mut self.layers[index]
}
fn hud(&self) -> &HudTileLayer {
&self.hud
}
fn hud_mut(&mut self) -> &mut HudTileLayer {
&mut self.hud
}
fn sprite(&self, index: usize) -> &Sprite {
&self.sprites[index]
}
@ -289,20 +265,11 @@ impl Gfx {
};
let len = w * h;
let layers = [
ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16),
];
Self {
w,
h,
front: vec![0; len],
back: vec![0; len],
layers,
hud: HudTileLayer::new(64, 32),
glyph_banks,
sprites: [EMPTY_SPRITE; 512],
scene_fade_level: 31,
@ -588,192 +555,171 @@ impl Gfx {
// 2. Main layers and prioritized sprites.
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
for i in 0..self.layers.len() {
let bank_id = self.layers[i].bank_id as usize;
if let Some(bank) = self.glyph_banks.glyph_bank_slot(bank_id) {
Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[i].map,
&bank,
self.layers[i].scroll_x,
self.layers[i].scroll_y,
);
}
// Draw sprites that belong to this depth level
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[i + 1],
&self.sprites,
&*self.glyph_banks,
);
}
// for i in 0..self.layers.len() {
// let bank_id = self.layers[i].bank_id as usize;
// if let Some(bank) = self.glyph_banks.glyph_bank_slot(bank_id) {
// Self::draw_tile_map(
// &mut self.back,
// self.w,
// self.h,
// &self.layers[i].map,
// &bank,
// self.layers[i].scroll_x,
// self.layers[i].scroll_y,
// );
// }
//
// // Draw sprites that belong to this depth level
// Self::draw_bucket_on_buffer(
// &mut self.back,
// self.w,
// self.h,
// &self.priority_buckets[i + 1],
// &self.sprites,
// &*self.glyph_banks,
// );
// }
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
// 5. HUD: The fixed interface layer, always drawn on top of the world.
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.glyph_banks);
// Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.glyph_banks);
// 6. HUD Fade: Independent fade effect for the UI.
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
}
/// Renders a specific game layer.
pub fn render_layer(&mut self, layer_idx: usize) {
if layer_idx >= self.layers.len() {
return;
}
// /// Renders a specific game layer.
// pub fn render_layer(&mut self, layer_idx: usize) {
// if layer_idx >= self.layers.len() {
// return;
// }
//
// let bank_id = self.layers[layer_idx].bank_id as usize;
// let scroll_x = self.layers[layer_idx].scroll_x;
// let scroll_y = self.layers[layer_idx].scroll_y;
//
// let bank = match self.glyph_banks.glyph_bank_slot(bank_id) {
// Some(b) => b,
// _ => return,
// };
//
// Self::draw_tile_map(
// &mut self.back,
// self.w,
// self.h,
// &self.layers[layer_idx].map,
// &bank,
// scroll_x,
// scroll_y,
// );
// }
let bank_id = self.layers[layer_idx].bank_id as usize;
let scroll_x = self.layers[layer_idx].scroll_x;
let scroll_y = self.layers[layer_idx].scroll_y;
// /// Rasterizes a TileMap into the provided pixel buffer using scrolling.
// fn draw_tile_map(
// back: &mut [u16],
// screen_w: usize,
// screen_h: usize,
// map: &TileMap,
// bank: &GlyphBank,
// scroll_x: i32,
// scroll_y: i32,
// ) {
// let tile_size = bank.tile_size as usize;
//
// // 1. Determine the range of visible tiles based on the scroll position.
// // We add a margin of 1 tile to ensure smooth pixel-perfect scrolling at the borders.
// let visible_tiles_x = (screen_w / tile_size) + 1;
// let visible_tiles_y = (screen_h / tile_size) + 1;
//
// // 2. Calculate offsets within the tilemap.
// let start_tile_x = scroll_x / tile_size as i32;
// let start_tile_y = scroll_y / tile_size as i32;
//
// // 3. Fine scroll: how many pixels the tiles are shifted within the first visible cell.
// let fine_scroll_x = scroll_x % tile_size as i32;
// let fine_scroll_y = scroll_y % tile_size as i32;
//
// // 4. Iterate only through the tiles that are actually visible on screen.
// for ty in 0..visible_tiles_y {
// for tx in 0..visible_tiles_x {
// let map_x = (start_tile_x + tx as i32) as usize;
// let map_y = (start_tile_y + ty as i32) as usize;
//
// // Bounds check: don't draw if the camera is outside the map.
// if map_x >= map.width || map_y >= map.height {
// continue;
// }
//
// let tile = map.tiles[map_y * map.width + map_x];
//
// // Optimized skip for empty (ID 0) tiles.
// if !tile.active {
// continue;
// }
//
// // 5. Project the tile pixels to screen space.
// let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
// let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
//
// Self::draw_tile_pixels(
// back,
// screen_w,
// screen_h,
// screen_tile_x,
// screen_tile_y,
// tile,
// bank,
// );
// }
// }
// }
let bank = match self.glyph_banks.glyph_bank_slot(bank_id) {
Some(b) => b,
_ => return,
};
Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[layer_idx].map,
&bank,
scroll_x,
scroll_y,
);
}
/// Renders the HUD (fixed position, no scroll).
pub fn render_hud(&mut self) {
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.glyph_banks);
}
fn render_hud_with_pool(
back: &mut [u16],
w: usize,
h: usize,
hud: &HudTileLayer,
glyph_banks: &dyn GlyphBankPoolAccess,
) {
let bank_id = hud.bank_id as usize;
let bank = match glyph_banks.glyph_bank_slot(bank_id) {
Some(b) => b,
_ => return,
};
Self::draw_tile_map(back, w, h, &hud.map, &bank, 0, 0);
}
/// Rasterizes a TileMap into the provided pixel buffer using scrolling.
fn draw_tile_map(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
map: &TileMap,
bank: &GlyphBank,
scroll_x: i32,
scroll_y: i32,
) {
let tile_size = bank.tile_size as usize;
// 1. Determine the range of visible tiles based on the scroll position.
// We add a margin of 1 tile to ensure smooth pixel-perfect scrolling at the borders.
let visible_tiles_x = (screen_w / tile_size) + 1;
let visible_tiles_y = (screen_h / tile_size) + 1;
// 2. Calculate offsets within the tilemap.
let start_tile_x = scroll_x / tile_size as i32;
let start_tile_y = scroll_y / tile_size as i32;
// 3. Fine scroll: how many pixels the tiles are shifted within the first visible cell.
let fine_scroll_x = scroll_x % tile_size as i32;
let fine_scroll_y = scroll_y % tile_size as i32;
// 4. Iterate only through the tiles that are actually visible on screen.
for ty in 0..visible_tiles_y {
for tx in 0..visible_tiles_x {
let map_x = (start_tile_x + tx as i32) as usize;
let map_y = (start_tile_y + ty as i32) as usize;
// Bounds check: don't draw if the camera is outside the map.
if map_x >= map.width || map_y >= map.height {
continue;
}
let tile = map.tiles[map_y * map.width + map_x];
// Optimized skip for empty (ID 0) tiles.
if !tile.active {
continue;
}
// 5. Project the tile pixels to screen space.
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
Self::draw_tile_pixels(
back,
screen_w,
screen_h,
screen_tile_x,
screen_tile_y,
tile,
bank,
);
}
}
}
/// Internal helper to copy a single tile's pixels to the framebuffer.
/// Handles flipping and palette resolution.
fn draw_tile_pixels(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
x: i32,
y: i32,
tile: Tile,
bank: &GlyphBank,
) {
let size = bank.tile_size as usize;
for local_y in 0..size {
let world_y = y + local_y as i32;
if world_y < 0 || world_y >= screen_h as i32 {
continue;
}
for local_x in 0..size {
let world_x = x + local_x as i32;
if world_x < 0 || world_x >= screen_w as i32 {
continue;
}
// Handle flip flags by reversing the fetch coordinates.
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
// 1. Get the pixel color index (0-15) from the bank.
let px_index = bank.get_pixel_index(tile.glyph.glyph_id, fetch_x, fetch_y);
// 2. Hardware rule: Color index 0 is always fully transparent.
if px_index == 0 {
continue;
}
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
let color = bank.resolve_color(tile.glyph.palette_id, px_index);
back[world_y as usize * screen_w + world_x as usize] = color.raw();
}
}
}
// /// Internal helper to copy a single tile's pixels to the framebuffer.
// /// Handles flipping and palette resolution.
// fn draw_tile_pixels(
// back: &mut [u16],
// screen_w: usize,
// screen_h: usize,
// x: i32,
// y: i32,
// tile: Tile,
// bank: &GlyphBank,
// ) {
// let size = bank.tile_size as usize;
//
// for local_y in 0..size {
// let world_y = y + local_y as i32;
// if world_y < 0 || world_y >= screen_h as i32 {
// continue;
// }
//
// for local_x in 0..size {
// let world_x = x + local_x as i32;
// if world_x < 0 || world_x >= screen_w as i32 {
// continue;
// }
//
// // Handle flip flags by reversing the fetch coordinates.
// let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
// let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
//
// // 1. Get the pixel color index (0-15) from the bank.
// let px_index = bank.get_pixel_index(tile.glyph.glyph_id, fetch_x, fetch_y);
//
// // 2. Hardware rule: Color index 0 is always fully transparent.
// if px_index == 0 {
// continue;
// }
//
// // 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
// let color = bank.resolve_color(tile.glyph.palette_id, px_index);
//
// back[world_y as usize * screen_w + world_x as usize] = color.raw();
// }
// }
// }
fn draw_bucket_on_buffer(
back: &mut [u16],

View File

@ -2,8 +2,8 @@ use crate::asset::AssetManager;
use crate::audio::Audio;
use crate::gfx::Gfx;
use crate::memory_banks::{
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SoundBankPoolAccess,
SoundBankPoolInstaller,
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolInstaller,
SoundBankPoolAccess, SoundBankPoolInstaller,
};
use crate::pad::Pad;
use crate::touch::Touch;
@ -102,6 +102,7 @@ impl Hardware {
AssetsPayloadSource::empty(),
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
Arc::clone(&memory_banks) as Arc<dyn SceneBankPoolInstaller>,
),
}
}

View File

@ -1,4 +1,5 @@
use prometeu_hal::glyph_bank::GlyphBank;
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::sound_bank::SoundBank;
use std::sync::{Arc, RwLock};
@ -30,6 +31,20 @@ pub trait SoundBankPoolInstaller: Send + Sync {
fn install_sound_bank(&self, slot: usize, bank: Arc<SoundBank>);
}
/// Non-generic interface for peripherals to access scene banks.
pub trait SceneBankPoolAccess: Send + Sync {
/// Returns a reference to the resident SceneBank in the specified slot, if any.
fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>>;
/// Returns the total number of slots available in this bank.
fn scene_bank_slot_count(&self) -> usize;
}
/// Non-generic interface for the AssetManager to install scene banks.
pub trait SceneBankPoolInstaller: Send + Sync {
/// Atomically swaps the resident SceneBank in the specified slot.
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>);
}
/// Centralized container for all hardware memory banks.
///
/// MemoryBanks represent the actual hardware slot state.
@ -38,6 +53,7 @@ pub trait SoundBankPoolInstaller: Send + Sync {
pub struct MemoryBanks {
glyph_bank_pool: Arc<RwLock<[Option<Arc<GlyphBank>>; 16]>>,
sound_bank_pool: Arc<RwLock<[Option<Arc<SoundBank>>; 16]>>,
scene_bank_pool: Arc<RwLock<[Option<Arc<SceneBank>>; 16]>>,
}
impl Default for MemoryBanks {
@ -52,6 +68,7 @@ impl MemoryBanks {
Self {
glyph_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
sound_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
scene_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
}
}
}
@ -95,3 +112,23 @@ impl SoundBankPoolInstaller for MemoryBanks {
}
}
}
impl SceneBankPoolAccess for MemoryBanks {
fn scene_bank_slot(&self, slot: usize) -> Option<Arc<SceneBank>> {
let pool = self.scene_bank_pool.read().unwrap();
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
}
fn scene_bank_slot_count(&self) -> usize {
16
}
}
impl SceneBankPoolInstaller for MemoryBanks {
fn install_scene_bank(&self, slot: usize, bank: Arc<SceneBank>) {
let mut pool = self.scene_bank_pool.write().unwrap();
if slot < 16 {
pool[slot] = Some(bank);
}
}
}

View File

@ -8,6 +8,7 @@ pub type AssetId = i32;
pub enum BankType {
GLYPH,
SOUNDS,
SCENE,
// TILEMAPS,
// BLOBS,
}
@ -133,4 +134,8 @@ impl SlotRef {
pub fn audio(index: usize) -> Self {
Self { asset_type: BankType::SOUNDS, index }
}
pub fn scene(index: usize) -> Self {
Self { asset_type: BankType::SCENE, index }
}
}

View File

@ -1,6 +1,5 @@
use crate::color::Color;
use crate::sprite::Sprite;
use crate::tile_layer::{HudTileLayer, ScrollableTileLayer};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
@ -48,15 +47,9 @@ pub trait GfxBridge {
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color);
fn present(&mut self);
fn render_all(&mut self);
fn render_layer(&mut self, layer_idx: usize);
fn render_hud(&mut self);
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 layer(&self, index: usize) -> &ScrollableTileLayer;
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer;
fn hud(&self) -> &HudTileLayer;
fn hud_mut(&mut self) -> &mut HudTileLayer;
fn sprite(&self, index: usize) -> &Sprite;
fn sprite_mut(&mut self, index: usize) -> &mut Sprite;

View File

@ -1,4 +1,6 @@
#[derive(Clone, Copy, Debug, Default)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Glyph {
pub glyph_id: u16,
pub palette_id: u8,

View File

@ -1,10 +1,11 @@
use crate::color::Color;
use serde::{Deserialize, Serialize};
pub const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
pub const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
/// Standard sizes for square tiles.
#[derive(Clone, Copy, Debug, PartialEq)]
#[derive(Clone, Copy, Debug, PartialEq, Deserialize, Serialize)]
pub enum TileSize {
/// 8x8 pixels.
Size8 = 8,

View File

@ -18,12 +18,14 @@ pub mod native_helpers;
pub mod native_interface;
pub mod pad_bridge;
pub mod sample;
pub mod scene_bank;
pub mod scene_layer;
pub mod sound_bank;
pub mod sprite;
pub mod syscalls;
pub mod telemetry;
pub mod tile;
pub mod tile_layer;
pub mod tilemap;
pub mod touch_bridge;
pub mod vm_fault;
pub mod window;

View File

@ -0,0 +1,51 @@
use crate::scene_layer::SceneLayer;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SceneBank {
pub layers: [SceneLayer; 4],
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::glyph_bank::TileSize;
use crate::scene_layer::MotionFactor;
use crate::tile::Tile;
use crate::tilemap::TileMap;
fn layer(glyph_bank_id: u8, motion_x: f32, motion_y: f32, glyph_id: u16) -> SceneLayer {
SceneLayer {
active: true,
glyph_bank_id,
tile_size: TileSize::Size16,
motion_factor: MotionFactor { x: motion_x, y: motion_y },
tilemap: TileMap {
width: 1,
height: 1,
tiles: vec![Tile {
active: true,
glyph: Glyph { glyph_id, palette_id: glyph_bank_id },
flip_x: false,
flip_y: false,
}],
},
}
}
#[test]
fn scene_bank_owns_exactly_four_layers() {
let scene = SceneBank {
layers: [
layer(0, 1.0, 1.0, 10),
layer(1, 0.5, 1.0, 11),
layer(2, 1.0, 0.5, 12),
layer(3, 0.25, 0.25, 13),
],
};
assert_eq!(scene.layers.len(), 4);
assert_eq!(scene.layers[3].tilemap.tiles[0].glyph.glyph_id, 13);
}
}

View File

@ -0,0 +1,60 @@
use crate::glyph_bank::TileSize;
use crate::tilemap::TileMap;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Deserialize, Serialize)]
pub struct MotionFactor {
pub x: f32,
pub y: f32,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SceneLayer {
pub active: bool,
pub glyph_bank_id: u8,
pub tile_size: TileSize,
pub motion_factor: MotionFactor,
pub tilemap: TileMap,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
use crate::tile::Tile;
#[test]
fn scene_layer_preserves_motion_factor_and_tilemap_ownership() {
let layer = SceneLayer {
active: true,
glyph_bank_id: 7,
tile_size: TileSize::Size16,
motion_factor: MotionFactor { x: 0.5, y: 0.75 },
tilemap: TileMap {
width: 2,
height: 1,
tiles: vec![
Tile {
active: true,
glyph: Glyph { glyph_id: 21, palette_id: 3 },
flip_x: false,
flip_y: false,
},
Tile {
active: false,
glyph: Glyph { glyph_id: 22, palette_id: 4 },
flip_x: true,
flip_y: false,
},
],
},
};
assert_eq!(layer.glyph_bank_id, 7);
assert_eq!(layer.motion_factor.x, 0.5);
assert_eq!(layer.motion_factor.y, 0.75);
assert_eq!(layer.tilemap.width, 2);
assert_eq!(layer.tilemap.tiles[1].glyph.glyph_id, 22);
assert!(layer.tilemap.tiles[1].flip_x);
}
}

View File

@ -1,9 +1,10 @@
use crate::glyph::Glyph;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)]
pub struct Tile {
pub glyph: Glyph,
pub active: bool,
pub glyph: Glyph,
pub flip_x: bool,
pub flip_y: bool,
}

View File

@ -1,94 +0,0 @@
use crate::glyph_bank::TileSize;
use crate::glyph_bank::TileSize::Size8;
use crate::tile::Tile;
pub struct TileMap {
pub width: usize,
pub height: usize,
pub tiles: Vec<Tile>,
}
impl TileMap {
fn create(width: usize, height: usize) -> Self {
Self { width, height, tiles: vec![Tile::default(); width * height] }
}
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
if x < self.width && y < self.height {
self.tiles[y * self.width + x] = tile;
}
}
}
pub struct TileLayer {
pub bank_id: u8,
pub tile_size: TileSize,
pub map: TileMap,
}
impl TileLayer {
fn create(width: usize, height: usize, tile_size: TileSize) -> Self {
Self { bank_id: 0, tile_size, map: TileMap::create(width, height) }
}
}
impl std::ops::Deref for TileLayer {
type Target = TileMap;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl std::ops::DerefMut for TileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
pub struct ScrollableTileLayer {
pub layer: TileLayer,
pub scroll_x: i32,
pub scroll_y: i32,
}
impl ScrollableTileLayer {
pub fn new(width: usize, height: usize, tile_size: TileSize) -> Self {
Self { layer: TileLayer::create(width, height, tile_size), scroll_x: 0, scroll_y: 0 }
}
}
impl std::ops::Deref for ScrollableTileLayer {
type Target = TileLayer;
fn deref(&self) -> &Self::Target {
&self.layer
}
}
impl std::ops::DerefMut for ScrollableTileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.layer
}
}
pub struct HudTileLayer {
pub layer: TileLayer,
}
impl HudTileLayer {
pub fn new(width: usize, height: usize) -> Self {
Self { layer: TileLayer::create(width, height, Size8) }
}
}
impl std::ops::Deref for HudTileLayer {
type Target = TileLayer;
fn deref(&self) -> &Self::Target {
&self.layer
}
}
impl std::ops::DerefMut for HudTileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.layer
}
}

View File

@ -0,0 +1,33 @@
use crate::tile::Tile;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct TileMap {
pub width: usize,
pub height: usize,
pub tiles: Vec<Tile>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::glyph::Glyph;
#[test]
fn tilemap_tile_write_and_read_remain_canonical() {
let mut map = TileMap { width: 2, height: 2, tiles: vec![Tile::default(); 4] };
let index = 3;
map.tiles[index] = Tile {
active: true,
glyph: Glyph { glyph_id: 99, palette_id: 5 },
flip_x: true,
flip_y: false,
};
assert_eq!(map.tiles[index].glyph.glyph_id, 99);
assert_eq!(map.tiles[index].glyph.palette_id, 5);
assert!(map.tiles[index].flip_x);
assert!(map.tiles[index].active);
}
}

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":25,"AGD":25,"DEC":13,"PLN":11,"LSN":30,"CLSN":1}}
{"type":"meta","next_id":{"DSC":26,"AGD":26,"DEC":14,"PLN":16,"LSN":30,"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"}]}
@ -17,6 +17,7 @@
{"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":"open","ticket":"scene-bank-and-viewport-cache-refactor","title":"Agenda - Scene Bank and Viewport Cache Refactor","created_at":"2026-04-11","updated_at":"2026-04-13","tags":["gfx","tilemap","runtime","render"],"agendas":[{"id":"AGD-0025","file":"AGD-0025-scene-bank-and-viewport-cache-refactor.md","status":"accepted","created_at":"2026-04-11","updated_at":"2026-04-13"}],"decisions":[{"id":"DEC-0013","file":"DEC-0013-scene-bank-and-viewport-cache-model.md","status":"accepted","created_at":"2026-04-13","updated_at":"2026-04-13","ref_agenda":"AGD-0025"}],"plans":[{"id":"PLN-0011","file":"PLN-0011-scene-core-types-and-bank-contract.md","status":"accepted","created_at":"2026-04-13","updated_at":"2026-04-13","ref_decisions":["DEC-0013"]},{"id":"PLN-0012","file":"PLN-0012-scene-viewport-cache-structure.md","status":"accepted","created_at":"2026-04-13","updated_at":"2026-04-13","ref_decisions":["DEC-0013"]},{"id":"PLN-0013","file":"PLN-0013-scene-viewport-resolver-and-rematerialization.md","status":"accepted","created_at":"2026-04-13","updated_at":"2026-04-13","ref_decisions":["DEC-0013"]},{"id":"PLN-0014","file":"PLN-0014-renderer-migration-to-scene-viewport-cache.md","status":"accepted","created_at":"2026-04-13","updated_at":"2026-04-13","ref_decisions":["DEC-0013"]},{"id":"PLN-0015","file":"PLN-0015-api-bank-integration-and-tests.md","status":"accepted","created_at":"2026-04-13","updated_at":"2026-04-13","ref_decisions":["DEC-0013"]}],"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"}]}

View File

@ -0,0 +1,408 @@
---
id: AGD-0025
ticket: scene-bank-and-viewport-cache-refactor
title: Agenda - Scene Bank and Viewport Cache Refactor
status: open
created: 2026-04-11
resolved:
decision:
tags: [gfx, tilemap, runtime, render]
---
# Agenda - Scene Bank and Viewport Cache Refactor
## Contexto
Hoje o runtime mistura diretamente:
- o mapa canonico da layer;
- o estado de scroll;
- e o consumo direto pelo renderer.
Na implementacao atual, os `64x64` das `ScrollableTileLayer` criadas em `Gfx::new()` sao o proprio mapa residente da layer que o renderer consulta diretamente. Nao existe hoje separacao explicita entre:
- fonte canonica do mundo carregado;
- layer canonica da scene;
- view/cache materializada para render.
Ao longo desta discussao, o foco deixou de ser ringbuffer como tema principal. A convergencia real passou a ser um refactor do sistema de tilemap para separar melhor:
- modelo canonico do mundo;
- viewport materializada;
- camera;
- composicao incremental no `back`.
Tambem ficou claro que:
- `Tile` nao carrega RGB; ele referencia glyph/palette/flags e continua relativamente leve;
- por isso, manter o tilemap canonico inteiro em memoria e defensavel no baseline atual;
- o ganho mais promissor parece estar em reduzir recomposicao bruta, nao em residentizar parcialmente o mundo logo de inicio.
## Problema
Ainda nao esta definido como reorganizar o sistema de tilemap para:
- manter um modelo canonico simples e previsivel;
- materializar apenas o necessario para render;
- permitir camera desacoplada do tilemap;
- evitar full redraw bruto sempre que a camera ou o mundo se movem;
- preparar o renderer para compor a world a partir de cache de viewport.
## Pontos Criticos
1. O `SceneBank` precisa ser a fonte da verdade.
Gameplay, fisica e futuras extensoes nao devem depender do cache de viewport.
2. O `SceneViewportCache` precisa ser operacional, nao semantico.
Ele existe para render, nao para redefinir o contrato do mundo.
3. O refactor pode quebrar o shape atual.
Nao ha necessidade de preservar `ScrollableTileLayer` nem compatibilidade com a modelagem existente.
4. A camera deve ficar desacoplada.
O bank nao deve conhecer detalhes de camera; um componente intermediario resolve camera -> viewport -> cache.
5. O custo principal a atacar continua sendo composicao.
A materializacao do cache precisa conversar com uma estrategia de redraw menos destrutiva no `back`.
## Opcoes
### Opcao A - Manter o modelo atual e otimizar o renderer em volta
- **Abordagem:** preservar `TileLayer`/`ScrollableTileLayer` como hoje e atacar apenas dirtying, blit e composicao no `back`.
- **Pros:** menor refactor estrutural.
- **Contras:** mantem o acoplamento entre mapa, scroll e renderer; pior base para evolucao arquitetural.
### Opcao B - Refatorar para `SceneBank` + `SceneViewportCache`
- **Abordagem:** separar modelo canonico da scene e cache materializado de viewport, com camera desacoplada e algoritmo proprio de rematerializacao.
- **Pros:** arquitetura mais clara; melhor separacao de responsabilidades; base melhor para dirtying e composicao incremental.
- **Contras:** refactor maior; exige redefinir tipos e fluxo do renderer.
## Sugestao / Recomendacao
A recomendacao atual e seguir com a **Opcao B**.
O refactor deve:
1. manter o mundo canonico inteiro no `SceneBank`;
2. substituir o consumo direto de layer/mapa por um `SceneViewportCache`;
3. desacoplar camera do modelo canonico;
4. preparar o renderer para compor a world a partir de cache materializado;
5. preservar sprites, HUD e fades como sistemas separados da invalidacao do cache de viewport.
## Direcao Atual Consolidada
Fica aceito, a menos de revisao explicita posterior, que:
- o runtime podera carregar ate **16** entradas em `BankType::TILEMAP`;
- cada entrada carregada representa um **`SceneBank`**;
- cada `SceneBank` contem as **4 layers** da scene;
- `background` e `parallax` passam a ser tratados dentro das proprias `SceneLayer`s, nao como tipo separado desta V1;
- cada `SceneLayer` porta seu proprio `TileMap`;
- cada `TileMap` e um conjunto de `Tile`;
- o renderer nao deve consumir o `SceneBank` diretamente;
- o renderer deve consumir um **`SceneViewportCache`** derivado do `SceneBank`;
- `ScrollableTileLayer` **morre** nesta arquitetura;
- nao ha obrigacao de compatibilidade com o shape atual.
## Modelo Alvo de V1
### Hierarquia conceitual
- `Scene` (`BankType::SCENE`)
- agregado canonico carregado
- contem as `SceneLayer`s da scene
- `SceneLayer`
- layer canonica da scene
- contem metadados da layer e seu `TileMap`
- pode representar layer normal, background ou parallax pela sua configuracao
- `TileMap`
- grade de tiles de uma layer
- `Tile`
- unidade basica de conteudo por celula
- `SceneViewportCache`
- cache/view materializada para render
- derivada da `Scene`
- contem ringbuffers internos, um por layer
### Papel de cada tipo
- `Scene`
- fonte da verdade
- usado por gameplay, fisica e materializacao
- `SceneViewportCache`
- usado pelo renderer
- sabe scroll fino em pixels
- nao e fonte da verdade do mundo
- e a fonte imediata de copia para o blit
- `SceneLayer`
- passa a carregar tambem metadados de movimento relativos ao totem mestre
- isso permite compor layers normais e parallax sob o mesmo contrato base
### O que um tile do cache pode guardar
V1 deve partir de cache leve. Alem do `Tile` cru, ele pode guardar alguns dados derivados para acelerar raster:
- `glyph_id` pronto para consulta;
- `palette_id` pronto para uso;
- flags de flip compactadas;
- marca rapida de `active/empty`;
- referencia do glyph bank ja resolvida por layer;
- metadados de dirty local do proprio cache.
V1 nao deve:
- guardar pixels RGB resolvidos por tile;
- duplicar desnecessariamente fisica no cache de render;
- virar um mini-framebuffer por tile.
### Movimento por layer
Fica aceito que cada `SceneLayer` pode carregar um fator de movimento relativo ao totem mestre.
Leitura:
- existe um **totem mestre** resolvido pelo sistema de camera/viewport;
- cada layer resolve seu proprio deslocamento efetivo a partir desse totem mestre;
- layers com fator `1.0` acompanham o mundo principal;
- layers com fator diferente de `1.0` permitem efeito de parallax;
- isso mantem `background` e `parallax` dentro do mesmo modelo estrutural de `SceneLayer`.
## Camera e Resolver
Fica aceito que:
- a camera existe em **pixel space**;
- a `Scene` nao conhece camera;
- um componente externo faz a ligacao entre camera e cache materializado;
- esse componente calcula viewport logica, totem mestre, drift, clamp e rematerializacao.
Nome conceitual provisório:
- `SceneViewportResolver`
- ou equivalente com a mesma responsabilidade.
Direcao aceita para o `SceneViewportResolver`:
- ele e responsavel pelos totens:
- totem mestre
- totens derivados por layer
- ele recebe posicao de camera de uma entidade externa;
- internamente, a posicao da camera e tratada como insumo para o totem mestre;
- ele clampa o totem mestre;
- ele decide se o cache precisa ou nao ser atualizado;
- se necessario, ele dispara atualizacao do `SceneViewportCache`;
- ele tambem deve saber instrumentalizar requests de copia por layer a partir da posicao da camera e do estado do cache;
- ele **nao executa a copia** no `back`, apenas informa o que deve ser copiado e de onde.
## Viewport, Halo e Tamanho da Janela
Dados atuais do runtime:
- resolucao interna: **320x180**
- world layers: **16x16**
- HUD: **8x8**
Para world `16x16` em `320x180`, a viewport raster minima atual equivale a:
- **21x12 tiles**
Direcao aceita para V1:
- o `SceneViewportCache` materializa mais do que a viewport raster minima;
- o tamanho alvo de V1 passa a ser **25x16 tiles**;
- isso da folga suficiente para velocidades agressivas de camera sem inflar demais o cache;
- `64x64` deixa de ser tamanho alvo e passa a ser apenas referencia historica do modelo atual.
Ordem de grandeza de memoria para `25x16`:
- `400 tiles`
- usando `36 bytes/tile` como estimativa conservadora com margem, o cache fica em ~`14.1 KiB`
## Algoritmo de Totem, Drift e Histerese
### Modelo aceito
- existe um totem mestre em `tile space`: `(i, j)`
- existe uma camera em `pixel space`: `(x, y)`
- com tile `16x16`, o centro do totem mestre em pixels e:
- `cx = 16 * i + 8`
- `cy = 16 * j + 8`
- o drift por eixo e:
- `dx = x - cx`
- `dy = y - cy`
### Histerese aceita para V1
Fica aceito como baseline:
- `safe = 12 px`
- `trigger = 20 px`
Interpretacao:
- dentro de `[-12, +12]`, nada acontece;
- entre `12` e `20`, existe tolerancia sem rematerializacao;
- ao ultrapassar `20`, o totem anda em tiles e o cache rematerializa as faixas necessarias.
### Regra por eixo
Horizontal:
- se `dx > +20`, o totem anda `+1` tile em `x`
- se `dx < -20`, o totem anda `-1` tile em `x`
Vertical:
- se `dy > +20`, o totem anda `+1` tile em `y`
- se `dy < -20`, o totem anda `-1` tile em `y`
Depois de cada movimento:
- recalcula-se o centro do totem;
- recalcula-se o drift;
- se ainda houver excesso, o processo repete.
### Motivo da histerese
Histerese fica cravada na agenda como tecnica explicita para evitar flick/thrash de borda.
Ela existe para:
- evitar vai-e-volta quando camera/player oscilam perto do limite;
- manter o `SceneViewportCache` estavel;
- disparar rematerializacao por eventos discretos, nao por ruido de borda.
### Politica de atualizacao do cache
Fica aceito que:
- a atualizacao normal do cache ocorre por **linha/coluna**;
- se houver trigger simultaneo em `x` e `y`, o algoritmo deve permitir atualizacao por **area/regiao** para evitar carregar tiles ja presentes;
- troca de `Scene` invalida o `SceneViewportCache` completamente;
- scroll/camera nao devem invalidar a janela inteira como regra normal.
### `SceneViewportCache` e ringbuffer
Leitura atual da agenda:
- **sim, o `SceneViewportCache` deve preferencialmente usar ringbuffer internamente**;
- **nao** deve virar contrato semantico do mundo nem da `Scene`;
- a principal vantagem aparece justamente no padrao aceito de atualizacao por linha/coluna e por area nos cantos;
- com layers de parallax, esse beneficio aumenta, porque cada layer pode materializar sua propria janela efetiva a partir do totem mestre sem precisar copiar janelas inteiras sempre.
Direcao aceita:
- `SceneViewportCache` usa armazenamento em anel internamente como implementacao preferida;
- existe **um ringbuffer por layer**;
- isso fica encapsulado dentro do cache;
- `Scene`, `SceneLayer` e `TileMap` continuam semanticamente simples;
- o renderer deve consumir o cache como view materializada, nao como estrutura ciclica exposta.
- na primeira leva, o blit/composite no `back` ainda pode continuar destrutivo;
- o ganho esperado do ringbuffer fica principalmente em evitar copias internas repetidas do proprio cache antes do blit final.
### Clamp de borda
Direcao atual:
- o totem e um valor de referencia em `x/y`;
- o clamp inicial pode ser pensado como:
- minimos em torno de `w/2` e `h/2`
- maximos em torno de `layer_size - (w/2, h/2)`
- nos cantos, camera e cache nao precisam coincidir de forma simetrica;
- o cache pode ficar clampado ao limite do `SceneBank`.
## Composicao no Back
Fica aceito que:
- `HUD` continua sempre por cima;
- `sprites` podem aparecer entre layers;
- a ordem observavel de composicao continua sendo algo como:
- `layer 0`
- `sprites relevantes`
- `layer 1`
- `sprites relevantes`
- `layer 2`
- `sprites relevantes`
- `layer 3`
- `sprites relevantes`
- `HUD`
Leitura atual:
- ainda pode haver redraw completo da ordem de composicao observavel;
- o ganho principal esperado vem de parar de resolver o tilemap bruto toda vez e passar a compor a partir do `SceneViewportCache`;
- `sprites`, `HUD` e `fades` ficam separados da politica de invalidacao do cache de viewport.
Invariantes aceitos:
1. `SceneViewportCache` nao e fonte da verdade de dados, mas e a fonte imediata de copia para o blit;
2. `sprites`, `HUD` e `fades` nao entram no cache;
3. a ordem observavel continua:
- `layer 0`
- sprites intermediarios
- `layer 1`
- sprites intermediarios
- `layer 2`
- sprites intermediarios
- `layer 3`
- sprites intermediarios
- `HUD`
## Open Questions Prioritarias
### 1. Shape do `SceneBank`
- **Direcao aceita:** o agregado canonico passa a se chamar **`Scene`**, com `BankType::SCENE`.
- **Direcao revisada:** `background` e `parallax` entram no mesmo contrato de `SceneLayer`, diferenciados por seus metadados e fator de movimento relativo ao totem mestre.
- **Direcao aceita:** a `Scene` deve expor leitura/escrita por tile **e** operacoes por regiao/faixa.
### 2. Shape do `SceneViewportCache`
- **Direcao aceita:** o cache e um unico agregado por scene, contendo as 4 layers internamente.
- **Direcao aceita:** entram na V1 campos derivados leves para acelerar raster, incluindo `glyph_id`, `palette_id`, flags de flip, marca de `active/empty` e referencia rapida de bank por layer.
- **Direcao aceita:** o cache usara ringbuffer internamente desde a V1 como implementacao preferida, com um ringbuffer por layer.
### 3. Resolver camera -> cache
- **Direcao aceita:** drift, trigger, clamp e posicao do totem moram no resolver.
- **Direcao aceita:** o resolver trabalha com um totem mestre e deriva deslocamentos efetivos por layer quando houver fator de movimento diferente.
- **Direcao aceita:** o cache permanece focado em armazenar a materializacao para render.
### 4. Composicao incremental
- **Leitura aceita desta agenda:** neste momento nao ha seguranca para assumir algo mais sofisticado do que composicao destrutiva total da ordem observavel no `back`.
- **Direcao atual:** o ganho esperado continua vindo de deixar de resolver o tilemap bruto toda vez, mesmo que a composicao no `back` continue destrutiva.
### 5. Camera
- **Direcao aceita:** camera completa fica fora desta decisao.
- **Direcao aceita:** esta decisao trabalha apenas com uma API que tenta mover o totem; o contrato completo de camera sera discutido separadamente.
## Criterio para Encerrar
Esta agenda pode ser encerrada quando houver alinhamento explicito sobre:
- shape minimo de `Scene`, `SceneLayer`, `TileMap`, `Tile`, `SceneViewportCache` e `SceneViewportResolver`;
- contrato do resolver camera -> viewport cache;
- politica de rematerializacao por faixa/regiao;
- relacao entre `SceneViewportCache` e composicao no `back`;
- invariantes que devem aparecer na decisao posterior para renderer, camera e scene loading.
## Estado Atual da Agenda
Leitura consolidada desta agenda neste momento:
- a direcao arquitetural principal ja esta aceita;
- o tema deixou de ser ringbuffer generico e virou refactor do sistema de tilemap/render para `Scene` + `SceneViewportCache`;
- `background` e `parallax` passam a ser absorvidos pelo proprio contrato de `SceneLayer`, via fator de movimento relativo ao totem mestre;
- `ScrollableTileLayer` deixa de existir na arquitetura alvo;
- a decisao seguinte deve cristalizar tipos, responsabilidades e invariantes, em vez de reabrir o debate macro.

View File

@ -0,0 +1,227 @@
---
id: DEC-0013
ticket: scene-bank-and-viewport-cache-refactor
title: Decision - Scene Bank and Viewport Cache Model
status: accepted
created: 2026-04-13
accepted: 2026-04-13
agenda: AGD-0025
plans: [PLN-0011, PLN-0012, PLN-0013, PLN-0014, PLN-0015]
tags: [gfx, tilemap, runtime, render]
---
## Status
Accepted.
This decision normatively locks the V1 model for scene-backed tilemap rendering around:
- `Scene` as canonical loaded state;
- `SceneLayer` as the canonical layer unit;
- `SceneViewportCache` as the materialized render cache;
- `SceneViewportResolver` as the owner of totems, drift, hysteresis, clamp, and cache update decisions.
## Contexto
The current runtime couples three concerns too tightly:
- canonical layer data;
- scroll state;
- direct renderer consumption.
In the current implementation, the `64x64` maps created for scrollable layers are not an independent viewport cache. They are the actual layer maps consumed directly by the renderer. That shape makes it harder to:
- keep canonical world state simple;
- materialize only the viewport-relevant subset for render;
- evolve camera separately from the tilemap model;
- reduce repeated tilemap resolution work before the final destructive blit into `back`.
During agenda `AGD-0025`, the discussion initially explored ringbuffer as a general topic, but converged on a different architectural target: a clean split between canonical scene state and render-oriented viewport materialization.
## Decisao
The runtime SHALL adopt the following V1 model:
1. Canonical loaded tilemap state SHALL be represented by `Scene`.
2. A `Scene` SHALL contain four canonical `SceneLayer`s.
3. Each `SceneLayer` SHALL own its canonical `TileMap`.
4. Each `TileMap` SHALL remain the canonical grid of `Tile`s for that layer.
5. The renderer MUST NOT consume canonical `Scene` data directly for normal world composition.
6. The renderer SHALL consume a `SceneViewportCache` derived from `Scene`.
7. `SceneViewportCache` SHALL be operational render state only and MUST NOT become the semantic source of truth for gameplay or physics.
8. `SceneViewportCache` SHOULD use internal ringbuffer storage as the preferred V1 implementation strategy.
9. Ringbuffer details MUST remain encapsulated inside `SceneViewportCache` and MUST NOT leak into the semantic contract of `Scene`, `SceneLayer`, `TileMap`, or `Tile`.
10. `ScrollableTileLayer` SHALL be removed from the architecture and MUST NOT be preserved as a canonical runtime concept.
11. `background` and `parallax` SHALL remain expressible through `SceneLayer` itself, not through a separate V1 background type.
12. Each `SceneLayer` SHALL carry a relative movement factor against a master totem, allowing normal layers, background-like layers, and parallax-like layers under the same layer contract.
## Rationale
This decision prefers architectural clarity over preserving the current type graph.
The key arguments are:
- `Tile` remains light enough that keeping canonical tilemaps resident is acceptable in the current baseline.
- The more pressing cost appears to be repeated world resolution and composition work, not merely the existence of tilemap data in memory.
- A dedicated viewport cache gives the renderer a better contract without forcing gameplay and physics to depend on residency mechanics.
- Internal ringbuffering in the cache is useful because the first expected gain is avoiding repeated cache-side copies, even if final composition into `back` remains destructive in V1.
- Keeping parallax inside `SceneLayer` avoids prematurely splitting the scene model into separate layer families while still allowing differentiated movement behavior.
This model also keeps room for later refinement:
- richer camera semantics can evolve separately;
- background-specific types can still be introduced later if the layer contract becomes insufficient;
- bank-type wiring can still be finalized during planning/implementation;
- cache implementation can improve while the canonical scene contract remains stable.
## Invariantes / Contrato
### 1. Canonical Scene Model
- `Scene` SHALL be the canonical loaded aggregate.
- `Scene` SHALL contain four canonical `SceneLayer`s.
- `SceneLayer` SHALL be the canonical replacement for the old `TileLayer`.
- `SceneLayer` SHALL minimally include:
- `glyph_bank_id`
- `tile_size`
- `active`
- `totem_factor (x, y)`
- `tilemap`
- `TileMap` SHALL remain a canonical grid of `Tile`.
- `Tile` SHALL remain lightweight and MUST NOT be expanded into resolved RGB pixel payload for canonical storage.
### 2. Viewport Cache Model
- `SceneViewportCache` SHALL be a single cache aggregate for one `Scene`.
- It SHALL contain four internal layer caches, one per canonical scene layer.
- The preferred V1 implementation is one internal ringbuffer per layer cache.
- Ringbuffer details MUST stay internal to the cache implementation.
- `SceneViewportCache` MAY store lightweight derived fields to accelerate raster, including:
- resolved `glyph_id`
- resolved `palette_id`
- packed flip flags
- `active/empty` markers
- fast layer-local glyph bank references
- `SceneViewportCache` MUST NOT duplicate full physics state unless a later decision explicitly requires it.
- `SceneViewportCache` SHALL be the immediate source of copy data for world blits, but MUST NOT become the source of truth for world semantics.
### 3. Resolver Contract
- `SceneViewportResolver` SHALL own:
- master totem
- per-layer derived totems
- drift calculation
- hysteresis
- clamp logic
- cache update decisions
- An external caller SHALL provide camera position to the resolver.
- The resolver SHALL treat that camera position as input for the master totem flow.
- The resolver SHALL decide whether cache updates are needed.
- The resolver SHALL trigger cache update operations when needed.
- The resolver SHALL know how to instrument per-layer copy requests from the cache to the final compositor.
- The resolver MUST NOT perform the actual framebuffer copy itself.
### 4. Viewport Size and Cache Update Policy
- V1 world viewport cache size SHALL be materially larger than the minimum visible world tile window and MUST include explicit halo for cache stability.
- An initial working target in the order of `25x16` tiles is accepted as planning guidance, but exact numeric sizing MAY be finalized in the implementation plan.
- Normal cache update SHALL occur by line and/or column.
- When simultaneous X and Y movement requires corner refresh, the cache MUST support area/region refresh to avoid reloading tiles already present.
- Swapping to a different `Scene` SHALL fully invalidate `SceneViewportCache`.
- Normal camera motion MUST NOT invalidate the whole viewport cache as the default path.
### 5. Totem, Drift, and Hysteresis
- The resolver SHALL use a master totem in tile space.
- Camera input SHALL remain in pixel space.
- For `16x16` tiles, the master totem center SHALL be computed as:
- `cx = 16 * i + 8`
- `cy = 16 * j + 8`
- Drift SHALL be computed as:
- `dx = x - cx`
- `dy = y - cy`
- V1 SHALL use hysteresis with:
- an internal safe band where no cache movement occurs;
- an external trigger band that advances the totem and requests cache refresh.
- Initial working values in the order of `safe = 12 px` and `trigger = 20 px` are accepted as planning guidance, but exact numeric tuning MAY be finalized in the implementation plan.
- Inside the safe band, no cache movement SHALL occur.
- Between safe and trigger, the system SHALL tolerate drift without rematerialization.
- Beyond trigger, the resolver SHALL move the totem discretely in tile steps and request cache refresh.
- Hysteresis is mandatory in V1 to prevent edge flick/thrash.
### 6. Clamp Behavior
- The resolver SHALL clamp the master totem against scene bounds.
- Initial clamp reasoning SHALL be based on minimums around `(w/2, h/2)` and maximums around `layer_size - (w/2, h/2)`.
- Near bounds, the cache MAY remain asymmetrically aligned relative to camera expectations.
- That asymmetry at scene edges SHALL be considered expected behavior, not an error.
### 7. Composition Contract
- `HUD` SHALL remain above world composition.
- `sprites` MAY appear between canonical world layers.
- The observable composition order SHALL remain:
- `layer 0`
- intermediate sprites
- `layer 1`
- intermediate sprites
- `layer 2`
- intermediate sprites
- `layer 3`
- intermediate sprites
- `HUD`
- `sprites`, `HUD`, and `fades` MUST remain outside the invalidation contract of `SceneViewportCache`.
- V1 MAY still use destructive full composition ordering in `back`.
- The expected win in V1 comes from avoiding repeated brute-force canonical tilemap resolution before that final destructive composition.
## Impactos
This decision impacts the runtime model directly:
- `TileLayer` will be replaced by `SceneLayer`.
- `ScrollableTileLayer` will be removed.
- bank integration and naming at the asset-bank enum level SHALL be aligned during planning/implementation.
- The renderer contract will shift from direct map consumption to cache consumption.
- Scene loading, viewport cache maintenance, and composition responsibilities become explicitly separated.
Expected propagation areas:
- bank/domain model types;
- tilemap/layer runtime structures;
- renderer world composition flow;
- camera-to-render adapter logic;
- future plan/implementation artifacts derived from this decision.
## Referencias
- Agenda: [AGD-0025-scene-bank-and-viewport-cache-refactor.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0025-scene-bank-and-viewport-cache-refactor.md)
- Current tile/layer model:
- [tile_layer.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/tile_layer.rs:1)
- [tile.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/tile.rs:1)
- [glyph.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/glyph.rs:1)
- Current renderer usage:
- [gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs:291)
- [gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs:594)
- [gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs:673)
## Propagacao Necessaria
The following work MUST derive from this decision:
1. Introduce the new canonical runtime types:
- `Scene`
- `SceneLayer`
- `SceneViewportCache`
- `SceneViewportResolver`
2. Remove or retire `ScrollableTileLayer`.
3. Migrate renderer world composition to consume `SceneViewportCache`.
4. Define the concrete cache update API for:
- line
- column
- area/region
5. Define the concrete blit instrumentation API emitted by the resolver.
6. Write an implementation plan before code changes.
## Revision Log
- 2026-04-13: Initial accepted decision from AGD-0025.

View File

@ -0,0 +1,152 @@
---
id: PLN-0011
ticket: scene-bank-and-viewport-cache-refactor
title: Plan - Scene Core Types and Bank Contract
status: accepted
created: 2026-04-13
completed:
tags: [gfx, tilemap, runtime, render]
---
## Objective
Introduce the canonical type model required by `DEC-0013`: `SceneBank`, `SceneLayer`, `TileMap`, and `Tile`, plus the asset-bank/domain contract changes needed to carry scene-backed tilemap data as the new source of truth.
## Background
`DEC-0013` locks the canonical runtime model around `SceneBank` and `SceneLayer`, replacing the old `TileLayer`/`ScrollableTileLayer` coupling. This plan isolates the domain-model and bank-contract refactor before cache or renderer migration begins.
## Scope
### Included
- Introduce `SceneBank` and `SceneLayer` in the HAL/runtime model.
- Preserve `TileMap` and `Tile` as canonical grid/unit concepts.
- Define the minimum `SceneLayer` fields required by the decision.
- Update bank/domain enums and payload contracts so scenes can exist as first-class banked content.
- Add compile-time migration shims only when strictly needed to keep the tree buildable during the refactor.
### Excluded
- `SceneViewportCache`
- `SceneViewportResolver`
- renderer migration to cache consumption
- rematerialization algorithm
- final composition changes
- final binary `SCENE` payload format and decoder implementation
## Execution Steps
### Step 1 - Introduce canonical scene types in HAL
**What:**
Add `SceneBank` and `SceneLayer` to the HAL model and define the minimum canonical fields required by the decision.
**How:**
- Replace or retire the old `TileLayer`-centric model in [tile_layer.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/tile_layer.rs).
- Keep `TileMap` as the canonical grid owner and `Tile` as the canonical cell unit.
- Define `SceneLayer` with:
- `glyph_bank_id`
- `tile_size`
- `active`
- `motion_factor`
- `tilemap`
- Define `SceneBank` as the aggregate of exactly four `SceneLayer`s using a fixed array.
**File(s):**
- [crates/console/prometeu-hal/src/tile_layer.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/tile_layer.rs)
- [crates/console/prometeu-hal/src/tile.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/tile.rs)
- [crates/console/prometeu-hal/src/glyph.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/glyph.rs)
- HAL module export files as needed
### Step 2 - Remove `ScrollableTileLayer` from the canonical model
**What:**
Delete the canonical role of `ScrollableTileLayer` and any direct ownership path that makes scroll part of the canonical scene representation.
**How:**
- Remove `ScrollableTileLayer` and `HudTileLayer` from the HAL tile-layer model or reduce them to non-canonical transitional wrappers only if needed to keep compilation moving during the refactor.
- Ensure canonical scene state no longer encodes direct renderer scroll ownership.
**File(s):**
- [crates/console/prometeu-hal/src/tile_layer.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/tile_layer.rs)
- [crates/console/prometeu-hal/src/gfx_bridge.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/gfx_bridge.rs)
### Step 3 - Introduce the bank/domain contract for scene-backed content
**What:**
Add or revise the asset-bank/domain model so the canonical loaded scene can be represented in the bank contract.
**How:**
- Update `BankType` and related asset/bank metadata in HAL to support `Scene`-backed content at the domain level.
- Adjust slot/reference types where necessary so scene content can be addressed consistently with existing banks.
- Keep the exact asset-bank enum wiring minimal and domain-first; do not conflate it with renderer cache details.
**File(s):**
- [crates/console/prometeu-hal/src/asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/asset.rs)
- [crates/console/prometeu-hal/src/syscalls/domains/bank.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/syscalls/domains/bank.rs)
- [crates/console/prometeu-hal/src/syscalls/domains/asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/syscalls/domains/asset.rs)
- Any bank-facing shared types used by runtime and drivers
### Step 4 - Thread the new scene types through driver-facing runtime structures
**What:**
Prepare the driver layer to reference canonical `SceneBank` types instead of old layer/map ownership.
**How:**
- Identify the driver-side structs currently owning the old layer model.
- Replace those type references with the new canonical scene types without yet switching renderer logic to the viewport cache.
- Keep this step focused on compile-safe type replacement, not behavior changes.
**File(s):**
- [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs)
- [crates/console/prometeu-drivers/src/hardware.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/hardware.rs)
### Step 5 - Add baseline tests for the canonical scene model
**What:**
Protect the new canonical scene types with focused tests before cache and renderer work begins.
**How:**
- Add unit tests for:
- scene-bank construction
- layer ownership of tilemaps
- tile write/read behavior
- preservation of `motion_factor`
- Keep tests domain-focused and independent of viewport caching.
**File(s):**
- HAL tests colocated with scene/tile modules
- Driver tests only if required to preserve build integrity
## Test Requirements
### Unit Tests
- `SceneBank` owns exactly four canonical `SceneLayer`s.
- `SceneLayer` exposes the required minimal fields.
- `TileMap` indexing and mutation still behave canonically.
- `motion_factor` survives construction and mutation paths.
### Integration Tests
- Asset/bank domain types accept the new scene-backed contract without breaking existing glyph/sound usage.
- `SCENE` binary decode remains intentionally open and out of scope for this plan.
### Manual Verification
- Build the console crates and verify no canonical runtime path still treats `ScrollableTileLayer` as the source of truth.
## Acceptance Criteria
- [ ] `SceneBank` and `SceneLayer` exist as canonical runtime types.
- [ ] `ScrollableTileLayer` is no longer the canonical scene model.
- [ ] Bank/domain types can represent scene-backed content.
- [ ] Driver-facing runtime structures compile against the new canonical scene model.
- [ ] Baseline tests protect the new type graph.
## Dependencies
- Source decision: `DEC-0013`
- No dependency on later plans; this is the foundation for the rest of the family.
## Risks
- Breaking too many public or cross-crate type references at once can stall compilation.
- Bank enum changes can ripple into syscalls and telemetry if not scoped carefully.
- Transitional wrappers can accidentally survive longer than intended if not explicitly retired in later plans.

View File

@ -0,0 +1,147 @@
---
id: PLN-0012
ticket: scene-bank-and-viewport-cache-refactor
title: Plan - Scene Viewport Cache Structure
status: accepted
created: 2026-04-13
completed:
tags: [gfx, tilemap, runtime, render]
---
## Objective
Implement the `SceneViewportCache` as the operational render cache for one `Scene`, including one internal ringbuffer per layer and lightweight derived cache entries for raster acceleration.
## Background
`DEC-0013` locks `SceneViewportCache` as the renderer-facing view of scene data and prefers internal ringbuffer storage per layer. This plan isolates the cache data structure and update API before resolver and renderer integration.
## Scope
### Included
- Define `SceneViewportCache`.
- Define the per-layer internal ringbuffer structure.
- Define the cached tile entry format.
- Define cache update APIs for line, column, and area/region.
- Define full invalidation on scene swap.
### Excluded
- camera logic
- drift/hysteresis logic
- final renderer migration
- execution of blits into `back`
## Execution Steps
### Step 1 - Define cache entry and layer-cache structures
**What:**
Create the internal data model for one viewport cache layer and for cached tile entries.
**How:**
- Introduce a cache tile entry type that stores:
- resolved `glyph_id`
- resolved `palette_id`
- packed flip flags
- `active/empty`
- fast layer-local glyph bank reference/index
- Introduce one ringbuffer-backed layer-cache structure per scene layer.
- Keep ringbuffer internals encapsulated and out of public semantic APIs.
**File(s):**
- New HAL or driver-side viewport-cache module(s), likely under:
- `crates/console/prometeu-hal/src/`
- and/or `crates/console/prometeu-drivers/src/`
### Step 2 - Define `SceneViewportCache` as a four-layer aggregate
**What:**
Create the top-level cache aggregate for one `Scene`.
**How:**
- Represent one cache aggregate containing four internal layer caches.
- Thread through cache dimensions and tile-size assumptions for V1.
- Make the cache explicitly scene-derived and non-canonical.
**File(s):**
- New cache module(s)
- Any related exports in HAL/driver public surfaces
### Step 3 - Implement cache mutation APIs
**What:**
Define the update surface that later plans will use for rematerialization.
**How:**
- Add explicit APIs for:
- refresh line
- refresh column
- refresh area/region
- invalidate whole cache on scene swap
- Ensure corner refresh can use region updates without duplicate work on already-present tiles.
**File(s):**
- New cache module(s)
- Any helper modules shared with resolver integration
### Step 4 - Implement scene-to-cache materialization helpers
**What:**
Add helpers that copy canonical scene data into cache entries.
**How:**
- Build helpers that read canonical `SceneLayer`/`TileMap` data and populate cache entries.
- Keep the helpers unaware of camera policy; they should only perform requested materialization work.
- Ensure per-layer movement factors remain metadata of the scene layer, not cache-only state.
**File(s):**
- New cache/materialization helper module(s)
### Step 5 - Add focused tests for cache structure and update rules
**What:**
Protect the cache shape and update operations before wiring the resolver.
**How:**
- Add tests for:
- ringbuffer wrap behavior
- line refresh
- column refresh
- region refresh
- scene-swap invalidation
- no duplicate reload for corner-style region updates
**File(s):**
- Tests colocated with cache modules
## Test Requirements
### Unit Tests
- Each cache layer maintains ringbuffer invariants under wrap.
- Cache entry fields match the expected derived values from canonical scene tiles.
- Line/column/region refresh APIs only rewrite the requested area.
### Integration Tests
- Materialization from `Scene` into `SceneViewportCache` succeeds across all four layers.
### Manual Verification
- Inspect debug output or temporary probes to confirm cache updates do not expose ringbuffer details outside the cache boundary.
## Acceptance Criteria
- [ ] `SceneViewportCache` exists as a four-layer aggregate.
- [ ] Each layer cache uses internal ringbuffer storage.
- [ ] Cache entries store the lightweight derived raster fields defined by the decision.
- [ ] Explicit APIs exist for line, column, area/region, and full invalidation.
- [ ] Tests cover wrap and non-duplicative corner updates.
## Dependencies
- Depends on `PLN-0011` for canonical `Scene` / `SceneLayer` / `TileMap` types.
- Source decision: `DEC-0013`
## Risks
- Over-designing cache entry shape can accidentally turn the cache into a second heavy scene representation.
- Ringbuffer implementation bugs can stay hidden until resolver integration if tests are too weak.
- Region refresh semantics can become ambiguous if API boundaries are not explicit early.

View File

@ -0,0 +1,161 @@
---
id: PLN-0013
ticket: scene-bank-and-viewport-cache-refactor
title: Plan - Scene Viewport Resolver and Rematerialization
status: accepted
created: 2026-04-13
completed:
tags: [gfx, tilemap, runtime, render]
---
## Objective
Implement `SceneViewportResolver` as the owner of master totem, per-layer totems, drift, hysteresis, clamp, cache update decisions, and per-layer copy instrumentation metadata.
## Background
`DEC-0013` makes the resolver the owner of movement policy between camera input and viewport cache updates. This plan isolates the decision logic before final renderer migration.
## Scope
### Included
- Define `SceneViewportResolver`.
- Implement master totem and per-layer derived totems.
- Implement drift and hysteresis.
- Implement clamp behavior.
- Implement cache-update triggering and copy instrumentation metadata.
### Excluded
- actual framebuffer copy execution
- sprite/HUD/fade composition logic
- broad renderer migration
## Execution Steps
### Step 1 - Define resolver state and inputs
**What:**
Create the resolver state model and its public input surface.
**How:**
- Define resolver state to own:
- master totem
- per-layer derived totems
- viewport dimensions
- hysteresis thresholds
- clamp-relevant scene bounds
- Define the external entry point that accepts camera position input.
**File(s):**
- New resolver module(s), likely under HAL or drivers depending on final ownership
### Step 2 - Implement master-to-layer totem derivation
**What:**
Make layer motion derive from the master totem using each layers `totem_factor`.
**How:**
- Compute per-layer effective motion from the master totem.
- Preserve support for normal layers (`1.0`) and parallax/background-like layers (`!= 1.0`).
- Keep the derivation explicit and testable per axis.
**File(s):**
- Resolver module(s)
- Any math/helper module(s) needed for factor handling
### Step 3 - Implement drift, hysteresis, and clamp
**What:**
Translate camera motion into discrete totem advancement safely.
**How:**
- Implement drift calculation in pixel space against totem centers in tile space.
- Implement hysteresis with:
- internal safe band
- external trigger band
- Implement clamp against scene bounds.
- Ensure edge behavior is explicitly asymmetric when clamped.
**File(s):**
- Resolver module(s)
### Step 4 - Connect resolver decisions to cache update requests
**What:**
Turn resolver state changes into concrete cache update requests.
**How:**
- Emit requests for:
- line refresh
- column refresh
- area/region refresh for corner updates
- Ensure no duplicate work is scheduled for already-covered cache content.
- Keep the resolver in charge of “what to refresh,” not “how cache storage performs it.”
**File(s):**
- Resolver module(s)
- Shared request/command structs between resolver and cache
### Step 5 - Add copy instrumentation outputs for later renderer use
**What:**
Make the resolver capable of describing which cache slice/region should be copied for each layer.
**How:**
- Define a per-layer copy request/instrumentation type.
- Include enough information for a later compositor to copy from cache into `back` without re-deciding viewport math.
- Do not execute the copy here.
**File(s):**
- Resolver module(s)
- Shared copy-request types
### Step 6 - Add focused resolver tests
**What:**
Protect resolver correctness before renderer integration.
**How:**
- Add tests for:
- master totem updates
- per-layer totem factor derivation
- hysteresis stability
- repeated high-speed movement
- clamp at scene edges
- corner-trigger conversion into region refresh requests
**File(s):**
- Tests colocated with resolver modules
## Test Requirements
### Unit Tests
- Hysteresis prevents edge flick/thrash.
- Totem movement occurs discretely in tile steps.
- Per-layer totems follow the master according to `totem_factor`.
- Clamp behavior is correct near scene edges.
### Integration Tests
- Resolver outputs valid update requests consumable by `SceneViewportCache`.
### Manual Verification
- Instrument logs or debug traces to confirm no unnecessary refresh churn during back-and-forth movement near edges.
## Acceptance Criteria
- [ ] `SceneViewportResolver` exists with the full state required by `DEC-0013`.
- [ ] Master and per-layer totems are derived correctly.
- [ ] Hysteresis and clamp are implemented and tested.
- [ ] Cache refresh requests are emitted by line, column, and region as required.
- [ ] Copy instrumentation metadata is available for later renderer use.
## Dependencies
- Depends on `PLN-0011` and `PLN-0012`.
- Source decision: `DEC-0013`
## Risks
- Resolver logic can become hard to reason about if movement, clamp, and copy instrumentation are not clearly separated.
- Parallax factor derivation can introduce subtle off-by-one or drift mismatch issues per layer.
- Region-update scheduling can duplicate work if X/Y corner movement is not normalized carefully.

View File

@ -0,0 +1,142 @@
---
id: PLN-0014
ticket: scene-bank-and-viewport-cache-refactor
title: Plan - Renderer Migration to Scene Viewport Cache
status: accepted
created: 2026-04-13
completed:
tags: [gfx, tilemap, runtime, render]
---
## Objective
Migrate world rendering from direct canonical map consumption to `SceneViewportCache` consumption while preserving the accepted observable composition order.
## Background
`DEC-0013` makes `SceneViewportCache` the immediate source of copy data for world blits and preserves the visible order of world layers, interleaved sprites, and HUD. This plan focuses on the renderer migration itself.
## Scope
### Included
- Remove direct world-layer map reads from the renderer hot path.
- Make world-layer composition consume cache-backed copy requests.
- Preserve sprite interleaving and HUD ordering.
- Keep V1 destructive `back` composition acceptable.
### Excluded
- scene domain model refactor
- cache structural implementation
- resolver movement logic
- non-world rendering systems beyond required integration points
## Execution Steps
### Step 1 - Identify and isolate direct canonical map consumption in the renderer
**What:**
Locate current renderer paths that read canonical layer maps directly.
**How:**
- Replace direct `TileMap` reads in [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs) with abstraction points prepared to accept cache-sourced copy requests.
- Keep the migration incremental enough to preserve buildability.
**File(s):**
- [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs)
### Step 2 - Introduce world-layer copy paths driven by cache requests
**What:**
Teach the renderer to copy world content from `SceneViewportCache`.
**How:**
- Add renderer-facing entry points that consume per-layer copy instrumentation emitted by the resolver.
- Ensure the renderer treats the cache as the immediate world source for blit operations.
- Do not let the renderer re-own totem/drift/clamp policy.
**File(s):**
- [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs)
- Supporting cache/resolver integration modules
### Step 3 - Preserve accepted composition order
**What:**
Keep the visible ordering of layers, sprites, and HUD intact during migration.
**How:**
- Preserve composition order:
- world layer 0
- sprites
- world layer 1
- sprites
- world layer 2
- sprites
- world layer 3
- sprites
- HUD
- Keep `HUD`, sprites, and fades outside the viewport-cache invalidation model.
**File(s):**
- [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs)
### Step 4 - Preserve destructive `back` composition for V1
**What:**
Accept destructive composition while still benefiting from cache-backed world inputs.
**How:**
- Keep the first migrated renderer implementation destructive in `back`.
- Ensure the code path clearly separates:
- canonical scene data
- viewport cache data
- final composition buffer
- Avoid mixing “cache update” with “final buffer copy” concerns in the renderer.
**File(s):**
- [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs)
### Step 5 - Add renderer regression coverage
**What:**
Protect the migrated composition order and world rendering behavior.
**How:**
- Add tests or golden-style checks for:
- world layers rendered from cache, not canonical maps
- visible ordering of sprites between layers
- HUD remaining on top
- Add probes or assertions preventing accidental fallback to direct canonical map reads.
**File(s):**
- Renderer tests in [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs) and adjacent test modules
## Test Requirements
### Unit Tests
- Cache-driven world copy code produces the same visible layer ordering as the prior renderer.
### Integration Tests
- Full `render_all()` path uses `SceneViewportCache` for world layers.
- Sprites/HUD/fades remain correctly ordered relative to world layers.
### Manual Verification
- Render representative scenes with multiple active layers and sprites to confirm no visible ordering regressions.
## Acceptance Criteria
- [ ] Renderer world composition no longer reads canonical scene maps directly in the hot path.
- [ ] World layers are copied from `SceneViewportCache`.
- [ ] Sprite interleaving and HUD ordering remain correct.
- [ ] V1 destructive composition still works end-to-end.
- [ ] Regression coverage protects the migrated path.
## Dependencies
- Depends on `PLN-0011`, `PLN-0012`, and `PLN-0013`.
- Source decision: `DEC-0013`
## Risks
- Renderer migration can accidentally duplicate world-copy work if cache and compositor responsibilities blur.
- Sprite ordering regressions are easy to introduce during world-layer refactoring.
- Temporary fallback paths can linger unless tests explicitly block them.

View File

@ -0,0 +1,141 @@
---
id: PLN-0015
ticket: scene-bank-and-viewport-cache-refactor
title: Plan - API, Bank Integration, and Tests
status: accepted
created: 2026-04-13
completed:
tags: [gfx, tilemap, runtime, render]
---
## Objective
Finish the scene/viewport-cache migration by aligning exposed APIs, bank-loading integration, and test coverage across HAL, drivers, and system entry points.
## Background
After core types, cache, resolver, and renderer migration exist, the remaining work is to align the surrounding surfaces so the new architecture is usable as the runtimes operational model.
## Scope
### Included
- Update bridge APIs and exposed runtime surfaces.
- Align bank-loading/integration paths with scene-backed content.
- Remove stale tile-layer-era interfaces.
- Add cross-layer regression coverage.
### Excluded
- additional architectural redesign
- feature work beyond the accepted decision
## Execution Steps
### Step 1 - Update HAL and bridge surfaces
**What:**
Remove or replace public APIs still shaped around the old scrollable tile-layer model.
**How:**
- Update `GfxBridge` and adjacent bridge traits so they no longer expose obsolete canonical layer ownership assumptions.
- Introduce scene/cache/resolver-oriented access only where required by runtime consumers.
**File(s):**
- [crates/console/prometeu-hal/src/gfx_bridge.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/gfx_bridge.rs)
- Other bridge traits affected by scene model changes
### Step 2 - Align driver/hardware construction paths
**What:**
Ensure hardware and driver initialization paths can construct and own the new scene/cache/resolver model.
**How:**
- Update hardware bootstrap and driver ownership paths to instantiate the canonical scene and viewport-cache stack.
- Remove any remaining ownership assumptions tied to `ScrollableTileLayer`.
**File(s):**
- [crates/console/prometeu-drivers/src/hardware.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/hardware.rs)
- [crates/console/prometeu-drivers/src/gfx.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/gfx.rs)
### Step 3 - Align asset/bank integration paths
**What:**
Make sure bank-loading and asset integration paths understand the scene-backed model at the operational layer.
**How:**
- Update asset-side or loader-side integration points so scene-backed content can be staged, installed, and referenced coherently.
- Keep the work scoped to the bank-loading path; do not reopen the canonical model here.
**File(s):**
- [crates/console/prometeu-drivers/src/asset.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/asset.rs)
- [crates/console/prometeu-drivers/src/memory_banks.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-drivers/src/memory_banks.rs)
- [crates/console/prometeu-hal/src/cartridge_loader.rs](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/crates/console/prometeu-hal/src/cartridge_loader.rs)
- Related asset/bank shared types
### Step 4 - Remove stale type and API remnants
**What:**
Clean out old interfaces that would leave the codebase in a dual-model state.
**How:**
- Remove obsolete references to:
- `TileLayer`
- `ScrollableTileLayer`
- direct old-layer scroll ownership APIs
- Keep only the canonical scene model and the viewport-cache/render pipeline required by the decision.
**File(s):**
- HAL and driver modules touched by earlier plans
### Step 5 - Add full-stack regression coverage
**What:**
Add the minimum test family needed to execute the migration safely one plan at a time.
**How:**
- Add tests for:
- canonical scene ownership
- viewport-cache update behavior
- resolver drift/hysteresis
- renderer composition order
- scene swap invalidation
- absence of stale old-layer APIs
- Add at least one integration path that exercises:
- scene load
- cache population
- resolver update
- renderer world composition
**File(s):**
- Test modules across HAL, drivers, and system crates as needed
## Test Requirements
### Unit Tests
- Public/runtime-facing APIs no longer depend on `ScrollableTileLayer`.
- Cache, resolver, and renderer modules remain individually covered.
### Integration Tests
- End-to-end scene load -> cache update -> renderer composition path succeeds.
- Scene swap invalidates cache and repopulates correctly.
### Manual Verification
- Build the runtime and exercise representative world scenes to confirm no stale assumptions remain in construction or render flow.
## Acceptance Criteria
- [ ] Bridge and runtime-facing APIs align with the new scene/cache model.
- [ ] Hardware/driver construction paths instantiate the new architecture correctly.
- [ ] Asset/bank integration paths accept the scene-backed model.
- [ ] Stale old-layer APIs are removed.
- [ ] Full-stack regression coverage exists for the migration.
## Dependencies
- Depends on `PLN-0011`, `PLN-0012`, `PLN-0013`, and `PLN-0014`.
- Source decision: `DEC-0013`
## Risks
- API cleanup is where hidden dependencies on the old model are most likely to surface.
- Asset/bank integration can expand in scope if loader assumptions are too tightly coupled to current bank enums.
- If stale APIs are not removed aggressively, the codebase can get stuck in a fragile dual-model transition.