diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 493eccd5..eaf9b204 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -191,10 +191,10 @@ impl GlyphAssetSlotIndex { } } -const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64; +const GLYPH_BANK_MAX_PALETTE_COUNT_V1: usize = 64; const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16; -const GLYPH_BANK_PALETTE_BYTES_V1: usize = - GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * size_of::(); +const GLYPH_BANK_PALETTE_BYTES_PER_PALETTE: usize = + GLYPH_BANK_COLORS_PER_PALETTE * size_of::(); /// Resident metadata for a decoded/materialized asset inside a BankPolicy. #[derive(Debug)] @@ -599,7 +599,7 @@ impl AssetBridge for AssetManager { impl AssetManager { fn decode_glyph_bank_layout( entry: &AssetEntry, - ) -> Result<(TileSize, usize, usize, usize), String> { + ) -> Result<(TileSize, usize, usize, usize, usize, usize), String> { let meta = entry.metadata_as_glyph_bank()?; let tile_size = match meta.tile_size { @@ -609,7 +609,8 @@ impl AssetManager { _ => return Err(format!("Invalid tile_size: {}", meta.tile_size)), }; - if meta.palette_count as usize != GLYPH_BANK_PALETTE_COUNT_V1 { + let palette_count = meta.palette_count as usize; + if !(1..=GLYPH_BANK_MAX_PALETTE_COUNT_V1).contains(&palette_count) { return Err(format!("Invalid palette_count: {}", meta.palette_count)); } @@ -618,11 +619,14 @@ impl AssetManager { let logical_pixels = width.checked_mul(height).ok_or("GlyphBank dimensions overflow")?; let serialized_pixel_bytes = logical_pixels.div_ceil(2); + let palette_bytes = palette_count + .checked_mul(GLYPH_BANK_PALETTE_BYTES_PER_PALETTE) + .ok_or("GlyphBank palette size overflow")?; let serialized_size = serialized_pixel_bytes - .checked_add(GLYPH_BANK_PALETTE_BYTES_V1) + .checked_add(palette_bytes) .ok_or("GlyphBank serialized size overflow")?; let decoded_size = logical_pixels - .checked_add(GLYPH_BANK_PALETTE_BYTES_V1) + .checked_add(palette_bytes) .ok_or("GlyphBank decoded size overflow")?; if entry.size != serialized_size as u64 { @@ -639,7 +643,7 @@ impl AssetManager { )); } - Ok((tile_size, width, height, serialized_pixel_bytes)) + Ok((tile_size, width, height, serialized_pixel_bytes, palette_count, palette_bytes)) } fn unpack_glyph_bank_pixels(packed_pixels: &[u8], logical_pixels: usize) -> Vec { @@ -1200,8 +1204,9 @@ impl AssetManager { entry: &AssetEntry, buffer: &[u8], ) -> Result { - let (tile_size, width, height, packed_pixel_bytes) = Self::decode_glyph_bank_layout(entry)?; - if buffer.len() < packed_pixel_bytes + GLYPH_BANK_PALETTE_BYTES_V1 { + let (tile_size, width, height, packed_pixel_bytes, palette_count, palette_bytes) = + Self::decode_glyph_bank_layout(entry)?; + if buffer.len() < packed_pixel_bytes + palette_bytes { return Err("Buffer too small for GLYPHBANK".to_string()); } @@ -1209,10 +1214,10 @@ impl AssetManager { let packed_pixels = &buffer[0..packed_pixel_bytes]; let pixel_indices = Self::unpack_glyph_bank_pixels(packed_pixels, logical_pixels); let palette_data = - &buffer[packed_pixel_bytes..packed_pixel_bytes + GLYPH_BANK_PALETTE_BYTES_V1]; + &buffer[packed_pixel_bytes..packed_pixel_bytes + palette_bytes]; let mut palettes = - vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1]; + vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; palette_count]; for (p, pal) in palettes.iter_mut().enumerate() { for (c, slot) in pal.iter_mut().enumerate() { let offset = (p * 16 + c) * 4; @@ -1232,7 +1237,8 @@ impl AssetManager { entry: &AssetEntry, reader: &mut impl Read, ) -> Result { - let (tile_size, width, height, packed_pixel_bytes) = Self::decode_glyph_bank_layout(entry)?; + let (tile_size, width, height, packed_pixel_bytes, palette_count, palette_bytes) = + Self::decode_glyph_bank_layout(entry)?; let logical_pixels = width * height; let mut packed_pixels = vec![0_u8; packed_pixel_bytes]; reader @@ -1241,13 +1247,13 @@ impl AssetManager { let pixel_indices = Self::unpack_glyph_bank_pixels(&packed_pixels, logical_pixels); - let mut palette_data = [0_u8; GLYPH_BANK_PALETTE_BYTES_V1]; + let mut palette_data = vec![0_u8; palette_bytes]; reader .read_exact(&mut palette_data) .map_err(|_| "Buffer too small for GLYPHBANK".to_string())?; let mut palettes = - vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1]; + vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; palette_count]; for (p, pal) in palettes.iter_mut().enumerate() { for (c, slot) in pal.iter_mut().enumerate() { let offset = (p * 16 + c) * 4; @@ -1883,35 +1889,56 @@ mod tests { use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; - fn expected_glyph_payload_size(width: usize, height: usize) -> usize { - (width * height).div_ceil(2) + GLYPH_BANK_PALETTE_BYTES_V1 + fn expected_glyph_payload_size(width: usize, height: usize, palette_count: usize) -> usize { + (width * height).div_ceil(2) + (palette_count * GLYPH_BANK_PALETTE_BYTES_PER_PALETTE) } - fn expected_glyph_decoded_size(width: usize, height: usize) -> usize { - width * height + GLYPH_BANK_PALETTE_BYTES_V1 + fn expected_glyph_decoded_size(width: usize, height: usize, palette_count: usize) -> usize { + width * height + (palette_count * GLYPH_BANK_PALETTE_BYTES_PER_PALETTE) } fn test_glyph_asset_data() -> Vec { + test_glyph_asset_data_with_palette_count(GLYPH_BANK_MAX_PALETTE_COUNT_V1) + } + + fn test_glyph_asset_data_with_palette_count(palette_count: usize) -> Vec { let mut data = vec![0x11u8; 128]; - data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_BYTES_V1]); + data.extend_from_slice(&vec![ + 0u8; + palette_count * GLYPH_BANK_PALETTE_BYTES_PER_PALETTE + ]); data } fn test_glyph_asset_entry(asset_name: &str, width: usize, height: usize) -> AssetEntry { + test_glyph_asset_entry_with_palette_count( + asset_name, + width, + height, + GLYPH_BANK_MAX_PALETTE_COUNT_V1, + ) + } + + fn test_glyph_asset_entry_with_palette_count( + asset_name: &str, + width: usize, + height: usize, + palette_count: usize, + ) -> AssetEntry { AssetEntry { asset_id: 0, asset_name: asset_name.to_string(), bank_type: BankType::GLYPH, offset: 0, - size: expected_glyph_payload_size(width, height) as u64, - decoded_size: expected_glyph_decoded_size(width, height) as u64, + size: expected_glyph_payload_size(width, height, palette_count) as u64, + decoded_size: expected_glyph_decoded_size(width, height, palette_count) as u64, codec: AssetCodec::None, metadata: serde_json::json!({ "tile_size": 16, "width": width, "height": height, - "palette_count": GLYPH_BANK_PALETTE_COUNT_V1, - "palette_authored": GLYPH_BANK_PALETTE_COUNT_V1 + "palette_count": palette_count, + "palette_authored": palette_count }), } } @@ -2034,22 +2061,69 @@ mod tests { #[test] fn test_decode_glyph_bank_unpacks_packed_pixels_and_reads_palette_colors() { - let entry = test_glyph_asset_entry("glyphs", 2, 2); + let entry = test_glyph_asset_entry_with_palette_count("glyphs", 2, 2, 1); let mut data = vec![0x10, 0x23]; - data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_BYTES_V1]); + data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_BYTES_PER_PALETTE]); data[2..6].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]); let bank = AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("glyph decode"); assert_eq!(bank.pixel_indices, vec![1, 0, 2, 3]); + assert_eq!(bank.palette_count(), 1); assert_eq!(bank.palettes[0][0], Color::from_raw(0x12345678)); } + #[test] + fn test_decode_glyph_bank_accepts_intermediate_palette_count() { + let entry = test_glyph_asset_entry_with_palette_count("glyphs", 16, 16, 7); + let data = test_glyph_asset_data_with_palette_count(7); + + let bank = + AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("glyph decode"); + + assert_eq!(bank.palette_count(), 7); + } + + #[test] + fn test_decode_glyph_bank_accepts_max_palette_count() { + let entry = test_glyph_asset_entry_with_palette_count( + "glyphs", + 16, + 16, + GLYPH_BANK_MAX_PALETTE_COUNT_V1, + ); + let data = test_glyph_asset_data_with_palette_count(GLYPH_BANK_MAX_PALETTE_COUNT_V1); + + let bank = + AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("glyph decode"); + + assert_eq!(bank.palette_count(), GLYPH_BANK_MAX_PALETTE_COUNT_V1); + } + + #[test] + fn test_decode_glyph_bank_reader_matches_buffer_for_variable_palette_count() { + let entry = test_glyph_asset_entry_with_palette_count("glyphs", 16, 16, 3); + let data = test_glyph_asset_data_with_palette_count(3); + + let from_buffer = + AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("buffer decode"); + let mut reader = std::io::Cursor::new(data); + let from_reader = + AssetManager::decode_glyph_bank_from_reader(&entry, &mut reader).expect("reader decode"); + + assert_eq!(from_buffer.palette_count(), from_reader.palette_count()); + assert_eq!(from_buffer.pixel_indices, from_reader.pixel_indices); + assert_eq!(from_buffer.palettes, from_reader.palettes); + } + #[test] fn test_decode_glyph_bank_rejects_short_packed_buffer() { let entry = test_glyph_asset_entry("glyphs", 16, 16); - let data = vec![0u8; expected_glyph_payload_size(16, 16) - 1]; + let data = vec![ + 0u8; + expected_glyph_payload_size(16, 16, GLYPH_BANK_MAX_PALETTE_COUNT_V1) - 1 + ]; let err = match AssetManager::decode_glyph_bank_from_buffer(&entry, &data) { Ok(_) => panic!("glyph decode should reject short buffer"), @@ -2060,9 +2134,9 @@ mod tests { } #[test] - fn test_decode_glyph_bank_requires_palette_count_64() { + fn test_decode_glyph_bank_rejects_palette_count_zero() { let mut entry = test_glyph_asset_entry("glyphs", 16, 16); - entry.metadata["palette_count"] = serde_json::json!(32); + entry.metadata["palette_count"] = serde_json::json!(0); let err = match AssetManager::decode_glyph_bank_from_buffer(&entry, &test_glyph_asset_data()) { @@ -2070,7 +2144,44 @@ mod tests { Err(err) => err, }; - assert_eq!(err, "Invalid palette_count: 32"); + assert_eq!(err, "Invalid palette_count: 0"); + } + + #[test] + fn test_decode_glyph_bank_rejects_palette_count_above_v1_max() { + let mut entry = test_glyph_asset_entry("glyphs", 16, 16); + entry.metadata["palette_count"] = serde_json::json!(65); + + let err = + match AssetManager::decode_glyph_bank_from_buffer(&entry, &test_glyph_asset_data()) { + Ok(_) => panic!("glyph decode should reject invalid palette_count"), + Err(err) => err, + }; + + assert_eq!(err, "Invalid palette_count: 65"); + } + + #[test] + fn test_decode_glyph_bank_rejects_mismatched_variable_decoded_size() { + let mut entry = test_glyph_asset_entry_with_palette_count("glyphs", 16, 16, 3); + entry.decoded_size += 1; + + let err = match AssetManager::decode_glyph_bank_from_buffer( + &entry, + &test_glyph_asset_data_with_palette_count(3), + ) { + Ok(_) => panic!("glyph decode should reject invalid decoded_size"), + Err(err) => err, + }; + + assert_eq!( + err, + format!( + "Invalid GLYPHBANK decoded_size: expected {}, got {}", + expected_glyph_decoded_size(16, 16, 3), + expected_glyph_decoded_size(16, 16, 3) + 1 + ) + ); } #[test] diff --git a/discussion/index.ndjson b/discussion/index.ndjson index 5a6c9ef3..82002b15 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":"open","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":"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":[]} diff --git a/discussion/workflow/plans/PLN-0169-asset-decode-validation-for-variable-glyph-palettes.md b/discussion/workflow/plans/PLN-0169-asset-decode-validation-for-variable-glyph-palettes.md index c44e6f66..5976ca85 100644 --- a/discussion/workflow/plans/PLN-0169-asset-decode-validation-for-variable-glyph-palettes.md +++ b/discussion/workflow/plans/PLN-0169-asset-decode-validation-for-variable-glyph-palettes.md @@ -2,7 +2,8 @@ id: PLN-0169 ticket: runtime-owned-variable-glyph-bank-palette-protocol title: Asset Decode Validation for Variable Glyph Palettes -status: open +status: done +completed: 2026-07-14 created: 2026-07-14 ref_decisions: [DEC-0041] tags: [runtime, assets, glyph-bank, decode, validation]