dev/ajustments-asset-entry (#12)
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: #12 Co-authored-by: bQUARKz <bquarkz@gmail.com> Co-committed-by: bQUARKz <bquarkz@gmail.com>
This commit is contained in:
parent
c4aca95635
commit
035e9d5676
@ -1,25 +1,25 @@
|
|||||||
#![allow(clippy::collapsible_if)]
|
#![allow(clippy::collapsible_if)]
|
||||||
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
|
use crate::memory_banks::{GlyphBankPoolInstaller, SoundBankPoolInstaller};
|
||||||
use prometeu_hal::AssetBridge;
|
use prometeu_hal::AssetBridge;
|
||||||
use prometeu_hal::asset::{
|
use prometeu_hal::asset::{
|
||||||
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
|
AssetCodec, AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId,
|
||||||
PreloadEntry, SlotRef, SlotStats,
|
LoadStatus, PreloadEntry, SlotRef, SlotStats,
|
||||||
};
|
};
|
||||||
use prometeu_hal::cartridge::AssetsPayloadSource;
|
use prometeu_hal::cartridge::AssetsPayloadSource;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||||
use prometeu_hal::sample::Sample;
|
use prometeu_hal::sample::Sample;
|
||||||
use prometeu_hal::sound_bank::SoundBank;
|
use prometeu_hal::sound_bank::SoundBank;
|
||||||
use prometeu_hal::tile_bank::{TileBank, TileSize};
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::sync::{Arc, Mutex, RwLock};
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
const TILE_BANK_PALETTE_COUNT_V1: usize = 64;
|
const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
|
||||||
const TILE_BANK_COLORS_PER_PALETTE: usize = 16;
|
const GLYPH_BANK_COLORS_PER_PALETTE: usize = 16;
|
||||||
const TILE_BANK_PALETTE_BYTES_V1: usize =
|
const GLYPH_BANK_PALETTE_BYTES_V1: usize =
|
||||||
TILE_BANK_PALETTE_COUNT_V1 * TILE_BANK_COLORS_PER_PALETTE * std::mem::size_of::<u16>();
|
GLYPH_BANK_PALETTE_COUNT_V1 * GLYPH_BANK_COLORS_PER_PALETTE * std::mem::size_of::<u16>();
|
||||||
|
|
||||||
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
/// Resident metadata for a decoded/materialized asset inside a BankPolicy.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@ -115,15 +115,15 @@ pub struct AssetManager {
|
|||||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
|
|
||||||
/// Narrow hardware interfaces
|
/// Narrow hardware interfaces
|
||||||
gfx_installer: Arc<dyn TileBankPoolInstaller>,
|
gfx_installer: Arc<dyn GlyphBankPoolInstaller>,
|
||||||
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
||||||
|
|
||||||
/// Track what is installed in each hardware slot (for stats/info).
|
/// Track what is installed in each hardware slot (for stats/info).
|
||||||
gfx_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
|
gfx_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
|
||||||
sound_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
|
sound_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
|
||||||
|
|
||||||
/// Residency policy for GFX tile banks.
|
/// Residency policy for GFX glyph banks.
|
||||||
gfx_policy: BankPolicy<TileBank>,
|
gfx_policy: BankPolicy<GlyphBank>,
|
||||||
/// Residency policy for sound banks.
|
/// Residency policy for sound banks.
|
||||||
sound_policy: BankPolicy<SoundBank>,
|
sound_policy: BankPolicy<SoundBank>,
|
||||||
|
|
||||||
@ -179,55 +179,44 @@ impl AssetBridge for AssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AssetManager {
|
impl AssetManager {
|
||||||
fn codec_is_none_or_legacy_raw(codec: &str) -> bool {
|
fn decode_glyph_bank_layout(
|
||||||
matches!(codec, "NONE" | "RAW")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_tile_bank_layout(
|
|
||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
) -> Result<(TileSize, usize, usize, usize), String> {
|
) -> Result<(TileSize, usize, usize, usize), String> {
|
||||||
let tile_size_val =
|
let meta = entry.metadata_as_glyph_bank()?;
|
||||||
entry.metadata.get("tile_size").and_then(|v| v.as_u64()).ok_or("Missing tile_size")?;
|
|
||||||
let width =
|
|
||||||
entry.metadata.get("width").and_then(|v| v.as_u64()).ok_or("Missing width")? as usize;
|
|
||||||
let height =
|
|
||||||
entry.metadata.get("height").and_then(|v| v.as_u64()).ok_or("Missing height")? as usize;
|
|
||||||
let palette_count = entry
|
|
||||||
.metadata
|
|
||||||
.get("palette_count")
|
|
||||||
.and_then(|v| v.as_u64())
|
|
||||||
.ok_or("Missing palette_count")? as usize;
|
|
||||||
|
|
||||||
let tile_size = match tile_size_val {
|
let tile_size = match meta.tile_size {
|
||||||
8 => TileSize::Size8,
|
8 => TileSize::Size8,
|
||||||
16 => TileSize::Size16,
|
16 => TileSize::Size16,
|
||||||
32 => TileSize::Size32,
|
32 => TileSize::Size32,
|
||||||
_ => return Err(format!("Invalid tile_size: {}", tile_size_val)),
|
_ => return Err(format!("Invalid tile_size: {}", meta.tile_size)),
|
||||||
};
|
};
|
||||||
|
|
||||||
if palette_count != TILE_BANK_PALETTE_COUNT_V1 {
|
if meta.palette_count as usize != GLYPH_BANK_PALETTE_COUNT_V1 {
|
||||||
return Err(format!("Invalid palette_count: {}", palette_count));
|
return Err(format!("Invalid palette_count: {}", meta.palette_count));
|
||||||
}
|
}
|
||||||
|
|
||||||
let logical_pixels = width.checked_mul(height).ok_or("TileBank dimensions overflow")?;
|
let width = meta.width as usize;
|
||||||
|
let height = meta.height as usize;
|
||||||
|
|
||||||
|
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 serialized_size = serialized_pixel_bytes
|
let serialized_size = serialized_pixel_bytes
|
||||||
.checked_add(TILE_BANK_PALETTE_BYTES_V1)
|
.checked_add(GLYPH_BANK_PALETTE_BYTES_V1)
|
||||||
.ok_or("TileBank serialized size overflow")?;
|
.ok_or("GlyphBank serialized size overflow")?;
|
||||||
let decoded_size = logical_pixels
|
let decoded_size = logical_pixels
|
||||||
.checked_add(TILE_BANK_PALETTE_BYTES_V1)
|
.checked_add(GLYPH_BANK_PALETTE_BYTES_V1)
|
||||||
.ok_or("TileBank 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!(
|
||||||
"Invalid TILEBANK serialized size: expected {}, got {}",
|
"Invalid GLYPHBANK serialized size: expected {}, got {}",
|
||||||
serialized_size, entry.size
|
serialized_size, entry.size
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
if entry.decoded_size != decoded_size as u64 {
|
if entry.decoded_size != decoded_size as u64 {
|
||||||
return Err(format!(
|
return Err(format!(
|
||||||
"Invalid TILEBANK decoded_size: expected {}, got {}",
|
"Invalid GLYPHBANK decoded_size: expected {}, got {}",
|
||||||
decoded_size, entry.decoded_size
|
decoded_size, entry.decoded_size
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
@ -235,7 +224,7 @@ impl AssetManager {
|
|||||||
Ok((tile_size, width, height, serialized_pixel_bytes))
|
Ok((tile_size, width, height, serialized_pixel_bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unpack_tile_bank_pixels(packed_pixels: &[u8], logical_pixels: usize) -> Vec<u8> {
|
fn unpack_glyph_bank_pixels(packed_pixels: &[u8], logical_pixels: usize) -> Vec<u8> {
|
||||||
let mut pixel_indices = Vec::with_capacity(logical_pixels);
|
let mut pixel_indices = Vec::with_capacity(logical_pixels);
|
||||||
for &packed in packed_pixels {
|
for &packed in packed_pixels {
|
||||||
if pixel_indices.len() < logical_pixels {
|
if pixel_indices.len() < logical_pixels {
|
||||||
@ -249,21 +238,16 @@ impl AssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn op_mode_for(entry: &AssetEntry) -> Result<AssetOpMode, String> {
|
fn op_mode_for(entry: &AssetEntry) -> Result<AssetOpMode, String> {
|
||||||
match entry.bank_type {
|
match (entry.bank_type, entry.codec) {
|
||||||
BankType::TILES if Self::codec_is_none_or_legacy_raw(entry.codec.as_str()) => {
|
(BankType::GLYPH, AssetCodec::None) => Ok(AssetOpMode::StageInMemory),
|
||||||
Ok(AssetOpMode::StageInMemory)
|
(BankType::SOUNDS, AssetCodec::None) => Ok(AssetOpMode::DirectFromSlice),
|
||||||
}
|
|
||||||
BankType::SOUNDS if Self::codec_is_none_or_legacy_raw(entry.codec.as_str()) => {
|
|
||||||
Ok(AssetOpMode::DirectFromSlice)
|
|
||||||
}
|
|
||||||
_ => Err(format!("Unsupported codec: {}", entry.codec)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
assets: Vec<AssetEntry>,
|
assets: Vec<AssetEntry>,
|
||||||
assets_data: AssetsPayloadSource,
|
assets_data: AssetsPayloadSource,
|
||||||
gfx_installer: Arc<dyn TileBankPoolInstaller>,
|
gfx_installer: Arc<dyn GlyphBankPoolInstaller>,
|
||||||
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
sound_installer: Arc<dyn SoundBankPoolInstaller>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let mut asset_map = HashMap::new();
|
let mut asset_map = HashMap::new();
|
||||||
@ -312,9 +296,9 @@ impl AssetManager {
|
|||||||
if let Some(entry) = entry_opt {
|
if let Some(entry) = entry_opt {
|
||||||
let slot_index = item.slot;
|
let slot_index = item.slot;
|
||||||
match entry.bank_type {
|
match entry.bank_type {
|
||||||
BankType::TILES => {
|
BankType::GLYPH => {
|
||||||
if let Ok(bank) =
|
if let Ok(bank) =
|
||||||
Self::perform_load_tile_bank(&entry, self.assets_data.clone())
|
Self::perform_load_glyph_bank(&entry, self.assets_data.clone())
|
||||||
{
|
{
|
||||||
let bank_arc = Arc::new(bank);
|
let bank_arc = Arc::new(bank);
|
||||||
self.gfx_policy.put_resident(
|
self.gfx_policy.put_resident(
|
||||||
@ -322,7 +306,7 @@ impl AssetManager {
|
|||||||
Arc::clone(&bank_arc),
|
Arc::clone(&bank_arc),
|
||||||
entry.decoded_size as usize,
|
entry.decoded_size as usize,
|
||||||
);
|
);
|
||||||
self.gfx_installer.install_tile_bank(slot_index, bank_arc);
|
self.gfx_installer.install_glyph_bank(slot_index, bank_arc);
|
||||||
let mut slots = self.gfx_slots.write().unwrap();
|
let mut slots = self.gfx_slots.write().unwrap();
|
||||||
if slot_index < slots.len() {
|
if slot_index < slots.len() {
|
||||||
slots[slot_index] = Some(entry.asset_id);
|
slots[slot_index] = Some(entry.asset_id);
|
||||||
@ -383,7 +367,7 @@ impl AssetManager {
|
|||||||
assets.get(&asset_id).ok_or(AssetLoadError::AssetNotFound)?.clone()
|
assets.get(&asset_id).ok_or(AssetLoadError::AssetNotFound)?.clone()
|
||||||
};
|
};
|
||||||
let slot = match entry.bank_type {
|
let slot = match entry.bank_type {
|
||||||
BankType::TILES => SlotRef::gfx(slot_index),
|
BankType::GLYPH => SlotRef::gfx(slot_index),
|
||||||
BankType::SOUNDS => SlotRef::audio(slot_index),
|
BankType::SOUNDS => SlotRef::audio(slot_index),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -393,7 +377,7 @@ impl AssetManager {
|
|||||||
|
|
||||||
// Check if already resident (Dedup)
|
// Check if already resident (Dedup)
|
||||||
let already_resident = match entry.bank_type {
|
let already_resident = match entry.bank_type {
|
||||||
BankType::TILES => {
|
BankType::GLYPH => {
|
||||||
if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
|
if let Some(bank) = self.gfx_policy.get_resident(asset_id) {
|
||||||
self.gfx_policy.stage(handle_id, bank);
|
self.gfx_policy.stage(handle_id, bank);
|
||||||
true
|
true
|
||||||
@ -451,8 +435,8 @@ impl AssetManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match entry_clone.bank_type {
|
match entry_clone.bank_type {
|
||||||
BankType::TILES => {
|
BankType::GLYPH => {
|
||||||
let result = Self::perform_load_tile_bank(&entry_clone, assets_data);
|
let result = Self::perform_load_glyph_bank(&entry_clone, assets_data);
|
||||||
if let Ok(tilebank) = result {
|
if let Ok(tilebank) = result {
|
||||||
let bank_arc = Arc::new(tilebank);
|
let bank_arc = Arc::new(tilebank);
|
||||||
let resident_arc = {
|
let resident_arc = {
|
||||||
@ -523,10 +507,10 @@ impl AssetManager {
|
|||||||
Ok(handle_id)
|
Ok(handle_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_load_tile_bank(
|
fn perform_load_glyph_bank(
|
||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
assets_data: Arc<RwLock<AssetsPayloadSource>>,
|
||||||
) -> Result<TileBank, String> {
|
) -> Result<GlyphBank, String> {
|
||||||
let op_mode = Self::op_mode_for(entry)?;
|
let op_mode = Self::op_mode_for(entry)?;
|
||||||
let slice = {
|
let slice = {
|
||||||
let assets_data = assets_data.read().unwrap();
|
let assets_data = assets_data.read().unwrap();
|
||||||
@ -539,30 +523,33 @@ impl AssetManager {
|
|||||||
AssetOpMode::StageInMemory => {
|
AssetOpMode::StageInMemory => {
|
||||||
let buffer =
|
let buffer =
|
||||||
slice.read_all().map_err(|_| "Asset payload read failed".to_string())?;
|
slice.read_all().map_err(|_| "Asset payload read failed".to_string())?;
|
||||||
Self::decode_tile_bank_from_buffer(entry, &buffer)
|
Self::decode_glyph_bank_from_buffer(entry, &buffer)
|
||||||
}
|
}
|
||||||
AssetOpMode::DirectFromSlice => {
|
AssetOpMode::DirectFromSlice => {
|
||||||
let mut reader =
|
let mut reader =
|
||||||
slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?;
|
slice.open_reader().map_err(|_| "Asset payload read failed".to_string())?;
|
||||||
Self::decode_tile_bank_from_reader(entry, &mut reader)
|
Self::decode_glyph_bank_from_reader(entry, &mut reader)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_tile_bank_from_buffer(entry: &AssetEntry, buffer: &[u8]) -> Result<TileBank, String> {
|
fn decode_glyph_bank_from_buffer(
|
||||||
let (tile_size, width, height, packed_pixel_bytes) = Self::decode_tile_bank_layout(entry)?;
|
entry: &AssetEntry,
|
||||||
if buffer.len() < packed_pixel_bytes + TILE_BANK_PALETTE_BYTES_V1 {
|
buffer: &[u8],
|
||||||
return Err("Buffer too small for TILEBANK".to_string());
|
) -> 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 {
|
||||||
|
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_tile_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 + TILE_BANK_PALETTE_BYTES_V1];
|
&buffer[packed_pixel_bytes..packed_pixel_bytes + GLYPH_BANK_PALETTE_BYTES_V1];
|
||||||
|
|
||||||
let mut palettes =
|
let mut palettes =
|
||||||
[[Color::BLACK; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1];
|
[[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) * 2;
|
let offset = (p * 16 + c) * 2;
|
||||||
@ -572,29 +559,29 @@ impl AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
|
Ok(GlyphBank { tile_size, width, height, pixel_indices, palettes })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_tile_bank_from_reader(
|
fn decode_glyph_bank_from_reader(
|
||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
reader: &mut impl Read,
|
reader: &mut impl Read,
|
||||||
) -> Result<TileBank, String> {
|
) -> Result<GlyphBank, String> {
|
||||||
let (tile_size, width, height, packed_pixel_bytes) = Self::decode_tile_bank_layout(entry)?;
|
let (tile_size, width, height, packed_pixel_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
|
||||||
.read_exact(&mut packed_pixels)
|
.read_exact(&mut packed_pixels)
|
||||||
.map_err(|_| "Buffer too small for TILEBANK".to_string())?;
|
.map_err(|_| "Buffer too small for GLYPHBANK".to_string())?;
|
||||||
|
|
||||||
let pixel_indices = Self::unpack_tile_bank_pixels(&packed_pixels, logical_pixels);
|
let pixel_indices = Self::unpack_glyph_bank_pixels(&packed_pixels, logical_pixels);
|
||||||
|
|
||||||
let mut palette_data = [0_u8; TILE_BANK_PALETTE_BYTES_V1];
|
let mut palette_data = [0_u8; GLYPH_BANK_PALETTE_BYTES_V1];
|
||||||
reader
|
reader
|
||||||
.read_exact(&mut palette_data)
|
.read_exact(&mut palette_data)
|
||||||
.map_err(|_| "Buffer too small for TILEBANK".to_string())?;
|
.map_err(|_| "Buffer too small for GLYPHBANK".to_string())?;
|
||||||
|
|
||||||
let mut palettes =
|
let mut palettes =
|
||||||
[[Color::BLACK; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1];
|
[[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) * 2;
|
let offset = (p * 16 + c) * 2;
|
||||||
@ -604,7 +591,7 @@ impl AssetManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(TileBank { tile_size, width, height, pixel_indices, palettes })
|
Ok(GlyphBank { tile_size, width, height, pixel_indices, palettes })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn perform_load_sound_bank(
|
fn perform_load_sound_bank(
|
||||||
@ -637,8 +624,8 @@ impl AssetManager {
|
|||||||
entry: &AssetEntry,
|
entry: &AssetEntry,
|
||||||
buffer: &[u8],
|
buffer: &[u8],
|
||||||
) -> Result<SoundBank, String> {
|
) -> Result<SoundBank, String> {
|
||||||
let sample_rate =
|
let meta = entry.metadata_as_sound_bank()?;
|
||||||
entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32;
|
let sample_rate = meta.sample_rate;
|
||||||
|
|
||||||
let mut data = Vec::with_capacity(buffer.len() / 2);
|
let mut data = Vec::with_capacity(buffer.len() / 2);
|
||||||
for i in (0..buffer.len()).step_by(2) {
|
for i in (0..buffer.len()).step_by(2) {
|
||||||
@ -711,9 +698,9 @@ impl AssetManager {
|
|||||||
if let Some(h) = handles.get_mut(&handle_id) {
|
if let Some(h) = handles.get_mut(&handle_id) {
|
||||||
if h.status == LoadStatus::READY {
|
if h.status == LoadStatus::READY {
|
||||||
match h.slot.asset_type {
|
match h.slot.asset_type {
|
||||||
BankType::TILES => {
|
BankType::GLYPH => {
|
||||||
if let Some(bank) = self.gfx_policy.take_staging(handle_id) {
|
if let Some(bank) = self.gfx_policy.take_staging(handle_id) {
|
||||||
self.gfx_installer.install_tile_bank(h.slot.index, bank);
|
self.gfx_installer.install_glyph_bank(h.slot.index, bank);
|
||||||
let mut slots = self.gfx_slots.write().unwrap();
|
let mut slots = self.gfx_slots.write().unwrap();
|
||||||
if h.slot.index < slots.len() {
|
if h.slot.index < slots.len() {
|
||||||
slots[h.slot.index] = Some(h._asset_id);
|
slots[h.slot.index] = Some(h._asset_id);
|
||||||
@ -739,7 +726,7 @@ impl AssetManager {
|
|||||||
|
|
||||||
pub fn bank_info(&self, kind: BankType) -> BankStats {
|
pub fn bank_info(&self, kind: BankType) -> BankStats {
|
||||||
match kind {
|
match kind {
|
||||||
BankType::TILES => {
|
BankType::GLYPH => {
|
||||||
let mut used_bytes = 0;
|
let mut used_bytes = 0;
|
||||||
{
|
{
|
||||||
let resident = self.gfx_policy.resident.read().unwrap();
|
let resident = self.gfx_policy.resident.read().unwrap();
|
||||||
@ -830,7 +817,7 @@ impl AssetManager {
|
|||||||
|
|
||||||
pub fn slot_info(&self, slot: SlotRef) -> SlotStats {
|
pub fn slot_info(&self, slot: SlotRef) -> SlotStats {
|
||||||
match slot.asset_type {
|
match slot.asset_type {
|
||||||
BankType::TILES => {
|
BankType::GLYPH => {
|
||||||
let slots = self.gfx_slots.read().unwrap();
|
let slots = self.gfx_slots.read().unwrap();
|
||||||
let asset_id = slots.get(slot.index).and_then(|s| *s);
|
let asset_id = slots.get(slot.index).and_then(|s| *s);
|
||||||
|
|
||||||
@ -888,93 +875,94 @@ impl AssetManager {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::memory_banks::{MemoryBanks, SoundBankPoolAccess, TileBankPoolAccess};
|
use crate::memory_banks::{GlyphBankPoolAccess, MemoryBanks, SoundBankPoolAccess};
|
||||||
|
use prometeu_hal::asset::AssetCodec;
|
||||||
|
|
||||||
fn expected_tile_payload_size(width: usize, height: usize) -> usize {
|
fn expected_glyph_payload_size(width: usize, height: usize) -> usize {
|
||||||
(width * height).div_ceil(2) + TILE_BANK_PALETTE_BYTES_V1
|
(width * height).div_ceil(2) + GLYPH_BANK_PALETTE_BYTES_V1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expected_tile_decoded_size(width: usize, height: usize) -> usize {
|
fn expected_glyph_decoded_size(width: usize, height: usize) -> usize {
|
||||||
width * height + TILE_BANK_PALETTE_BYTES_V1
|
width * height + GLYPH_BANK_PALETTE_BYTES_V1
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_tile_asset_data() -> Vec<u8> {
|
fn test_glyph_asset_data() -> Vec<u8> {
|
||||||
let mut data = vec![0x11u8; 128];
|
let mut data = vec![0x11u8; 128];
|
||||||
data.extend_from_slice(&[0u8; TILE_BANK_PALETTE_BYTES_V1]);
|
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_BYTES_V1]);
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_tile_asset_entry(asset_name: &str, width: usize, height: usize) -> AssetEntry {
|
fn test_glyph_asset_entry(asset_name: &str, width: usize, height: 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::TILES,
|
bank_type: BankType::GLYPH,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
size: expected_tile_payload_size(width, height) as u64,
|
size: expected_glyph_payload_size(width, height) as u64,
|
||||||
decoded_size: expected_tile_decoded_size(width, height) as u64,
|
decoded_size: expected_glyph_decoded_size(width, height) as u64,
|
||||||
codec: "NONE".to_string(),
|
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": TILE_BANK_PALETTE_COUNT_V1
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||||
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decode_tile_bank_unpacks_packed_pixels_and_reads_palette_colors() {
|
fn test_decode_glyph_bank_unpacks_packed_pixels_and_reads_palette_colors() {
|
||||||
let entry = test_tile_asset_entry("tiles", 2, 2);
|
let entry = test_glyph_asset_entry("glyphs", 2, 2);
|
||||||
let mut data = vec![0x10, 0x23];
|
let mut data = vec![0x10, 0x23];
|
||||||
data.extend_from_slice(&[0u8; TILE_BANK_PALETTE_BYTES_V1]);
|
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_BYTES_V1]);
|
||||||
data[2] = 0x34;
|
data[2] = 0x34;
|
||||||
data[3] = 0x12;
|
data[3] = 0x12;
|
||||||
|
|
||||||
let bank = AssetManager::decode_tile_bank_from_buffer(&entry, &data).expect("tile decode");
|
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.pixel_indices, vec![1, 0, 2, 3]);
|
||||||
assert_eq!(bank.palettes[0][0], Color(0x1234));
|
assert_eq!(bank.palettes[0][0], Color(0x1234));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decode_tile_bank_rejects_short_packed_buffer() {
|
fn test_decode_glyph_bank_rejects_short_packed_buffer() {
|
||||||
let entry = test_tile_asset_entry("tiles", 16, 16);
|
let entry = test_glyph_asset_entry("glyphs", 16, 16);
|
||||||
let data = vec![0u8; expected_tile_payload_size(16, 16) - 1];
|
let data = vec![0u8; expected_glyph_payload_size(16, 16) - 1];
|
||||||
|
|
||||||
let err = match AssetManager::decode_tile_bank_from_buffer(&entry, &data) {
|
let err = match AssetManager::decode_glyph_bank_from_buffer(&entry, &data) {
|
||||||
Ok(_) => panic!("tile decode should reject short buffer"),
|
Ok(_) => panic!("glyph decode should reject short buffer"),
|
||||||
Err(err) => err,
|
Err(err) => err,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(err, "Buffer too small for TILEBANK");
|
assert_eq!(err, "Buffer too small for GLYPHBANK");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_decode_tile_bank_requires_palette_count_64() {
|
fn test_decode_glyph_bank_requires_palette_count_64() {
|
||||||
let mut entry = test_tile_asset_entry("tiles", 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!(32);
|
||||||
|
|
||||||
let err = match AssetManager::decode_tile_bank_from_buffer(&entry, &test_tile_asset_data())
|
let err =
|
||||||
{
|
match AssetManager::decode_glyph_bank_from_buffer(&entry, &test_glyph_asset_data()) {
|
||||||
Ok(_) => panic!("tile decode should reject invalid palette_count"),
|
Ok(_) => panic!("glyph decode should reject invalid palette_count"),
|
||||||
Err(err) => err,
|
Err(err) => err,
|
||||||
};
|
};
|
||||||
|
|
||||||
assert_eq!(err, "Invalid palette_count: 32");
|
assert_eq!(err, "Invalid palette_count: 32");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_op_mode_for_tiles_none_stages_in_memory() {
|
fn test_op_mode_for_glyphs_none_stages_in_memory() {
|
||||||
let entry = test_tile_asset_entry("tiles", 16, 16);
|
let entry = test_glyph_asset_entry("glyphs", 16, 16);
|
||||||
|
|
||||||
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::StageInMemory));
|
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::StageInMemory));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_op_mode_for_tiles_raw_is_legacy_alias() {
|
fn test_op_mode_for_glyphs_none_uses_typed_codec() {
|
||||||
let mut entry = test_tile_asset_entry("tiles", 16, 16);
|
let entry = test_glyph_asset_entry("glyphs", 16, 16);
|
||||||
entry.codec = "RAW".to_string();
|
|
||||||
|
|
||||||
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::StageInMemory));
|
assert_eq!(AssetManager::op_mode_for(&entry), Ok(AssetOpMode::StageInMemory));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -987,9 +975,10 @@ mod tests {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
size: 8,
|
size: 8,
|
||||||
decoded_size: 8,
|
decoded_size: 8,
|
||||||
codec: "NONE".to_string(),
|
codec: AssetCodec::None,
|
||||||
metadata: serde_json::json!({
|
metadata: serde_json::json!({
|
||||||
"sample_rate": 44100
|
"sample_rate": 44100,
|
||||||
|
"channels": 1
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -999,11 +988,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_asset_loading_flow() {
|
fn test_asset_loading_flow() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
|
|
||||||
let data = test_tile_asset_data();
|
let data = test_glyph_asset_data();
|
||||||
let asset_entry = test_tile_asset_entry("test_tiles", 16, 16);
|
let asset_entry = test_glyph_asset_entry("test_glyphs", 16, 16);
|
||||||
|
|
||||||
let am = AssetManager::new(
|
let am = AssetManager::new(
|
||||||
vec![asset_entry],
|
vec![asset_entry],
|
||||||
@ -1031,17 +1020,17 @@ mod tests {
|
|||||||
am.apply_commits();
|
am.apply_commits();
|
||||||
|
|
||||||
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
|
assert_eq!(am.status(handle), LoadStatus::COMMITTED);
|
||||||
assert!(banks.tile_bank_slot(0).is_some());
|
assert!(banks.glyph_bank_slot(0).is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_asset_dedup() {
|
fn test_asset_dedup() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
|
|
||||||
let data = test_tile_asset_data();
|
let data = test_glyph_asset_data();
|
||||||
let asset_entry = test_tile_asset_entry("test_tiles", 16, 16);
|
let asset_entry = test_glyph_asset_entry("test_glyphs", 16, 16);
|
||||||
|
|
||||||
let am = AssetManager::new(
|
let am = AssetManager::new(
|
||||||
vec![asset_entry],
|
vec![asset_entry],
|
||||||
@ -1068,7 +1057,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_sound_asset_loading() {
|
fn test_sound_asset_loading() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
|
|
||||||
// 100 samples of 16-bit PCM (zeros)
|
// 100 samples of 16-bit PCM (zeros)
|
||||||
@ -1081,9 +1070,10 @@ mod tests {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
size: data.len() as u64,
|
size: data.len() as u64,
|
||||||
decoded_size: data.len() as u64,
|
decoded_size: data.len() as u64,
|
||||||
codec: "NONE".to_string(),
|
codec: AssetCodec::None,
|
||||||
metadata: serde_json::json!({
|
metadata: serde_json::json!({
|
||||||
"sample_rate": 44100
|
"sample_rate": 44100,
|
||||||
|
"channels": 1
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1112,7 +1102,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_preload_on_init() {
|
fn test_preload_on_init() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
|
|
||||||
let data = vec![0u8; 200];
|
let data = vec![0u8; 200];
|
||||||
@ -1124,9 +1114,10 @@ mod tests {
|
|||||||
offset: 0,
|
offset: 0,
|
||||||
size: data.len() as u64,
|
size: data.len() as u64,
|
||||||
decoded_size: data.len() as u64,
|
decoded_size: data.len() as u64,
|
||||||
codec: "NONE".to_string(),
|
codec: AssetCodec::None,
|
||||||
metadata: serde_json::json!({
|
metadata: serde_json::json!({
|
||||||
"sample_rate": 44100
|
"sample_rate": 44100,
|
||||||
|
"channels": 1
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1152,7 +1143,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_load_returns_asset_not_found() {
|
fn test_load_returns_asset_not_found() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
let am =
|
let am =
|
||||||
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
|
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
|
||||||
@ -1165,11 +1156,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_load_returns_slot_index_invalid() {
|
fn test_load_returns_slot_index_invalid() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
let data = test_tile_asset_data();
|
let data = test_glyph_asset_data();
|
||||||
let am = AssetManager::new(
|
let am = AssetManager::new(
|
||||||
vec![test_tile_asset_entry("test_tiles", 16, 16)],
|
vec![test_glyph_asset_entry("test_glyphs", 16, 16)],
|
||||||
AssetsPayloadSource::from_bytes(data),
|
AssetsPayloadSource::from_bytes(data),
|
||||||
gfx_installer,
|
gfx_installer,
|
||||||
sound_installer,
|
sound_installer,
|
||||||
@ -1183,7 +1174,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_status_returns_unknown_handle() {
|
fn test_status_returns_unknown_handle() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
let am =
|
let am =
|
||||||
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
|
AssetManager::new(vec![], AssetsPayloadSource::empty(), gfx_installer, sound_installer);
|
||||||
@ -1194,11 +1185,11 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_commit_and_cancel_return_explicit_statuses() {
|
fn test_commit_and_cancel_return_explicit_statuses() {
|
||||||
let banks = Arc::new(MemoryBanks::new());
|
let banks = Arc::new(MemoryBanks::new());
|
||||||
let gfx_installer = Arc::clone(&banks) as Arc<dyn TileBankPoolInstaller>;
|
let gfx_installer = Arc::clone(&banks) as Arc<dyn GlyphBankPoolInstaller>;
|
||||||
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
let sound_installer = Arc::clone(&banks) as Arc<dyn SoundBankPoolInstaller>;
|
||||||
let data = test_tile_asset_data();
|
let data = test_glyph_asset_data();
|
||||||
let am = AssetManager::new(
|
let am = AssetManager::new(
|
||||||
vec![test_tile_asset_entry("test_tiles", 16, 16)],
|
vec![test_glyph_asset_entry("test_glyphs", 16, 16)],
|
||||||
AssetsPayloadSource::from_bytes(data),
|
AssetsPayloadSource::from_bytes(data),
|
||||||
gfx_installer,
|
gfx_installer,
|
||||||
sound_installer,
|
sound_installer,
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
use crate::memory_banks::TileBankPoolAccess;
|
use crate::memory_banks::GlyphBankPoolAccess;
|
||||||
use prometeu_hal::GfxBridge;
|
use prometeu_hal::GfxBridge;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::glyph::Glyph;
|
||||||
|
use prometeu_hal::glyph_bank::{GlyphBank, TileSize};
|
||||||
use prometeu_hal::sprite::Sprite;
|
use prometeu_hal::sprite::Sprite;
|
||||||
use prometeu_hal::tile::Tile;
|
use prometeu_hal::tile::Tile;
|
||||||
use prometeu_hal::tile_bank::{TileBank, TileSize};
|
|
||||||
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
|
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
@ -56,12 +57,12 @@ pub struct Gfx {
|
|||||||
/// Back buffer: the "Work RAM" where new frames are composed.
|
/// Back buffer: the "Work RAM" where new frames are composed.
|
||||||
back: Vec<u16>,
|
back: Vec<u16>,
|
||||||
|
|
||||||
/// 4 scrollable background layers. Each can have its own scroll (X, Y) and TileBank.
|
/// 4 scrollable background layers. Each can have its own scroll (X, Y) and GlyphBank.
|
||||||
pub layers: [ScrollableTileLayer; 4],
|
pub layers: [ScrollableTileLayer; 4],
|
||||||
/// 1 fixed layer for User Interface (HUD). It doesn't scroll.
|
/// 1 fixed layer for User Interface (HUD). It doesn't scroll.
|
||||||
pub hud: HudTileLayer,
|
pub hud: HudTileLayer,
|
||||||
/// Shared access to graphical memory banks (tiles and palettes).
|
/// Shared access to graphical memory banks (tiles and palettes).
|
||||||
pub tile_banks: Arc<dyn TileBankPoolAccess>,
|
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
|
||||||
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
|
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
|
||||||
pub sprites: [Sprite; 512],
|
pub sprites: [Sprite; 512],
|
||||||
|
|
||||||
@ -273,9 +274,11 @@ impl GfxBridge for Gfx {
|
|||||||
|
|
||||||
impl Gfx {
|
impl Gfx {
|
||||||
/// Initializes the graphics system with a specific resolution and shared memory banks.
|
/// Initializes the graphics system with a specific resolution and shared memory banks.
|
||||||
pub fn new(w: usize, h: usize, tile_banks: Arc<dyn TileBankPoolAccess>) -> Self {
|
pub fn new(w: usize, h: usize, glyph_banks: Arc<dyn GlyphBankPoolAccess>) -> Self {
|
||||||
|
const EMPTY_GLYPH: Glyph = Glyph { glyph_id: 0, palette_id: 0 };
|
||||||
|
|
||||||
const EMPTY_SPRITE: Sprite = Sprite {
|
const EMPTY_SPRITE: Sprite = Sprite {
|
||||||
tile: Tile { id: 0, flip_x: false, flip_y: false, palette_id: 0 },
|
glyph: EMPTY_GLYPH,
|
||||||
x: 0,
|
x: 0,
|
||||||
y: 0,
|
y: 0,
|
||||||
bank_id: 0,
|
bank_id: 0,
|
||||||
@ -300,7 +303,7 @@ impl Gfx {
|
|||||||
back: vec![0; len],
|
back: vec![0; len],
|
||||||
layers,
|
layers,
|
||||||
hud: HudTileLayer::new(64, 32),
|
hud: HudTileLayer::new(64, 32),
|
||||||
tile_banks,
|
glyph_banks,
|
||||||
sprites: [EMPTY_SPRITE; 512],
|
sprites: [EMPTY_SPRITE; 512],
|
||||||
scene_fade_level: 31,
|
scene_fade_level: 31,
|
||||||
scene_fade_color: Color::BLACK,
|
scene_fade_color: Color::BLACK,
|
||||||
@ -580,14 +583,14 @@ impl Gfx {
|
|||||||
self.h,
|
self.h,
|
||||||
&self.priority_buckets[0],
|
&self.priority_buckets[0],
|
||||||
&self.sprites,
|
&self.sprites,
|
||||||
&*self.tile_banks,
|
&*self.glyph_banks,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Main layers and prioritized sprites.
|
// 2. Main layers and prioritized sprites.
|
||||||
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
|
||||||
for i in 0..self.layers.len() {
|
for i in 0..self.layers.len() {
|
||||||
let bank_id = self.layers[i].bank_id as usize;
|
let bank_id = self.layers[i].bank_id as usize;
|
||||||
if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) {
|
if let Some(bank) = self.glyph_banks.glyph_bank_slot(bank_id) {
|
||||||
Self::draw_tile_map(
|
Self::draw_tile_map(
|
||||||
&mut self.back,
|
&mut self.back,
|
||||||
self.w,
|
self.w,
|
||||||
@ -606,7 +609,7 @@ impl Gfx {
|
|||||||
self.h,
|
self.h,
|
||||||
&self.priority_buckets[i + 1],
|
&self.priority_buckets[i + 1],
|
||||||
&self.sprites,
|
&self.sprites,
|
||||||
&*self.tile_banks,
|
&*self.glyph_banks,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -614,7 +617,7 @@ impl Gfx {
|
|||||||
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
|
||||||
|
|
||||||
// 5. HUD: The fixed interface layer, always drawn on top of the world.
|
// 5. HUD: The fixed interface layer, always drawn on top of the world.
|
||||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
|
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.glyph_banks);
|
||||||
|
|
||||||
// 6. HUD Fade: Independent fade effect for the UI.
|
// 6. HUD Fade: Independent fade effect for the UI.
|
||||||
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
|
||||||
@ -630,7 +633,7 @@ impl Gfx {
|
|||||||
let scroll_x = self.layers[layer_idx].scroll_x;
|
let scroll_x = self.layers[layer_idx].scroll_x;
|
||||||
let scroll_y = self.layers[layer_idx].scroll_y;
|
let scroll_y = self.layers[layer_idx].scroll_y;
|
||||||
|
|
||||||
let bank = match self.tile_banks.tile_bank_slot(bank_id) {
|
let bank = match self.glyph_banks.glyph_bank_slot(bank_id) {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
@ -648,7 +651,7 @@ impl Gfx {
|
|||||||
|
|
||||||
/// Renders the HUD (fixed position, no scroll).
|
/// Renders the HUD (fixed position, no scroll).
|
||||||
pub fn render_hud(&mut self) {
|
pub fn render_hud(&mut self) {
|
||||||
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
|
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.glyph_banks);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_hud_with_pool(
|
fn render_hud_with_pool(
|
||||||
@ -656,10 +659,10 @@ impl Gfx {
|
|||||||
w: usize,
|
w: usize,
|
||||||
h: usize,
|
h: usize,
|
||||||
hud: &HudTileLayer,
|
hud: &HudTileLayer,
|
||||||
tile_banks: &dyn TileBankPoolAccess,
|
glyph_banks: &dyn GlyphBankPoolAccess,
|
||||||
) {
|
) {
|
||||||
let bank_id = hud.bank_id as usize;
|
let bank_id = hud.bank_id as usize;
|
||||||
let bank = match tile_banks.tile_bank_slot(bank_id) {
|
let bank = match glyph_banks.glyph_bank_slot(bank_id) {
|
||||||
Some(b) => b,
|
Some(b) => b,
|
||||||
_ => return,
|
_ => return,
|
||||||
};
|
};
|
||||||
@ -673,7 +676,7 @@ impl Gfx {
|
|||||||
screen_w: usize,
|
screen_w: usize,
|
||||||
screen_h: usize,
|
screen_h: usize,
|
||||||
map: &TileMap,
|
map: &TileMap,
|
||||||
bank: &TileBank,
|
bank: &GlyphBank,
|
||||||
scroll_x: i32,
|
scroll_x: i32,
|
||||||
scroll_y: i32,
|
scroll_y: i32,
|
||||||
) {
|
) {
|
||||||
@ -706,7 +709,7 @@ impl Gfx {
|
|||||||
let tile = map.tiles[map_y * map.width + map_x];
|
let tile = map.tiles[map_y * map.width + map_x];
|
||||||
|
|
||||||
// Optimized skip for empty (ID 0) tiles.
|
// Optimized skip for empty (ID 0) tiles.
|
||||||
if tile.id == 0 {
|
if !tile.active {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -736,7 +739,7 @@ impl Gfx {
|
|||||||
x: i32,
|
x: i32,
|
||||||
y: i32,
|
y: i32,
|
||||||
tile: Tile,
|
tile: Tile,
|
||||||
bank: &TileBank,
|
bank: &GlyphBank,
|
||||||
) {
|
) {
|
||||||
let size = bank.tile_size as usize;
|
let size = bank.tile_size as usize;
|
||||||
|
|
||||||
@ -757,7 +760,7 @@ impl Gfx {
|
|||||||
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
|
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
|
||||||
|
|
||||||
// 1. Get the pixel color index (0-15) from the bank.
|
// 1. Get the pixel color index (0-15) from the bank.
|
||||||
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
|
let px_index = bank.get_pixel_index(tile.glyph.glyph_id, fetch_x, fetch_y);
|
||||||
|
|
||||||
// 2. Hardware rule: Color index 0 is always fully transparent.
|
// 2. Hardware rule: Color index 0 is always fully transparent.
|
||||||
if px_index == 0 {
|
if px_index == 0 {
|
||||||
@ -765,7 +768,7 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
|
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
|
||||||
let color = bank.resolve_color(tile.palette_id, px_index);
|
let color = bank.resolve_color(tile.glyph.palette_id, px_index);
|
||||||
|
|
||||||
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
||||||
}
|
}
|
||||||
@ -778,12 +781,12 @@ impl Gfx {
|
|||||||
screen_h: usize,
|
screen_h: usize,
|
||||||
bucket: &[usize],
|
bucket: &[usize],
|
||||||
sprites: &[Sprite],
|
sprites: &[Sprite],
|
||||||
tile_banks: &dyn TileBankPoolAccess,
|
glyph_banks: &dyn GlyphBankPoolAccess,
|
||||||
) {
|
) {
|
||||||
for &idx in bucket {
|
for &idx in bucket {
|
||||||
let s = &sprites[idx];
|
let s = &sprites[idx];
|
||||||
let bank_id = s.bank_id as usize;
|
let bank_id = s.bank_id as usize;
|
||||||
if let Some(bank) = tile_banks.tile_bank_slot(bank_id) {
|
if let Some(bank) = glyph_banks.glyph_bank_slot(bank_id) {
|
||||||
Self::draw_sprite_pixel_by_pixel(back, screen_w, screen_h, s, &bank);
|
Self::draw_sprite_pixel_by_pixel(back, screen_w, screen_h, s, &bank);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -794,7 +797,7 @@ impl Gfx {
|
|||||||
screen_w: usize,
|
screen_w: usize,
|
||||||
screen_h: usize,
|
screen_h: usize,
|
||||||
sprite: &Sprite,
|
sprite: &Sprite,
|
||||||
bank: &TileBank,
|
bank: &GlyphBank,
|
||||||
) {
|
) {
|
||||||
// ... (same bounds/clipping calculation we already had) ...
|
// ... (same bounds/clipping calculation we already had) ...
|
||||||
let size = bank.tile_size as usize;
|
let size = bank.tile_size as usize;
|
||||||
@ -812,7 +815,7 @@ 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 };
|
||||||
|
|
||||||
// 1. Get index
|
// 1. Get index
|
||||||
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
|
let px_index = bank.get_pixel_index(sprite.glyph.glyph_id, fetch_x, fetch_y);
|
||||||
|
|
||||||
// 2. Transparency
|
// 2. Transparency
|
||||||
if px_index == 0 {
|
if px_index == 0 {
|
||||||
@ -820,7 +823,7 @@ impl Gfx {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 3. Resolve color via palette (from the tile inside the sprite)
|
// 3. Resolve color via palette (from the tile inside the sprite)
|
||||||
let color = bank.resolve_color(sprite.tile.palette_id, px_index);
|
let color = bank.resolve_color(sprite.glyph.palette_id, px_index);
|
||||||
|
|
||||||
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
back[world_y as usize * screen_w + world_x as usize] = color.raw();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,8 +2,8 @@ use crate::asset::AssetManager;
|
|||||||
use crate::audio::Audio;
|
use crate::audio::Audio;
|
||||||
use crate::gfx::Gfx;
|
use crate::gfx::Gfx;
|
||||||
use crate::memory_banks::{
|
use crate::memory_banks::{
|
||||||
MemoryBanks, SoundBankPoolAccess, SoundBankPoolInstaller, TileBankPoolAccess,
|
GlyphBankPoolAccess, GlyphBankPoolInstaller, MemoryBanks, SoundBankPoolAccess,
|
||||||
TileBankPoolInstaller,
|
SoundBankPoolInstaller,
|
||||||
};
|
};
|
||||||
use crate::pad::Pad;
|
use crate::pad::Pad;
|
||||||
use crate::touch::Touch;
|
use crate::touch::Touch;
|
||||||
@ -32,7 +32,7 @@ pub struct Hardware {
|
|||||||
pub pad: Pad,
|
pub pad: Pad,
|
||||||
/// The absolute pointer input device (Mouse/Touchscreen).
|
/// The absolute pointer input device (Mouse/Touchscreen).
|
||||||
pub touch: Touch,
|
pub touch: Touch,
|
||||||
/// The Asset Management system (DMA). Handles loading data into VRAM (TileBanks) and ARAM (SoundBanks).
|
/// The Asset Management system (DMA). Handles loading data into VRAM (GlyphBanks) and ARAM (SoundBanks).
|
||||||
pub assets: AssetManager,
|
pub assets: AssetManager,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ impl Hardware {
|
|||||||
gfx: Gfx::new(
|
gfx: Gfx::new(
|
||||||
Self::W,
|
Self::W,
|
||||||
Self::H,
|
Self::H,
|
||||||
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolAccess>,
|
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolAccess>,
|
||||||
),
|
),
|
||||||
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
audio: Audio::new(Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolAccess>),
|
||||||
pad: Pad::default(),
|
pad: Pad::default(),
|
||||||
@ -100,7 +100,7 @@ impl Hardware {
|
|||||||
assets: AssetManager::new(
|
assets: AssetManager::new(
|
||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::empty(),
|
AssetsPayloadSource::empty(),
|
||||||
Arc::clone(&memory_banks) as Arc<dyn TileBankPoolInstaller>,
|
Arc::clone(&memory_banks) as Arc<dyn GlyphBankPoolInstaller>,
|
||||||
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
|
Arc::clone(&memory_banks) as Arc<dyn SoundBankPoolInstaller>,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
|
use prometeu_hal::glyph_bank::GlyphBank;
|
||||||
use prometeu_hal::sound_bank::SoundBank;
|
use prometeu_hal::sound_bank::SoundBank;
|
||||||
use prometeu_hal::tile_bank::TileBank;
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
/// Non-generic interface for peripherals to access graphical tile banks.
|
/// Non-generic interface for peripherals to access graphical glyph banks.
|
||||||
pub trait TileBankPoolAccess: Send + Sync {
|
pub trait GlyphBankPoolAccess: Send + Sync {
|
||||||
/// Returns a reference to the resident TileBank in the specified slot, if any.
|
/// Returns a reference to the resident GlyphBank in the specified slot, if any.
|
||||||
fn tile_bank_slot(&self, slot: usize) -> Option<Arc<TileBank>>;
|
fn glyph_bank_slot(&self, slot: usize) -> Option<Arc<GlyphBank>>;
|
||||||
/// Returns the total number of slots available in this bank.
|
/// Returns the total number of slots available in this bank.
|
||||||
fn tile_bank_slot_count(&self) -> usize;
|
fn glyph_bank_slot_count(&self) -> usize;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-generic interface for the AssetManager to install graphical tile banks.
|
/// Non-generic interface for the AssetManager to install graphical glyph banks.
|
||||||
pub trait TileBankPoolInstaller: Send + Sync {
|
pub trait GlyphBankPoolInstaller: Send + Sync {
|
||||||
/// Atomically swaps the resident TileBank in the specified slot.
|
/// Atomically swaps the resident GlyphBank in the specified slot.
|
||||||
fn install_tile_bank(&self, slot: usize, bank: Arc<TileBank>);
|
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-generic interface for peripherals to access sound banks.
|
/// Non-generic interface for peripherals to access sound banks.
|
||||||
@ -36,7 +36,7 @@ pub trait SoundBankPoolInstaller: Send + Sync {
|
|||||||
/// Peripherals consume this state via narrow, non-generic traits.
|
/// Peripherals consume this state via narrow, non-generic traits.
|
||||||
/// AssetManager coordinates residency and installs assets into these slots.
|
/// AssetManager coordinates residency and installs assets into these slots.
|
||||||
pub struct MemoryBanks {
|
pub struct MemoryBanks {
|
||||||
tile_bank_pool: Arc<RwLock<[Option<Arc<TileBank>>; 16]>>,
|
glyph_bank_pool: Arc<RwLock<[Option<Arc<GlyphBank>>; 16]>>,
|
||||||
sound_bank_pool: Arc<RwLock<[Option<Arc<SoundBank>>; 16]>>,
|
sound_bank_pool: Arc<RwLock<[Option<Arc<SoundBank>>; 16]>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,26 +50,26 @@ impl MemoryBanks {
|
|||||||
/// Creates a new set of memory banks with empty slots.
|
/// Creates a new set of memory banks with empty slots.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
tile_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
glyph_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||||
sound_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
sound_bank_pool: Arc::new(RwLock::new(std::array::from_fn(|_| None))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileBankPoolAccess for MemoryBanks {
|
impl GlyphBankPoolAccess for MemoryBanks {
|
||||||
fn tile_bank_slot(&self, slot: usize) -> Option<Arc<TileBank>> {
|
fn glyph_bank_slot(&self, slot: usize) -> Option<Arc<GlyphBank>> {
|
||||||
let pool = self.tile_bank_pool.read().unwrap();
|
let pool = self.glyph_bank_pool.read().unwrap();
|
||||||
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
|
pool.get(slot).and_then(|s| s.as_ref().map(Arc::clone))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tile_bank_slot_count(&self) -> usize {
|
fn glyph_bank_slot_count(&self) -> usize {
|
||||||
16
|
16
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileBankPoolInstaller for MemoryBanks {
|
impl GlyphBankPoolInstaller for MemoryBanks {
|
||||||
fn install_tile_bank(&self, slot: usize, bank: Arc<TileBank>) {
|
fn install_glyph_bank(&self, slot: usize, bank: Arc<GlyphBank>) {
|
||||||
let mut pool = self.tile_bank_pool.write().unwrap();
|
let mut pool = self.glyph_bank_pool.write().unwrap();
|
||||||
if slot < 16 {
|
if slot < 16 {
|
||||||
pool[slot] = Some(bank);
|
pool[slot] = Some(bank);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,12 +6,18 @@ pub type AssetId = i32;
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
#[allow(non_camel_case_types)]
|
#[allow(non_camel_case_types)]
|
||||||
pub enum BankType {
|
pub enum BankType {
|
||||||
TILES,
|
GLYPH,
|
||||||
SOUNDS,
|
SOUNDS,
|
||||||
// TILEMAPS,
|
// TILEMAPS,
|
||||||
// BLOBS,
|
// BLOBS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||||
|
pub enum AssetCodec {
|
||||||
|
#[serde(rename = "NONE")]
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct AssetEntry {
|
pub struct AssetEntry {
|
||||||
pub asset_id: AssetId,
|
pub asset_id: AssetId,
|
||||||
@ -20,10 +26,49 @@ pub struct AssetEntry {
|
|||||||
pub offset: u64,
|
pub offset: u64,
|
||||||
pub size: u64,
|
pub size: u64,
|
||||||
pub decoded_size: u64,
|
pub decoded_size: u64,
|
||||||
pub codec: String, // e.g., "NONE" (legacy alias: "RAW")
|
pub codec: AssetCodec,
|
||||||
pub metadata: serde_json::Value,
|
pub metadata: serde_json::Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct GlyphBankMetadata {
|
||||||
|
pub tile_size: u32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub palette_count: u32,
|
||||||
|
pub palette_authored: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct SoundBankMetadata {
|
||||||
|
pub sample_rate: u32,
|
||||||
|
pub channels: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssetEntry {
|
||||||
|
pub fn metadata_as_glyph_bank(&self) -> Result<GlyphBankMetadata, String> {
|
||||||
|
if self.bank_type != BankType::GLYPH {
|
||||||
|
return Err(format!(
|
||||||
|
"Asset {} is not a GLYPH bank (type: {:?})",
|
||||||
|
self.asset_id, self.bank_type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
serde_json::from_value(self.metadata.clone())
|
||||||
|
.map_err(|e| format!("Invalid GLYPH metadata for asset {}: {}", self.asset_id, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn metadata_as_sound_bank(&self) -> Result<SoundBankMetadata, String> {
|
||||||
|
if self.bank_type != BankType::SOUNDS {
|
||||||
|
return Err(format!(
|
||||||
|
"Asset {} is not a SOUNDS bank (type: {:?})",
|
||||||
|
self.asset_id, self.bank_type
|
||||||
|
));
|
||||||
|
}
|
||||||
|
serde_json::from_value(self.metadata.clone())
|
||||||
|
.map_err(|e| format!("Invalid SOUNDS metadata for asset {}: {}", self.asset_id, e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
pub struct PreloadEntry {
|
pub struct PreloadEntry {
|
||||||
pub asset_id: AssetId,
|
pub asset_id: AssetId,
|
||||||
@ -85,7 +130,7 @@ pub struct SlotRef {
|
|||||||
|
|
||||||
impl SlotRef {
|
impl SlotRef {
|
||||||
pub fn gfx(index: usize) -> Self {
|
pub fn gfx(index: usize) -> Self {
|
||||||
Self { asset_type: BankType::TILES, index }
|
Self { asset_type: BankType::GLYPH, index }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn audio(index: usize) -> Self {
|
pub fn audio(index: usize) -> Self {
|
||||||
|
|||||||
@ -198,9 +198,9 @@ fn validate_preload(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::asset::{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::tile_bank::TILE_BANK_PALETTE_COUNT_V1;
|
use crate::glyph_bank::GLYPH_BANK_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};
|
||||||
@ -363,16 +363,17 @@ mod tests {
|
|||||||
AssetEntry {
|
AssetEntry {
|
||||||
asset_id: 7,
|
asset_id: 7,
|
||||||
asset_name: "tiles".to_string(),
|
asset_name: "tiles".to_string(),
|
||||||
bank_type: BankType::TILES,
|
bank_type: BankType::GLYPH,
|
||||||
offset,
|
offset,
|
||||||
size,
|
size,
|
||||||
decoded_size: 16 * 16 + (TILE_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
||||||
codec: "NONE".to_string(),
|
codec: AssetCodec::None,
|
||||||
metadata: json!({
|
metadata: json!({
|
||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": TILE_BANK_PALETTE_COUNT_V1
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||||
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -445,16 +446,17 @@ mod tests {
|
|||||||
AssetEntry {
|
AssetEntry {
|
||||||
asset_id: 8,
|
asset_id: 8,
|
||||||
asset_name: "other_tiles".to_string(),
|
asset_name: "other_tiles".to_string(),
|
||||||
bank_type: BankType::TILES,
|
bank_type: BankType::GLYPH,
|
||||||
offset: 4,
|
offset: 4,
|
||||||
size: 4,
|
size: 4,
|
||||||
decoded_size: 16 * 16 + (TILE_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
||||||
codec: "NONE".to_string(),
|
codec: AssetCodec::None,
|
||||||
metadata: json!({
|
metadata: json!({
|
||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": TILE_BANK_PALETTE_COUNT_V1
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||||
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@ -497,4 +499,88 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_unknown_codec_string_in_assets_pa_header() {
|
||||||
|
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||||
|
let header = serde_json::json!({
|
||||||
|
"asset_table": [{
|
||||||
|
"asset_id": 7,
|
||||||
|
"asset_name": "tiles",
|
||||||
|
"bank_type": "GLYPH",
|
||||||
|
"offset": 0,
|
||||||
|
"size": 4,
|
||||||
|
"decoded_size": 768,
|
||||||
|
"codec": "LZ4",
|
||||||
|
"metadata": {
|
||||||
|
"tile_size": 16,
|
||||||
|
"width": 16,
|
||||||
|
"height": 16,
|
||||||
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||||
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"preload": []
|
||||||
|
});
|
||||||
|
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
|
||||||
|
let prelude = AssetsPackPrelude {
|
||||||
|
magic: ASSETS_PA_MAGIC,
|
||||||
|
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||||
|
header_len: header_bytes.len() as u32,
|
||||||
|
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
|
||||||
|
flags: 0,
|
||||||
|
reserved: 0,
|
||||||
|
header_checksum: 0,
|
||||||
|
};
|
||||||
|
let mut bytes = prelude.to_bytes().to_vec();
|
||||||
|
bytes.extend_from_slice(&header_bytes);
|
||||||
|
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
|
||||||
|
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
|
||||||
|
|
||||||
|
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_rejects_legacy_raw_codec_string_in_assets_pa_header() {
|
||||||
|
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||||
|
let header = serde_json::json!({
|
||||||
|
"asset_table": [{
|
||||||
|
"asset_id": 7,
|
||||||
|
"asset_name": "tiles",
|
||||||
|
"bank_type": "GLYPH",
|
||||||
|
"offset": 0,
|
||||||
|
"size": 4,
|
||||||
|
"decoded_size": 768,
|
||||||
|
"codec": "RAW",
|
||||||
|
"metadata": {
|
||||||
|
"tile_size": 16,
|
||||||
|
"width": 16,
|
||||||
|
"height": 16,
|
||||||
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||||
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
"preload": []
|
||||||
|
});
|
||||||
|
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
|
||||||
|
let prelude = AssetsPackPrelude {
|
||||||
|
magic: ASSETS_PA_MAGIC,
|
||||||
|
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||||
|
header_len: header_bytes.len() as u32,
|
||||||
|
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
|
||||||
|
flags: 0,
|
||||||
|
reserved: 0,
|
||||||
|
header_checksum: 0,
|
||||||
|
};
|
||||||
|
let mut bytes = prelude.to_bytes().to_vec();
|
||||||
|
bytes.extend_from_slice(&header_bytes);
|
||||||
|
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
|
||||||
|
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
|
||||||
|
|
||||||
|
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||||
|
|
||||||
|
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
crates/console/prometeu-hal/src/glyph.rs
Normal file
5
crates/console/prometeu-hal/src/glyph.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct Glyph {
|
||||||
|
pub glyph_id: u16,
|
||||||
|
pub palette_id: u8,
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
use crate::color::Color;
|
use crate::color::Color;
|
||||||
|
|
||||||
pub const TILE_BANK_PALETTE_COUNT_V1: usize = 64;
|
pub const GLYPH_BANK_PALETTE_COUNT_V1: usize = 64;
|
||||||
pub const TILE_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.
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
@ -16,12 +16,12 @@ pub enum TileSize {
|
|||||||
|
|
||||||
/// A container for graphical assets.
|
/// A container for graphical assets.
|
||||||
///
|
///
|
||||||
/// A TileBank stores the decoded runtime representation of a tile-bank asset.
|
/// A GlyphBank stores the decoded runtime representation of a glyph-bank asset.
|
||||||
///
|
///
|
||||||
/// Serialized `assets.pa` payloads keep pixel indices packed as `4bpp` nibbles.
|
/// Serialized `assets.pa` payloads keep pixel indices packed as `4bpp` nibbles.
|
||||||
/// After decode, the runtime expands them to one `u8` palette index per pixel
|
/// After decode, the runtime expands them to one `u8` palette index per pixel
|
||||||
/// while preserving the same `0..15` logical range.
|
/// while preserving the same `0..15` logical range.
|
||||||
pub struct TileBank {
|
pub struct GlyphBank {
|
||||||
/// Dimension of each individual tile in the bank.
|
/// Dimension of each individual tile in the bank.
|
||||||
pub tile_size: TileSize,
|
pub tile_size: TileSize,
|
||||||
/// Width of the full bank sheet in pixels.
|
/// Width of the full bank sheet in pixels.
|
||||||
@ -34,18 +34,18 @@ pub struct TileBank {
|
|||||||
/// Index 0 is always reserved for transparency.
|
/// Index 0 is always reserved for transparency.
|
||||||
pub pixel_indices: Vec<u8>,
|
pub pixel_indices: Vec<u8>,
|
||||||
/// Runtime-facing v1 palette table: 64 palettes of 16 RGB565 colors each.
|
/// Runtime-facing v1 palette table: 64 palettes of 16 RGB565 colors each.
|
||||||
pub palettes: [[Color; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1],
|
pub palettes: [[Color; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TileBank {
|
impl GlyphBank {
|
||||||
/// Creates an empty tile bank with the specified dimensions.
|
/// Creates an empty glyph bank with the specified dimensions.
|
||||||
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
|
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tile_size,
|
tile_size,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
pixel_indices: vec![0; width * height], // Index 0 = Transparent
|
pixel_indices: vec![0; width * height], // Index 0 = Transparent
|
||||||
palettes: [[Color::BLACK; TILE_BANK_COLORS_PER_PALETTE]; TILE_BANK_PALETTE_COUNT_V1],
|
palettes: [[Color::BLACK; GLYPH_BANK_COLORS_PER_PALETTE]; GLYPH_BANK_PALETTE_COUNT_V1],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -7,6 +7,8 @@ pub mod cartridge_loader;
|
|||||||
pub mod color;
|
pub mod color;
|
||||||
pub mod debugger_protocol;
|
pub mod debugger_protocol;
|
||||||
pub mod gfx_bridge;
|
pub mod gfx_bridge;
|
||||||
|
pub mod glyph;
|
||||||
|
pub mod glyph_bank;
|
||||||
pub mod hardware_bridge;
|
pub mod hardware_bridge;
|
||||||
pub mod host_context;
|
pub mod host_context;
|
||||||
pub mod host_return;
|
pub mod host_return;
|
||||||
@ -21,7 +23,6 @@ pub mod sprite;
|
|||||||
pub mod syscalls;
|
pub mod syscalls;
|
||||||
pub mod telemetry;
|
pub mod telemetry;
|
||||||
pub mod tile;
|
pub mod tile;
|
||||||
pub mod tile_bank;
|
|
||||||
pub mod tile_layer;
|
pub mod tile_layer;
|
||||||
pub mod touch_bridge;
|
pub mod touch_bridge;
|
||||||
pub mod vm_fault;
|
pub mod vm_fault;
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
use crate::tile::Tile;
|
use crate::glyph::Glyph;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct Sprite {
|
pub struct Sprite {
|
||||||
pub tile: Tile,
|
pub glyph: Glyph,
|
||||||
pub x: i32,
|
pub x: i32,
|
||||||
pub y: i32,
|
pub y: i32,
|
||||||
pub bank_id: u8,
|
pub bank_id: u8,
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
|
use crate::glyph::Glyph;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct Tile {
|
pub struct Tile {
|
||||||
pub id: u16,
|
pub glyph: Glyph,
|
||||||
|
pub active: bool,
|
||||||
pub flip_x: bool,
|
pub flip_x: bool,
|
||||||
pub flip_y: bool,
|
pub flip_y: bool,
|
||||||
pub palette_id: u8,
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
|
use crate::glyph_bank::TileSize;
|
||||||
|
use crate::glyph_bank::TileSize::Size8;
|
||||||
use crate::tile::Tile;
|
use crate::tile::Tile;
|
||||||
use crate::tile_bank::TileSize;
|
|
||||||
use crate::tile_bank::TileSize::Size8;
|
|
||||||
|
|
||||||
pub struct TileMap {
|
pub struct TileMap {
|
||||||
pub width: usize,
|
pub width: usize,
|
||||||
|
|||||||
@ -4,10 +4,10 @@ use prometeu_bytecode::{TRAP_INVALID_SYSCALL, TRAP_OOB, TRAP_TYPE, Value};
|
|||||||
use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
|
use prometeu_hal::asset::{AssetId, AssetOpStatus, BankType, SlotRef};
|
||||||
use prometeu_hal::cartridge::AppMode;
|
use prometeu_hal::cartridge::AppMode;
|
||||||
use prometeu_hal::color::Color;
|
use prometeu_hal::color::Color;
|
||||||
|
use prometeu_hal::glyph::Glyph;
|
||||||
use prometeu_hal::log::{LogLevel, LogSource};
|
use prometeu_hal::log::{LogLevel, LogSource};
|
||||||
use prometeu_hal::sprite::Sprite;
|
use prometeu_hal::sprite::Sprite;
|
||||||
use prometeu_hal::syscalls::Syscall;
|
use prometeu_hal::syscalls::Syscall;
|
||||||
use prometeu_hal::tile::Tile;
|
|
||||||
use prometeu_hal::vm_fault::VmFault;
|
use prometeu_hal::vm_fault::VmFault;
|
||||||
use prometeu_hal::{
|
use prometeu_hal::{
|
||||||
AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool,
|
AudioOpStatus, GfxOpStatus, HostContext, HostReturn, NativeInterface, SyscallId, expect_bool,
|
||||||
@ -139,7 +139,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
let index = expect_int(args, 1)? as usize;
|
let index = expect_int(args, 1)? as usize;
|
||||||
let x = expect_int(args, 2)? as i32;
|
let x = expect_int(args, 2)? as i32;
|
||||||
let y = expect_int(args, 3)? as i32;
|
let y = expect_int(args, 3)? as i32;
|
||||||
let tile_id = expect_int(args, 4)? as u16;
|
let glyph_id = expect_int(args, 4)? as u16;
|
||||||
let palette_id = expect_int(args, 5)? as u8;
|
let palette_id = expect_int(args, 5)? as u8;
|
||||||
let active = expect_bool(args, 6)?;
|
let active = expect_bool(args, 6)?;
|
||||||
let flip_x = expect_bool(args, 7)?;
|
let flip_x = expect_bool(args, 7)?;
|
||||||
@ -162,7 +162,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
}
|
}
|
||||||
|
|
||||||
*hw.gfx_mut().sprite_mut(index) = Sprite {
|
*hw.gfx_mut().sprite_mut(index) = Sprite {
|
||||||
tile: Tile { id: tile_id, flip_x: false, flip_y: false, palette_id },
|
glyph: Glyph { glyph_id, palette_id },
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
bank_id,
|
bank_id,
|
||||||
@ -481,7 +481,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
}
|
}
|
||||||
Syscall::BankInfo => {
|
Syscall::BankInfo => {
|
||||||
let asset_type = match expect_int(args, 0)? as u32 {
|
let asset_type = match expect_int(args, 0)? as u32 {
|
||||||
0 => BankType::TILES,
|
0 => BankType::GLYPH,
|
||||||
1 => BankType::SOUNDS,
|
1 => BankType::SOUNDS,
|
||||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
||||||
};
|
};
|
||||||
@ -492,7 +492,7 @@ impl NativeInterface for VirtualMachineRuntime {
|
|||||||
}
|
}
|
||||||
Syscall::BankSlotInfo => {
|
Syscall::BankSlotInfo => {
|
||||||
let asset_type = match expect_int(args, 0)? as u32 {
|
let asset_type = match expect_int(args, 0)? as u32 {
|
||||||
0 => BankType::TILES,
|
0 => BankType::GLYPH,
|
||||||
1 => BankType::SOUNDS,
|
1 => BankType::SOUNDS,
|
||||||
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
_ => return Err(VmFault::Trap(TRAP_TYPE, "Invalid asset type".to_string())),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -8,10 +8,12 @@ use prometeu_drivers::hardware::Hardware;
|
|||||||
use prometeu_hal::AudioOpStatus;
|
use prometeu_hal::AudioOpStatus;
|
||||||
use prometeu_hal::GfxOpStatus;
|
use prometeu_hal::GfxOpStatus;
|
||||||
use prometeu_hal::InputSignals;
|
use prometeu_hal::InputSignals;
|
||||||
use prometeu_hal::asset::{AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus};
|
use prometeu_hal::asset::{
|
||||||
|
AssetCodec, AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus,
|
||||||
|
};
|
||||||
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge};
|
||||||
|
use prometeu_hal::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
||||||
use prometeu_hal::syscalls::caps;
|
use prometeu_hal::syscalls::caps;
|
||||||
use prometeu_hal::tile_bank::TILE_BANK_PALETTE_COUNT_V1;
|
|
||||||
use prometeu_vm::VmInitError;
|
use prometeu_vm::VmInitError;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -92,36 +94,37 @@ fn serialized_single_function_module_with_consts(
|
|||||||
.serialize()
|
.serialize()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_tile_payload_size(width: usize, height: usize) -> usize {
|
fn test_glyph_payload_size(width: usize, height: usize) -> usize {
|
||||||
(width * height).div_ceil(2) + (TILE_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
|
(width * height).div_ceil(2) + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_tile_decoded_size(width: usize, height: usize) -> usize {
|
fn test_glyph_decoded_size(width: usize, height: usize) -> usize {
|
||||||
width * height + (TILE_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
|
width * height + (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * std::mem::size_of::<u16>())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_tile_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
|
fn test_glyph_asset_entry(asset_name: &str, data_len: usize) -> AssetEntry {
|
||||||
AssetEntry {
|
AssetEntry {
|
||||||
asset_id: 7,
|
asset_id: 7,
|
||||||
asset_name: asset_name.to_string(),
|
asset_name: asset_name.to_string(),
|
||||||
bank_type: BankType::TILES,
|
bank_type: BankType::GLYPH,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
size: data_len as u64,
|
size: data_len as u64,
|
||||||
decoded_size: test_tile_decoded_size(16, 16) as u64,
|
decoded_size: test_glyph_decoded_size(16, 16) as u64,
|
||||||
codec: "NONE".to_string(),
|
codec: AssetCodec::None,
|
||||||
metadata: serde_json::json!({
|
metadata: serde_json::json!({
|
||||||
"tile_size": 16,
|
"tile_size": 16,
|
||||||
"width": 16,
|
"width": 16,
|
||||||
"height": 16,
|
"height": 16,
|
||||||
"palette_count": TILE_BANK_PALETTE_COUNT_V1
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
||||||
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn test_tile_asset_data() -> Vec<u8> {
|
fn test_glyph_asset_data() -> Vec<u8> {
|
||||||
let mut data =
|
let mut data =
|
||||||
vec![0x11u8; test_tile_payload_size(16, 16) - (TILE_BANK_PALETTE_COUNT_V1 * 16 * 2)];
|
vec![0x11u8; test_glyph_payload_size(16, 16) - (GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2)];
|
||||||
data.extend_from_slice(&[0u8; TILE_BANK_PALETTE_COUNT_V1 * 16 * 2]);
|
data.extend_from_slice(&[0u8; GLYPH_BANK_PALETTE_COUNT_V1 * 16 * 2]);
|
||||||
data
|
data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -436,10 +439,10 @@ fn tick_gfx_set_sprite_invalid_range_returns_status_not_crash() {
|
|||||||
}],
|
}],
|
||||||
);
|
);
|
||||||
let cartridge = cartridge_with_program(program, caps::GFX);
|
let cartridge = cartridge_with_program(program, caps::GFX);
|
||||||
let asset_data = test_tile_asset_data();
|
let asset_data = test_glyph_asset_data();
|
||||||
|
|
||||||
hardware.assets.initialize_for_cartridge(
|
hardware.assets.initialize_for_cartridge(
|
||||||
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
||||||
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
|
vec![prometeu_hal::asset::PreloadEntry { asset_id: 7, slot: 0 }],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
@ -634,9 +637,9 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() {
|
|||||||
let mut vm = VirtualMachine::default();
|
let mut vm = VirtualMachine::default();
|
||||||
let mut hardware = Hardware::new();
|
let mut hardware = Hardware::new();
|
||||||
let signals = InputSignals::default();
|
let signals = InputSignals::default();
|
||||||
let asset_data = test_tile_asset_data();
|
let asset_data = test_glyph_asset_data();
|
||||||
hardware.assets.initialize_for_cartridge(
|
hardware.assets.initialize_for_cartridge(
|
||||||
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
@ -718,9 +721,9 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() {
|
|||||||
);
|
);
|
||||||
let cartridge = cartridge_with_program(program, caps::ASSET);
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
||||||
|
|
||||||
let asset_data = test_tile_asset_data();
|
let asset_data = test_glyph_asset_data();
|
||||||
hardware.assets.initialize_for_cartridge(
|
hardware.assets.initialize_for_cartridge(
|
||||||
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
@ -779,9 +782,9 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() {
|
|||||||
);
|
);
|
||||||
let cartridge = cartridge_with_program(program, caps::ASSET);
|
let cartridge = cartridge_with_program(program, caps::ASSET);
|
||||||
|
|
||||||
let asset_data = test_tile_asset_data();
|
let asset_data = test_glyph_asset_data();
|
||||||
hardware.assets.initialize_for_cartridge(
|
hardware.assets.initialize_for_cartridge(
|
||||||
vec![test_tile_asset_entry("tile_asset", asset_data.len())],
|
vec![test_glyph_asset_entry("tile_asset", asset_data.len())],
|
||||||
vec![],
|
vec![],
|
||||||
AssetsPayloadSource::from_bytes(asset_data),
|
AssetsPayloadSource::from_bytes(asset_data),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -166,7 +166,7 @@ impl VirtualMachineRuntime {
|
|||||||
|
|
||||||
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
|
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
|
||||||
|
|
||||||
let gfx_stats = hw.assets().bank_info(BankType::TILES);
|
let gfx_stats = hw.assets().bank_info(BankType::GLYPH);
|
||||||
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
|
self.telemetry_current.gfx_used_bytes = gfx_stats.used_bytes;
|
||||||
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
|
self.telemetry_current.gfx_inflight_bytes = gfx_stats.inflight_bytes;
|
||||||
self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32;
|
self.telemetry_current.gfx_slots_occupied = gfx_stats.slots_occupied as u32;
|
||||||
|
|||||||
@ -67,7 +67,7 @@ pub fn generate() -> Result<()> {
|
|||||||
const_pool: vec![
|
const_pool: vec![
|
||||||
ConstantPoolEntry::String("stress".into()),
|
ConstantPoolEntry::String("stress".into()),
|
||||||
ConstantPoolEntry::String("frame".into()),
|
ConstantPoolEntry::String("frame".into()),
|
||||||
ConstantPoolEntry::String("missing_tile_bank".into()),
|
ConstantPoolEntry::String("missing_glyph_bank".into()),
|
||||||
],
|
],
|
||||||
functions,
|
functions,
|
||||||
code: rom,
|
code: rom,
|
||||||
|
|||||||
@ -1,22 +1,23 @@
|
|||||||
{"type":"meta","next_id":{"DSC":21,"AGD":19,"DEC":3,"PLN":3,"LSN":21,"CLSN":1}}
|
{"type":"meta","next_id":{"DSC":23,"AGD":21,"DEC":7,"PLN":6,"LSN":26,"CLSN":1}}
|
||||||
... (mantendo as linhas anteriores) ...
|
|
||||||
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
{"type":"discussion","id":"DSC-0020","status":"done","ticket":"jenkins-gitea-integration","title":"Jenkins Gitea Integration and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins","gitea"],"agendas":[{"id":"AGD-0018","file":"workflow/agendas/AGD-0018-jenkins-gitea-integration-and-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0003","file":"workflow/decisions/DEC-0003-jenkins-gitea-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0003","file":"workflow/plans/PLN-0003-jenkins-gitea-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0021","file":"lessons/DSC-0020-jenkins-gitea-integration/LSN-0021-jenkins-gitea-integration.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0021","status":"done","ticket":"asset-entry-codec-enum-with-metadata","title":"Asset Entry Codec Enum Contract","created_at":"2026-04-09","updated_at":"2026-04-09","tags":["asset","runtime","codec","metadata"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0024","file":"lessons/DSC-0021-asset-entry-codec-enum-contract/LSN-0024-string-on-the-wire-enum-in-runtime.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
|
{"type":"discussion","id":"DSC-0022","status":"done","ticket":"tile-bank-vs-glyph-bank-domain-naming","title":"Glyph Bank Domain Naming Contract","created_at":"2026-04-09","updated_at":"2026-04-10","tags":["gfx","runtime","naming","domain-model"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0025","file":"lessons/DSC-0022-glyph-bank-domain-naming/LSN-0025-rename-artifact-by-meaning-not-by-token.md","status":"done","created_at":"2026-04-10","updated_at":"2026-04-10"}]}
|
||||||
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
{"type":"discussion","id":"DSC-0001","status":"done","ticket":"legacy-runtime-learn-import","title":"Import legacy runtime learn into discussion lessons","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["migration","tech-debt"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0001","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0001-prometeu-learn-index.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0002","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0002-historical-asset-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0003","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0003-historical-audio-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0004","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0004-historical-cartridge-boot-protocol-and-manifest-authority.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0005","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0005-historical-game-memcard-slots-surface-and-semantics.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0006","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0006-historical-gfx-status-first-fault-and-return-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0007","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0007-historical-retired-fault-and-input-decisions.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0008","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0008-historical-vm-core-and-assets.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0009","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0009-mental-model-asset-management.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0010","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0010-mental-model-audio.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0011","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0011-mental-model-gfx.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0012","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0012-mental-model-input.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0013","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0013-mental-model-observability-and-debugging.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0014","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0014-mental-model-portability-and-cross-platform.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0015","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0015-mental-model-save-memory-and-memcard.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0016","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0016-mental-model-status-first-and-fault-thinking.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0017","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0017-mental-model-time-and-cycles.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"},{"id":"LSN-0018","file":"lessons/DSC-0001-runtime-learn-legacy-import/LSN-0018-mental-model-touch.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0002","status":"open","ticket":"runtime-edge-test-plan","title":"Agenda - Runtime Edge Test Plan","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0001","file":"workflow/agendas/AGD-0001-runtime-edge-test-plan.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0003","status":"open","ticket":"packed-cartridge-loader-pmc","title":"Agenda - Packed Cartridge Loader PMC","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0002","file":"workflow/agendas/AGD-0002-packed-cartridge-loader-pmc.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0004","status":"open","ticket":"system-run-cart","title":"Agenda - System Run Cart","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0003","file":"workflow/agendas/AGD-0003-system-run-cart.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0005","status":"open","ticket":"system-fault-semantics-and-control-surface","title":"Agenda - System Fault Semantics and Control Surface","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0004","file":"workflow/agendas/AGD-0004-system-fault-semantics-and-control-surface.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0006","status":"open","ticket":"vm-owned-random-service","title":"Agenda - VM-Owned Random Service","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0005","file":"workflow/agendas/AGD-0005-vm-owned-random-service.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0007","status":"open","ticket":"app-home-filesystem-surface-and-semantics","title":"Agenda - App Home Filesystem Surface and Semantics","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0006","file":"workflow/agendas/AGD-0006-app-home-filesystem-surface-and-semantics.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0008","status":"open","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0007","file":"AGD-0007-perf-runtime-telemetry-hot-path.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0008","status":"open","ticket":"perf-runtime-telemetry-hot-path","title":"Agenda - [PERF] Runtime Telemetry Hot Path","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0007","file":"workflow/agendas/AGD-0007-perf-runtime-telemetry-hot-path.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0009","status":"open","ticket":"perf-async-background-work-lanes-for-assets-and-fs","title":"Agenda - [PERF] Async Background Work Lanes for Assets and FS","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0008","file":"workflow/agendas/AGD-0008-perf-async-background-work-lanes-for-assets-and-fs.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0010","status":"open","ticket":"perf-host-desktop-frame-pacing-and-presentation","title":"Agenda - [PERF] Host Desktop Frame Pacing and Presentation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0009","file":"workflow/agendas/AGD-0009-perf-host-desktop-frame-pacing-and-presentation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0011","status":"open","ticket":"perf-gfx-render-pipeline-and-dirty-regions","title":"Agenda - [PERF] GFX Render Pipeline and Dirty Regions","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0010","file":"workflow/agendas/AGD-0010-perf-gfx-render-pipeline-and-dirty-regions.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0012","status":"open","ticket":"perf-runtime-introspection-syscalls","title":"Agenda - [PERF] Runtime Introspection Syscalls","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0011","file":"workflow/agendas/AGD-0011-perf-runtime-introspection-syscalls.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0012","file":"AGD-0012-perf-host-debug-overlay-isolation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0013","status":"open","ticket":"perf-host-debug-overlay-isolation","title":"Agenda - [PERF] Host Debug Overlay Isolation","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0012","file":"workflow/agendas/AGD-0012-perf-host-debug-overlay-isolation.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0014","status":"open","ticket":"perf-vm-allocation-and-copy-pressure","title":"Agenda - [PERF] VM Allocation and Copy Pressure","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0013","file":"workflow/agendas/AGD-0013-perf-vm-allocation-and-copy-pressure.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0015","status":"open","ticket":"perf-cartridge-boot-and-program-ownership","title":"Agenda - [PERF] Cartridge Boot and Program Ownership","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0014","file":"workflow/agendas/AGD-0014-perf-cartridge-boot-and-program-ownership.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
||||||
{"type":"discussion","id":"DSC-0016","status":"open","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0015","file":"AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0016","status":"done","ticket":"tilemap-empty-cell-vs-tile-id-zero","title":"Tilemap Empty Cell vs Tile ID Zero","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0015","file":"workflow/agendas/AGD-0015-tilemap-empty-cell-vs-tile-id-zero.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0022","file":"lessons/DSC-0016-tilemap-empty-cell-semantics/LSN-0022-tilemap-empty-cell-convergence.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
{"type":"discussion","id":"DSC-0017","status":"open","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":[],"agendas":[{"id":"AGD-0016","file":"AGD-0016-asset-entry-metadata-normalization-contract.md","status":"open","created_at":"2026-03-27","updated_at":"2026-03-27"}],"decisions":[],"plans":[],"lessons":[]}
|
{"type":"discussion","id":"DSC-0017","status":"done","ticket":"asset-entry-metadata-normalization-contract","title":"Asset Entry Metadata Normalization Contract","created_at":"2026-03-27","updated_at":"2026-04-09","tags":[],"agendas":[{"id":"AGD-0016","file":"workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md","status":"done","created_at":"2026-03-27","updated_at":"2026-04-09"}],"decisions":[{"id":"DEC-0004","file":"workflow/decisions/DEC-0004-asset-entry-metadata-normalization-contract.md","status":"accepted","created_at":"2026-04-09","updated_at":"2026-04-09"}],"plans":[],"lessons":[{"id":"LSN-0023","file":"lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md","status":"done","created_at":"2026-04-09","updated_at":"2026-04-09"}]}
|
||||||
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
{"type":"discussion","id":"DSC-0018","status":"done","ticket":"asset-load-asset-id-int-contract","title":"Asset Load Asset ID Int Contract","created_at":"2026-03-27","updated_at":"2026-03-27","tags":["asset","runtime","abi"],"agendas":[],"decisions":[],"plans":[],"lessons":[{"id":"LSN-0019","file":"lessons/DSC-0018-asset-load-asset-id-int-contract/LSN-0019-asset-load-id-abi-convergence.md","status":"done","created_at":"2026-03-27","updated_at":"2026-03-27"}]}
|
||||||
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
{"type":"discussion","id":"DSC-0019","status":"done","ticket":"jenkinsfile-correction","title":"Jenkinsfile Correction and Relocation","created_at":"2026-04-07","updated_at":"2026-04-07","tags":["ci","jenkins"],"agendas":[{"id":"AGD-0017","file":"workflow/agendas/AGD-0017-jenkinsfile-correction.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"decisions":[{"id":"DEC-0002","file":"workflow/decisions/DEC-0002-jenkinsfile-strategy.md","status":"accepted","created_at":"2026-04-07","updated_at":"2026-04-07"}],"plans":[{"id":"PLN-0002","file":"workflow/plans/PLN-0002-jenkinsfile-execution.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}],"lessons":[{"id":"LSN-0020","file":"lessons/DSC-0019-jenkins-ci-standardization/LSN-0020-jenkins-standard-relocation.md","status":"done","created_at":"2026-04-07","updated_at":"2026-04-07"}]}
|
||||||
|
|||||||
@ -123,7 +123,7 @@ The current asset model works better if you do not treat every bank as equally g
|
|||||||
|
|
||||||
Some banks are specialized:
|
Some banks are specialized:
|
||||||
|
|
||||||
- `TILES`
|
- `GLYPH`
|
||||||
- `SOUNDS`
|
- `SOUNDS`
|
||||||
|
|
||||||
For these, the bank contract already carries most of the important format rules. That means:
|
For these, the bank contract already carries most of the important format rules. That means:
|
||||||
@ -163,7 +163,7 @@ It does not say:
|
|||||||
|
|
||||||
One of the most important asset-management lessons in the current runtime is that `codec = NONE` does not mean "bitwise identical to in-memory layout".
|
One of the most important asset-management lessons in the current runtime is that `codec = NONE` does not mean "bitwise identical to in-memory layout".
|
||||||
|
|
||||||
The tile-bank path is the concrete example:
|
The glyph-bank path is the concrete example:
|
||||||
|
|
||||||
- there is no additional generic codec layer for the asset;
|
- there is no additional generic codec layer for the asset;
|
||||||
- the serialized payload stores indexed pixels as packed `4bpp`;
|
- the serialized payload stores indexed pixels as packed `4bpp`;
|
||||||
@ -182,13 +182,13 @@ That is why `AssetEntry.size` and `AssetEntry.decoded_size` must be thought of a
|
|||||||
|
|
||||||
If those two numbers are treated as interchangeable, telemetry, budgets, and validation all become misleading.
|
If those two numbers are treated as interchangeable, telemetry, budgets, and validation all become misleading.
|
||||||
|
|
||||||
## Tile Banks Teach The Real Boundary
|
## Glyph Banks Teach The Real Boundary
|
||||||
|
|
||||||
Tile banks are useful because they show the real separation of concerns:
|
Glyph banks are useful because they show the real separation of concerns:
|
||||||
|
|
||||||
- `assets.pa` defines the serialized envelope and metadata needed to reconstruct the bank;
|
- `assets.pa` defines the serialized envelope and metadata needed to reconstruct the bank;
|
||||||
- the runtime validates that metadata against the expected v1 contract;
|
- the runtime validates that metadata against the expected v1 contract;
|
||||||
- the resident `TileBank` is a runtime object, not a direct view over the cold bytes.
|
- the resident `GlyphBank` is a runtime object, not a direct view over the cold bytes.
|
||||||
|
|
||||||
This is the right PROMETEU mental model:
|
This is the right PROMETEU mental model:
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ PROMETEU treats graphics as an explicit peripheral, not as a modern GPU.
|
|||||||
The right mental model is a retro 2D machine with:
|
The right mental model is a retro 2D machine with:
|
||||||
|
|
||||||
- framebuffer;
|
- framebuffer;
|
||||||
- tile banks;
|
- glyph banks;
|
||||||
- tile layers;
|
- tile layers;
|
||||||
- sprites ordered by draw order;
|
- sprites ordered by draw order;
|
||||||
- deterministic composition per frame.
|
- deterministic composition per frame.
|
||||||
@ -67,7 +67,7 @@ That enables:
|
|||||||
- HUD themes;
|
- HUD themes;
|
||||||
- day and night cycles.
|
- day and night cycles.
|
||||||
|
|
||||||
In the current tile-bank v1 baseline, this palette model is intentionally bounded:
|
In the current glyph-bank v1 baseline, this palette model is intentionally bounded:
|
||||||
|
|
||||||
- each bank carries `64` palettes;
|
- each bank carries `64` palettes;
|
||||||
- each palette carries `16` colors;
|
- each palette carries `16` colors;
|
||||||
@ -75,7 +75,7 @@ In the current tile-bank v1 baseline, this palette model is intentionally bounde
|
|||||||
|
|
||||||
That limit is not incidental bookkeeping. It is part of how art packaging, runtime validation, and rendering stay aligned.
|
That limit is not incidental bookkeeping. It is part of how art packaging, runtime validation, and rendering stay aligned.
|
||||||
|
|
||||||
## Tile Banks Are Decoded Runtime Objects
|
## Glyph Banks Are Decoded Runtime Objects
|
||||||
|
|
||||||
The most useful intuition is to separate three layers:
|
The most useful intuition is to separate three layers:
|
||||||
|
|
||||||
@ -83,13 +83,13 @@ The most useful intuition is to separate three layers:
|
|||||||
- serialized cart payload;
|
- serialized cart payload;
|
||||||
- resident runtime bank.
|
- resident runtime bank.
|
||||||
|
|
||||||
For tile banks in v1:
|
For glyph banks in v1:
|
||||||
|
|
||||||
- authored pixels are logical indices `0..15`;
|
- authored pixels are logical indices `0..15`;
|
||||||
- serialized payload stores those indices as packed `4bpp`;
|
- serialized payload stores those indices as packed `4bpp`;
|
||||||
- runtime memory expands them to one `u8` index per pixel after decode.
|
- runtime memory expands them to one `u8` index per pixel after decode.
|
||||||
|
|
||||||
So when reading the graphics model, do not imagine the renderer reading packed nibbles directly from cartridge storage. The renderer consumes a decoded `TileBank` object whose memory shape is optimized for runtime lookup, not for transport density.
|
So when reading the graphics model, do not imagine the renderer reading packed nibbles directly from cartridge storage. The renderer consumes a decoded `GlyphBank` object whose memory shape is optimized for runtime lookup, not for transport density.
|
||||||
|
|
||||||
## Use Cases
|
## Use Cases
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,20 @@
|
|||||||
|
# LSN-0022: Tilemap Empty Cell Semantics and Glyph Convergence
|
||||||
|
|
||||||
|
## Context
|
||||||
|
During the initial runtime design, `tile_id = 0` was used as a sentinel value for "empty" or "missing" tiles in tilemaps. However, as the asset banking and packer systems evolved, it became clear that `0` should be a valid index for any asset bank, including glyph banks. This conflict was formally tracked in `AGD-0015`.
|
||||||
|
|
||||||
|
## Lessons Learned
|
||||||
|
|
||||||
|
### 1. Explicit Presence Over Magic Values
|
||||||
|
Modeling absence with a magic value (like `tile_id = 0`) creates friction when that value is also a valid member of the domain (index 0). The project moved towards explicit presence checks.
|
||||||
|
|
||||||
|
### 2. Convergence During Parallel Implementation
|
||||||
|
The resolution of the tilemap empty cell semantics (moving to explicit presence) was crystallized during the implementation of the `glyph` system. Instead of maintaining two different ways of handling "empty" slots, the tilemap logic was harmonized with the newer, cleaner approach used for glyphs and other render paths.
|
||||||
|
|
||||||
|
### 3. Impact on Render Pipeline
|
||||||
|
By allowing `tile_id = 0` to be a valid tile, the render pipeline now relies on a separate bit or `Option`-like structure to determine if a cell should be drawn. This makes the code more robust and the asset indices more intuitive.
|
||||||
|
|
||||||
|
## References
|
||||||
|
- Discussion: `DSC-0016`
|
||||||
|
- Agenda: `AGD-0015` (Tilemap Empty Cell vs Tile ID Zero)
|
||||||
|
- Implementation: Glyph system integration
|
||||||
@ -0,0 +1,33 @@
|
|||||||
|
# LSN-0023: Typed Helpers for Asset Metadata
|
||||||
|
|
||||||
|
Status: Published
|
||||||
|
Decisions: [[DEC-0004]]
|
||||||
|
Tags: [asset, runtime, rust, pattern]
|
||||||
|
|
||||||
|
## Contexto
|
||||||
|
|
||||||
|
A decisão [[DEC-0004]] estabeleceu um contrato de metadados segmentados para assets, mantendo campos críticos na raiz do JSON e detalhes técnicos em subárvores (`codec`, `pipeline`). No entanto, o `AssetEntry.metadata` no runtime é um `serde_json::Value` dinâmico.
|
||||||
|
|
||||||
|
## Lição
|
||||||
|
|
||||||
|
O uso de `serde_json::Value` diretamente nos loaders do runtime introduz riscos de runtime (erros de tipo, campos ausentes) e polui o código com chamadas repetitivas de `.get()`, `.as_u64()`, etc.
|
||||||
|
|
||||||
|
### Abordagem Adotada
|
||||||
|
|
||||||
|
Para mitigar isso, implementamos o padrão de **Typed Metadata Helpers**:
|
||||||
|
|
||||||
|
1. **Structs Dedicadas**: Criamos structs Rust (ex: `GlyphsMetadata`, `SoundsMetadata`) que representam o contrato exato de cada banco.
|
||||||
|
2. **Conversion Methods**: Adicionamos métodos ao `AssetEntry` (ex: `metadata_as_glyphs()`) que utilizam `serde_json::from_value` para realizar o "cast" do JSON dinâmico para a struct tipada.
|
||||||
|
3. **Fail-Fast**: A falha no parsing dos metadados deve ser tratada como erro de carregamento do asset, garantindo que o motor não opere com metadados corrompidos ou incompletos.
|
||||||
|
|
||||||
|
### Benefícios
|
||||||
|
|
||||||
|
- **Segurança de Tipo**: Erros de estrutura de metadados são detectados no momento do carregamento.
|
||||||
|
- **Ergonomia**: O código dos drivers passa a usar `meta.tile_size` em vez de parsing manual.
|
||||||
|
- **Desacoplamento**: A complexidade do JSON fica encapsulada nos helpers de conversão.
|
||||||
|
|
||||||
|
## Referências
|
||||||
|
|
||||||
|
- DEC-0004: Asset Entry Metadata Normalization Contract
|
||||||
|
- `prometeu-hal/src/asset.rs`
|
||||||
|
- `prometeu-drivers/src/asset.rs`
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0024
|
||||||
|
ticket: asset-entry-codec-enum-with-metadata
|
||||||
|
title: String on the Wire, Enum in the Runtime
|
||||||
|
created: 2026-04-09
|
||||||
|
tags: [asset, runtime, codec, rust, contract]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
This discussion closed the contract for `AssetEntry.codec` in the runtime asset model. The system already serialized `assets.pa` as JSON produced by the Studio in Java, while the runtime consumed that JSON in Rust. The original runtime model kept `codec` as a free-form `String`, which allowed legacy aliases such as `RAW` and pushed validation and branching into scattered string comparisons.
|
||||||
|
|
||||||
|
The implemented change moved the runtime to a typed enum while preserving the existing transport simplicity of the pack format.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Asset Entry Codec Enum Contract
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
Keep `codec` as a JSON string in `assets.pa`, but deserialize it directly into a Rust enum in `AssetEntry`. The initial codec set contains only `None`, serialized canonically as `NONE` in `SCREAMING_SNAKE_CASE`.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
This preserves a simple cross-language wire contract for the Studio packer while making the runtime honest about the domain model it operates on. The loader now rejects unknown codecs early, and runtime consumers branch on enum variants instead of raw strings.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
The runtime loses open-ended tolerance for arbitrary codec names, which is intentional. Future codecs with payload-specific metadata still require a dedicated follow-up decision once a real codec exists.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
- Use a stable textual discriminant on the wire and a typed enum in the runtime model.
|
||||||
|
- Let deserialization enforce the supported variant set instead of adding compatibility shims deeper in the execution path.
|
||||||
|
- Keep the discriminant contract canonical across producers and consumers; do not allow per-producer aliases.
|
||||||
|
- Preserve editorial or bank-specific metadata outside the codec discriminant so future codec payload metadata can be added without collapsing the whole asset model into an untyped blob.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- A runtime that accepts legacy aliases such as `RAW` keeps historical ambiguity alive and makes it harder to reason about the true contract.
|
||||||
|
- If fixtures are constructed directly in tests, typed metadata helpers can expose missing required fields that were previously masked by weaker validation.
|
||||||
|
- Cross-language contracts become fragile if the wire spelling is not explicitly canonized. The producer must conform exactly to the runtime contract.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- Use a string on the wire when cross-language transport simplicity matters, but deserialize into an enum as soon as the runtime needs semantic guarantees.
|
||||||
|
- Canonical spelling belongs to the shared contract, not to whichever producer happens to serialize first.
|
||||||
|
- Rejecting unknown codec values at asset-entry validation time keeps failures local, early, and easier to debug.
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
id: LSN-0025
|
||||||
|
ticket: tile-bank-vs-glyph-bank-domain-naming
|
||||||
|
title: Rename Artifact by Meaning, Not by Token
|
||||||
|
created: 2026-04-10
|
||||||
|
tags: [gfx, runtime, naming, refactor, documentation]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The runtime used `TileBank` as the name of the concrete graphical bank artifact while also using `tile` for map, layer, and geometric concepts such as `TileLayer`, `TileMap`, and `TileSize`. This overloaded the term `tile` across two different meanings.
|
||||||
|
|
||||||
|
The implemented migration renamed only the concrete bank artifact to `GlyphBank` and renamed the corresponding asset-bank contract from `BankType::TILES` to `BankType::GLYPH`, while explicitly preserving `TileLayer`, `TileMap`, and `TileSize`.
|
||||||
|
|
||||||
|
## Key Decisions
|
||||||
|
|
||||||
|
### Glyph Bank Domain Naming Contract
|
||||||
|
|
||||||
|
**What:**
|
||||||
|
Rename the concrete graphical bank artifact and its runtime interfaces to `GlyphBank`, but keep tile-domain structures unchanged where `tile` still correctly describes layers, maps, grids, and geometry.
|
||||||
|
|
||||||
|
**Why:**
|
||||||
|
The goal was domain clarity, not behavior change. The project needed a more precise name for the concrete graphical bank without reopening rendering architecture or erasing valid uses of `tile`.
|
||||||
|
|
||||||
|
**Trade-offs:**
|
||||||
|
This requires more care than a blind search-and-replace. The migration is broader editorially, but safer semantically, because it avoids damaging concepts that still belong in the tile domain.
|
||||||
|
|
||||||
|
## Patterns and Algorithms
|
||||||
|
|
||||||
|
- Rename by artifact meaning, not by string token frequency.
|
||||||
|
- When a term is overloaded, split the rename along domain boundaries instead of trying to enforce total lexical uniformity.
|
||||||
|
- Let the runtime contract adopt the new canonical artifact name (`GlyphBank`, `BankType::GLYPH`) while preserving existing terms for excluded concepts (`TileLayer`, `TileMap`, `TileSize`).
|
||||||
|
- Apply the same semantic rule to docs and lessons, not only to code.
|
||||||
|
|
||||||
|
## Pitfalls
|
||||||
|
|
||||||
|
- Blind replacement would have incorrectly renamed tile-layer and tile-size concepts that were intentionally out of scope.
|
||||||
|
- Partial renames create mixed vocabulary, which is worse than either old or new terminology used consistently.
|
||||||
|
- Historical documentation is especially easy to corrupt if rewritten mechanically instead of by artifact meaning.
|
||||||
|
|
||||||
|
## Takeaways
|
||||||
|
|
||||||
|
- A naming refactor can be rename-only in behavior while still requiring architectural discipline in wording.
|
||||||
|
- Use one canonical name per artifact, but do not force unrelated domains to share that rename.
|
||||||
|
- Documentation migrations should follow the same semantic boundary as code migrations, or the codebase will drift back into conceptual ambiguity.
|
||||||
@ -1,110 +0,0 @@
|
|||||||
---
|
|
||||||
id: AGD-0015
|
|
||||||
ticket: tilemap-empty-cell-vs-tile-id-zero
|
|
||||||
title: Tilemap Empty Cell vs Tile ID Zero
|
|
||||||
status: open
|
|
||||||
created: 2026-03-27
|
|
||||||
resolved:
|
|
||||||
decision:
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Tilemap Empty Cell vs Tile ID Zero
|
|
||||||
|
|
||||||
## Contexto
|
|
||||||
|
|
||||||
O runtime hoje usa `tile.id == 0` como atalho para "tile ausente" no caminho de render de tilemap.
|
|
||||||
|
|
||||||
Ao mesmo tempo, o contrato de `tile bank` em evolucao no packer quer permitir que `tile_id = 0` seja um tile real e valido dentro do bank.
|
|
||||||
|
|
||||||
Isso cria conflito direto entre:
|
|
||||||
|
|
||||||
- semantica atual do tilemap no runtime;
|
|
||||||
- e o contrato de packing/runtime asset bank que trata `tile_id` como indice valido do bank desde zero.
|
|
||||||
|
|
||||||
## Problema
|
|
||||||
|
|
||||||
Precisamos decidir qual e a semantica correta para representar ausencia de tile em tilemaps.
|
|
||||||
|
|
||||||
Hoje o comportamento implicito e:
|
|
||||||
|
|
||||||
- `tile.id == 0` significa "nao desenhar";
|
|
||||||
- portanto o tile de indice `0` no bank fica inutilizavel no caminho de tilemap;
|
|
||||||
- sprites, por outro lado, nao carregam a mesma semantica implicita de vazio.
|
|
||||||
|
|
||||||
Isso deixa o modelo inconsistente:
|
|
||||||
|
|
||||||
1. o mesmo `tile_id` nao significa a mesma coisa em todos os consumidores;
|
|
||||||
2. o bank `TileBank` nao explicita reserva de `tile_id = 0`;
|
|
||||||
3. o packer teria que compensar uma regra de consumidor que nao esta modelada no contrato do asset bank.
|
|
||||||
|
|
||||||
## Pontos Criticos
|
|
||||||
|
|
||||||
- `tile_id` deve representar um indice valido no bank, nao um sentinel escondido, sempre que possivel;
|
|
||||||
- ausencia de tile e um conceito de tilemap/layer, nao necessariamente de bank asset;
|
|
||||||
- usar indices fora da faixa como sentinel tende a piorar validacao e debug;
|
|
||||||
- mudar o shape do tilemap pode ter custo de ABI/runtime, mas pode deixar o contrato mais honesto;
|
|
||||||
- manter o comportamento atual preserva compatibilidade local, mas cristaliza uma semantica implicita fraca.
|
|
||||||
|
|
||||||
## Opcoes
|
|
||||||
|
|
||||||
### Opcao A - Manter `tile.id == 0` como empty sentinel
|
|
||||||
|
|
||||||
O runtime segue tratando `tile_id = 0` como ausencia de tile no tilemap.
|
|
||||||
|
|
||||||
Consequencia:
|
|
||||||
|
|
||||||
- o primeiro tile real do bank precisaria comecar em `1` para o caminho de tilemap;
|
|
||||||
- o packer precisaria compensar isso;
|
|
||||||
- sprites e tilemaps continuariam com semanticas diferentes para o mesmo id.
|
|
||||||
|
|
||||||
### Opcao B - Tornar vazio explicito no tilemap model
|
|
||||||
|
|
||||||
O tilemap passa a representar vazio explicitamente, por exemplo com:
|
|
||||||
|
|
||||||
- `Option<Tile>`;
|
|
||||||
- ou `Tile { active/present, ... }`.
|
|
||||||
|
|
||||||
Consequencia:
|
|
||||||
|
|
||||||
- `tile_id = 0` volta a ser um tile valido no bank;
|
|
||||||
- ausencia de tile deixa de depender de valor especial do id;
|
|
||||||
- o runtime fica semanticamente mais consistente.
|
|
||||||
|
|
||||||
### Opcao C - Usar sentinel fora da faixa valida
|
|
||||||
|
|
||||||
O tilemap passa a tratar um `tile_id >= tile_capacity` como "empty".
|
|
||||||
|
|
||||||
Consequencia:
|
|
||||||
|
|
||||||
- evita usar `0` como sentinel;
|
|
||||||
- mas cria acoplamento ruim entre tilemap e capacidade do bank;
|
|
||||||
- e torna a validacao mais fragil e menos didatica.
|
|
||||||
|
|
||||||
## Sugestao / Recomendacao
|
|
||||||
|
|
||||||
Adotar `Opcao B`.
|
|
||||||
|
|
||||||
Ausencia de tile deve ser modelada explicitamente no tilemap/layer, e nao por um `tile_id` especial.
|
|
||||||
|
|
||||||
Direcao recomendada:
|
|
||||||
|
|
||||||
1. `tile_id` deve significar apenas "indice do tile no bank";
|
|
||||||
2. `tile_id = 0` deve ser permitido como tile real;
|
|
||||||
3. tilemaps devem ter vazio explicito no proprio modelo;
|
|
||||||
4. o render do tilemap deve testar esse estado explicito, nao `tile.id == 0`.
|
|
||||||
|
|
||||||
## Perguntas em Aberto
|
|
||||||
|
|
||||||
1. Qual shape e melhor para o primeiro wave:
|
|
||||||
`Option<Tile>` ou `Tile` com flag `active/present`?
|
|
||||||
2. Essa mudanca afeta apenas o tilemap path ou tambem deve harmonizar outros consumidores do modelo `Tile`?
|
|
||||||
3. Precisamos de estrategia de compatibilidade/migracao para codigo e testes que hoje assumem `tile.id == 0` como vazio?
|
|
||||||
|
|
||||||
## Criterio para Encerrar
|
|
||||||
|
|
||||||
Esta agenda pode ser encerrada quando houver decisao clara sobre:
|
|
||||||
|
|
||||||
- como vazio e representado no tilemap;
|
|
||||||
- se `tile_id = 0` volta a ser indice valido universal do bank;
|
|
||||||
- e quais pontos de spec/codigo do runtime precisam de propagacao.
|
|
||||||
@ -1,100 +0,0 @@
|
|||||||
---
|
|
||||||
id: AGD-0016
|
|
||||||
ticket: asset-entry-metadata-normalization-contract
|
|
||||||
title: Asset Entry Metadata Normalization Contract
|
|
||||||
status: open
|
|
||||||
created: 2026-03-27
|
|
||||||
resolved:
|
|
||||||
decision:
|
|
||||||
tags: []
|
|
||||||
---
|
|
||||||
|
|
||||||
# Asset Entry Metadata Normalization Contract
|
|
||||||
|
|
||||||
Status: Open
|
|
||||||
Domain Owner: `docs/runtime`
|
|
||||||
Cross-Domain Impact: `../studio/docs/packer`, `shipper`, `asset` loader
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Normatizar como `AssetEntry.metadata` deve preservar a convergencia entre metadata autoral, metadata de codec e metadata de pipeline sem colapsar tudo num mapa plano ambiguo.
|
|
||||||
|
|
||||||
## Problem
|
|
||||||
|
|
||||||
O lado produtor (`packer`) ja convergiu para um contrato em que o runtime precisa ler campos obrigatorios diretamente de `AssetEntry.metadata`, mas tambem precisa manter segmentacao suficiente para nao perder significado entre:
|
|
||||||
|
|
||||||
- `asset.json.output.metadata`
|
|
||||||
- `asset.json.output.codec_configuration`
|
|
||||||
- `asset.json.output.pipeline`
|
|
||||||
|
|
||||||
Sem um contrato explicito no runtime:
|
|
||||||
|
|
||||||
- o packer pode materializar estruturas diferentes entre formatos;
|
|
||||||
- o loader/runtime pode passar a depender de flattening incidental;
|
|
||||||
- tooling e debug surfaces perdem previsibilidade;
|
|
||||||
- futuros formatos tendem a misturar metadata efetiva com detalhe interno de pipeline.
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
No ciclo atual de `tile bank`, o produtor ja fechou esta direcao:
|
|
||||||
|
|
||||||
- `asset.json.output.metadata` -> `AssetEntry.metadata`
|
|
||||||
- `asset.json.output.codec_configuration` -> `AssetEntry.metadata.codec`
|
|
||||||
- `asset.json.output.pipeline` -> `AssetEntry.metadata.pipeline`
|
|
||||||
|
|
||||||
Ao mesmo tempo, o runtime ainda consome alguns campos obrigatorios do tile bank diretamente no nivel raiz de `AssetEntry.metadata`, em especial:
|
|
||||||
|
|
||||||
- `tile_size`
|
|
||||||
- `width`
|
|
||||||
- `height`
|
|
||||||
- `palette_count`
|
|
||||||
|
|
||||||
A agenda precisa fechar se esse shape vira contrato geral de runtime para metadata normalizada de assets, e como o consumidor deve tratar campos obrigatorios format-specific versus subtrees segmentadas.
|
|
||||||
|
|
||||||
## Options
|
|
||||||
|
|
||||||
### Option A - Flat effective metadata map only
|
|
||||||
|
|
||||||
Tudo converge para um unico mapa plano em `AssetEntry.metadata`.
|
|
||||||
|
|
||||||
### Option B - Root effective metadata plus stable segmented subtrees
|
|
||||||
|
|
||||||
Campos runtime-obrigatorios ficam legiveis no nivel raiz, enquanto dados de codec e pipeline ficam em subtrees estaveis:
|
|
||||||
|
|
||||||
- `metadata.<field>`
|
|
||||||
- `metadata.codec.<field>`
|
|
||||||
- `metadata.pipeline.<field>`
|
|
||||||
|
|
||||||
### Option C - Fully segmented metadata only
|
|
||||||
|
|
||||||
Nada fica no nivel raiz; todo consumo passa por subtrees por origem.
|
|
||||||
|
|
||||||
## Tradeoffs
|
|
||||||
|
|
||||||
- Option A simplifica leitura curta, mas perde origem semantica e aumenta risco de colisao.
|
|
||||||
- Option B preserva leitura direta para campos obrigatorios do runtime e mantem segmentacao estavel para evolucao futura.
|
|
||||||
- Option C e semanticamente limpa, mas quebra o consumo direto atual de formatos como `tile bank` e introduz custo de migracao desnecessario agora.
|
|
||||||
|
|
||||||
## Recommendation
|
|
||||||
|
|
||||||
Adotar `Option B`.
|
|
||||||
|
|
||||||
Direcao recomendada:
|
|
||||||
|
|
||||||
- campos format-specific obrigatorios para decode/runtime continuam legiveis no nivel raiz de `AssetEntry.metadata`;
|
|
||||||
- `output.codec_configuration` materializa em `AssetEntry.metadata.codec`;
|
|
||||||
- `output.pipeline` materializa em `AssetEntry.metadata.pipeline`;
|
|
||||||
- o runtime nao deve exigir flattening total para consumir metadata segmentada;
|
|
||||||
- specs format-specific devem declarar explicitamente quais campos sao obrigatorios no nivel raiz.
|
|
||||||
|
|
||||||
## Open Questions
|
|
||||||
|
|
||||||
1. O contrato deve tratar o subtree raiz como semanticamente equivalente a `output.metadata` ou como effective metadata map mais amplo?
|
|
||||||
2. Quais readers/helpers do runtime devem ser criados para evitar parsing manual disperso de `metadata.codec` e `metadata.pipeline`?
|
|
||||||
|
|
||||||
## Expected Follow-up
|
|
||||||
|
|
||||||
1. Converter esta agenda em decision no `runtime`.
|
|
||||||
2. Propagar a decisao para `15-asset-management.md`.
|
|
||||||
3. Ajustar loaders format-specific para usar helpers consistentes de metadata quando necessario.
|
|
||||||
4. Alinhar o `packer` e testes de conformance com o shape final.
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
---
|
|
||||||
id: AGD-0017
|
|
||||||
ticket: jenkinsfile-correction
|
|
||||||
title: Agenda - Jenkinsfile Correction and Relocation
|
|
||||||
status: open
|
|
||||||
created: 2026-04-07
|
|
||||||
resolved:
|
|
||||||
decision:
|
|
||||||
tags: ["ci", "jenkins", "infrastructure"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agenda - Jenkinsfile Correction and Relocation
|
|
||||||
|
|
||||||
## Contexto
|
|
||||||
|
|
||||||
O projeto possui um `Jenkinsfile` localizado em `files/config/Jenkinsfile`. O conteúdo deste arquivo está básico e utiliza uma versão do Rust (`1.77`) que pode não ser a ideal em comparação com o resto do projeto que usa a versão estável definida em `rust-toolchain.toml`. Além disso, o pipeline de CI principal do projeto está definido no GitHub Actions e o comportamento esperado de CI (formatação, clippy, testes) também está documentado no `Makefile`.
|
|
||||||
|
|
||||||
## Problema
|
|
||||||
|
|
||||||
1. **Localização Não Convencional**: O `Jenkinsfile` na pasta `files/config/` dificulta a descoberta e manutenção, além de fugir do padrão do Jenkins de buscar o arquivo na raiz do repositório.
|
|
||||||
2. **Desalinhamento de Comandos**: O `Jenkinsfile` atual executa comandos de forma ligeiramente diferente do `Makefile` e do GitHub Actions (ex: `cargo fmt --all` vs `cargo fmt -- --check`).
|
|
||||||
3. **Falta de Padronização**: Não há garantias de que o pipeline do Jenkins execute as mesmas verificações de qualidade que o pipeline do GitHub.
|
|
||||||
|
|
||||||
## Pontos Críticos
|
|
||||||
|
|
||||||
- **Sincronia com o Makefile**: O `Jenkinsfile` deve idealmente delegar para o `Makefile` para evitar duplicidade de lógica de comandos.
|
|
||||||
- **Ambiente Docker**: A imagem Docker utilizada deve ser compatível com as ferramentas necessárias (ex: `make`, `cargo`).
|
|
||||||
- **Workspace Completo**: Deve garantir que todas as crates do workspace sejam testadas.
|
|
||||||
|
|
||||||
## Opções
|
|
||||||
|
|
||||||
1. **Manter como está**: Apenas corrigir o conteúdo no local atual.
|
|
||||||
2. **Mover para a raiz e atualizar**: Seguir o padrão de mercado movendo para a raiz e alinhando o conteúdo com o `Makefile`.
|
|
||||||
3. **Remover o Jenkinsfile**: Se o projeto foca apenas no GitHub Actions (como sugere o `dist-workspace.toml`), o Jenkinsfile pode ser redundante. Contudo, a solicitação explícita foi para corrigi-lo.
|
|
||||||
|
|
||||||
## Sugestão / Recomendação
|
|
||||||
|
|
||||||
**Opção 2**: Mover o `Jenkinsfile` para a raiz do projeto e atualizar seu conteúdo para utilizar o comando `make ci` definido no `Makefile`. Isso garante consistência entre o ambiente local, o Jenkins e o GitHub Actions.
|
|
||||||
|
|
||||||
## Perguntas em Aberto
|
|
||||||
|
|
||||||
- Existe alguma restrição técnica no ambiente Jenkins deste projeto que exija o arquivo em `files/config/`? (Assumindo que não, dada a solicitação de "corrigir").
|
|
||||||
- A imagem Docker `rust:stable` (ou similar) possui as dependências necessárias para rodar o `Makefile`?
|
|
||||||
|
|
||||||
## Criterio para Encerrar
|
|
||||||
|
|
||||||
- O `Jenkinsfile` estar na raiz do projeto.
|
|
||||||
- O conteúdo refletir as mesmas etapas de validação do resto do projeto (fmt, clippy, test).
|
|
||||||
- O arquivo antigo em `files/config/` ter sido removido.
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
id: AGD-0018
|
|
||||||
ticket: jenkins-gitea-integration-and-relocation
|
|
||||||
title: Agenda - Jenkins Gitea Integration and Relocation
|
|
||||||
status: open
|
|
||||||
created: 2026-04-07
|
|
||||||
resolved:
|
|
||||||
decision:
|
|
||||||
tags: ["ci", "jenkins", "gitea"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Agenda - Jenkins Gitea Integration and Relocation
|
|
||||||
|
|
||||||
## Contexto
|
|
||||||
|
|
||||||
Na sessão anterior, o `Jenkinsfile` foi movido para a raiz do repositório para seguir padrões comuns de mercado. No entanto, o usuário solicitou explicitamente que ele permaneça em `files/config/Jenkinsfile`. Além disso, a estratégia de CI mudou de GitHub Actions para Jenkins integrado ao Gitea.
|
|
||||||
|
|
||||||
## Problema
|
|
||||||
|
|
||||||
1. O local atual do `Jenkinsfile` (raiz no histórico, mas residindo em `files/config` no FS atual) precisa ser consolidado como `files/config/Jenkinsfile` para cumprir o requisito do usuário.
|
|
||||||
2. A integração do CI deve ser com o Gitea, exigindo a propagação de status dos commits.
|
|
||||||
3. Não deve haver dependência ou uso do GitHub CI para este projeto.
|
|
||||||
|
|
||||||
## Pontos Críticos
|
|
||||||
|
|
||||||
- **Sincronização de Status**: Garantir que o Jenkins envie o feedback de `make ci` (testes/lint) corretamente para o Gitea.
|
|
||||||
- **Localização não-padrão**: Jenkins precisa ser configurado no lado do servidor para buscar o script de pipeline em `files/config/Jenkinsfile` (o que é trivial, mas foge do padrão `Jenkinsfile` na raiz).
|
|
||||||
- **Abandono do GitHub CI**: Remover qualquer resquício de configuração voltada ao GitHub.
|
|
||||||
|
|
||||||
## Opções
|
|
||||||
|
|
||||||
1. **Opção A**: Manter na raiz (rejeitada pelo usuário).
|
|
||||||
2. **Opção B**: Manter em `files/config/Jenkinsfile` e usar o plugin de Gitea no Jenkins para notificação automática ou via `giteaStatus` no pipeline.
|
|
||||||
|
|
||||||
## Sugestão / Recomendação
|
|
||||||
|
|
||||||
Adotar a **Opção B**. Atualizar o `Jenkinsfile` para incluir blocos de `post` que notifiquem o Gitea sobre o sucesso ou falha do pipeline.
|
|
||||||
|
|
||||||
## Perguntas em Aberto
|
|
||||||
|
|
||||||
- O Jenkins em questão já tem o plugin do Gitea configurado? (Assumiremos que sim ou que o pipeline deve usar o comando padrão `giteaStatus`).
|
|
||||||
- Existem arquivos `.github/workflows` que devem ser removidos? (Verificar e remover).
|
|
||||||
|
|
||||||
## Criterio para Encerrar
|
|
||||||
|
|
||||||
- `Jenkinsfile` atualizado e testado localmente (validado sintaticamente).
|
|
||||||
- Documentação da decisão no framework.
|
|
||||||
- Localização confirmada em `files/config/Jenkinsfile`.
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
---
|
|
||||||
id: DEC-0002
|
|
||||||
discussion: DSC-0019
|
|
||||||
title: Decision - Jenkinsfile Location and Strategy
|
|
||||||
status: accepted
|
|
||||||
created: 2026-04-07
|
|
||||||
resolved:
|
|
||||||
tags: ["ci", "jenkins"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Decision - Jenkinsfile Location and Strategy
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Accepted.
|
|
||||||
|
|
||||||
## Contexto
|
|
||||||
|
|
||||||
O arquivo `Jenkinsfile` estava localizado em `files/config/Jenkinsfile`, o que dificultava a manutenção e automação via Jenkins (que por padrão busca na raiz). Além disso, o conteúdo estava divergente das definições de CI do `Makefile` e do GitHub Actions.
|
|
||||||
|
|
||||||
## Decisao
|
|
||||||
|
|
||||||
1. **Mover** o `Jenkinsfile` para a raiz do repositório.
|
|
||||||
2. **Atualizar** o conteúdo do `Jenkinsfile` para utilizar uma imagem Docker `rust:stable` (conforme `rust-toolchain.toml`).
|
|
||||||
3. **Delegar** a execução do pipeline para o comando `make ci` definido no `Makefile`.
|
|
||||||
4. **Remover** o arquivo residual em `files/config/Jenkinsfile`.
|
|
||||||
|
|
||||||
## Rationale
|
|
||||||
|
|
||||||
- **Padronização**: Seguir o padrão de mercado de manter o arquivo de configuração de pipeline na raiz.
|
|
||||||
- **DRY (Don't Repeat Yourself)**: Ao usar o `Makefile`, evitamos duplicar os comandos de `fmt`, `clippy` e `test` em múltiplos lugares (Makefile, GHA e Jenkins).
|
|
||||||
- **Consistência**: Garante que o desenvolvedor rodando `make ci` localmente tenha o mesmo resultado que o servidor de CI.
|
|
||||||
|
|
||||||
## Invariantes / Contrato
|
|
||||||
|
|
||||||
- O comando `make ci` deve sempre englobar as verificações mínimas de qualidade (format, clippy, tests).
|
|
||||||
- O `Jenkinsfile` deve sempre usar um ambiente que possua `make` e `rust`.
|
|
||||||
|
|
||||||
## Impactos
|
|
||||||
|
|
||||||
- **Jenkins**: A configuração do job no Jenkins pode precisar ser atualizada se o "Script Path" estiver explicitamente apontando para `files/config/Jenkinsfile`. (Geralmente aponta para `Jenkinsfile` na raiz).
|
|
||||||
- **Manutenção**: Facilita a manutenção, pois mudanças no processo de build só precisam ser feitas no `Makefile`.
|
|
||||||
|
|
||||||
## Referencias
|
|
||||||
|
|
||||||
- `.github/workflows/ci.yml`
|
|
||||||
- `Makefile`
|
|
||||||
- `rust-toolchain.toml`
|
|
||||||
|
|
||||||
## Propagacao Necessaria
|
|
||||||
|
|
||||||
- N/A.
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
---
|
|
||||||
id: DEC-0003
|
|
||||||
agenda: AGD-0018
|
|
||||||
title: Decisão - Jenkins Gitea Integration and Relocation
|
|
||||||
status: accepted
|
|
||||||
created: 2026-04-07
|
|
||||||
tags: ["ci", "jenkins", "gitea"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Decisão - Jenkins Gitea Integration and Relocation
|
|
||||||
|
|
||||||
## Status
|
|
||||||
|
|
||||||
Aceito.
|
|
||||||
|
|
||||||
## Contexto
|
|
||||||
|
|
||||||
O projeto deve utilizar Jenkins integrado ao Gitea para o pipeline de CI, ignorando o GitHub Actions. O arquivo `Jenkinsfile` deve residir em um local específico solicitado pelo usuário: `files/config/Jenkinsfile`.
|
|
||||||
|
|
||||||
## Decisao
|
|
||||||
|
|
||||||
1. **Localização**: O `Jenkinsfile` será mantido em `files/config/Jenkinsfile`.
|
|
||||||
2. **Integração Gitea**: O pipeline deve utilizar comandos compatíveis com o plugin do Gitea no Jenkins para propagar o status da execução (Success/Failure/Pending).
|
|
||||||
3. **Remoção de GitHub CI**: Qualquer configuração de `.github/workflows` relacionada ao CI será removida para evitar confusão.
|
|
||||||
|
|
||||||
## Rationale
|
|
||||||
|
|
||||||
- Cumpre o requisito direto do usuário sobre a localização do arquivo.
|
|
||||||
- Alinha a infraestrutura de CI com o servidor Git interno (Gitea).
|
|
||||||
- Centraliza a execução no `Makefile` (`make ci`) para manter o `Jenkinsfile` simples e portável.
|
|
||||||
|
|
||||||
## Invariantes / Contrato
|
|
||||||
|
|
||||||
- O `Jenkinsfile` deve sempre chamar `make ci` para garantir que o mesmo padrão de qualidade seja aplicado localmente e no CI.
|
|
||||||
- Notificações de status devem ser enviadas ao Gitea no início e no fim da execução.
|
|
||||||
|
|
||||||
## Impactos
|
|
||||||
|
|
||||||
- **Jenkins**: O job no Jenkins deve ser configurado para apontar o "Script Path" para `files/config/Jenkinsfile`.
|
|
||||||
- **Desenvolvedores**: Devem focar no Gitea para verificar o status dos builds.
|
|
||||||
|
|
||||||
## Referencias
|
|
||||||
|
|
||||||
- AGD-0018
|
|
||||||
|
|
||||||
## Propagacao Necessaria
|
|
||||||
|
|
||||||
- Comunicar ao time de infraestrutura sobre o novo local do `Jenkinsfile` para ajuste no Job do Jenkins.
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
---
|
|
||||||
id: PLN-0002
|
|
||||||
discussion: DSC-0019
|
|
||||||
title: Plan - Jenkinsfile Relocation and Content Alignment
|
|
||||||
status: open
|
|
||||||
created: 2026-04-07
|
|
||||||
resolved:
|
|
||||||
tags: ["ci", "jenkins"]
|
|
||||||
---
|
|
||||||
|
|
||||||
# Plan - Jenkinsfile Relocation and Content Alignment
|
|
||||||
|
|
||||||
## Briefing
|
|
||||||
|
|
||||||
Este plano descreve as etapas técnicas para mover o `Jenkinsfile` de sua localização atual para a raiz do repositório e atualizar seu conteúdo para delegar as tarefas de CI ao `Makefile`.
|
|
||||||
|
|
||||||
## Decisions de Origem
|
|
||||||
|
|
||||||
- DEC-0002: Jenkinsfile Location and Strategy
|
|
||||||
|
|
||||||
## Alvo
|
|
||||||
|
|
||||||
- `files/config/Jenkinsfile` (Remoção)
|
|
||||||
- `/Jenkinsfile` (Criação/Movimentação)
|
|
||||||
|
|
||||||
## Escopo
|
|
||||||
|
|
||||||
- Movimentação do arquivo no sistema de arquivos.
|
|
||||||
- Edição do conteúdo Groovy do Jenkinsfile.
|
|
||||||
- Validação básica da sintaxe.
|
|
||||||
|
|
||||||
## Fora de Escopo
|
|
||||||
|
|
||||||
- Configuração do servidor Jenkins externo.
|
|
||||||
- Criação de novos comandos no `Makefile` (usaremos o `make ci` existente).
|
|
||||||
|
|
||||||
## Plano de Execucao
|
|
||||||
|
|
||||||
1. Criar o novo `Jenkinsfile` na raiz com o conteúdo atualizado.
|
|
||||||
2. Remover o arquivo original em `files/config/Jenkinsfile`.
|
|
||||||
3. Validar se o `Makefile` está acessível no ambiente Docker especificado.
|
|
||||||
|
|
||||||
## Criterios de Aceite
|
|
||||||
|
|
||||||
- O arquivo `Jenkinsfile` deve existir na raiz.
|
|
||||||
- O arquivo `files/config/Jenkinsfile` não deve mais existir.
|
|
||||||
- O novo `Jenkinsfile` deve conter uma chamada para `make ci`.
|
|
||||||
|
|
||||||
## Tests / Validacao
|
|
||||||
|
|
||||||
- Verificar a existência dos arquivos via terminal.
|
|
||||||
- Simular a execução do comando `make ci` (opcional, já validado pelo GHA).
|
|
||||||
|
|
||||||
## Riscos
|
|
||||||
|
|
||||||
- **Quebra de Pipeline Existente**: Se o Jenkins estiver configurado para ler especificamente de `files/config/Jenkinsfile`, o pipeline quebrará até que a configuração do Job seja atualizada. (Risco baixo, pois o padrão é a raiz).
|
|
||||||
@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
id: PLN-0003
|
|
||||||
decisions: ["DEC-0003"]
|
|
||||||
title: Plano de Execução - Jenkins Gitea Integration
|
|
||||||
status: open
|
|
||||||
created: 2026-04-07
|
|
||||||
---
|
|
||||||
|
|
||||||
# Plano de Execução - Jenkins Gitea Integration
|
|
||||||
|
|
||||||
## Briefing
|
|
||||||
|
|
||||||
Atualizar o Jenkinsfile para integração com Gitea e garantir sua localização em `files/config/Jenkinsfile`. Remover resquícios de GitHub CI.
|
|
||||||
|
|
||||||
## Decisions de Origem
|
|
||||||
|
|
||||||
- DEC-0003
|
|
||||||
|
|
||||||
## Alvo
|
|
||||||
|
|
||||||
- `files/config/Jenkinsfile`
|
|
||||||
- `.github/workflows/` (limpeza)
|
|
||||||
|
|
||||||
## Escopo
|
|
||||||
|
|
||||||
- Atualização do script Groovy do `Jenkinsfile` com suporte a `giteaStatus`.
|
|
||||||
- Garantir que o diretório `files/config` existe.
|
|
||||||
- Remover diretório `.github/workflows` se existir.
|
|
||||||
|
|
||||||
## Fora de Escopo
|
|
||||||
|
|
||||||
- Configuração real do servidor Jenkins (fora do repositório).
|
|
||||||
|
|
||||||
## Plano de Execucao
|
|
||||||
|
|
||||||
1. Verificar existência do diretório `files/config` e criar se necessário.
|
|
||||||
2. Atualizar/Mover o `Jenkinsfile` para `files/config/Jenkinsfile`.
|
|
||||||
3. Adicionar blocos `post` no `Jenkinsfile` para notificação ao Gitea.
|
|
||||||
4. Excluir `.github/workflows` se presente.
|
|
||||||
5. Validar sintaxe básica do Jenkinsfile.
|
|
||||||
|
|
||||||
## Criterios de Aceite
|
|
||||||
|
|
||||||
- O arquivo `Jenkinsfile` reside em `files/config/Jenkinsfile`.
|
|
||||||
- O conteúdo do `Jenkinsfile` inclui `make ci` e chamadas ao Gitea.
|
|
||||||
- Não existem workflows de GitHub CI.
|
|
||||||
|
|
||||||
## Tests / Validacao
|
|
||||||
|
|
||||||
- Execução manual de `make ci` para garantir que o comando base funciona.
|
|
||||||
- Verificação visual do `Jenkinsfile`.
|
|
||||||
|
|
||||||
## Riscos
|
|
||||||
|
|
||||||
- **Incompatibilidade de Plugin**: Se o Jenkins do usuário não tiver o plugin do Gitea, as chamadas `giteaStatus` podem falhar. No entanto, estamos seguindo o requisito de "propagar resultados para gitea".
|
|
||||||
@ -110,37 +110,46 @@ This table describes content identity and storage layout, not live residency.
|
|||||||
|
|
||||||
### 4.1 `TILES` asset contract in v1
|
### 4.1 `TILES` asset contract in v1
|
||||||
|
|
||||||
For `BankType::TILES`, the runtime-facing v1 contract is:
|
Para `BankType::TILES`, o contrato v1 voltado para o runtime é:
|
||||||
|
|
||||||
- `codec = NONE`
|
- `codec = NONE`
|
||||||
- serialized pixels use packed `u4` palette indices
|
- pixels serializados usam índices de paleta `u4` empacotados
|
||||||
- serialized palettes use `RGB565` (`u16`, little-endian)
|
- paletas serializadas usam `RGB565` (`u16`, little-endian)
|
||||||
- `palette_count = 64`
|
- `palette_count = 64`
|
||||||
- runtime materialization may expand pixel indices to one `u8` per pixel
|
- a materialização em runtime pode expandir índices de pixel para um `u8` por pixel
|
||||||
|
|
||||||
`NONE` for `TILES` means there is no additional generic codec layer beyond the bank contract itself.
|
`NONE` para `TILES` significa que não há camada de codec genérica adicional além do próprio contrato do banco.
|
||||||
|
|
||||||
For the current transition window:
|
Para a janela de transição atual:
|
||||||
|
|
||||||
- `RAW` is a deprecated legacy alias of `NONE`
|
- `RAW` é um alias legado e depreciado de `NONE`
|
||||||
- new published material must use `NONE` as the canonical value
|
- novos materiais publicados devem usar `NONE` como valor canônico
|
||||||
|
|
||||||
Even with `codec = NONE`, `TILES` still requires deterministic bank-specific decode from its serialized payload. The serialized byte layout is therefore not required to be identical to the in-memory layout.
|
Mesmo com `codec = NONE`, `TILES` ainda requer decode específico de banco a partir de seu payload serializado. O layout de bytes serializados não precisa, portanto, ser idêntico ao layout em memória.
|
||||||
|
|
||||||
Required `AssetEntry.metadata` fields for `TILES`:
|
#### 4.1.1 Metadata Normalization
|
||||||
|
|
||||||
- `tile_size`: tile edge in pixels; valid values are `8`, `16`, or `32`
|
Seguindo a `DEC-0004`, o campo `AssetEntry.metadata` deve ser estruturado de forma segmentada para evitar ambiguidades.
|
||||||
- `width`: full bank sheet width in pixels
|
|
||||||
- `height`: full bank sheet height in pixels
|
|
||||||
- `palette_count`: number of palettes serialized for the bank
|
|
||||||
|
|
||||||
Validation rules for `TILES` v1:
|
Campos de metadados obrigatórios (efetivos) para `TILES` no nível raiz:
|
||||||
|
|
||||||
- `palette_count` must be `64`
|
- `tile_size`: aresta do tile em pixels; valores válidos são `8`, `16`, ou `32`
|
||||||
- `width * height` defines the number of logical indexed pixels in the decoded sheet
|
- `width`: largura total da folha do banco em pixels
|
||||||
- additional metadata may exist, but the runtime contract must not depend on it to reconstruct the bank in memory
|
- `height`: altura total da folha do banco em pixels
|
||||||
|
- `palette_count`: número de paletas serializadas para o banco
|
||||||
|
|
||||||
Serialized payload layout for `TILES` v1:
|
Subárvores opcionais e informativas:
|
||||||
|
|
||||||
|
- `metadata.codec`: Configuração específica do codec/compressor (ex: dicionários, flags de compressão).
|
||||||
|
- `metadata.pipeline`: Metadados informativos do processo de build do Studio (ex: source hashes, timestamps, tool versions).
|
||||||
|
|
||||||
|
Regras de validação para `TILES` v1:
|
||||||
|
|
||||||
|
- `palette_count` deve ser `64`
|
||||||
|
- `width * height` define o número de pixels indexados lógicos na folha decodificada
|
||||||
|
- metadados adicionais podem existir, mas o contrato do runtime não deve depender deles para reconstruir o banco em memória (exceto se definidos na raiz como campos efetivos).
|
||||||
|
|
||||||
|
#### 4.1.2 Payload Layout
|
||||||
|
|
||||||
1. packed indexed pixels for the full sheet, using `ceil(width * height / 2)` bytes;
|
1. packed indexed pixels for the full sheet, using `ceil(width * height / 2)` bytes;
|
||||||
2. palette table, using `palette_count * 16 * 2` bytes.
|
2. palette table, using `palette_count * 16 * 2` bytes.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user