From 52d4a91c71cac18b4da31395445fd3fdfbdf9631 Mon Sep 17 00:00:00 2001 From: bQUARKz Date: Thu, 9 Apr 2026 08:25:37 +0100 Subject: [PATCH] Asset Entry Metadata Normalization Contract --- crates/console/prometeu-drivers/src/asset.rs | 27 ++--- crates/console/prometeu-hal/src/asset.rs | 37 +++++++ discussion/index.ndjson | 4 +- .../LSN-0023-typed-asset-metadata-helpers.md | 33 ++++++ ...t-entry-metadata-normalization-contract.md | 100 ------------------ docs/specs/runtime/15-asset-management.md | 47 ++++---- 6 files changed, 110 insertions(+), 138 deletions(-) create mode 100644 discussion/lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md delete mode 100644 discussion/workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 26b82eab..b5df47e4 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -186,29 +186,22 @@ impl AssetManager { fn decode_tile_bank_layout( entry: &AssetEntry, ) -> Result<(TileSize, usize, usize, usize), String> { - let tile_size_val = - 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 meta = entry.metadata_as_tiles()?; - let tile_size = match tile_size_val { + let tile_size = match meta.tile_size { 8 => TileSize::Size8, 16 => TileSize::Size16, 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 { - return Err(format!("Invalid palette_count: {}", palette_count)); + if meta.palette_count as usize != TILE_BANK_PALETTE_COUNT_V1 { + return Err(format!("Invalid palette_count: {}", meta.palette_count)); } + let width = meta.width as usize; + let height = meta.height as usize; + let logical_pixels = width.checked_mul(height).ok_or("TileBank dimensions overflow")?; let serialized_pixel_bytes = logical_pixels.div_ceil(2); let serialized_size = serialized_pixel_bytes @@ -637,8 +630,8 @@ impl AssetManager { entry: &AssetEntry, buffer: &[u8], ) -> Result { - let sample_rate = - entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32; + let meta = entry.metadata_as_sounds()?; + let sample_rate = meta.sample_rate; let mut data = Vec::with_capacity(buffer.len() / 2); for i in (0..buffer.len()).step_by(2) { diff --git a/crates/console/prometeu-hal/src/asset.rs b/crates/console/prometeu-hal/src/asset.rs index 6ca8ee49..ccab4b2d 100644 --- a/crates/console/prometeu-hal/src/asset.rs +++ b/crates/console/prometeu-hal/src/asset.rs @@ -24,6 +24,43 @@ pub struct AssetEntry { pub metadata: serde_json::Value, } +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TilesMetadata { + pub tile_size: u32, + pub width: u32, + pub height: u32, + pub palette_count: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct SoundsMetadata { + pub sample_rate: u32, +} + +impl AssetEntry { + pub fn metadata_as_tiles(&self) -> Result { + if self.bank_type != BankType::TILES { + return Err(format!( + "Asset {} is not a TILES bank (type: {:?})", + self.asset_id, self.bank_type + )); + } + serde_json::from_value(self.metadata.clone()) + .map_err(|e| format!("Invalid TILES metadata for asset {}: {}", self.asset_id, e)) + } + + pub fn metadata_as_sounds(&self) -> Result { + 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)] pub struct PreloadEntry { pub asset_id: AssetId, diff --git a/discussion/index.ndjson b/discussion/index.ndjson index e4ed9054..2de2de00 100644 --- a/discussion/index.ndjson +++ b/discussion/index.ndjson @@ -1,4 +1,4 @@ -{"type":"meta","next_id":{"DSC":21,"AGD":19,"DEC":3,"PLN":3,"LSN":23,"CLSN":1}} +{"type":"meta","next_id":{"DSC":21,"AGD":19,"DEC":5,"PLN":3,"LSN":24,"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-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"}]} @@ -17,6 +17,6 @@ {"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":"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":"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":"workflow/agendas/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-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"}]} diff --git a/discussion/lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md b/discussion/lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md new file mode 100644 index 00000000..51b462c5 --- /dev/null +++ b/discussion/lessons/DSC-0017-asset-metadata-normalization/LSN-0023-typed-asset-metadata-helpers.md @@ -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: `TilesMetadata`, `SoundsMetadata`) que representam o contrato exato de cada banco. +2. **Conversion Methods**: Adicionamos métodos ao `AssetEntry` (ex: `metadata_as_tiles()`) 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` diff --git a/discussion/workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md b/discussion/workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md deleted file mode 100644 index e9df8640..00000000 --- a/discussion/workflow/agendas/AGD-0016-asset-entry-metadata-normalization-contract.md +++ /dev/null @@ -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.` -- `metadata.codec.` -- `metadata.pipeline.` - -### 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. diff --git a/docs/specs/runtime/15-asset-management.md b/docs/specs/runtime/15-asset-management.md index c14093fa..3feb76eb 100644 --- a/docs/specs/runtime/15-asset-management.md +++ b/docs/specs/runtime/15-asset-management.md @@ -110,37 +110,46 @@ This table describes content identity and storage layout, not live residency. ### 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` -- serialized pixels use packed `u4` palette indices -- serialized palettes use `RGB565` (`u16`, little-endian) +- pixels serializados usam índices de paleta `u4` empacotados +- paletas serializadas usam `RGB565` (`u16`, little-endian) - `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` -- new published material must use `NONE` as the canonical value +- `RAW` é um alias legado e depreciado de `NONE` +- 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` -- `width`: full bank sheet width in pixels -- `height`: full bank sheet height in pixels -- `palette_count`: number of palettes serialized for the bank +Seguindo a `DEC-0004`, o campo `AssetEntry.metadata` deve ser estruturado de forma segmentada para evitar ambiguidades. -Validation rules for `TILES` v1: +Campos de metadados obrigatórios (efetivos) para `TILES` no nível raiz: -- `palette_count` must be `64` -- `width * height` defines the number of logical indexed pixels in the decoded sheet -- additional metadata may exist, but the runtime contract must not depend on it to reconstruct the bank in memory +- `tile_size`: aresta do tile em pixels; valores válidos são `8`, `16`, ou `32` +- `width`: largura total da folha do banco em pixels +- `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; 2. palette table, using `palette_count * 16 * 2` bytes.