dev/runtime-owned-variable-glyph-bank-palette-protocol #37
@ -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());
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<u32, String> = 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);
|
||||
|
||||
@ -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":[]}
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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.
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user