diff --git a/Cargo.lock b/Cargo.lock index ada203b3..e3fbe577 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1475,6 +1475,8 @@ version = "0.1.0" dependencies = [ "anyhow", "prometeu-bytecode", + "prometeu-hal", + "serde_json", ] [[package]] diff --git a/crates/tools/pbxgen-stress/Cargo.toml b/crates/tools/pbxgen-stress/Cargo.toml index b2ce9839..31da4f74 100644 --- a/crates/tools/pbxgen-stress/Cargo.toml +++ b/crates/tools/pbxgen-stress/Cargo.toml @@ -5,4 +5,6 @@ edition = "2021" [dependencies] prometeu-bytecode = { path = "../../console/prometeu-bytecode" } +prometeu-hal = { path = "../../console/prometeu-hal" } anyhow = "1" +serde_json = "1" diff --git a/crates/tools/pbxgen-stress/src/lib.rs b/crates/tools/pbxgen-stress/src/lib.rs index d8137e23..2600c12d 100644 --- a/crates/tools/pbxgen-stress/src/lib.rs +++ b/crates/tools/pbxgen-stress/src/lib.rs @@ -3,7 +3,25 @@ use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{ BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl, }; +use prometeu_hal::asset::{ + AssetCodec, AssetEntry, BankType, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1, + SCENE_LAYER_COUNT_V1, SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1, +}; +use prometeu_hal::cartridge::{ + AssetsPackHeader, AssetsPackPrelude, ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, + ASSETS_PA_SCHEMA_VERSION, +}; +use prometeu_hal::color::Color; +use prometeu_hal::glyph::Glyph; +use prometeu_hal::glyph_bank::{ + TileSize, GLYPH_BANK_COLORS_PER_PALETTE, GLYPH_BANK_PALETTE_COUNT_V1, +}; +use prometeu_hal::scene_bank::SceneBank; +use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; +use prometeu_hal::tile::Tile; +use prometeu_hal::tilemap::TileMap; use std::fs; +use std::mem::size_of; use std::path::PathBuf; fn asm(s: &str) -> Vec { @@ -20,13 +38,6 @@ pub fn generate() -> Result<()> { arg_slots: 1, ret_slots: 0, }, - SyscallDecl { - module: "gfx".into(), - name: "draw_disc".into(), - version: 1, - arg_slots: 5, - ret_slots: 0, - }, SyscallDecl { module: "gfx".into(), name: "draw_text".into(), @@ -41,6 +52,20 @@ pub fn generate() -> Result<()> { arg_slots: 2, ret_slots: 0, }, + SyscallDecl { + module: "composer".into(), + name: "bind_scene".into(), + version: 1, + arg_slots: 1, + ret_slots: 1, + }, + SyscallDecl { + module: "composer".into(), + name: "set_camera".into(), + version: 1, + arg_slots: 2, + ret_slots: 0, + }, SyscallDecl { module: "composer".into(), name: "emit_sprite".into(), @@ -59,7 +84,7 @@ pub fn generate() -> Result<()> { param_slots: 0, local_slots: 2, return_slots: 0, - max_stack_slots: 16, + max_stack_slots: 32, }]; let module = BytecodeModule { @@ -67,7 +92,6 @@ pub fn generate() -> Result<()> { const_pool: vec![ ConstantPoolEntry::String("stress".into()), ConstantPoolEntry::String("frame".into()), - ConstantPoolEntry::String("missing_glyph_bank".into()), ], functions, code: rom, @@ -89,129 +113,326 @@ pub fn generate() -> Result<()> { out_dir.push("stress-console"); fs::create_dir_all(&out_dir)?; fs::write(out_dir.join("program.pbx"), bytes)?; - let assets_pa_path = out_dir.join("assets.pa"); - if assets_pa_path.exists() { - fs::remove_file(&assets_pa_path)?; - } - fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\"]\n}\n")?; + fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?; + fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?; Ok(()) } -#[allow(dead_code)] fn heavy_load(rom: &mut Vec) { // Single function 0: main - // Everything runs here — no coroutines, no SPAWN, no YIELD. - // - // Global 0 = t (frame counter) - // Local 0 = scratch - // Local 1 = loop counter for discs - // - // Loop: - // t = (t + 1) - // clear screen - // draw 500 discs using t for animation - // draw 20 texts using t for animation - // RET (runtime handles the frame loop) + // Global 0 = frame counter + // Global 1 = scene bound flag + // Local 0 = sprite row + // Local 1 = sprite col - // --- init locals --- - // local 0: scratch - // local 1: loop counter for discs - rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0\nPUSH_I32 0\nSET_LOCAL 1")); - - // --- t = (t + 1) --- - // t is global 0 to persist across prepare_call resets rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); - // --- clear screen --- + rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ")); + let jif_bind_done_offset = rom.len() + 2; + rom.extend(asm("JMP_IF_FALSE 0")); + + rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1")); + let bind_done_target = rom.len() as u32; + rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); - // --- call composer-domain sprite emission path once per frame and drop status --- + rom.extend(asm( - "PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1", + "GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4", )); - // --- draw 500 discs --- - rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); - let disc_loop_start = rom.len() as u32; - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 500\nLT")); - let jif_disc_end_offset = rom.len() + 2; + rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0")); + let row_loop_start = rom.len() as u32; + rom.extend(asm("GET_LOCAL 0\nPUSH_I32 16\nLT")); + let jif_row_end_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - // x = (t * (i+7) + i * 13) % 320 - rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 7\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 13\nMUL\nADD\nPUSH_I32 320\nMOD")); - // y = (t * (i+11) + i * 17) % 180 - rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 11\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 17\nMUL\nADD\nPUSH_I32 180\nMOD")); - // r = ( (i*13) % 20 ) + 5 - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 13\nMUL\nPUSH_I32 20\nMOD\nPUSH_I32 5\nADD")); - // border color = (i * 1234) & 0xFFFF - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND")); - // fill color = (i * 5678 + t) & 0xFFFF - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND")); - // HOSTCALL gfx.draw_disc (x, y, r, border, fill) - rom.extend(asm("HOSTCALL 1")); - - // i++ - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); - let jmp_disc_loop_offset = rom.len() + 2; - rom.extend(asm("JMP 0")); - let disc_loop_end = rom.len() as u32; - - // --- draw 20 texts --- rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); - let text_loop_start = rom.len() as u32; - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 20\nLT")); - let jif_text_end_offset = rom.len() + 2; + let col_loop_start = rom.len() as u32; + rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT")); + let jif_col_end_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - // x = (t * 3 + i * 40) % 320 rom.extend(asm( - "GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD", + "PUSH_I32 0\n\ + GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\ + GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\ + GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\ + GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\ + PUSH_I32 0\n\ + GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\ + GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\ + GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\ + HOSTCALL 5\nPOP_N 1", )); - // y = (i * 30 + t) % 180 - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 30\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 180\nMOD")); - // string (toggle between "stress" and "frame") - rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ")); - let jif_text_alt_offset = rom.len() + 2; - rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_CONST 0")); // "stress" - let jmp_text_join_offset = rom.len() + 2; - rom.extend(asm("JMP 0")); - let text_alt_target = rom.len() as u32; - rom.extend(asm("PUSH_CONST 1")); // "frame" - let text_join_target = rom.len() as u32; - // color = (t * 10 + i * 1000) & 0xFFFF - rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND")); - // HOSTCALL gfx.draw_text (x, y, str, color) - rom.extend(asm("HOSTCALL 2")); - - // i++ rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); - let jmp_text_loop_offset = rom.len() + 2; + let jmp_col_loop_offset = rom.len() + 2; rom.extend(asm("JMP 0")); - let text_loop_end = rom.len() as u32; + let col_loop_end = rom.len() as u32; + + rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0")); + let jmp_row_loop_offset = rom.len() + 2; + rom.extend(asm("JMP 0")); + let row_loop_end = rom.len() as u32; + + rom.extend(asm( + "PUSH_I32 8\n\ + PUSH_I32 8\n\ + PUSH_CONST 0\n\ + GET_GLOBAL 0\nPUSH_I32 2047\nMUL\nPUSH_I32 65535\nBIT_AND\n\ + HOSTCALL 1", + )); + rom.extend(asm( + "PUSH_I32 8\n\ + PUSH_I32 20\n\ + PUSH_CONST 1\n\ + GET_GLOBAL 0\nPUSH_I32 4093\nMUL\nPUSH_I32 65535\nBIT_AND\n\ + HOSTCALL 1", + )); - // --- log every 60 frames --- rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ")); let jif_log_offset = rom.len() + 2; rom.extend(asm("JMP_IF_FALSE 0")); - rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 3")); + rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2")); let after_log = rom.len() as u32; - // --- end of function --- rom.extend(asm("FRAME_SYNC\nRET")); - // --- Patch jump targets --- let patch = |buf: &mut Vec, imm_offset: usize, target: u32| { buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes()); }; - patch(rom, jif_disc_end_offset, disc_loop_end); - patch(rom, jmp_disc_loop_offset, disc_loop_start); - - patch(rom, jif_text_end_offset, text_loop_end); - patch(rom, jif_text_alt_offset, text_alt_target); - patch(rom, jmp_text_join_offset, text_join_target); - patch(rom, jmp_text_loop_offset, text_loop_start); - + patch(rom, jif_bind_done_offset, bind_done_target); + patch(rom, jif_row_end_offset, row_loop_end); + patch(rom, jif_col_end_offset, col_loop_end); + patch(rom, jmp_col_loop_offset, col_loop_start); + patch(rom, jmp_row_loop_offset, row_loop_start); patch(rom, jif_log_offset, after_log); } + +fn build_assets_pack() -> Result> { + let (glyph_entry, glyph_payload) = build_glyph_asset(); + let scene = build_scene_bank(); + let scene_payload = encode_scene_payload(&scene); + let scene_entry = AssetEntry { + asset_id: 1, + asset_name: "stress_scene".into(), + bank_type: BankType::SCENE, + offset: glyph_payload.len() as u64, + size: scene_payload.len() as u64, + decoded_size: expected_scene_decoded_size(&scene) as u64, + codec: AssetCodec::None, + metadata: serde_json::json!({}), + }; + + let asset_table = vec![glyph_entry, scene_entry]; + let preload = + vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 1, slot: 0 }]; + let payload_len = glyph_payload.len() + scene_payload.len(); + let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })?; + let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64; + let prelude = AssetsPackPrelude { + magic: ASSETS_PA_MAGIC, + schema_version: ASSETS_PA_SCHEMA_VERSION, + header_len: header.len() as u32, + payload_offset, + flags: 0, + reserved: 0, + header_checksum: 0, + }; + + let mut bytes = prelude.to_bytes().to_vec(); + bytes.extend_from_slice(&header); + bytes.extend_from_slice(&glyph_payload); + bytes.extend_from_slice(&scene_payload); + debug_assert_eq!(bytes.len(), payload_offset as usize + payload_len); + Ok(bytes) +} + +fn build_glyph_asset() -> (AssetEntry, Vec) { + let pixel_indices = vec![1_u8; 8 * 8]; + let mut payload = pack_4bpp(&pixel_indices); + payload.extend_from_slice(&build_palette_bytes()); + + let entry = AssetEntry { + asset_id: 0, + asset_name: "stress_square".into(), + bank_type: BankType::GLYPH, + offset: 0, + size: payload.len() as u64, + decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2) + as u64, + codec: AssetCodec::None, + metadata: serde_json::json!({ + "tile_size": 8, + "width": 8, + "height": 8, + "palette_count": GLYPH_BANK_PALETTE_COUNT_V1, + "palette_authored": GLYPH_BANK_PALETTE_COUNT_V1 + }), + }; + + (entry, payload) +} + +fn build_palette_bytes() -> Vec { + let mut bytes = + Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2); + for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 { + for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE { + let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK }; + bytes.extend_from_slice(&color.raw().to_le_bytes()); + } + } + bytes +} + +fn stress_color(palette_id: usize) -> Color { + let r = ((palette_id * 53) % 256) as u8; + let g = ((palette_id * 97 + 64) % 256) as u8; + let b = ((palette_id * 29 + 128) % 256) as u8; + Color::rgb(r, g, b) +} + +fn pack_4bpp(indices: &[u8]) -> Vec { + let mut packed = Vec::with_capacity(indices.len().div_ceil(2)); + for chunk in indices.chunks(2) { + let hi = chunk[0] & 0x0f; + let lo = chunk.get(1).copied().unwrap_or(0) & 0x0f; + packed.push((hi << 4) | lo); + } + packed +} + +fn build_scene_bank() -> SceneBank { + let mut layers = std::array::from_fn(|layer_index| { + let mut tiles = vec![ + Tile { + active: false, + glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 }, + flip_x: false, + flip_y: false, + }; + 64 * 32 + ]; + + for step in 0..8 { + let x = 4 + step * 7 + layer_index * 2; + let y = 2 + step * 3 + layer_index * 2; + let index = y * 64 + x; + tiles[index] = Tile { + active: true, + glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 }, + flip_x: false, + flip_y: false, + }; + } + + SceneLayer { + active: true, + glyph_bank_id: 0, + tile_size: TileSize::Size8, + parallax_factor: match layer_index { + 0 => ParallaxFactor { x: 1.0, y: 1.0 }, + 1 => ParallaxFactor { x: 0.75, y: 0.75 }, + 2 => ParallaxFactor { x: 0.5, y: 0.5 }, + _ => ParallaxFactor { x: 0.25, y: 0.25 }, + }, + tilemap: TileMap { width: 64, height: 32, tiles }, + } + }); + + // Keep the farthest layer a bit sparser so the diagonal remains visually readable. + for step in 0..4 { + let x = 10 + step * 12; + let y = 4 + step * 5; + let index = y * 64 + x; + layers[3].tilemap.tiles[index].active = false; + } + + SceneBank { layers } +} + +fn expected_scene_decoded_size(scene: &SceneBank) -> usize { + scene + .layers + .iter() + .map(|layer| { + SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1 + layer.tilemap.tiles.len() * size_of::() + }) + .sum() +} + +fn encode_scene_payload(scene: &SceneBank) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&SCENE_PAYLOAD_MAGIC_V1); + data.extend_from_slice(&SCENE_PAYLOAD_VERSION_V1.to_le_bytes()); + data.extend_from_slice(&(SCENE_LAYER_COUNT_V1 as u16).to_le_bytes()); + data.extend_from_slice(&0_u32.to_le_bytes()); + + for layer in &scene.layers { + let layer_flags = if layer.active { 0b0000_0001 } else { 0 }; + data.push(layer_flags); + data.push(layer.glyph_bank_id); + data.push(layer.tile_size as u8); + data.push(0); + data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes()); + data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes()); + data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes()); + data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes()); + data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes()); + data.extend_from_slice(&0_u32.to_le_bytes()); + + for tile in &layer.tilemap.tiles { + let mut tile_flags = 0_u8; + if tile.active { + tile_flags |= 0b0000_0001; + } + if tile.flip_x { + tile_flags |= 0b0000_0010; + } + if tile.flip_y { + tile_flags |= 0b0000_0100; + } + data.push(tile_flags); + data.push(tile.glyph.palette_id); + data.extend_from_slice(&tile.glyph.glyph_id.to_le_bytes()); + } + } + + data +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn assets_pack_contains_preloaded_glyph_and_scene_assets() { + let bytes = build_assets_pack().expect("assets pack"); + let prelude = + AssetsPackPrelude::from_bytes(&bytes[..ASSETS_PA_PRELUDE_SIZE]).expect("prelude"); + assert_eq!(prelude.magic, ASSETS_PA_MAGIC); + assert_eq!(prelude.schema_version, ASSETS_PA_SCHEMA_VERSION); + + let header_start = ASSETS_PA_PRELUDE_SIZE; + let header_end = header_start + prelude.header_len as usize; + let header: AssetsPackHeader = + serde_json::from_slice(&bytes[header_start..header_end]).expect("header"); + + assert_eq!(header.asset_table.len(), 2); + assert_eq!(header.preload.len(), 2); + assert_eq!(header.asset_table[0].bank_type, BankType::GLYPH); + assert_eq!(header.asset_table[1].bank_type, BankType::SCENE); + assert_eq!(header.preload[0].slot, 0); + assert_eq!(header.preload[1].slot, 0); + assert_eq!(header.asset_table[0].offset, 0); + assert_eq!(header.asset_table[1].offset, header.asset_table[0].size); + assert_eq!( + bytes.len(), + prelude.payload_offset as usize + + header.asset_table[0].size as usize + + header.asset_table[1].size as usize + ); + } +} diff --git a/discussion/index.ndjson b/discussion/index.ndjson index c8617971..68838273 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":28,"AGD":28,"DEC":16,"PLN":26,"LSN":31,"CLSN":1}} +{"type":"meta","next_id":{"DSC":29,"AGD":29,"DEC":17,"PLN":30,"LSN":31,"CLSN":1}} {"type":"discussion","id":"DSC-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} @@ -20,6 +20,7 @@ {"type":"discussion","id":"DSC-0025","status":"done","ticket":"scene-bank-and-viewport-cache-refactor","title":"Scene Bank and Viewport Cache Refactor","created_at":"2026-04-11","updated_at":"2026-04-14","tags":["gfx","tilemap","runtime","render"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"lessons/DSC-0025-scene-bank-and-viewport-cache-refactor/LSN-0030-canonical-scene-cache-and-resolver-split.md","status":"done","created_at":"2026-04-14","updated_at":"2026-04-14"}]} {"type":"discussion","id":"DSC-0026","status":"open","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["gfx","runtime","render","camera","scene"],"agendas":[{"id":"AGD-0026","file":"workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15"}],"decisions":[{"id":"DEC-0014","file":"workflow/decisions/DEC-0014-frame-composer-render-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0026"}],"plans":[{"id":"PLN-0017","file":"workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0018","file":"workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0019","file":"workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0020","file":"workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0021","file":"workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]}],"lessons":[]} {"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"lessons":[]} {"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} diff --git a/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md b/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md new file mode 100644 index 00000000..677f31b7 --- /dev/null +++ b/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md @@ -0,0 +1,140 @@ +--- +id: AGD-0028 +ticket: deferred-overlay-and-primitive-composition +title: Deferred Overlay and Primitive Composition over FrameComposer +status: accepted +created: 2026-04-18 +updated: 2026-04-18 +resolved: 2026-04-18 +decision: DEC-0016 +tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud] +--- + +## Contexto + +`FrameComposer.render_frame()` hoje recompõe o `back` no fim da logical frame. Quando há scene bound, o caminho `render_scene_from_cache(...)` limpa o buffer e desenha scene + sprites, o que apaga qualquer primitive ou `draw_text(...)` emitido antes via `gfx`. + +Isso expôs um conflito de modelo: + +- `composer.*` já é o caminho canônico de orquestração de frame; +- `gfx.draw_text(...)` e demais primitives ainda escrevem diretamente no `back`; +- o runtime só chama `render_frame()` no final do frame, então a escrita imediata em `back` deixou de ser semanticamente estável. +- As primitives de `gfx` não são o mecanismo desejado para composição de jogos com scene/tile/sprite; elas existem principalmente como debug, instrumentação visual e artefatos rápidos. + +Conteúdo relevante migrado de [AGD-0010](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md): + +- a arquitetura aceita continua sendo de framebuffer destrutivo em memória, não scene graph ou renderer tipo GPU; +- otimizações em primitives devem preservar a semântica observável, mesmo quando ganharem fast paths internos; +- existe preocupação explícita com custo por classe de primitive e com orçamento de memória no alvo handheld; +- caminhos de spans/linhas/clears são desejáveis como aceleração interna, mas sem reabrir o modelo operacional do pipeline do jogo. + +## Problema + +Precisamos decidir qual é o modelo canônico para primitives e texto no pipeline pós-`FrameComposer`. + +Sem isso: + +- texto e primitives continuam com comportamento dependente da ordem interna do renderer; +- o stress test e qualquer cartridge que combine `composer.*` com `gfx.*` terão resultado inconsistente; +- fica indefinido se primitives pertencem ao mundo, ao HUD, ou a um overlay final. + +## Pontos Criticos + +- `draw_text(...)` e primitives screen-space não podem depender de escrita imediata em `back`. +- Para esta thread, primitives de `gfx` devem permanecer agnósticas ao pipeline canônico de render do jogo e não devem ser mescladas semanticamente com tiles/sprites. +- A ordem de composição precisa ser explícita e estável: `scene -> sprites -> HUD -> primitives/debug overlay`, ou outra ordem formal equivalente. +- Precisamos decidir se o contrato público de `gfx.*` muda semanticamente sem mudar ABI, ou se parte dessa superfície migra para `composer.*`. +- A solução deve preservar o caminho sem scene bound. +- A implementação deve evitar contaminar a infraestrutura de `gfx` responsável por scene, sprites e HUD com estado misto de overlay/debug; se necessário, o overlay deve ter fila/fase própria. +- melhorias internas de primitive path devem continuar permitidas, desde que não mudem a semântica de overlay final e não exijam buffers extras incompatíveis com o orçamento de memória aceito. + +## Opcoes + +### Opcao 1 - Manter escrita direta em `back` + +- **Abordagem:** manter `gfx.draw_text(...)` e primitives rasterizando imediatamente. +- **Pro:** zero mudança estrutural agora. +- **Contra:** o modelo continua quebrado sempre que `render_frame()` recompõe o buffer depois. +- **Tradeoff:** só funciona de forma confiável fora do caminho canônico do `FrameComposer`. + +### Opcao 2 - Fila única de draw commands pós-scene/pós-sprite + +- **Abordagem:** transformar texto e primitives em comandos diferidos, drenados depois de `scene + sprites`. +- **Pro:** resolve o problema imediato de overlay/HUD e estabiliza o stress test. +- **Contra:** mistura HUD e primitives/debug sob o mesmo conceito, reduzindo clareza contratual mesmo quando a ordem prática for a mesma. +- **Tradeoff:** simples para V1, mas semanticamente mais fraco do que separar overlay de jogo e overlay de debug. + +### Opcao 3 - Separar HUD diferido de primitives/debug overlay final + +- **Abordagem:** tratar `gfx.draw_text(...)` e demais primitives de `gfx` como overlay/debug final, separado da composição canônica de jogo (`scene + sprites + HUD`). +- **Pro:** casa com a intenção declarada para `gfx.*`: debug, artefato rápido e instrumentação visual acima do frame do jogo. +- **Contra:** exige modelar explicitamente uma fase extra no pipeline. +- **Tradeoff:** aumenta a clareza contratual e evita mesclar primitives com o domínio de jogo. + +### Opcao 4 - Manter HUD e primitives no mesmo estágio final, mas com categorias separadas + +- **Abordagem:** drenar HUD e primitives ambos no fim do frame, porém com filas/categorias distintas e ordem formal `HUD -> primitives`. +- **Pro:** preserva implementação próxima entre caminhos similares, mantendo contrato separado. +- **Contra:** é mais custoso que a opção 3 sem entregar muito valor adicional imediato. +- **Tradeoff:** bom se já houver expectativa de HUD canônico separado no curtíssimo prazo. + +## Sugestao / Recomendacao + +Seguir com a **Opcao 3**. + +Minha recomendação é: + +- retirar a escrita direta em `back` como contrato operacional para `gfx.draw_text(...)` e demais primitives de `gfx`; +- introduzir uma fila diferida canônica de primitives/debug overlay drenada no fim do frame; +- tratar `gfx.*` primitive/text como superfície agnóstica ao pipeline de jogo e explicitamente acima da composição canônica; +- não misturar semanticamente primitives com scene/tile/sprite/HUD. +- evitar compartilhar indevidamente o mesmo mecanismo operacional de composição entre overlay/debug e os caminhos de scene/sprite/HUD, mesmo quando o backend de rasterização reutilizado for o mesmo. + +Ordem recomendada para o frame canônico: + +1. limpar/compor scene; +2. compor sprites; +3. compor HUD canônico, se existir; +4. aplicar `scene_fade`; +5. aplicar `hud_fade`; +6. drenar primitives/debug overlay de `gfx.*`. + +## Perguntas em Aberto + +- `draw_text(...)` e as demais primitives de `gfx` entram todas na mesma família de overlay final já na V1, ou começamos só com `draw_text(...)`? +- `render_no_scene_frame()` deve usar a mesma fila diferida para manter semântica idêntica com e sem scene? +- HUD canônico precisa existir explicitamente nesta mesma thread, ou pode continuar implícito/externo enquanto as primitives já migram para overlay final? +- quais fast paths internos de primitives continuam desejáveis nessa nova fase, por exemplo spans horizontais/verticais, fills e clears, sem misturar isso com a composição do jogo? +- o overlay/debug final precisa de dirtying próprio por classe de primitive ou isso pode ficar fora da primeira migração? + +## Criterio para Encerrar + +Esta agenda pode ser encerrada quando tivermos uma resposta explícita para: + +- o destino semântico de `draw_text(...)`; +- se haverá uma fila própria para primitives/debug overlay e qual a relação dela com HUD; +- a ordem canônica de composição do frame; +- o escopo exato da primeira migração implementável sem reabrir o restante do pipeline. + +## Resolucao Parcial + +Direção já aceita nesta agenda: + +- primitives e `draw_text(...)` de `gfx.*` devem ser tratadas como overlay/debug final; +- esse overlay deve ser drenado **depois** de `hud_fade`; +- scene, sprites e HUD canônico não devem ser semanticamente misturados com o overlay/debug; +- a implementação deve preservar separação operacional suficiente para que o `gfx` usado pelo pipeline do jogo não passe a depender do estado transitório de primitives/debug; +- otimizações de primitive path discutidas na `AGD-0010` continuam válidas, mas passam a operar dentro do domínio de overlay/debug final, não como parte da composição canônica de scene/sprite/HUD. + +## Resolucao + +Esta agenda fica aceita com os seguintes pontos fechados: + +- `gfx.draw_text(...)` e as demais primitives públicas de `gfx.*` pertencem à mesma família V1 de overlay/debug final; +- esse overlay/debug fica **fora** do `FrameComposer`; +- `FrameComposer` continua restrito à composição canônica do jogo (`scene`, `sprites` e HUD canônico quando existir); +- o overlay/debug deve ser drenado depois de `hud_fade`; +- o caminho sem scene bound deve observar a mesma semântica final de overlay/debug; +- HUD canônico explícito não faz parte desta thread e pode permanecer implícito/externo por enquanto; +- fast paths internos de primitives continuam permitidos, desde que preservem a semântica observável do overlay/debug final; +- dirtying granular ou otimizações finas por classe de primitive não fazem parte da primeira migração normativa desta thread. diff --git a/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md b/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md new file mode 100644 index 00000000..be739372 --- /dev/null +++ b/discussion/workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md @@ -0,0 +1,150 @@ +--- +id: DEC-0016 +ticket: deferred-overlay-and-primitive-composition +title: Deferred GFX Overlay Outside FrameComposer +status: accepted +created: 2026-04-18 +accepted: 2026-04-18 +agenda: AGD-0028 +plans: [PLN-0026, PLN-0027, PLN-0028, PLN-0029] +tags: [gfx, runtime, render, frame-composer, overlay, primitives, hud] +--- + +## Status + +Accepted. + +## Contexto + +`DEC-0014` and `DEC-0015` established `FrameComposer` as the canonical orchestration path for game-frame composition and exposed that orchestration publicly through `composer.*`. + +That migration left `gfx.draw_text(...)` and other `gfx` primitives with their historical immediate-write behavior against the working framebuffer. Once the runtime moved to end-of-frame composition through `FrameComposer.render_frame()`, those immediate writes became unstable: scene-backed frame composition can rebuild the backbuffer after primitive calls have already touched it. + +The resulting conflict is not about whether primitives should remain available. It is about their semantic place in the pipeline. The accepted direction of this thread is that `gfx` primitives are not part of the canonical game composition model. They are primarily for debug, quick visual instrumentation, and rapid artifacts, and they must remain agnostic to scene/tile/sprite/HUD composition. + +Relevant performance context migrated from `AGD-0010` also remains in force: + +- the renderer continues to be a destructive software framebuffer model, not a retained scene graph or GPU-style renderer; +- internal primitive fast paths remain desirable; +- memory growth must remain constrained for the handheld target; +- optimization of primitive execution must not alter observable semantics. + +## Decisao + +`gfx.*` primitives and text SHALL move to a deferred final overlay model that lives outside `FrameComposer`. + +Normatively: + +- `FrameComposer` SHALL remain responsible only for canonical game-frame composition: + - scene composition; + - sprite composition; + - canonical HUD composition when such a HUD stage exists. +- `FrameComposer` MUST NOT become the owner of debug/primitive overlay state. +- Public `gfx.*` primitives, including `gfx.draw_text(...)`, SHALL belong to a V1 `gfx` overlay/debug family. +- That overlay/debug family SHALL be deferred rather than written immediately as the stable operational contract. +- The deferred overlay/debug stage SHALL be drained after `hud_fade`. +- The deferred overlay/debug stage SHALL be above scene, sprites, and canonical HUD in final visual order. +- The no-scene path MUST preserve the same final overlay/debug semantics. +- `gfx.*` primitives MUST remain semantically separate from scene/tile/sprite/HUD composition. +- The implementation MUST preserve operational separation sufficient to prevent the canonical game pipeline from depending on transient primitive/debug state. + +## Rationale + +This decision keeps the architectural boundary clean. + +`FrameComposer` exists to own the canonical game frame. Debug primitives do not belong to that contract. Pulling them into `FrameComposer` would make the orchestration service responsible for a second semantic domain with different goals: + +- game composition must be deterministic and canonical; +- primitive/text overlay must be opportunistic, screen-space, and pipeline-agnostic. + +Keeping overlay/debug outside `FrameComposer` also aligns with the stated product intent: these primitives are useful helpers, but they are not meant to become a second composition language for games. + +Draining them after `hud_fade` preserves the user-visible requirement that debug/overlay content stay truly on top and legible. This is more faithful to the accepted intent than treating primitives as part of HUD or world composition. + +Finally, separating semantic ownership still leaves room for implementation reuse. Raster backends, span paths, and buffer-writing helpers may still be shared internally, provided the public operational model remains separate. + +## Invariantes / Contrato + +### 1. Ownership Boundary + +- `FrameComposer` MUST own only canonical game-frame composition. +- Primitive/debug overlay state MUST live outside `FrameComposer`. +- The canonical game pipeline MUST NOT depend on primitive/debug overlay state for correctness. + +### 2. Overlay Semantics + +- `gfx.draw_text(...)` and sibling `gfx` primitives SHALL be treated as deferred final overlay/debug operations. +- Immediate direct writes to `back` MUST NOT remain the stable operational contract for these primitives. +- Final overlay/debug output MUST appear after: + - scene composition; + - sprite composition; + - canonical HUD composition, if present; + - `scene_fade`; + - `hud_fade`. + +### 3. Separation from Game Composition + +- Primitive/debug overlay MUST NOT be reinterpreted as scene content. +- Primitive/debug overlay MUST NOT be reinterpreted as sprite content. +- Primitive/debug overlay MUST NOT be the vehicle for canonical HUD composition. +- The public `gfx.*` primitive surface SHALL remain pipeline-agnostic relative to `composer.*`. + +### 4. Consistency Across Frame Paths + +- The scene-bound path and no-scene path MUST expose the same final overlay/debug behavior. +- Users MUST NOT need to know whether a scene is bound for `gfx.*` primitives to appear as final overlay/debug content. + +### 5. Internal Optimization Contract + +- Internal fast paths for lines, spans, fills, clears, or similar primitive operations MAY be introduced. +- Such fast paths MUST preserve the observable deferred overlay/debug semantics. +- This decision DOES NOT require fine-grained dirtying or per-primitive-class invalidation in the first migration. + +## Impactos + +### Runtime / Drivers + +- The runtime frame-end sequence must gain a distinct overlay/debug drain stage outside `FrameComposer`. +- `gfx.draw_text(...)` and peer primitives can no longer rely on stable immediate framebuffer writes once this migration lands. + +### GFX Backend + +- `Gfx` will need an explicit deferred overlay/debug command path or equivalent subsystem boundary. +- Shared raster helpers remain allowed, but the overlay/debug phase must stay semantically distinct from scene/sprite/HUD composition. + +### FrameComposer + +- `FrameComposer` must remain free of primitive/debug overlay ownership. +- Any future HUD integration must not collapse that boundary. + +### Spec / Docs + +- The canonical graphics/runtime spec must describe `gfx.*` primitives as deferred final overlay/debug operations rather than stable immediate backbuffer writes. +- Documentation that describes frame ordering must show overlay/debug after `hud_fade`. + +### Performance Follow-up + +- `AGD-0010` remains the home for broader renderer performance work, dirtying strategy, and low-level primitive optimization policy. +- Primitive optimization carried out under that thread must respect the normative separation established here. + +## Referencias + +- [AGD-0028-deferred-overlay-and-primitive-composition.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md) +- [AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md) +- [DEC-0014-frame-composer-render-integration.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0014-frame-composer-render-integration.md) +- [DEC-0015-frame-composer-public-syscall-surface.md](/Users/niltonconstantino/personal/workspace.personal/intrepid/prometeu/runtime/discussion/workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md) + +## Propagacao Necessaria + +- A new implementation plan MUST be created before code changes. +- That plan MUST cover: + - deferred overlay/debug ownership outside `FrameComposer`; + - runtime frame-end ordering changes; + - no-scene path parity; + - spec/documentation updates for `gfx.*` primitive semantics. +- The implementation plan MUST NOT reopen the ownership boundary accepted here. + +## Revision Log + +- 2026-04-18: Initial accepted decision from `AGD-0028`. +- 2026-04-18: Linked implementation plan family `PLN-0026` through `PLN-0029`. diff --git a/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md b/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md new file mode 100644 index 00000000..ab33e3d4 --- /dev/null +++ b/discussion/workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md @@ -0,0 +1,93 @@ +--- +id: PLN-0026 +ticket: deferred-overlay-and-primitive-composition +title: Plan - GFX Overlay Contract and Spec Propagation +status: accepted +created: 2026-04-18 +completed: +tags: [gfx, runtime, spec, overlay, primitives, hud] +--- + +## Objective + +Propagate `DEC-0016` into the canonical specs and internal contracts so `gfx.*` primitives are defined as deferred final overlay/debug operations outside `FrameComposer`. + +## Background + +`DEC-0016` locks a new semantic boundary: + +- `FrameComposer` remains the owner of canonical game-frame composition; +- `gfx.*` primitives and `draw_text(...)` become deferred final overlay/debug operations; +- that overlay lives outside `FrameComposer` and is drained after `hud_fade`. + +Execution must start by updating the normative contract before implementation changes spread through runtime and drivers. + +## Scope + +### Included +- update canonical runtime/gfx spec text to describe deferred overlay semantics +- update any ABI-facing or developer-facing docs that still imply direct stable writes to `back` +- align local contract comments and module docs where they currently imply immediate-write semantics as the stable model + +### Excluded +- implementation of the overlay subsystem +- runtime frame-end integration +- final repository-wide CI + +## Execution Steps + +### Step 1 - Update canonical graphics/runtime documentation + +**What:** +Publish the new semantic contract for `gfx.*` primitives. + +**How:** +- Update the canonical runtime/gfx spec so `gfx.draw_text(...)` and peer primitives are described as deferred final overlay/debug operations. +- State explicitly that primitives are not part of canonical scene/sprite/HUD composition. +- State the ordering rule that overlay/debug is drained after `hud_fade`. +- Ensure the no-scene and scene-bound paths are described consistently. + +**File(s):** +- canonical runtime/gfx spec files under `docs/specs/runtime/` + +### Step 2 - Align implementation-facing contract text + +**What:** +Remove stale implementation comments that imply immediate stable writes to the framebuffer. + +**How:** +- Inspect module-level comments and trait docs in `hal`, `drivers`, and runtime code for language that now contradicts `DEC-0016`. +- Update only the contract-bearing comments and docs that materially affect maintenance and implementation clarity. + +**File(s):** +- `crates/console/prometeu-hal/src/gfx_bridge.rs` +- `crates/console/prometeu-drivers/src/gfx.rs` +- `crates/console/prometeu-drivers/src/frame_composer.rs` +- runtime-adjacent modules where frame ordering is described + +## Test Requirements + +### Unit Tests +- none required for pure doc propagation + +### Integration Tests +- none required for pure doc propagation + +### Manual Verification +- inspect the updated spec and local contract comments to confirm they no longer describe primitives as stable direct writes to `back` + +## Acceptance Criteria + +- [ ] Canonical spec text describes `gfx.*` primitives as deferred final overlay/debug operations. +- [ ] The spec states that overlay/debug is outside `FrameComposer`. +- [ ] The spec states that overlay/debug is drained after `hud_fade`. +- [ ] Local implementation-facing contract comments no longer imply immediate-write semantics as the stable model. + +## Dependencies + +- Source decision: `DEC-0016` + +## Risks + +- Missing a normative doc location would leave code and published contract divergent. +- Over-editing local comments could unintentionally restate design choices outside the scope of `DEC-0016`. diff --git a/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md b/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md new file mode 100644 index 00000000..e746e8fb --- /dev/null +++ b/discussion/workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md @@ -0,0 +1,104 @@ +--- +id: PLN-0027 +ticket: deferred-overlay-and-primitive-composition +title: Plan - Deferred GFX Overlay Subsystem +status: accepted +created: 2026-04-18 +completed: +tags: [gfx, runtime, overlay, primitives, text, drivers] +--- + +## Objective + +Introduce a dedicated deferred overlay/debug subsystem for `gfx.*` primitives outside `FrameComposer`, with command capture for `draw_text(...)` and the primitive family selected for the first migration. + +## Background + +`DEC-0016` requires primitive/text overlay ownership to remain outside `FrameComposer` while still allowing shared raster helpers and low-level optimizations internally. The new subsystem must preserve semantic separation from scene/sprite/HUD composition. + +## Scope + +### Included +- introduce an overlay/debug command queue or equivalent subsystem outside `FrameComposer` +- route `gfx.draw_text(...)` into deferred command capture instead of stable direct framebuffer writes +- route the chosen V1 primitive family into the same deferred overlay/debug path +- keep raster helper reuse allowed without merging semantic ownership + +### Excluded +- runtime frame-end sequencing +- no-scene/scene parity tests at the runtime level +- final repository-wide CI + +## Execution Steps + +### Step 1 - Define overlay/debug state ownership in drivers + +**What:** +Create the subsystem that owns deferred `gfx.*` overlay/debug commands. + +**How:** +- Add a dedicated owner adjacent to `Gfx`/`Hardware`, but not inside `FrameComposer`. +- Define the minimal command model required for V1 operations. +- Keep the subsystem screen-space and explicitly pipeline-agnostic relative to `composer.*`. + +**File(s):** +- `crates/console/prometeu-drivers/src/*` +- `crates/console/prometeu-hal/src/*` if bridge traits need extension + +### Step 2 - Route text and selected primitives into deferred capture + +**What:** +Stop treating text/primitives as stable direct writes. + +**How:** +- Change `gfx.draw_text(...)` to enqueue deferred overlay/debug work. +- Migrate the selected V1 primitive set into the same deferred path. +- Keep any remaining unmigrated primitives either explicitly out of scope or routed consistently if they are already part of the accepted V1 set. +- Preserve internal raster helper reuse where useful. + +**File(s):** +- `crates/console/prometeu-drivers/src/gfx.rs` +- runtime dispatch call sites that submit `gfx.*` primitives + +### Step 3 - Add local driver-level tests for deferred capture semantics + +**What:** +Prove that overlay/debug commands are captured separately from game composition state. + +**How:** +- Add tests that assert text/primitives do not need direct stable writes to `back` to survive until overlay drain. +- Add tests that assert the overlay owner is independent from `FrameComposer` state. + +**File(s):** +- `crates/console/prometeu-drivers/src/gfx.rs` +- new or existing driver test modules + +## Test Requirements + +### Unit Tests +- command capture tests for `draw_text(...)` +- tests for each migrated V1 primitive class +- tests proving overlay/debug state is owned outside `FrameComposer` + +### Integration Tests +- none in this plan; runtime-level ordering is covered by the next plan + +### Manual Verification +- inspect driver ownership boundaries to confirm `FrameComposer` does not gain overlay/debug state + +## Acceptance Criteria + +- [ ] A dedicated deferred overlay/debug subsystem exists outside `FrameComposer`. +- [ ] `gfx.draw_text(...)` is captured as deferred overlay/debug work. +- [ ] The selected V1 primitive family is captured through the same subsystem. +- [ ] Driver-level tests prove overlay/debug state is operationally separate from canonical game composition state. + +## Dependencies + +- Source decision: `DEC-0016` +- Prefer to execute after `PLN-0026` + +## Risks + +- Accidentally reusing `FrameComposer` storage or state would violate the accepted ownership boundary. +- Migrating only part of the primitive family without explicit scoping could create inconsistent semantics across `gfx.*`. diff --git a/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md b/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md new file mode 100644 index 00000000..3fadea8e --- /dev/null +++ b/discussion/workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md @@ -0,0 +1,106 @@ +--- +id: PLN-0028 +ticket: deferred-overlay-and-primitive-composition +title: Plan - Runtime Frame-End Overlay Integration and Parity +status: accepted +created: 2026-04-18 +completed: +tags: [runtime, overlay, frame-composer, no-scene, regression, stress] +--- + +## Objective + +Integrate deferred overlay/debug draining into the runtime frame-end sequence so scene-bound and no-scene frames both present the same final `gfx.*` primitive behavior after `hud_fade`. + +## Background + +After `PLN-0027`, the overlay/debug subsystem will exist but still needs to be drained in the correct place relative to `FrameComposer.render_frame()`, fades, and present/present-adjacent behavior. This plan closes the observable runtime semantics required by `DEC-0016`. + +## Scope + +### Included +- runtime frame-end ordering changes +- scene-bound and no-scene parity +- regression coverage for overlay visibility above the canonical game frame +- stress-cartridge adjustments if needed to prove text/primitives now survive frame composition + +### Excluded +- broad renderer optimization work +- final repository-wide CI + +## Execution Steps + +### Step 1 - Insert overlay/debug drain into the frame-end path + +**What:** +Drain deferred overlay/debug after canonical game composition is complete. + +**How:** +- Update the runtime frame-end path so overlay/debug drain occurs after: + - `FrameComposer.render_frame()` + - `scene_fade` + - `hud_fade` +- Ensure the same ordering is respected in the no-scene path. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tick.rs` +- `crates/console/prometeu-drivers/src/hardware.rs` +- `crates/console/prometeu-drivers/src/gfx.rs` +- any bridge traits needed by the runtime/hardware path + +### Step 2 - Add runtime and driver regressions for final visual ordering + +**What:** +Lock the new visible behavior. + +**How:** +- Add tests proving `gfx.draw_text(...)` remains visible after scene-backed frame composition. +- Add tests proving the same behavior with no scene bound. +- Add tests proving overlay/debug sits above the canonical game frame rather than being erased by it. + +**File(s):** +- `crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs` +- driver-level render tests where helpful + +### Step 3 - Update stress/integration fixtures if needed + +**What:** +Restore or improve stress scenarios that rely on visible text/primitives. + +**How:** +- Update `pbxgen-stress` or related stress fixtures so text/primitives are once again a valid visible overlay signal. +- Keep the stress focused on the new model rather than reintroducing obsolete immediate-write assumptions. + +**File(s):** +- `crates/tools/pbxgen-stress/src/lib.rs` +- `test-cartridges/stress-console/*` + +## Test Requirements + +### Unit Tests +- local ordering tests where runtime integration depends on helper sequencing + +### Integration Tests +- runtime tests for scene-bound overlay/debug visibility +- runtime tests for no-scene parity +- stress/tooling validation that text or primitives are visible again as final overlay/debug + +### Manual Verification +- run the stress path and visually confirm overlay/debug survives on top of scene/sprites after frame composition + +## Acceptance Criteria + +- [ ] The runtime drains deferred overlay/debug after canonical game composition and after `hud_fade`. +- [ ] Scene-bound and no-scene paths expose the same overlay/debug semantics. +- [ ] Regression tests prove `draw_text(...)` is no longer erased by scene-backed frame composition. +- [ ] Stress/integration fixtures reflect the new final-overlay semantics where applicable. + +## Dependencies + +- Source decision: `DEC-0016` +- Depends on `PLN-0027` + +## Risks + +- If fades are still applied after overlay/debug drain, the visible contract will contradict `DEC-0016`. +- Incomplete parity between scene-bound and no-scene paths would leave runtime behavior mode-dependent. diff --git a/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md b/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md new file mode 100644 index 00000000..085d83c2 --- /dev/null +++ b/discussion/workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md @@ -0,0 +1,82 @@ +--- +id: PLN-0029 +ticket: deferred-overlay-and-primitive-composition +title: Plan - Final Overlay CI Validation and Polish +status: accepted +created: 2026-04-18 +completed: +tags: [ci, overlay, runtime, gfx, validation] +--- + +## Objective + +Run the final repository validation path for the deferred overlay/debug migration and perform the last compatibility, formatting, lint, and regression fixes required to close the thread cleanly. + +## Background + +`DEC-0016` changes visible runtime semantics and touches both specs and code paths around frame composition. A dedicated final-validation plan is needed so the implementation family can close on a clean CI signal rather than leaving integration fallout for later. + +## Scope + +### Included +- full-tree formatting, lint, and test validation +- stress-path smoke validation after overlay integration +- final cleanup fixes required to satisfy CI + +### Excluded +- new feature work outside the accepted overlay/debug migration + +## Execution Steps + +### Step 1 - Run focused validation before full CI + +**What:** +Catch local fallout in the touched areas before the full repository pass. + +**How:** +- Run targeted tests for drivers, runtime, and `pbxgen-stress`. +- Inspect touched files for stale immediate-write assumptions or missed contract updates. + +**File(s):** +- touched files from `PLN-0026` through `PLN-0028` + +### Step 2 - Run final repository CI + +**What:** +Validate the migration end to end. + +**How:** +- Run the repository validation path, including `make ci`. +- Fix any final formatting, lint, test, or generated-fixture fallout caused by the overlay/debug migration. +- Do not widen scope beyond the accepted thread. + +**File(s):** +- repository-wide + +## Test Requirements + +### Unit Tests +- all relevant crate unit tests pass after the migration + +### Integration Tests +- runtime and stress/integration tests pass after the migration +- `make ci` passes + +### Manual Verification +- inspect the tree for residual direct-write assumptions or incomplete overlay propagation + +## Acceptance Criteria + +- [ ] Targeted validation passes for the touched drivers/runtime/stress areas. +- [ ] `make ci` passes after the deferred overlay/debug migration family lands. +- [ ] No residual contract mismatch remains between spec text and code behavior in the touched thread. + +## Dependencies + +- Source decision: `DEC-0016` +- Depends on `PLN-0026`, `PLN-0027`, and `PLN-0028` + +## Risks + +- Final CI may surface unrelated renderer assumptions that still expect immediate-write semantics. +- Generated cartridge fixtures may drift if regeneration is forgotten during earlier plans. diff --git a/test-cartridges/stress-console/assets.pa b/test-cartridges/stress-console/assets.pa new file mode 100644 index 00000000..77c82efc Binary files /dev/null and b/test-cartridges/stress-console/assets.pa differ diff --git a/test-cartridges/stress-console/manifest.json b/test-cartridges/stress-console/manifest.json index d3d34606..6d77693e 100644 --- a/test-cartridges/stress-console/manifest.json +++ b/test-cartridges/stress-console/manifest.json @@ -5,5 +5,5 @@ "title": "Stress Console", "app_version": "0.1.0", "app_mode": "Game", - "capabilities": ["gfx", "log"] + "capabilities": ["gfx", "log", "asset"] } diff --git a/test-cartridges/stress-console/program.pbx b/test-cartridges/stress-console/program.pbx index 033fc6c9..c20885dc 100644 Binary files a/test-cartridges/stress-console/program.pbx and b/test-cartridges/stress-console/program.pbx differ