primitives pipeline adjustments

This commit is contained in:
bQUARKz 2026-04-18 09:50:13 +01:00
parent 4a5210f347
commit b0b8bb9028
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 1002 additions and 101 deletions

2
Cargo.lock generated
View File

@ -1475,6 +1475,8 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"prometeu-bytecode", "prometeu-bytecode",
"prometeu-hal",
"serde_json",
] ]
[[package]] [[package]]

View File

@ -5,4 +5,6 @@ edition = "2021"
[dependencies] [dependencies]
prometeu-bytecode = { path = "../../console/prometeu-bytecode" } prometeu-bytecode = { path = "../../console/prometeu-bytecode" }
prometeu-hal = { path = "../../console/prometeu-hal" }
anyhow = "1" anyhow = "1"
serde_json = "1"

View File

@ -3,7 +3,25 @@ use prometeu_bytecode::assembler::assemble;
use prometeu_bytecode::model::{ use prometeu_bytecode::model::{
BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl, BytecodeModule, ConstantPoolEntry, DebugInfo, Export, FunctionMeta, SyscallDecl,
}; };
use prometeu_hal::asset::{
AssetCodec, AssetEntry, BankType, PreloadEntry, SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1,
SCENE_LAYER_COUNT_V1, SCENE_PAYLOAD_MAGIC_V1, SCENE_PAYLOAD_VERSION_V1,
};
use prometeu_hal::cartridge::{
AssetsPackHeader, AssetsPackPrelude, ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE,
ASSETS_PA_SCHEMA_VERSION,
};
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{
TileSize, GLYPH_BANK_COLORS_PER_PALETTE, GLYPH_BANK_PALETTE_COUNT_V1,
};
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::tile::Tile;
use prometeu_hal::tilemap::TileMap;
use std::fs; use std::fs;
use std::mem::size_of;
use std::path::PathBuf; use std::path::PathBuf;
fn asm(s: &str) -> Vec<u8> { fn asm(s: &str) -> Vec<u8> {
@ -20,13 +38,6 @@ pub fn generate() -> Result<()> {
arg_slots: 1, arg_slots: 1,
ret_slots: 0, ret_slots: 0,
}, },
SyscallDecl {
module: "gfx".into(),
name: "draw_disc".into(),
version: 1,
arg_slots: 5,
ret_slots: 0,
},
SyscallDecl { SyscallDecl {
module: "gfx".into(), module: "gfx".into(),
name: "draw_text".into(), name: "draw_text".into(),
@ -41,6 +52,20 @@ pub fn generate() -> Result<()> {
arg_slots: 2, arg_slots: 2,
ret_slots: 0, 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 { SyscallDecl {
module: "composer".into(), module: "composer".into(),
name: "emit_sprite".into(), name: "emit_sprite".into(),
@ -59,7 +84,7 @@ pub fn generate() -> Result<()> {
param_slots: 0, param_slots: 0,
local_slots: 2, local_slots: 2,
return_slots: 0, return_slots: 0,
max_stack_slots: 16, max_stack_slots: 32,
}]; }];
let module = BytecodeModule { let module = BytecodeModule {
@ -67,7 +92,6 @@ pub fn generate() -> Result<()> {
const_pool: vec![ const_pool: vec![
ConstantPoolEntry::String("stress".into()), ConstantPoolEntry::String("stress".into()),
ConstantPoolEntry::String("frame".into()), ConstantPoolEntry::String("frame".into()),
ConstantPoolEntry::String("missing_glyph_bank".into()),
], ],
functions, functions,
code: rom, code: rom,
@ -89,129 +113,326 @@ pub fn generate() -> Result<()> {
out_dir.push("stress-console"); out_dir.push("stress-console");
fs::create_dir_all(&out_dir)?; fs::create_dir_all(&out_dir)?;
fs::write(out_dir.join("program.pbx"), bytes)?; fs::write(out_dir.join("program.pbx"), bytes)?;
let assets_pa_path = out_dir.join("assets.pa"); fs::write(out_dir.join("assets.pa"), build_assets_pack()?)?;
if assets_pa_path.exists() { fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\", \"asset\"]\n}\n")?;
fs::remove_file(&assets_pa_path)?;
}
fs::write(out_dir.join("manifest.json"), b"{\n \"magic\": \"PMTU\",\n \"cartridge_version\": 1,\n \"app_id\": 1,\n \"title\": \"Stress Console\",\n \"app_version\": \"0.1.0\",\n \"app_mode\": \"Game\",\n \"capabilities\": [\"gfx\", \"log\"]\n}\n")?;
Ok(()) Ok(())
} }
#[allow(dead_code)]
fn heavy_load(rom: &mut Vec<u8>) { fn heavy_load(rom: &mut Vec<u8>) {
// Single function 0: main // Single function 0: main
// Everything runs here — no coroutines, no SPAWN, no YIELD. // Global 0 = frame counter
// // Global 1 = scene bound flag
// Global 0 = t (frame counter) // Local 0 = sprite row
// Local 0 = scratch // Local 1 = sprite col
// Local 1 = loop counter for discs
//
// Loop:
// t = (t + 1)
// clear screen
// draw 500 discs using t for animation
// draw 20 texts using t for animation
// RET (runtime handles the frame loop)
// --- init locals ---
// local 0: scratch
// local 1: loop counter for discs
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0\nPUSH_I32 0\nSET_LOCAL 1"));
// --- t = (t + 1) ---
// t is global 0 to persist across prepare_call resets
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 1\nADD\nSET_GLOBAL 0"));
// --- clear screen --- rom.extend(asm("GET_GLOBAL 1\nPUSH_I32 0\nEQ"));
let jif_bind_done_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm("PUSH_I32 0\nHOSTCALL 3\nPOP_N 1\nPUSH_I32 1\nSET_GLOBAL 1"));
let bind_done_target = rom.len() as u32;
rom.extend(asm("PUSH_I32 0\nHOSTCALL 0")); rom.extend(asm("PUSH_I32 0\nHOSTCALL 0"));
// --- call composer-domain sprite emission path once per frame and drop status ---
rom.extend(asm( rom.extend(asm(
"PUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 4\nPOP_N 1", "GET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 192\nMOD\nGET_GLOBAL 0\nPUSH_I32 76\nMOD\nHOSTCALL 4",
)); ));
// --- draw 500 discs --- rom.extend(asm("PUSH_I32 0\nSET_LOCAL 0"));
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); let row_loop_start = rom.len() as u32;
let disc_loop_start = rom.len() as u32; rom.extend(asm("GET_LOCAL 0\nPUSH_I32 16\nLT"));
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 500\nLT")); let jif_row_end_offset = rom.len() + 2;
let jif_disc_end_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0")); rom.extend(asm("JMP_IF_FALSE 0"));
// x = (t * (i+7) + i * 13) % 320
rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 7\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 13\nMUL\nADD\nPUSH_I32 320\nMOD"));
// y = (t * (i+11) + i * 17) % 180
rom.extend(asm("GET_GLOBAL 0\nGET_LOCAL 1\nPUSH_I32 11\nADD\nMUL\nGET_LOCAL 1\nPUSH_I32 17\nMUL\nADD\nPUSH_I32 180\nMOD"));
// r = ( (i*13) % 20 ) + 5
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 13\nMUL\nPUSH_I32 20\nMOD\nPUSH_I32 5\nADD"));
// border color = (i * 1234) & 0xFFFF
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1234\nMUL\nPUSH_I32 65535\nBIT_AND"));
// fill color = (i * 5678 + t) & 0xFFFF
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 5678\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 65535\nBIT_AND"));
// HOSTCALL gfx.draw_disc (x, y, r, border, fill)
rom.extend(asm("HOSTCALL 1"));
// i++
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
let jmp_disc_loop_offset = rom.len() + 2;
rom.extend(asm("JMP 0"));
let disc_loop_end = rom.len() as u32;
// --- draw 20 texts ---
rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1")); rom.extend(asm("PUSH_I32 0\nSET_LOCAL 1"));
let text_loop_start = rom.len() as u32; let col_loop_start = rom.len() as u32;
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 20\nLT")); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 32\nLT"));
let jif_text_end_offset = rom.len() + 2; let jif_col_end_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0")); rom.extend(asm("JMP_IF_FALSE 0"));
// x = (t * 3 + i * 40) % 320
rom.extend(asm( rom.extend(asm(
"GET_GLOBAL 0\nPUSH_I32 3\nMUL\nGET_LOCAL 1\nPUSH_I32 40\nMUL\nADD\nPUSH_I32 320\nMOD", "PUSH_I32 0\n\
GET_LOCAL 0\nPUSH_I32 32\nMUL\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 15\nMOD\nPUSH_I32 1\nADD\n\
GET_LOCAL 1\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 320\nMOD\n\
GET_LOCAL 0\nPUSH_I32 10\nMUL\nGET_GLOBAL 0\nPUSH_I32 2\nMUL\nPUSH_I32 10\nMOD\nADD\nPUSH_I32 180\nMOD\n\
GET_LOCAL 0\nGET_LOCAL 1\nADD\nGET_GLOBAL 0\nADD\nPUSH_I32 4\nMOD\n\
PUSH_I32 0\n\
GET_LOCAL 1\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
GET_LOCAL 0\nGET_GLOBAL 0\nADD\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ\n\
GET_LOCAL 0\nGET_LOCAL 1\nADD\nPUSH_I32 4\nMOD\n\
HOSTCALL 5\nPOP_N 1",
)); ));
// y = (i * 30 + t) % 180
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 30\nMUL\nGET_GLOBAL 0\nADD\nPUSH_I32 180\nMOD"));
// string (toggle between "stress" and "frame")
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nBIT_AND\nPUSH_I32 0\nNEQ"));
let jif_text_alt_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm("PUSH_CONST 0")); // "stress"
let jmp_text_join_offset = rom.len() + 2;
rom.extend(asm("JMP 0"));
let text_alt_target = rom.len() as u32;
rom.extend(asm("PUSH_CONST 1")); // "frame"
let text_join_target = rom.len() as u32;
// color = (t * 10 + i * 1000) & 0xFFFF
rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 10\nMUL\nGET_LOCAL 1\nPUSH_I32 1000\nMUL\nADD\nPUSH_I32 65535\nBIT_AND"));
// HOSTCALL gfx.draw_text (x, y, str, color)
rom.extend(asm("HOSTCALL 2"));
// i++
rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1")); rom.extend(asm("GET_LOCAL 1\nPUSH_I32 1\nADD\nSET_LOCAL 1"));
let jmp_text_loop_offset = rom.len() + 2; let jmp_col_loop_offset = rom.len() + 2;
rom.extend(asm("JMP 0")); rom.extend(asm("JMP 0"));
let text_loop_end = rom.len() as u32; let col_loop_end = rom.len() as u32;
rom.extend(asm("GET_LOCAL 0\nPUSH_I32 1\nADD\nSET_LOCAL 0"));
let jmp_row_loop_offset = rom.len() + 2;
rom.extend(asm("JMP 0"));
let row_loop_end = rom.len() as u32;
rom.extend(asm(
"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")); rom.extend(asm("GET_GLOBAL 0\nPUSH_I32 60\nMOD\nPUSH_I32 0\nEQ"));
let jif_log_offset = rom.len() + 2; let jif_log_offset = rom.len() + 2;
rom.extend(asm("JMP_IF_FALSE 0")); rom.extend(asm("JMP_IF_FALSE 0"));
rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 3")); rom.extend(asm("PUSH_I32 2\nPUSH_CONST 0\nHOSTCALL 2"));
let after_log = rom.len() as u32; let after_log = rom.len() as u32;
// --- end of function ---
rom.extend(asm("FRAME_SYNC\nRET")); rom.extend(asm("FRAME_SYNC\nRET"));
// --- Patch jump targets ---
let patch = |buf: &mut Vec<u8>, imm_offset: usize, target: u32| { let patch = |buf: &mut Vec<u8>, imm_offset: usize, target: u32| {
buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes()); buf[imm_offset..imm_offset + 4].copy_from_slice(&target.to_le_bytes());
}; };
patch(rom, jif_disc_end_offset, disc_loop_end); patch(rom, jif_bind_done_offset, bind_done_target);
patch(rom, jmp_disc_loop_offset, disc_loop_start); patch(rom, jif_row_end_offset, row_loop_end);
patch(rom, jif_col_end_offset, col_loop_end);
patch(rom, jif_text_end_offset, text_loop_end); patch(rom, jmp_col_loop_offset, col_loop_start);
patch(rom, jif_text_alt_offset, text_alt_target); patch(rom, jmp_row_loop_offset, row_loop_start);
patch(rom, jmp_text_join_offset, text_join_target);
patch(rom, jmp_text_loop_offset, text_loop_start);
patch(rom, jif_log_offset, after_log); patch(rom, jif_log_offset, after_log);
} }
fn build_assets_pack() -> Result<Vec<u8>> {
let (glyph_entry, glyph_payload) = build_glyph_asset();
let scene = build_scene_bank();
let scene_payload = encode_scene_payload(&scene);
let scene_entry = AssetEntry {
asset_id: 1,
asset_name: "stress_scene".into(),
bank_type: BankType::SCENE,
offset: glyph_payload.len() as u64,
size: scene_payload.len() as u64,
decoded_size: expected_scene_decoded_size(&scene) as u64,
codec: AssetCodec::None,
metadata: serde_json::json!({}),
};
let asset_table = vec![glyph_entry, scene_entry];
let preload =
vec![PreloadEntry { asset_id: 0, slot: 0 }, PreloadEntry { asset_id: 1, slot: 0 }];
let payload_len = glyph_payload.len() + scene_payload.len();
let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })?;
let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64;
let prelude = AssetsPackPrelude {
magic: ASSETS_PA_MAGIC,
schema_version: ASSETS_PA_SCHEMA_VERSION,
header_len: header.len() as u32,
payload_offset,
flags: 0,
reserved: 0,
header_checksum: 0,
};
let mut bytes = prelude.to_bytes().to_vec();
bytes.extend_from_slice(&header);
bytes.extend_from_slice(&glyph_payload);
bytes.extend_from_slice(&scene_payload);
debug_assert_eq!(bytes.len(), payload_offset as usize + payload_len);
Ok(bytes)
}
fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
let pixel_indices = vec![1_u8; 8 * 8];
let mut payload = pack_4bpp(&pixel_indices);
payload.extend_from_slice(&build_palette_bytes());
let entry = AssetEntry {
asset_id: 0,
asset_name: "stress_square".into(),
bank_type: BankType::GLYPH,
offset: 0,
size: payload.len() as u64,
decoded_size: (8 * 8 + GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2)
as u64,
codec: AssetCodec::None,
metadata: serde_json::json!({
"tile_size": 8,
"width": 8,
"height": 8,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
}),
};
(entry, payload)
}
fn build_palette_bytes() -> Vec<u8> {
let mut bytes =
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 2);
for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 {
for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE {
let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK };
bytes.extend_from_slice(&color.raw().to_le_bytes());
}
}
bytes
}
fn stress_color(palette_id: usize) -> Color {
let r = ((palette_id * 53) % 256) as u8;
let g = ((palette_id * 97 + 64) % 256) as u8;
let b = ((palette_id * 29 + 128) % 256) as u8;
Color::rgb(r, g, b)
}
fn pack_4bpp(indices: &[u8]) -> Vec<u8> {
let mut packed = Vec::with_capacity(indices.len().div_ceil(2));
for chunk in indices.chunks(2) {
let hi = chunk[0] & 0x0f;
let lo = chunk.get(1).copied().unwrap_or(0) & 0x0f;
packed.push((hi << 4) | lo);
}
packed
}
fn build_scene_bank() -> SceneBank {
let mut layers = std::array::from_fn(|layer_index| {
let mut tiles = vec![
Tile {
active: false,
glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 },
flip_x: false,
flip_y: false,
};
64 * 32
];
for step in 0..8 {
let x = 4 + step * 7 + layer_index * 2;
let y = 2 + step * 3 + layer_index * 2;
let index = y * 64 + x;
tiles[index] = Tile {
active: true,
glyph: Glyph { glyph_id: 0, palette_id: layer_index as u8 + 1 },
flip_x: false,
flip_y: false,
};
}
SceneLayer {
active: true,
glyph_bank_id: 0,
tile_size: TileSize::Size8,
parallax_factor: match layer_index {
0 => ParallaxFactor { x: 1.0, y: 1.0 },
1 => ParallaxFactor { x: 0.75, y: 0.75 },
2 => ParallaxFactor { x: 0.5, y: 0.5 },
_ => ParallaxFactor { x: 0.25, y: 0.25 },
},
tilemap: TileMap { width: 64, height: 32, tiles },
}
});
// Keep the farthest layer a bit sparser so the diagonal remains visually readable.
for step in 0..4 {
let x = 10 + step * 12;
let y = 4 + step * 5;
let index = y * 64 + x;
layers[3].tilemap.tiles[index].active = false;
}
SceneBank { layers }
}
fn expected_scene_decoded_size(scene: &SceneBank) -> usize {
scene
.layers
.iter()
.map(|layer| {
SCENE_DECODED_LAYER_OVERHEAD_BYTES_V1 + layer.tilemap.tiles.len() * size_of::<Tile>()
})
.sum()
}
fn encode_scene_payload(scene: &SceneBank) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&SCENE_PAYLOAD_MAGIC_V1);
data.extend_from_slice(&SCENE_PAYLOAD_VERSION_V1.to_le_bytes());
data.extend_from_slice(&(SCENE_LAYER_COUNT_V1 as u16).to_le_bytes());
data.extend_from_slice(&0_u32.to_le_bytes());
for layer in &scene.layers {
let layer_flags = if layer.active { 0b0000_0001 } else { 0 };
data.push(layer_flags);
data.push(layer.glyph_bank_id);
data.push(layer.tile_size as u8);
data.push(0);
data.extend_from_slice(&layer.parallax_factor.x.to_le_bytes());
data.extend_from_slice(&layer.parallax_factor.y.to_le_bytes());
data.extend_from_slice(&(layer.tilemap.width as u32).to_le_bytes());
data.extend_from_slice(&(layer.tilemap.height as u32).to_le_bytes());
data.extend_from_slice(&(layer.tilemap.tiles.len() as u32).to_le_bytes());
data.extend_from_slice(&0_u32.to_le_bytes());
for tile in &layer.tilemap.tiles {
let mut tile_flags = 0_u8;
if tile.active {
tile_flags |= 0b0000_0001;
}
if tile.flip_x {
tile_flags |= 0b0000_0010;
}
if tile.flip_y {
tile_flags |= 0b0000_0100;
}
data.push(tile_flags);
data.push(tile.glyph.palette_id);
data.extend_from_slice(&tile.glyph.glyph_id.to_le_bytes());
}
}
data
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn assets_pack_contains_preloaded_glyph_and_scene_assets() {
let bytes = build_assets_pack().expect("assets pack");
let prelude =
AssetsPackPrelude::from_bytes(&bytes[..ASSETS_PA_PRELUDE_SIZE]).expect("prelude");
assert_eq!(prelude.magic, ASSETS_PA_MAGIC);
assert_eq!(prelude.schema_version, ASSETS_PA_SCHEMA_VERSION);
let header_start = ASSETS_PA_PRELUDE_SIZE;
let header_end = header_start + prelude.header_len as usize;
let header: AssetsPackHeader =
serde_json::from_slice(&bytes[header_start..header_end]).expect("header");
assert_eq!(header.asset_table.len(), 2);
assert_eq!(header.preload.len(), 2);
assert_eq!(header.asset_table[0].bank_type, BankType::GLYPH);
assert_eq!(header.asset_table[1].bank_type, BankType::SCENE);
assert_eq!(header.preload[0].slot, 0);
assert_eq!(header.preload[1].slot, 0);
assert_eq!(header.asset_table[0].offset, 0);
assert_eq!(header.asset_table[1].offset, header.asset_table[0].size);
assert_eq!(
bytes.len(),
prelude.payload_offset as usize
+ header.asset_table[0].size as usize
+ header.asset_table[1].size as usize
);
}
}

View File

@ -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-0023","status":"done","ticket":"perf-full-migration-to-atomic-telemetry","title":"Agenda - [PERF] Full Migration to Atomic Telemetry","created_at":"2026-04-10","updated_at":"2026-04-10","tags":["perf","runtime","telemetry"],"agendas":[{"id":"AGD-0021","file":"workflow/agendas/AGD-0021-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"decisions":[{"id":"DEC-0008","file":"workflow/decisions/DEC-0008-full-migration-to-atomic-telemetry.md","status":"accepted","created_at":"2026-04-10","updated_at":"2026-04-10"}],"plans":[{"id":"PLN-0007","file":"workflow/plans/PLN-0007-full-migration-to-atomic-telemetry.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}],"lessons":[{"id":"LSN-0028","file":"lessons/DSC-0023-perf-full-migration-to-atomic-telemetry/LSN-0028-converging-to-single-atomic-telemetry-source.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]} {"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
@ -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-0025","status":"done","ticket":"scene-bank-and-viewport-cache-refactor","title":"Scene Bank and Viewport Cache Refactor","created_at":"2026-04-11","updated_at":"2026-04-14","tags":["gfx","tilemap","runtime","render"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0030","file":"lessons/DSC-0025-scene-bank-and-viewport-cache-refactor/LSN-0030-canonical-scene-cache-and-resolver-split.md","status":"done","created_at":"2026-04-14","updated_at":"2026-04-14"}]}
{"type":"discussion","id":"DSC-0026","status":"open","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["gfx","runtime","render","camera","scene"],"agendas":[{"id":"AGD-0026","file":"workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15"}],"decisions":[{"id":"DEC-0014","file":"workflow/decisions/DEC-0014-frame-composer-render-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0026"}],"plans":[{"id":"PLN-0017","file":"workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0018","file":"workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0019","file":"workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0020","file":"workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0021","file":"workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]}],"lessons":[]} {"type":"discussion","id":"DSC-0026","status":"open","ticket":"render-all-scene-cache-and-camera-integration","title":"Integrate render_all with Scene Cache and Camera","created_at":"2026-04-14","updated_at":"2026-04-15","tags":["gfx","runtime","render","camera","scene"],"agendas":[{"id":"AGD-0026","file":"workflow/agendas/AGD-0026-render-all-scene-cache-and-camera-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15"}],"decisions":[{"id":"DEC-0014","file":"workflow/decisions/DEC-0014-frame-composer-render-integration.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_agenda":"AGD-0026"}],"plans":[{"id":"PLN-0017","file":"workflow/plans/PLN-0017-frame-composer-core-and-hardware-ownership.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0018","file":"workflow/plans/PLN-0018-sprite-controller-and-frame-emission-model.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-14","ref_decisions":["DEC-0014"]},{"id":"PLN-0019","file":"workflow/plans/PLN-0019-scene-binding-camera-and-scene-status.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0020","file":"workflow/plans/PLN-0020-cache-refresh-and-render-frame-path.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]},{"id":"PLN-0021","file":"workflow/plans/PLN-0021-service-retirement-callsite-migration-and-regression.md","status":"accepted","created_at":"2026-04-14","updated_at":"2026-04-15","ref_decisions":["DEC-0014"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]} {"type":"discussion","id":"DSC-0027","status":"open","ticket":"frame-composer-public-syscall-surface","title":"Agenda - FrameComposer Public Syscall Surface","created_at":"2026-04-17","updated_at":"2026-04-17","tags":["gfx","runtime","syscall","abi","frame-composer","scene","camera","sprites"],"agendas":[{"id":"AGD-0027","file":"workflow/agendas/AGD-0027-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17"}],"decisions":[{"id":"DEC-0015","file":"workflow/decisions/DEC-0015-frame-composer-public-syscall-surface.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_agenda":"AGD-0027"}],"plans":[{"id":"PLN-0022","file":"workflow/plans/PLN-0022-composer-syscall-domain-and-spec-propagation.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0023","file":"workflow/plans/PLN-0023-composer-runtime-dispatch-and-legacy-removal.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0024","file":"workflow/plans/PLN-0024-composer-cartridge-tooling-and-regression-migration.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]},{"id":"PLN-0025","file":"workflow/plans/PLN-0025-final-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-17","updated_at":"2026-04-17","ref_decisions":["DEC-0015"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0028","status":"open","ticket":"deferred-overlay-and-primitive-composition","title":"Deferred Overlay and Primitive Composition over FrameComposer","created_at":"2026-04-18","updated_at":"2026-04-18","tags":["gfx","runtime","render","frame-composer","overlay","primitives","hud"],"agendas":[{"id":"AGD-0028","file":"workflow/agendas/AGD-0028-deferred-overlay-and-primitive-composition.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18"}],"decisions":[{"id":"DEC-0016","file":"workflow/decisions/DEC-0016-deferred-gfx-overlay-outside-frame-composer.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_agenda":"AGD-0028"}],"plans":[{"id":"PLN-0026","file":"workflow/plans/PLN-0026-gfx-overlay-contract-and-spec-propagation.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0027","file":"workflow/plans/PLN-0027-deferred-gfx-overlay-subsystem.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0028","file":"workflow/plans/PLN-0028-runtime-frame-end-overlay-integration-and-parity.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]},{"id":"PLN-0029","file":"workflow/plans/PLN-0029-final-overlay-ci-validation-and-polish.md","status":"accepted","created_at":"2026-04-18","updated_at":"2026-04-18","ref_decisions":["DEC-0016"]}],"lessons":[]}
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]} {"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]} {"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}

View File

@ -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.

View File

@ -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`.

View File

@ -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`.

View File

@ -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.*`.

View File

@ -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.

View File

@ -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.

Binary file not shown.

View File

@ -5,5 +5,5 @@
"title": "Stress Console", "title": "Stress Console",
"app_version": "0.1.0", "app_version": "0.1.0",
"app_mode": "Game", "app_mode": "Game",
"capabilities": ["gfx", "log"] "capabilities": ["gfx", "log", "asset"]
} }