dev/runtime-owned-variable-glyph-bank-palette-protocol #37

Merged
bquarkz merged 9 commits from dev/runtime-owned-variable-glyph-bank-palette-protocol into master 2026-07-14 15:25:05 +00:00
6 changed files with 139 additions and 12 deletions
Showing only changes of commit 4c0f1cb9ad - Show all commits

View File

@ -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());

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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":[]}

View File

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

View File

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