diff --git a/crates/console/prometeu-drivers/src/gfx.rs b/crates/console/prometeu-drivers/src/gfx.rs index bcd69f66..e984a3d1 100644 --- a/crates/console/prometeu-drivers/src/gfx.rs +++ b/crates/console/prometeu-drivers/src/gfx.rs @@ -812,10 +812,14 @@ impl Gfx { let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x }; let fetch_y = if tile.entry.flip_y() { size - 1 - local_y } else { local_y }; let px_index = tile.bank.get_pixel_index(tile.entry.glyph_id, fetch_x, fetch_y); - let color = tile - .bank - .resolve_color(tile.entry.palette_id, px_index) - .unwrap_or(Color::TRANSPARENT); + let color = tile.bank.resolve_color(tile.entry.palette_id, px_index).unwrap_or_else( + || { + panic!( + "SCENE composition fatal: palette_id {} is not resident for glyph asset {}", + tile.entry.palette_id, tile.entry.glyph_asset_id + ) + }, + ); if color.alpha() == 0 { continue; } @@ -865,9 +869,14 @@ impl Gfx { let fetch_y = if sprite.flip_y { size - 1 - local_y } else { local_y }; let px_index = bank.get_pixel_index(sprite.glyph.glyph_id, fetch_x, fetch_y); - let color = bank - .resolve_color(sprite.glyph.palette_id, px_index) - .unwrap_or(Color::TRANSPARENT); + let color = bank.resolve_color(sprite.glyph.palette_id, px_index).unwrap_or_else( + || { + panic!( + "SPRITE composition fatal: palette_id {} is not resident for glyph bank {}", + sprite.glyph.palette_id, sprite.bank_id + ) + }, + ); if color.alpha() == 0 { continue; } @@ -1162,6 +1171,26 @@ mod tests { assert_eq!(back[0], Color::RED.raw()); } + #[test] + #[should_panic(expected = "SPRITE composition fatal: palette_id 1 is not resident")] + fn sprite_draw_panics_for_invalid_palette_reference() { + let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::GREEN)]); + let mut back = vec![Color::BLACK.raw(); 8 * 8]; + let sprite = Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 1 }, + x: 0, + y: 0, + layer: 0, + flip_x: false, + flip_y: false, + bank_id: 0, + active: true, + priority: 0, + }; + + Gfx::draw_sprite_pixel_by_pixel(&mut back, 8, 8, &sprite, &bank); + } + #[test] fn test_cached_tile_draws_opaque_color_index_zero() { let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::GREEN)]); @@ -1204,6 +1233,26 @@ mod tests { assert_eq!(target.back[0], Color::RED.raw()); } + #[test] + #[should_panic(expected = "SCENE composition fatal: palette_id 1 is not resident")] + fn cached_tile_draw_panics_for_invalid_palette_reference() { + let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::GREEN)]); + let mut back = vec![Color::BLACK.raw(); 8 * 8]; + let mut target = RenderTarget { back: &mut back, screen_w: 8, screen_h: 8 }; + let entry = CachedTileEntry { + active: true, + glyph_id: 0, + palette_id: 1, + flags: 0, + glyph_asset_id: 0, + }; + + Gfx::draw_cached_tile_pixels( + &mut target, + CachedTileDraw { x: 0, y: 0, entry, bank: &bank, tile_size: TileSize::Size8 }, + ); + } + #[test] fn test_draw_rect() { let banks = Arc::new(MemoryBanks::new()); diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index dce0cd0b..bb375591 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -118,7 +118,11 @@ impl Game2DFrameComposer for Hardware { } fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus { - if self.gfx.glyph_banks.glyph_bank_slot(sprite.bank_id as usize).is_none() { + let Some(bank) = self.gfx.glyph_banks.glyph_bank_slot(sprite.bank_id as usize) else { + return prometeu_hal::ComposerOpStatus::BankInvalid; + }; + + if !bank.contains_palette(sprite.glyph.palette_id) { return prometeu_hal::ComposerOpStatus::BankInvalid; } @@ -433,4 +437,28 @@ mod tests { assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw()); } + + #[test] + fn emit_sprite_rejects_palette_id_outside_loaded_glyph_bank() { + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(make_glyph_bank())); + let mut hardware = Hardware::new_with_memory_banks(banks); + + let status = Game2DFrameComposer::emit_sprite( + &mut hardware, + Sprite { + glyph: Glyph { glyph_id: 0, palette_id: 1 }, + x: 0, + y: 0, + layer: 0, + bank_id: 0, + active: false, + flip_x: false, + flip_y: false, + priority: 0, + }, + ); + + assert_eq!(status, prometeu_hal::ComposerOpStatus::BankInvalid); + } } diff --git a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs index 7fedd15a..4e81ca75 100644 --- a/crates/console/prometeu-system/src/services/vm_runtime/tests.rs +++ b/crates/console/prometeu-system/src/services/vm_runtime/tests.rs @@ -1279,6 +1279,53 @@ fn tick_composer_emit_sprite_operational_error_returns_status_not_crash() { assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]); } +#[test] +fn tick_composer_emit_sprite_invalid_palette_returns_bank_invalid() { + let mut runtime = VirtualMachineRuntime::new(None); + let mut log_service = LogService::new(4096); + let mut fs = VirtualFS::new(); + let mut fs_state = FsState::Unmounted; + let mut memcard = MemcardService::new(); + let mut open_files: HashMap = HashMap::new(); + let mut next_handle = 1; + let mut vm = VirtualMachine::default(); + let banks = Arc::new(MemoryBanks::new()); + banks.install_glyph_bank(0, Arc::new(runtime_test_glyph_bank(TileSize::Size8, 0, Color::BLUE))); + let mut platform = TestPlatform::new_with_memory_banks(banks); + let signals = InputSignals::default(); + let code = assemble( + "PUSH_I32 0\nPUSH_I32 1\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_I32 0\nPUSH_BOOL 0\nPUSH_BOOL 0\nPUSH_I32 0\nHOSTCALL 0\nHALT", + ) + .expect("assemble"); + let program = serialized_single_function_module( + code, + vec![SyscallDecl { + module: "composer".into(), + name: "emit_sprite".into(), + version: 1, + arg_slots: 9, + ret_slots: 1, + }], + ); + let cartridge = cartridge_with_program(program, caps::GFX); + + runtime.initialize_vm(&mut log_service, &mut vm, &cartridge).expect("runtime must initialize"); + let report = runtime.tick( + &mut log_service, + &mut fs, + &mut fs_state, + &mut memcard, + &mut open_files, + &mut next_handle, + &mut vm, + &signals, + &mut platform, + ); + assert!(report.is_none(), "invalid palette must not crash VM execution"); + assert!(vm.is_halted()); + assert_eq!(vm.operand_stack_top(1), vec![Value::Int64(ComposerOpStatus::BankInvalid as i64)]); +} + #[test] fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() { let mut runtime = VirtualMachineRuntime::new(None); diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 82002b15..08a69a3c 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -44,4 +44,4 @@ {"type":"discussion","id":"DSC-0033","status":"done","ticket":"system-os-service-ownership-and-module-layout","title":"Agenda - SystemOS Service Ownership and Module Layout","created_at":"2026-05-14","updated_at":"2026-05-15","tags":["runtime","os","services","module-layout","vm","window-manager","logging"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0042","file":"discussion/lessons/DSC-0033-system-os-service-ownership-and-module-layout/LSN-0042-systemos-service-ownership-boundary.md","status":"done","created_at":"2026-05-15","updated_at":"2026-05-15"}]} {"type":"discussion","id":"DSC-0036","status":"done","ticket":"prometeu-hub-ui-direction","title":"Agenda - Prometeu Hub UI Direction","created_at":"2026-05-15","updated_at":"2026-05-22","tags":["hub","ui","shell","system-apps","lifecycle","design-system"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0045","file":"discussion/lessons/DSC-0036-prometeu-hub-ui-direction/LSN-0045-hub-ui-slices-should-prove-os-boundaries.md","status":"done","created_at":"2026-05-22","updated_at":"2026-05-22"}]} {"type":"discussion","id":"DSC-0037","status":"done","ticket":"rgba8888-framebuffer-and-pixel-format-direction","title":"Agenda - RGBA8888 Framebuffer and Pixel Format Direction","created_at":"2026-05-22","updated_at":"2026-05-23","tags":["gfx","framebuffer","rgb565","rgba8888","renderer","assets","host","backend"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0046","file":"discussion/lessons/DSC-0037-rgba8888-framebuffer-and-pixel-format-direction/LSN-0046-pixel-format-contracts-must-move-as-one-surface.md","status":"done","created_at":"2026-05-23","updated_at":"2026-05-23"}]} -{"type":"discussion","id":"DSC-0046","status":"in_progress","ticket":"runtime-owned-variable-glyph-bank-palette-protocol","title":"Runtime-Owned Variable Glyph Bank Palette Protocol","created_at":"2026-07-14","updated_at":"2026-07-14","tags":["runtime","gfx","assets","glyph-bank","palette-serialization","protocol"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-runtime-owned-variable-glyph-bank-palette-protocol.md","status":"accepted","created_at":"2026-07-14","updated_at":"2026-07-14"}],"decisions":[{"id":"DEC-0041","file":"DEC-0041-variable-glyph-bank-palette-protocol.md","status":"accepted","created_at":"2026-07-14","updated_at":"2026-07-14","ref_agenda":"AGD-0049"}],"plans":[{"id":"PLN-0167","file":"PLN-0167-spec-contract-update-for-variable-glyph-palettes.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0168","file":"PLN-0168-glyphbank-variable-palette-resident-model.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0169","file":"PLN-0169-asset-decode-validation-for-variable-glyph-palettes.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0170","file":"PLN-0170-composer-palette-reference-failure-semantics.md","status":"open","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0171","file":"PLN-0171-variable-glyph-palette-tests-fixtures-and-residue-scan.md","status":"open","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0172","file":"PLN-0172-runtime-spec-handoff-to-packer-and-studio.md","status":"open","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]}],"lessons":[]} +{"type":"discussion","id":"DSC-0046","status":"in_progress","ticket":"runtime-owned-variable-glyph-bank-palette-protocol","title":"Runtime-Owned Variable Glyph Bank Palette Protocol","created_at":"2026-07-14","updated_at":"2026-07-14","tags":["runtime","gfx","assets","glyph-bank","palette-serialization","protocol"],"agendas":[{"id":"AGD-0049","file":"AGD-0049-runtime-owned-variable-glyph-bank-palette-protocol.md","status":"accepted","created_at":"2026-07-14","updated_at":"2026-07-14"}],"decisions":[{"id":"DEC-0041","file":"DEC-0041-variable-glyph-bank-palette-protocol.md","status":"accepted","created_at":"2026-07-14","updated_at":"2026-07-14","ref_agenda":"AGD-0049"}],"plans":[{"id":"PLN-0167","file":"PLN-0167-spec-contract-update-for-variable-glyph-palettes.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0168","file":"PLN-0168-glyphbank-variable-palette-resident-model.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0169","file":"PLN-0169-asset-decode-validation-for-variable-glyph-palettes.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0170","file":"PLN-0170-composer-palette-reference-failure-semantics.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0171","file":"PLN-0171-variable-glyph-palette-tests-fixtures-and-residue-scan.md","status":"open","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]},{"id":"PLN-0172","file":"PLN-0172-runtime-spec-handoff-to-packer-and-studio.md","status":"open","created_at":"2026-07-14","updated_at":"2026-07-14","ref_decisions":["DEC-0041"]}],"lessons":[]} diff --git a/discussion/workflow/plans/PLN-0170-composer-palette-reference-failure-semantics.md b/discussion/workflow/plans/PLN-0170-composer-palette-reference-failure-semantics.md index 736d0df8..4a74f106 100644 --- a/discussion/workflow/plans/PLN-0170-composer-palette-reference-failure-semantics.md +++ b/discussion/workflow/plans/PLN-0170-composer-palette-reference-failure-semantics.md @@ -2,7 +2,8 @@ id: PLN-0170 ticket: runtime-owned-variable-glyph-bank-palette-protocol title: Composer Palette Reference Failure Semantics -status: open +status: done +completed: 2026-07-14 created: 2026-07-14 ref_decisions: [DEC-0041] tags: [runtime, gfx, composer, scene, sprite, validation] diff --git a/docs/specs/runtime/04-gfx-peripheral.md b/docs/specs/runtime/04-gfx-peripheral.md index b6f8105f..a64ada99 100644 --- a/docs/specs/runtime/04-gfx-peripheral.md +++ b/docs/specs/runtime/04-gfx-peripheral.md @@ -771,7 +771,8 @@ Rules: - missing glyph dependencies referenced by a resident scene are not a passive `status:int` case; - if scene activation discovers that a layer dependency cannot be resolved to a committed glyph asset, the machine MUST fail fatally and emit a clear log; - if scene composition later discovers that a layer dependency can no longer be resolved, the machine MUST fail fatally and emit a clear log; -- if scene activation or composition discovers that a tile references `palette_id >= palette_count` for its resolved glyph bank, the machine MUST fail explicitly and MUST NOT substitute transparent, black, or default color; +- if scene activation or composition discovers that a tile references `palette_id >= palette_count` for its resolved glyph bank, the machine MUST fail fatally and emit a clear log; +- invalid scene palette references are scene dependency failures and MUST NOT be rendered through transparent, black, or default color substitution; - runtime MUST NOT continue canonical scene composition after such a dependency failure. ### 20.2 `composer.emit_sprite` @@ -803,5 +804,6 @@ Operational notes: - the canonical public sprite contract is frame-emission based; - no caller-provided sprite index exists in the v1 canonical ABI; - no `active` flag exists in the v1 canonical ABI; -- `palette_id >= palette_count` for the referenced glyph bank is an explicit invalid palette reference and MUST NOT be rendered through transparent, black, or default color substitution; +- `palette_id >= palette_count` for the referenced glyph bank MUST return `BANK_INVALID`; +- invalid sprite palette references MUST NOT be rendered through transparent, black, or default color substitution; - overflow remains non-fatal and must not escalate to trap in v1.