Compare commits

...

8 Commits

Author SHA1 Message Date
7cced0709b
housekeep DSC-0046 variable glyph palette protocol
Some checks failed
Intrepid/Prometeu/Runtime/pipeline/head There was a failure building this commit
Intrepid/Prometeu/Runtime/pipeline/pr-master There was a failure building this commit
2026-07-14 16:08:17 +01:00
e2a5a67621
implements PLN-0172 runtime glyph palette handoff 2026-07-14 15:54:28 +01:00
fbe4d0cac0
implements PLN-0171 variable glyph palette test coverage 2026-07-14 15:53:31 +01:00
4c0f1cb9ad
implements PLN-0170 palette reference failures 2026-07-14 15:51:44 +01:00
94ffd241bd
implements PLN-0169 variable glyph palette decode 2026-07-14 15:48:57 +01:00
a415c172c7
implements PLN-0168 variable glyph bank residency 2026-07-14 15:46:50 +01:00
c89116b6f8
implements PLN-0167 variable glyph palette specs 2026-07-14 15:46:20 +01:00
dc279fe72b
Runtime-Owned Variable Glyph Bank Palette Protocol 2026-07-14 15:40:49 +01:00
14 changed files with 550 additions and 81 deletions

View File

@ -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::<u32>();
const GLYPH_BANK_PALETTE_BYTES_PER_PALETTE: usize =
GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u32>();
/// 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<u8> {
@ -1200,8 +1204,9 @@ impl AssetManager {
entry: &AssetEntry,
buffer: &[u8],
) -> Result<GlyphBank, String> {
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 =
[[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<GlyphBank, String> {
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 =
[[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<u8> {
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<u8> {
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,67 @@ 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]
fn test_decode_glyph_bank_rejects_mismatched_variable_serialized_size() {
let mut entry = test_glyph_asset_entry_with_palette_count("glyphs", 16, 16, 3);
entry.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 serialized size"),
Err(err) => err,
};
assert_eq!(
err,
format!(
"Invalid GLYPHBANK serialized size: expected {}, got {}",
expected_glyph_payload_size(16, 16, 3),
expected_glyph_payload_size(16, 16, 3) + 1
)
);
}
#[test]

View File

@ -557,8 +557,9 @@ mod tests {
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
let size = tile_size as usize;
let mut bank = GlyphBank::new(tile_size, size, size);
bank.palettes[palette_id as usize][1] = color;
let mut bank =
GlyphBank::with_palette_count(tile_size, size, size, palette_id as usize + 1);
bank.palette_mut(palette_id).unwrap()[1] = color;
for pixel in &mut bank.pixel_indices {
*pixel = 1;
}

View File

@ -812,7 +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);
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;
}
@ -862,7 +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);
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;
}
@ -1010,9 +1024,14 @@ mod tests {
fn make_glyph_bank(tile_size: TileSize, palette_colors: &[(u8, u8, Color)]) -> GlyphBank {
let size = tile_size as usize;
let mut bank = GlyphBank::new(tile_size, size, size);
let palette_count = palette_colors
.iter()
.map(|(palette_id, _, _)| *palette_id as usize + 1)
.max()
.unwrap_or(1);
let mut bank = GlyphBank::with_palette_count(tile_size, size, size, palette_count);
for (palette_id, color_index, color) in palette_colors {
bank.palettes[*palette_id as usize][*color_index as usize] = *color;
bank.palette_mut(*palette_id).unwrap()[*color_index as usize] = *color;
}
bank
}
@ -1152,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)]);
@ -1194,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;
}
@ -261,7 +265,7 @@ mod tests {
fn make_glyph_bank() -> GlyphBank {
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
bank.palettes[0][1] = Color::RED;
bank.palette_mut(0).unwrap()[1] = Color::RED;
for pixel in &mut bank.pixel_indices {
*pixel = 1;
}
@ -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

@ -196,7 +196,7 @@ mod tests {
fn make_glyph_bank() -> GlyphBank {
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
bank.palettes[0][1] = Color::WHITE;
bank.palette_mut(0).unwrap()[1] = Color::WHITE;
bank
}

View File

@ -203,7 +203,7 @@ mod tests {
use super::*;
use crate::asset::{AssetCodec, AssetEntry, BankType, PreloadEntry};
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
use crate::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
use crate::glyph_bank::GLYPH_BANK_MAX_PALETTE_COUNT_V1;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
@ -369,14 +369,14 @@ mod tests {
bank_type: BankType::GLYPH,
offset,
size,
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 4),
decoded_size: 16 * 16 + (GLYPH_BANK_MAX_PALETTE_COUNT_V1 as u64 * 16 * 4),
codec: AssetCodec::None,
metadata: json!({
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
}),
}
}
@ -452,14 +452,14 @@ mod tests {
bank_type: BankType::GLYPH,
offset: 4,
size: 4,
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 4),
decoded_size: 16 * 16 + (GLYPH_BANK_MAX_PALETTE_COUNT_V1 as u64 * 16 * 4),
codec: AssetCodec::None,
metadata: json!({
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
}),
},
];
@ -519,8 +519,8 @@ mod tests {
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
}
}],
"preload": []
@ -561,8 +561,8 @@ mod tests {
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
}
}],
"preload": []

View File

@ -1,7 +1,7 @@
use crate::color::Color;
use serde::{Deserialize, Serialize};
pub const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
pub const GLYPH_BANK_MAX_PALETTE_COUNT_V1: usize = 64;
pub const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
/// Standard sizes for square tiles.
@ -35,22 +35,59 @@ pub struct GlyphBank {
/// Palette indices are ordinary indices; transparency is resolved through
/// the RGBA alpha channel of the palette entry.
pub pixel_indices: Vec<u8>,
/// Runtime-facing v1 palette table: 64 palettes of 16 RGBA8888 colors each.
pub palettes: [[Color; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
/// Runtime-facing v1 palette table.
///
/// Palette identity is the direct index in this vector. A resident glyph
/// bank carries exactly the palettes materialized from its payload.
pub palettes: Vec<[Color; GLYPH_BANK_COLORS_PER_PALETTE]>,
}
impl GlyphBank {
/// Creates an empty glyph bank with the specified dimensions.
/// Creates an empty glyph bank with one palette.
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
Self::with_palette_count(tile_size, width, height, 1)
}
/// Creates an empty glyph bank with the specified resident palette count.
pub fn with_palette_count(
tile_size: TileSize,
width: usize,
height: usize,
palette_count: usize,
) -> Self {
assert!(
(1..=GLYPH_BANK_MAX_PALETTE_COUNT_V1).contains(&palette_count),
"glyph bank palette_count must be in 1..={}",
GLYPH_BANK_MAX_PALETTE_COUNT_V1
);
Self {
tile_size,
width,
height,
pixel_indices: vec![0; width * height],
palettes: [[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
palettes: vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; palette_count],
}
}
/// Returns the number of resident palettes.
pub fn palette_count(&self) -> usize {
self.palettes.len()
}
/// Returns true when the palette id is valid for this resident bank.
pub fn contains_palette(&self, palette_id: u8) -> bool {
(palette_id as usize) < self.palette_count()
}
/// Returns a mutable palette slot when the palette exists.
pub fn palette_mut(
&mut self,
palette_id: u8,
) -> Option<&mut [Color; GLYPH_BANK_COLORS_PER_PALETTE]> {
self.palettes.get_mut(palette_id as usize)
}
/// Resolves a global tile ID and local pixel coordinates to a palette index.
/// tile_id: the tile index in the bank
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
@ -71,11 +108,48 @@ impl GlyphBank {
}
/// Maps a 4-bit index to a real RGBA8888 Color using the specified palette.
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Color {
pub fn resolve_color(&self, palette_id: u8, pixel_index: u8) -> Option<Color> {
self.palettes
.get(palette_id as usize)
.and_then(|palette| palette.get(pixel_index as usize))
.copied()
.unwrap_or(Color::TRANSPARENT)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_glyph_bank_uses_single_resident_palette() {
let bank = GlyphBank::new(TileSize::Size8, 8, 8);
assert_eq!(bank.palette_count(), 1);
assert!(bank.contains_palette(0));
assert!(!bank.contains_palette(1));
}
#[test]
fn glyph_bank_can_materialize_v1_max_palette_count() {
let bank =
GlyphBank::with_palette_count(TileSize::Size8, 8, 8, GLYPH_BANK_MAX_PALETTE_COUNT_V1);
assert_eq!(bank.palette_count(), GLYPH_BANK_MAX_PALETTE_COUNT_V1);
assert!(bank.contains_palette((GLYPH_BANK_MAX_PALETTE_COUNT_V1 - 1) as u8));
}
#[test]
fn invalid_palette_lookup_is_distinct_from_transparent_color() {
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
bank.palette_mut(0).unwrap()[1] = Color::TRANSPARENT;
assert_eq!(bank.resolve_color(0, 1), Some(Color::TRANSPARENT));
assert_eq!(bank.resolve_color(1, 1), None);
}
#[test]
#[should_panic(expected = "glyph bank palette_count must be in 1..=64")]
fn glyph_bank_rejects_zero_resident_palettes() {
let _ = GlyphBank::with_palette_count(TileSize::Size8, 8, 8, 0);
}
}

View File

@ -15,7 +15,7 @@ use prometeu_hal::asset::{
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
use prometeu_hal::color::Color;
use prometeu_hal::glyph::Glyph;
use prometeu_hal::glyph_bank::{GLYPH_BANK_PALETTE_COUNT_V1, GlyphBank, TileSize};
use prometeu_hal::glyph_bank::{GLYPH_BANK_MAX_PALETTE_COUNT_V1, GlyphBank, TileSize};
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
use prometeu_hal::syscalls::caps;
@ -113,11 +113,12 @@ fn serialized_single_function_module_with_consts(
}
fn test_glyph_payload_size(width: usize, height: usize) -> usize {
(width * height).div_ceil(2) + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u32>())
(width * height).div_ceil(2)
+ (GLYPH_BANK_MAX_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u32>())
}
fn test_glyph_decoded_size(width: usize, height: usize) -> usize {
width * height + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u32>())
width * height + (GLYPH_BANK_MAX_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u32>())
}
fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
@ -133,23 +134,26 @@ fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
"tile_size": 16,
"width": 16,
"height": 16,
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
}),
}
}
fn test_glyph_asset_data() -> Vec<u8> {
let mut data =
vec![0x11u8; test_glyph_payload_size(16, 16) - (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 4)];
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 4]);
vec![
0x11u8;
test_glyph_payload_size(16, 16) - (GLYPH_BANK_MAX_PALETTE_COUNT_V1 * 16 * 4)
];
data.extend_from_slice(&[0u8; GLYPH_BANK_MAX_PALETTE_COUNT_V1 * 16 * 4]);
data
}
fn runtime_test_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
let size = tile_size as usize;
let mut bank = GlyphBank::new(tile_size, size, size);
bank.palettes[palette_id as usize][1] = color;
let mut bank = GlyphBank::with_palette_count(tile_size, size, size, palette_id as usize + 1);
bank.palette_mut(palette_id).unwrap()[1] = color;
for pixel in &mut bank.pixel_indices {
*pixel = 1;
}
@ -1275,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

@ -14,7 +14,7 @@ use prometeu_hal::cartridge::{
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,
TileSize, GLYPH_BANK_COLORS_PER_PALETTE, GLYPH_BANK_MAX_PALETTE_COUNT_V1,
};
use prometeu_hal::scene_bank::SceneBank;
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
@ -275,15 +275,16 @@ fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
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 * 4)
decoded_size: (8 * 8
+ GLYPH_BANK_MAX_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4)
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
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
}),
};
@ -292,8 +293,8 @@ fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
fn build_palette_bytes() -> Vec<u8> {
let mut bytes =
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4);
for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 {
Vec::with_capacity(GLYPH_BANK_MAX_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4);
for palette_id in 0..GLYPH_BANK_MAX_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_be_bytes());

0
discussion/.index.lock Normal file
View File

View File

@ -1,4 +1,4 @@
{"type":"meta","next_id":{"DSC":46,"AGD":49,"DEC":41,"PLN":167,"LSN":55,"CLSN":1}}
{"type":"meta","next_id":{"DSC":47,"AGD":50,"DEC":42,"PLN":173,"LSN":56,"CLSN":1}}
{"type":"discussion","id":"DSC-0044","status":"done","ticket":"hub-suspended-game-kill-affordance","title":"Hub Suspended Game Kill Affordance","created_at":"2026-07-05","updated_at":"2026-07-05","tags":["hub","lifecycle","game","ui"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0054","file":"discussion/lessons/DSC-0044-hub-suspended-game-kill-affordance/LSN-0054-manual-hub-kill-must-share-game-termination-cleanup.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]}
{"type":"discussion","id":"DSC-0043","status":"done","ticket":"system-os-cartridge-switch-orchestrator","title":"SystemOS Cartridge Switch Orchestrator","created_at":"2026-07-03","updated_at":"2026-07-05","tags":["runtime","os","lifecycle","game","cartridge","architecture"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0053","file":"discussion/lessons/DSC-0043-system-os-cartridge-switch-orchestrator/LSN-0053-game-switching-is-lifecycle-replacement-not-loader-work.md","status":"done","created_at":"2026-07-05","updated_at":"2026-07-05"}]}
{"type":"discussion","id":"DSC-0039","status":"abandoned","ticket":"render-pipeline-family-and-future-3d","title":"Render Pipeline Family and Future 3D","created_at":"2026-06-04","updated_at":"2026-06-04","tags":["gfx","renderer","runtime","architecture","pipeline"],"agendas":[{"id":"AGD-0039","file":"AGD-0039-render-pipeline-family-and-future-3d.md","status":"abandoned","created_at":"2026-06-04","updated_at":"2026-06-04","_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}],"decisions":[],"plans":[],"lessons":[],"_override_reason":"User explicitly chose to close this agenda without a new decision because DSC-0038 already established enough architecture for future extension, and 3D is intentionally deferred."}
@ -44,3 +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":"done","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":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0055","file":"discussion/lessons/DSC-0046-runtime-owned-variable-glyph-bank-palette-protocol/LSN-0055-runtime-owned-asset-protocols-must-align-payload-residency-and-lookup.md","status":"done","created_at":"2026-07-14","updated_at":"2026-07-14"}]}

View File

@ -0,0 +1,97 @@
---
id: LSN-0055
ticket: runtime-owned-variable-glyph-bank-palette-protocol
title: Runtime-Owned Asset Protocols Must Align Payload, Residency, and Lookup
created: 2026-07-14
tags: [runtime, assets, glyph-bank, palette-serialization, protocol]
decision: DEC-0041
---
## Context
The runtime changed the `GLYPH/indexed_v1` glyph-bank palette contract from a
fixed 64-palette payload and resident table to variable palette serialization
and variable resident palette storage.
The initiating pressure came from packer/studio: fixed RGBA8888 palette padding
made small glyph banks pay for `64 * 16 * 4` palette bytes even when they used
far fewer palettes. The important architectural point was that the runtime, not
the packer, had to decide the protocol. The packer can only emit payloads that
conform to the runtime contract once the runtime spec is published.
## Key Decisions
### Variable Glyph Bank Palette Protocol
**What:**
`palette_count` became the number of serialized palettes and the number of
resident palettes in the loaded `GlyphBank`. The valid range is `1..=64`.
`palette_id` remains a direct palette identity and is valid only when
`palette_id < palette_count` for the referenced resident bank.
**Why:**
Keeping serialized count variable while retaining a fixed 64-slot resident
table would preserve two meanings for palette capacity. The runtime would save
payload bytes but still carry a split contract between payload shape, resident
shape, and lookup validity. Making all three agree gives the runtime one
source of truth.
**Trade-offs:**
Palette validity is now bank-dependent. Scene and sprite composition must check
the referenced loaded bank instead of relying on a global `0..63` rule. This
adds validation work, but it prevents invalid references from silently becoming
transparent or default colors.
## Patterns and Algorithms
Let the runtime own runtime-facing asset protocols. Tooling can discover pain,
but the runtime spec must define effective metadata, payload shape, resident
shape, and failure semantics.
Make count fields material. A count such as `palette_count` should not describe
only what the producer authored or only what the payload happens to include. If
runtime lookup depends on it, the count must also define resident state and
validation boundaries.
Keep identity direct unless a real remapping problem exists. Sparse-to-dense
palette remapping was rejected because it would add a second identity layer
across scenes, sprites, packer output, and runtime lookup. V1 keeps palette id
`N` as palette slot `N` in the resident bank.
Separate transparent color from invalid lookup. RGBA alpha is valid color data.
An invalid palette reference must be observable as invalid; it must not be
collapsed into `Color::TRANSPARENT`.
Use residue scans after protocol migrations. Search for fixed byte formulas,
old constants, fixture helpers, and spec phrases. Legitimate hits should be
renamed as maximum-bound concepts, not left as accidental active contracts.
## Pitfalls
Partially variable specs are worse than fixed specs. The asset spec already had
variable size formulas in some places but still required `palette_count = 64`
elsewhere. That contradiction made it unclear which rule was canonical.
Resident data structures can preserve obsolete protocol assumptions after the
payload changes. A fixed array in `GlyphBank` would have kept the old model
alive even if decode accepted variable payloads.
Fallback rendering hides contract errors. Returning transparent for an invalid
palette id makes bad assets and bad scene references look like intentional
alpha, which is exactly the wrong failure mode for a runtime protocol.
Fixture generators are part of the contract surface. Stress cartridges and
test payload builders must express whether they are using a variable count or
the v1 maximum; otherwise they reintroduce fixed-padding assumptions.
## Takeaways
- Runtime-facing asset protocols should align payload size, resident memory,
and lookup validity.
- `palette_count` is effective runtime metadata, not producer commentary.
- Direct identity is simpler than remapping until a concrete remapping need
exists.
- Invalid palette references should fail explicitly; transparent remains a
valid RGBA color, not an error substitute.
- Protocol migrations need spec edits, code changes, fixture updates, and
residue scans in the same workflow.

View File

@ -625,7 +625,8 @@ Each tilemap cell contains:
Runtime-facing validity rule for v1:
- `palette_id` values are valid only in the range `0..63`
- `palette_id` values are valid only when `palette_id < palette_count` for the
resolved resident glyph bank
#### Sprite
@ -640,7 +641,8 @@ Each sprite draw contains:
Runtime-facing validity rule for v1:
- `palette_id` values are valid only in the range `0..63`
- `palette_id` values are valid only when `palette_id < palette_count` for the
referenced resident glyph bank
---
@ -649,7 +651,7 @@ Runtime-facing validity rule for v1:
The pipeline works like this:
1. Read indexed pixel from tile (value 0..15)
2. Resolve:
2. Validate and resolve:
- real_color = palette[palette_id][index]
3. Apply:
- flip
@ -667,6 +669,9 @@ else:
draw_or_blend(color)
```
The `palette_id` lookup above is valid only after the runtime has established
that the referenced resident glyph bank contains that palette.
---
### 19.7. Organization of Tile Banks
@ -766,6 +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 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`
@ -774,7 +781,7 @@ Rules:
ABI:
1. `glyph_id: int` — glyph index within the bank
2. `palette_id: int` — palette index
2. `palette_id: int` — palette index within the referenced bank; valid only when `palette_id < palette_count` for that resident glyph bank
3. `x: int` — x coordinate
4. `y: int` — y coordinate
5. `layer: int` — composition layer reference
@ -797,4 +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 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.

View File

@ -115,7 +115,7 @@ For `BankType::GLYPH`, the v1 runtime-facing contract is:
- `codec = NONE`
- serialized pixels use packed `u4` palette indices
- serialized palettes use `RGBA8888` with canonical RGBA channel order
- `palette_count = 64`
- `palette_count` is the number of serialized and resident palettes and must be in `1..=64`
- runtime materialization may expand pixel indices to one `u8` per pixel
For `GLYPH`, `NONE` means there is no additional generic codec layer beyond the bank contract itself.
@ -136,7 +136,7 @@ Required effective metadata fields for `GLYPH` at the root level:
- `tile_size`: tile edge in pixels; valid values are `8`, `16`, or `32`
- `width`: total sheet width in pixels
- `height`: total sheet height in pixels
- `palette_count`: number of serialized palettes for the bank
- `palette_count`: number of serialized palettes and resident runtime palettes for the bank
Optional informative subtrees:
@ -145,7 +145,7 @@ Optional informative subtrees:
Validation rules for `GLYPH` v1:
- `palette_count` must be `64`
- `palette_count` must be in the inclusive range `1..=64`
- `width * height` defines the number of logical indexed pixels in the decoded sheet
- extra metadata may exist, but the runtime contract must not depend on it to reconstruct the in-memory bank unless that data is defined at the root as an effective field.
@ -158,7 +158,16 @@ The tile-bank payload therefore separates serialized storage form from runtime m
- serialized pixel plane: packed `4bpp`
- decoded pixel plane: expanded `u8` indices, one entry per pixel
- palette table: `64 * 16` colors in RGBA8888 channel order
- palette table: `palette_count * 16` colors in RGBA8888 channel order
For `GLYPH` v1:
```text
serialized_pixel_bytes = ceil(width * height / 2)
palette_bytes = palette_count * 16 * 4
size = serialized_pixel_bytes + palette_bytes
decoded_size = (width * height) + palette_bytes
```
For `GLYPH` v1:
@ -172,6 +181,11 @@ before they are loaded by the runtime.
Palette indices are ordinary indices. Transparency is represented by the alpha
channel of the resolved RGBA8888 palette entry, not by reserving index `0`.
`palette_id` is valid only when it is lower than the referenced resident glyph
bank's `palette_count`. Canonical runtime composition MUST NOT substitute
transparent, black, or any other default color for `palette_id >=
palette_count`.
### 4.2 `SCENE` asset contract in v1
For `BankType::SCENE`, the v1 runtime-facing contract is: