Merge pull request 'dev/runtime-owned-variable-glyph-bank-palette-protocol' (#37) from dev/runtime-owned-variable-glyph-bank-palette-protocol into master
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Reviewed-on: #37
This commit is contained in:
commit
aa3b31deb5
@ -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_COLORS_PER_PALETTE: usize = 16;
|
||||||
const GLYPH_BANK_PALETTE_BYTES_V1: usize =
|
const GLYPH_BANK_PALETTE_BYTES_PER_PALETTE: usize =
|
||||||
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u32>();
|
GLYPH_BANK_COLORS_PER_PALETTE * size_of::<u32>();
|
||||||
|
|
||||||
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -599,7 +599,7 @@ impl AssetBridge for AssetManager {
|
|||||||
impl AssetManager {
|
impl AssetManager {
|
||||||
fn decode_glyph_bank_layout(
|
fn decode_glyph_bank_layout(
|
||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
) -> Result<(TileSize, usize, usize, usize), String> {
|
) -> Result<(TileSize, usize, usize, usize, usize, usize), String> {
|
||||||
let meta = entry.metadata_as_glyph_bank()?;
|
let meta = entry.metadata_as_glyph_bank()?;
|
||||||
|
|
||||||
let tile_size = match meta.tile_size {
|
let tile_size = match meta.tile_size {
|
||||||
@ -609,7 +609,8 @@ impl AssetManager {
|
|||||||
_ => return Err(format!("Invalid tile_size: {}", meta.tile_size)),
|
_ => 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));
|
return Err(format!("Invalid palette_count: {}", meta.palette_count));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,12 +619,14 @@ impl AssetManager {
|
|||||||
|
|
||||||
let logical_pixels = width.checked_mul(height).ok_or("GlyphBank dimensions overflow")?;
|
let logical_pixels = width.checked_mul(height).ok_or("GlyphBank dimensions overflow")?;
|
||||||
let serialized_pixel_bytes = logical_pixels.div_ceil(2);
|
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
|
let serialized_size = serialized_pixel_bytes
|
||||||
.checked_add(GLYPH_BANK_PALETTE_BYTES_V1)
|
.checked_add(palette_bytes)
|
||||||
.ok_or("GlyphBank serialized size overflow")?;
|
.ok_or("GlyphBank serialized size overflow")?;
|
||||||
let decoded_size = logical_pixels
|
let decoded_size =
|
||||||
.checked_add(GLYPH_BANK_PALETTE_BYTES_V1)
|
logical_pixels.checked_add(palette_bytes).ok_or("GlyphBank decoded size overflow")?;
|
||||||
.ok_or("GlyphBank decoded size overflow")?;
|
|
||||||
|
|
||||||
if entry.size != serialized_size as u64 {
|
if entry.size != serialized_size as u64 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
@ -639,7 +642,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> {
|
fn unpack_glyph_bank_pixels(packed_pixels: &[u8], logical_pixels: usize) -> Vec<u8> {
|
||||||
@ -1200,19 +1203,18 @@ impl AssetManager {
|
|||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
buffer: &[u8],
|
buffer: &[u8],
|
||||||
) -> Result<GlyphBank, String> {
|
) -> 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) =
|
||||||
if buffer.len() < packed_pixel_bytes + GLYPH_BANK_PALETTE_BYTES_V1 {
|
Self::decode_glyph_bank_layout(entry)?;
|
||||||
|
if buffer.len() < packed_pixel_bytes + palette_bytes {
|
||||||
return Err("Buffer too small for GLYPHBANK".to_string());
|
return Err("Buffer too small for GLYPHBANK".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let logical_pixels = width * height;
|
let logical_pixels = width * height;
|
||||||
let packed_pixels = &buffer[0..packed_pixel_bytes];
|
let packed_pixels = &buffer[0..packed_pixel_bytes];
|
||||||
let pixel_indices = Self::unpack_glyph_bank_pixels(packed_pixels, logical_pixels);
|
let pixel_indices = Self::unpack_glyph_bank_pixels(packed_pixels, logical_pixels);
|
||||||
let palette_data =
|
let palette_data = &buffer[packed_pixel_bytes..packed_pixel_bytes + palette_bytes];
|
||||||
&buffer[packed_pixel_bytes..packed_pixel_bytes + GLYPH_BANK_PALETTE_BYTES_V1];
|
|
||||||
|
|
||||||
let mut palettes =
|
let mut palettes = vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; palette_count];
|
||||||
[[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1];
|
|
||||||
for (p, pal) in palettes.iter_mut().enumerate() {
|
for (p, pal) in palettes.iter_mut().enumerate() {
|
||||||
for (c, slot) in pal.iter_mut().enumerate() {
|
for (c, slot) in pal.iter_mut().enumerate() {
|
||||||
let offset = (p * 16 + c) * 4;
|
let offset = (p * 16 + c) * 4;
|
||||||
@ -1232,7 +1234,8 @@ impl AssetManager {
|
|||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
reader: &mut impl Read,
|
reader: &mut impl Read,
|
||||||
) -> Result<GlyphBank, String> {
|
) -> 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 logical_pixels = width * height;
|
||||||
let mut packed_pixels = vec![0_u8; packed_pixel_bytes];
|
let mut packed_pixels = vec![0_u8; packed_pixel_bytes];
|
||||||
reader
|
reader
|
||||||
@ -1241,13 +1244,12 @@ impl AssetManager {
|
|||||||
|
|
||||||
let pixel_indices = Self::unpack_glyph_bank_pixels(&packed_pixels, logical_pixels);
|
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
|
reader
|
||||||
.read_exact(&mut palette_data)
|
.read_exact(&mut palette_data)
|
||||||
.map_err(|_| "Buffer too small for GLYPHBANK".to_string())?;
|
.map_err(|_| "Buffer too small for GLYPHBANK".to_string())?;
|
||||||
|
|
||||||
let mut palettes =
|
let mut palettes = vec![[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; palette_count];
|
||||||
[[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1];
|
|
||||||
for (p, pal) in palettes.iter_mut().enumerate() {
|
for (p, pal) in palettes.iter_mut().enumerate() {
|
||||||
for (c, slot) in pal.iter_mut().enumerate() {
|
for (c, slot) in pal.iter_mut().enumerate() {
|
||||||
let offset = (p * 16 + c) * 4;
|
let offset = (p * 16 + c) * 4;
|
||||||
@ -1883,35 +1885,53 @@ mod tests {
|
|||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
use prometeu_hal::tilemap::TileMap;
|
use prometeu_hal::tilemap::TileMap;
|
||||||
|
|
||||||
fn expected_glyph_payload_size(width: usize, height: usize) -> usize {
|
fn expected_glyph_payload_size(width: usize, height: usize, palette_count: usize) -> usize {
|
||||||
(width * height).div_ceil(2) + GLYPH_BANK_PALETTE_BYTES_V1
|
(width * height).div_ceil(2) + (palette_count * GLYPH_BANK_PALETTE_BYTES_PER_PALETTE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expected_glyph_decoded_size(width: usize, height: usize) -> usize {
|
fn expected_glyph_decoded_size(width: usize, height: usize, palette_count: usize) -> usize {
|
||||||
width * height + GLYPH_BANK_PALETTE_BYTES_V1
|
width * height + (palette_count * GLYPH_BANK_PALETTE_BYTES_PER_PALETTE)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_glyph_asset_data() -> Vec<u8> {
|
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];
|
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
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_glyph_asset_entry(asset_name: &str, width: usize, height: usize) -> AssetEntry {
|
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 {
|
AssetEntry {
|
||||||
asset_id: 0,
|
asset_id: 0,
|
||||||
asset_name: asset_name.to_string(),
|
asset_name: asset_name.to_string(),
|
||||||
bank_type: BankType::GLYPH,
|
bank_type: BankType::GLYPH,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
size: expected_glyph_payload_size(width, height) as u64,
|
size: expected_glyph_payload_size(width, height, palette_count) as u64,
|
||||||
decoded_size: expected_glyph_decoded_size(width, height) as u64,
|
decoded_size: expected_glyph_decoded_size(width, height, palette_count) as u64,
|
||||||
codec: AssetCodec::None,
|
codec: AssetCodec::None,
|
||||||
metadata: serde_json::json!({
|
metadata: serde_json::json!({
|
||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": width,
|
"width": width,
|
||||||
"height": height,
|
"height": height,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": palette_count,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": palette_count
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2034,22 +2054,67 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decode_glyph_bank_unpacks_packed_pixels_and_reads_palette_colors() {
|
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];
|
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]);
|
data[2..6].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]);
|
||||||
|
|
||||||
let bank =
|
let bank =
|
||||||
AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("glyph decode");
|
AssetManager::decode_glyph_bank_from_buffer(&entry, &data).expect("glyph decode");
|
||||||
|
|
||||||
assert_eq!(bank.pixel_indices, vec![1, 0, 2, 3]);
|
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));
|
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]
|
#[test]
|
||||||
fn test_decode_glyph_bank_rejects_short_packed_buffer() {
|
fn test_decode_glyph_bank_rejects_short_packed_buffer() {
|
||||||
let entry = test_glyph_asset_entry("glyphs", 16, 16);
|
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) {
|
let err = match AssetManager::decode_glyph_bank_from_buffer(&entry, &data) {
|
||||||
Ok(_) => panic!("glyph decode should reject short buffer"),
|
Ok(_) => panic!("glyph decode should reject short buffer"),
|
||||||
@ -2060,9 +2125,9 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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);
|
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 =
|
let err =
|
||||||
match AssetManager::decode_glyph_bank_from_buffer(&entry, &test_glyph_asset_data()) {
|
match AssetManager::decode_glyph_bank_from_buffer(&entry, &test_glyph_asset_data()) {
|
||||||
@ -2070,7 +2135,67 @@ mod tests {
|
|||||||
Err(err) => err,
|
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]
|
#[test]
|
||||||
|
|||||||
@ -557,8 +557,9 @@ mod tests {
|
|||||||
|
|
||||||
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
fn make_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
||||||
let size = tile_size as usize;
|
let size = tile_size as usize;
|
||||||
let mut bank = GlyphBank::new(tile_size, size, size);
|
let mut bank =
|
||||||
bank.palettes[palette_id as usize][1] = color;
|
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 {
|
for pixel in &mut bank.pixel_indices {
|
||||||
*pixel = 1;
|
*pixel = 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -812,7 +812,14 @@ impl Gfx {
|
|||||||
let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x };
|
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 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 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 {
|
if color.alpha() == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -862,7 +869,14 @@ impl Gfx {
|
|||||||
let fetch_y = if sprite.flip_y { size - 1 - local_y } else { local_y };
|
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 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 {
|
if color.alpha() == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1010,9 +1024,14 @@ mod tests {
|
|||||||
|
|
||||||
fn make_glyph_bank(tile_size: TileSize, palette_colors: &[(u8, u8, Color)]) -> GlyphBank {
|
fn make_glyph_bank(tile_size: TileSize, palette_colors: &[(u8, u8, Color)]) -> GlyphBank {
|
||||||
let size = tile_size as usize;
|
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 {
|
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
|
bank
|
||||||
}
|
}
|
||||||
@ -1152,6 +1171,26 @@ mod tests {
|
|||||||
assert_eq!(back[0], Color::RED.raw());
|
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]
|
#[test]
|
||||||
fn test_cached_tile_draws_opaque_color_index_zero() {
|
fn test_cached_tile_draws_opaque_color_index_zero() {
|
||||||
let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::GREEN)]);
|
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());
|
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]
|
#[test]
|
||||||
fn test_draw_rect() {
|
fn test_draw_rect() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
|
|||||||
@ -118,7 +118,11 @@ impl Game2DFrameComposer for Hardware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn emit_sprite(&mut self, sprite: Sprite) -> prometeu_hal::ComposerOpStatus {
|
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;
|
return prometeu_hal::ComposerOpStatus::BankInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,7 +265,7 @@ mod tests {
|
|||||||
|
|
||||||
fn make_glyph_bank() -> GlyphBank {
|
fn make_glyph_bank() -> GlyphBank {
|
||||||
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
|
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 {
|
for pixel in &mut bank.pixel_indices {
|
||||||
*pixel = 1;
|
*pixel = 1;
|
||||||
}
|
}
|
||||||
@ -433,4 +437,28 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(hardware.gfx.front_buffer()[0], Color::RED.raw());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -196,7 +196,7 @@ mod tests {
|
|||||||
|
|
||||||
fn make_glyph_bank() -> GlyphBank {
|
fn make_glyph_bank() -> GlyphBank {
|
||||||
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
|
let mut bank = GlyphBank::new(TileSize::Size8, 8, 8);
|
||||||
bank.palettes[0][1] = Color::WHITE;
|
bank.palette_mut(0).unwrap()[1] = Color::WHITE;
|
||||||
bank
|
bank
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -203,7 +203,7 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::asset::{AssetCodec, AssetEntry, BankType, PreloadEntry};
|
use crate::asset::{AssetCodec, AssetEntry, BankType, PreloadEntry};
|
||||||
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
|
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 serde_json::json;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
@ -369,14 +369,14 @@ mod tests {
|
|||||||
bank_type: BankType::GLYPH,
|
bank_type: BankType::GLYPH,
|
||||||
offset,
|
offset,
|
||||||
size,
|
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,
|
codec: AssetCodec::None,
|
||||||
metadata: json!({
|
metadata: json!({
|
||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -452,14 +452,14 @@ mod tests {
|
|||||||
bank_type: BankType::GLYPH,
|
bank_type: BankType::GLYPH,
|
||||||
offset: 4,
|
offset: 4,
|
||||||
size: 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,
|
codec: AssetCodec::None,
|
||||||
metadata: json!({
|
metadata: json!({
|
||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -519,8 +519,8 @@ mod tests {
|
|||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
"preload": []
|
"preload": []
|
||||||
@ -561,8 +561,8 @@ mod tests {
|
|||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
|
||||||
}
|
}
|
||||||
}],
|
}],
|
||||||
"preload": []
|
"preload": []
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::color::Color;
|
use crate::color::Color;
|
||||||
use serde::{Deserialize, Serialize};
|
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;
|
pub const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
|
||||||
|
|
||||||
/// Standard sizes for square tiles.
|
/// Standard sizes for square tiles.
|
||||||
@ -35,22 +35,59 @@ pub struct GlyphBank {
|
|||||||
/// Palette indices are ordinary indices; transparency is resolved through
|
/// Palette indices are ordinary indices; transparency is resolved through
|
||||||
/// the RGBA alpha channel of the palette entry.
|
/// the RGBA alpha channel of the palette entry.
|
||||||
pub pixel_indices: Vec<u8>,
|
pub pixel_indices: Vec<u8>,
|
||||||
/// Runtime-facing v1 palette table: 64 palettes of 16 RGBA8888 colors each.
|
/// Runtime-facing v1 palette table.
|
||||||
pub palettes: [[Color; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
///
|
||||||
|
/// 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 {
|
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 {
|
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 {
|
Self {
|
||||||
tile_size,
|
tile_size,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pixel_indices: vec![0; 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.
|
/// Resolves a global tile ID and local pixel coordinates to a palette index.
|
||||||
/// tile_id: the tile index in the bank
|
/// tile_id: the tile index in the bank
|
||||||
/// local_x, local_y: the pixel position inside the tile (0 to tile_size-1)
|
/// 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.
|
/// 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
|
self.palettes
|
||||||
.get(palette_id as usize)
|
.get(palette_id as usize)
|
||||||
.and_then(|palette| palette.get(pixel_index as usize))
|
.and_then(|palette| palette.get(pixel_index as usize))
|
||||||
.copied()
|
.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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,7 +15,7 @@ use prometeu_hal::asset::{
|
|||||||
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::glyph::Glyph;
|
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_bank::SceneBank;
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||||
use prometeu_hal::syscalls::caps;
|
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 {
|
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 {
|
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 {
|
fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
|
||||||
@ -133,23 +134,23 @@ fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
|
|||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_glyph_asset_data() -> Vec<u8> {
|
fn test_glyph_asset_data() -> Vec<u8> {
|
||||||
let mut data =
|
let mut data =
|
||||||
vec![0x11u8; test_glyph_payload_size(16, 16) - (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_PALETTE_COUNT_V1 * 16 * 4]);
|
data.extend_from_slice(&[0u8; GLYPH_BANK_MAX_PALETTE_COUNT_V1 * 16 * 4]);
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
fn runtime_test_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
fn runtime_test_glyph_bank(tile_size: TileSize, palette_id: u8, color: Color) -> GlyphBank {
|
||||||
let size = tile_size as usize;
|
let size = tile_size as usize;
|
||||||
let mut bank = GlyphBank::new(tile_size, size, size);
|
let mut bank = GlyphBank::with_palette_count(tile_size, size, size, palette_id as usize + 1);
|
||||||
bank.palettes[palette_id as usize][1] = color;
|
bank.palette_mut(palette_id).unwrap()[1] = color;
|
||||||
for pixel in &mut bank.pixel_indices {
|
for pixel in &mut bank.pixel_indices {
|
||||||
*pixel = 1;
|
*pixel = 1;
|
||||||
}
|
}
|
||||||
@ -1275,6 +1276,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)]);
|
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]
|
#[test]
|
||||||
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
|
fn tick_composer_emit_sprite_invalid_layer_returns_status_not_crash() {
|
||||||
let mut runtime = VirtualMachineRuntime::new(None);
|
let mut runtime = VirtualMachineRuntime::new(None);
|
||||||
|
|||||||
@ -14,7 +14,7 @@ use prometeu_hal::cartridge::{
|
|||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
use prometeu_hal::glyph::Glyph;
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::glyph_bank::{
|
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_bank::SceneBank;
|
||||||
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer};
|
||||||
@ -275,15 +275,15 @@ fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
|
|||||||
bank_type: BankType::GLYPH,
|
bank_type: BankType::GLYPH,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
size: payload.len() as u64,
|
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,
|
as u64,
|
||||||
codec: AssetCodec::None,
|
codec: AssetCodec::None,
|
||||||
metadata: serde_json::json!({
|
metadata: serde_json::json!({
|
||||||
"tile_size": 8,
|
"tile_size": 8,
|
||||||
"width": 8,
|
"width": 8,
|
||||||
"height": 8,
|
"height": 8,
|
||||||
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
"palette_count": GLYPH_BANK_MAX_PALETTE_COUNT_V1,
|
||||||
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
"palette_authored": GLYPH_BANK_MAX_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -292,8 +292,8 @@ fn build_glyph_asset() -> (AssetEntry, Vec<u8>) {
|
|||||||
|
|
||||||
fn build_palette_bytes() -> Vec<u8> {
|
fn build_palette_bytes() -> Vec<u8> {
|
||||||
let mut bytes =
|
let mut bytes =
|
||||||
Vec::with_capacity(GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4);
|
Vec::with_capacity(GLYPH_BANK_MAX_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * 4);
|
||||||
for palette_id in 0..GLYPH_BANK_PALETTE_COUNT_V1 {
|
for palette_id in 0..GLYPH_BANK_MAX_PALETTE_COUNT_V1 {
|
||||||
for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE {
|
for color_index in 0..GLYPH_BANK_COLORS_PER_PALETTE {
|
||||||
let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK };
|
let color = if color_index == 1 { stress_color(palette_id) } else { Color::BLACK };
|
||||||
bytes.extend_from_slice(&color.raw().to_be_bytes());
|
bytes.extend_from_slice(&color.raw().to_be_bytes());
|
||||||
|
|||||||
0
discussion/.index.lock
Normal file
0
discussion/.index.lock
Normal 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-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-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."}
|
{"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-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-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-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"}]}
|
||||||
|
|||||||
@ -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.
|
||||||
@ -625,7 +625,8 @@ Each tilemap cell contains:
|
|||||||
|
|
||||||
Runtime-facing validity rule for v1:
|
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
|
#### Sprite
|
||||||
|
|
||||||
@ -640,7 +641,8 @@ Each sprite draw contains:
|
|||||||
|
|
||||||
Runtime-facing validity rule for v1:
|
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:
|
The pipeline works like this:
|
||||||
|
|
||||||
1. Read indexed pixel from tile (value 0..15)
|
1. Read indexed pixel from tile (value 0..15)
|
||||||
2. Resolve:
|
2. Validate and resolve:
|
||||||
- real_color = palette[palette_id][index]
|
- real_color = palette[palette_id][index]
|
||||||
3. Apply:
|
3. Apply:
|
||||||
- flip
|
- flip
|
||||||
@ -667,6 +669,9 @@ else:
|
|||||||
draw_or_blend(color)
|
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
|
### 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;
|
- 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 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 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.
|
- runtime MUST NOT continue canonical scene composition after such a dependency failure.
|
||||||
|
|
||||||
### 20.2 `composer.emit_sprite`
|
### 20.2 `composer.emit_sprite`
|
||||||
@ -774,7 +781,7 @@ Rules:
|
|||||||
|
|
||||||
ABI:
|
ABI:
|
||||||
1. `glyph_id: int` — glyph index within the bank
|
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
|
3. `x: int` — x coordinate
|
||||||
4. `y: int` — y coordinate
|
4. `y: int` — y coordinate
|
||||||
5. `layer: int` — composition layer reference
|
5. `layer: int` — composition layer reference
|
||||||
@ -797,4 +804,6 @@ Operational notes:
|
|||||||
- the canonical public sprite contract is frame-emission based;
|
- the canonical public sprite contract is frame-emission based;
|
||||||
- no caller-provided sprite index exists in the v1 canonical ABI;
|
- no caller-provided sprite index exists in the v1 canonical ABI;
|
||||||
- no `active` flag 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.
|
- overflow remains non-fatal and must not escalate to trap in v1.
|
||||||
|
|||||||
@ -115,7 +115,7 @@ For `BankType::GLYPH`, the v1 runtime-facing contract is:
|
|||||||
- `codec = NONE`
|
- `codec = NONE`
|
||||||
- serialized pixels use packed `u4` palette indices
|
- serialized pixels use packed `u4` palette indices
|
||||||
- serialized palettes use `RGBA8888` with canonical RGBA channel order
|
- 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
|
- 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.
|
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`
|
- `tile_size`: tile edge in pixels; valid values are `8`, `16`, or `32`
|
||||||
- `width`: total sheet width in pixels
|
- `width`: total sheet width in pixels
|
||||||
- `height`: total sheet height 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:
|
Optional informative subtrees:
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ Optional informative subtrees:
|
|||||||
|
|
||||||
Validation rules for `GLYPH` v1:
|
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
|
- `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.
|
- 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`
|
- serialized pixel plane: packed `4bpp`
|
||||||
- decoded pixel plane: expanded `u8` indices, one entry per pixel
|
- 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:
|
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
|
Palette indices are ordinary indices. Transparency is represented by the alpha
|
||||||
channel of the resolved RGBA8888 palette entry, not by reserving index `0`.
|
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
|
### 4.2 `SCENE` asset contract in v1
|
||||||
|
|
||||||
For `BankType::SCENE`, the v1 runtime-facing contract is:
|
For `BankType::SCENE`, the v1 runtime-facing contract is:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user