asset preload alignment

This commit is contained in:
bQUARKz 2026-03-11 08:27:37 +00:00
parent d7c970a3e2
commit d576f7ddbd
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
11 changed files with 367 additions and 22 deletions

View File

@ -2,7 +2,7 @@
use crate::memory_banks::{SoundBankPoolInstaller, TileBankPoolInstaller};
use prometeu_hal::AssetBridge;
use prometeu_hal::asset::{
AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
AssetEntry, AssetId, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus,
PreloadEntry, SlotRef, SlotStats,
};
use prometeu_hal::cartridge::AssetsPayloadSource;
@ -48,7 +48,7 @@ impl<T> ResidentEntry<T> {
/// This is internal to the AssetManager and not visible to peripherals.
pub struct BankPolicy<T> {
/// Dedup table: asset_id -> resident entry (value + telemetry).
resident: Arc<RwLock<HashMap<u32, ResidentEntry<T>>>>,
resident: Arc<RwLock<HashMap<AssetId, ResidentEntry<T>>>>,
/// Staging area: handle -> value ready to commit.
staging: Arc<RwLock<HashMap<HandleId, Arc<T>>>>,
@ -63,7 +63,7 @@ impl<T> BankPolicy<T> {
}
/// Try get a resident value by asset_id (dedupe path).
pub fn get_resident(&self, asset_id: u32) -> Option<Arc<T>> {
pub fn get_resident(&self, asset_id: AssetId) -> Option<Arc<T>> {
let mut map = self.resident.write().unwrap();
let entry = map.get_mut(&asset_id)?;
entry.last_used = Instant::now();
@ -71,7 +71,7 @@ impl<T> BankPolicy<T> {
}
/// Insert or reuse a resident entry. Returns the resident Arc<T>.
pub fn put_resident(&self, asset_id: u32, value: Arc<T>, bytes: usize) -> Arc<T> {
pub fn put_resident(&self, asset_id: AssetId, value: Arc<T>, bytes: usize) -> Arc<T> {
let mut map = self.resident.write().unwrap();
match map.get_mut(&asset_id) {
Some(existing) => {
@ -104,8 +104,8 @@ impl<T> BankPolicy<T> {
}
pub struct AssetManager {
assets: Arc<RwLock<HashMap<u32, AssetEntry>>>,
name_to_id: Arc<RwLock<HashMap<String, u32>>>,
assets: Arc<RwLock<HashMap<AssetId, AssetEntry>>>,
name_to_id: Arc<RwLock<HashMap<String, AssetId>>>,
handles: Arc<RwLock<HashMap<HandleId, LoadHandleInfo>>>,
next_handle_id: Mutex<HandleId>,
assets_data: Arc<RwLock<AssetsPayloadSource>>,
@ -115,8 +115,8 @@ pub struct AssetManager {
sound_installer: Arc<dyn SoundBankPoolInstaller>,
/// Track what is installed in each hardware slot (for stats/info).
gfx_slots: Arc<RwLock<[Option<u32>; 16]>>,
sound_slots: Arc<RwLock<[Option<u32>; 16]>>,
gfx_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
sound_slots: Arc<RwLock<[Option<AssetId>; 16]>>,
/// Residency policy for GFX tile banks.
gfx_policy: BankPolicy<TileBank>,
@ -128,7 +128,7 @@ pub struct AssetManager {
}
struct LoadHandleInfo {
_asset_id: u32,
_asset_id: AssetId,
slot: SlotRef,
status: LoadStatus,
}
@ -238,8 +238,7 @@ impl AssetManager {
for item in preload {
let entry_opt = {
let assets = self.assets.read().unwrap();
let name_to_id = self.name_to_id.read().unwrap();
name_to_id.get(&item.asset_name).and_then(|id| assets.get(id)).cloned()
assets.get(&item.asset_id).cloned()
};
if let Some(entry) = entry_opt {
@ -302,10 +301,7 @@ impl AssetManager {
}
}
// else {
// eprintln!(
// "[AssetManager] Preload failed: asset '{}' not found in table",
// item.asset_name
// );
// eprintln!("[AssetManager] Preload failed: asset id '{}' not found in table", item.asset_id);
// }
}
}
@ -1047,7 +1043,7 @@ mod tests {
}),
};
let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }];
let preload = vec![PreloadEntry { asset_id: 2, slot: 5 }];
let am = AssetManager::new(
vec![],
@ -1076,7 +1072,7 @@ mod tests {
let mut asset_entry = test_tile_asset_entry("my_tiles", data.len());
asset_entry.asset_id = 10;
let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }];
let preload = vec![PreloadEntry { asset_id: 10, slot: 3 }];
let am = AssetManager::new(
vec![],

View File

@ -1,6 +1,7 @@
use serde::{Deserialize, Serialize};
pub type HandleId = u32;
pub type AssetId = i32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
#[allow(non_camel_case_types)]
@ -13,7 +14,7 @@ pub enum BankType {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AssetEntry {
pub asset_id: u32,
pub asset_id: AssetId,
pub asset_name: String,
pub bank_type: BankType,
pub offset: u64,
@ -25,7 +26,7 @@ pub struct AssetEntry {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PreloadEntry {
pub asset_name: String,
pub asset_id: AssetId,
pub slot: usize,
}
@ -70,7 +71,7 @@ pub struct BankStats {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlotStats {
pub asset_id: Option<u32>,
pub asset_id: Option<AssetId>,
pub asset_name: Option<String>,
pub generation: u32,
pub resident_bytes: usize,

View File

@ -1,3 +1,4 @@
use crate::asset::{AssetEntry, BankType};
use crate::cartridge::{
ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader,
AssetsPackPrelude, AssetsPayloadSource, Capability, Cartridge, CartridgeDTO, CartridgeError,
@ -168,11 +169,29 @@ fn parse_assets_pack(path: &Path) -> Result<ParsedAssetsPack, CartridgeError> {
file.read_exact(&mut header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
let header: AssetsPackHeader =
serde_json::from_slice(&header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
validate_preload(&header.asset_table, &header.preload)?;
let payload_len = u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len })
}
fn validate_preload(asset_table: &[AssetEntry], preload: &[crate::asset::PreloadEntry]) -> Result<(), CartridgeError> {
let mut claimed_slots = HashSet::<(BankType, usize)>::new();
for item in preload {
let entry = asset_table
.iter()
.find(|entry| entry.asset_id == item.asset_id)
.ok_or(CartridgeError::InvalidFormat)?;
if !claimed_slots.insert((entry.bank_type, item.slot)) {
return Err(CartridgeError::InvalidFormat);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
@ -380,7 +399,7 @@ mod tests {
let payload = vec![1_u8, 2, 3, 4];
dir.write_assets_pa(
vec![test_asset_entry(0, payload.len() as u64)],
vec![PreloadEntry { asset_name: "tiles".to_string(), slot: 2 }],
vec![PreloadEntry { asset_id: 7, slot: 2 }],
&payload,
);
@ -396,9 +415,52 @@ mod tests {
assert_eq!(cartridge.asset_table.len(), 1);
assert_eq!(cartridge.asset_table[0].asset_name, "tiles");
assert_eq!(cartridge.preload.len(), 1);
assert_eq!(cartridge.preload[0].asset_id, 7);
assert_eq!(cartridge.preload[0].slot, 2);
}
#[test]
fn load_rejects_preload_with_missing_asset_id() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
dir.write_assets_pa(
vec![test_asset_entry(0, 4)],
vec![PreloadEntry { asset_id: 999, slot: 2 }],
&[1_u8, 2, 3, 4],
);
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_preload_slot_clash_per_bank_type() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
let asset_table = vec![
test_asset_entry(0, 4),
AssetEntry {
asset_id: 8,
asset_name: "other_tiles".to_string(),
bank_type: BankType::TILES,
offset: 4,
size: 4,
decoded_size: 4,
codec: "RAW".to_string(),
metadata: json!({
"tile_size": 16,
"width": 16,
"height": 16
}),
},
];
let preload = vec![PreloadEntry { asset_id: 7, slot: 2 }, PreloadEntry { asset_id: 8, slot: 2 }];
dir.write_assets_pa(asset_table, preload, &[1_u8, 2, 3, 4, 5, 6, 7, 8]);
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidFormat));
}
#[test]
fn load_rejects_invalid_assets_pa_prelude() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));

View File

@ -0,0 +1,29 @@
# Agenda 024 - `assets.pa` Preload and Asset Table ID-based Contract (Fechada)
## Status
Fechada pela decisao:
- `../decisions/012-assets-preload-asset-id-contract.md`
## O Que Foi Fechado
1. `PreloadEntry` deixa de usar `asset_name` e passa a ser normativamente `{ asset_id, slot }`.
2. `asset_id` passa a ser o identificador operacional de preload com semantica de `java int`.
3. `asset_name` permanece apenas como metadata descritiva/debug e nao participa do preload.
4. `preload` deve ser validado contra a `asset_table` do proprio `assets.pa` durante o bootstrap.
5. `asset_id` ausente da `asset_table` e clash de slot por tipo caracterizam erro estrutural de formacao do cart.
6. Nao existe compatibilidade normativa para preload legado por `asset_name`.
## Efeito Pratico
- `spec 15` passa a precisar explicitar o shape normativo de `PreloadEntry` e a validacao estrutural de preload.
- loader, parser de `assets.pa` e `AssetManager` passam a consumir preload por `asset_id`.
- a modelagem atual em codigo baseada em `u32` para `asset_id` precisa ser revisitada onde o contrato exposto ainda assume unsigned.
## Follow-up
- propagar a decisao em `specs 13/15`;
- criar suporte de parse/validacao estrutural para preload por `asset_id`;
- remover o preload por `asset_name` do runtime e dos testes;
- manter `asset.load(name, kind, slot)` inalterado ate decisao futura sobre a superficie VM-facing.

View File

@ -24,7 +24,6 @@ As agendas atuais são:
- `020-perf-host-debug-overlay-isolation.md`
- `021-perf-vm-allocation-and-copy-pressure.md`
- `022-perf-cartridge-boot-and-program-ownership.md`
## Sequenciamento Recomendado [PERF]
Ordem sugerida para discussao das agendas de performance:

View File

@ -0,0 +1,157 @@
# Decision Record - `assets.pa` Preload Uses `asset_id`
## Status
Accepted
## Contexto
A `decision 011` fechou o envelope runtime-facing de `assets.pa`:
- `assets.pa` e o contrato primario de assets do runtime;
- `asset_table` e `preload` vivem no header interno do artefato;
- `preload` e consumido apenas no bootstrap.
Essa decisao, no entanto, nao definiu a semantica de identidade de `PreloadEntry`.
O estado atual do runtime ainda mistura dois modelos:
- `AssetEntry` ja trata `asset_id` como identidade operacional do asset;
- `PreloadEntry` ainda usa `asset_name` como chave pratica de resolucao no boot.
Isso preserva uma dependencia estrutural de campo mutavel justamente na fase em que o cart deveria ser mais deterministicamente validavel.
## Decisao
### 1. `PreloadEntry` passa a ser `{ asset_id, slot }`
O shape normativo de preload em `assets.pa` passa a ser:
```text
PreloadEntry {
asset_id,
slot
}
```
`asset_name` deixa de fazer parte do contrato operacional de preload.
### 2. `asset_id` tem semantica de `java int`
`asset_id` no contrato runtime-facing de assets passa a ser um inteiro de 32 bits com semantica de `java int`.
Qualquer representacao interna do runtime deve respeitar essa semantica e nao introduzir contrato unsigned por acidente.
### 3. `asset_name` permanece apenas como metadata descritiva
`asset_name` continua permitido em `AssetEntry` para:
- debug;
- observabilidade;
- tooling editorial;
- UX de inspecao.
Mas `asset_name` nao participa:
- da identidade normativa do asset;
- da validacao de preload;
- da resolucao operacional de preload.
### 4. `preload` deve ser validado contra a `asset_table`
Quando `assets.pa` existir, o runtime deve validar `preload` durante o bootstrap usando a `asset_table` carregada do mesmo artefato.
Validacoes minimas obrigatorias:
- todo `preload.asset_id` deve existir na `asset_table`;
- nao pode haver clash de slot por tipo.
### 5. Clash de slot por tipo e proibido
O namespace de slot de preload e derivado de `bank_type` da `asset_table`.
Portanto, depois de resolver cada `preload.asset_id` para sua respectiva `AssetEntry`, o cart e invalido se houver mais de uma entry de preload para o mesmo par:
```text
(bank_type, slot)
```
Isso vale mesmo que as entries apontem para o mesmo asset.
### 6. Nao ha compatibilidade normativa com preload legado por `asset_name`
O contrato do runtime nao oferece modo de compatibilidade para preload legado baseado em `asset_name`.
Artefatos que nao seguirem o shape novo devem ser considerados invalidos para o contrato vigente.
### 7. Falhas de preload invalido sao estruturais de cart
Os seguintes casos sao erros estruturais de formacao do cart:
- `preload.asset_id` ausente da `asset_table`;
- clash de slot por tipo dentro de `preload`.
Esses casos devem falhar no bootstrap do cart, antes da execucao normal, e nao pertencem ao espaco de `status` operacional de `asset.load`.
## Rationale
Esta decisao foi adotada porque:
- alinha preload com a identidade estavel do dominio de assets;
- remove dependencia de metadata mutavel no bootstrap;
- preserva `asset_table` como fonte de verdade unica para identidade e tipo;
- torna a validacao do cart mais deterministica;
- impede que compatibilidade legado vire contrato permanente por inercia.
O uso de `java int` foi escolhido para manter alinhamento de semantica de ID com a fronteira de linguagem/plataforma desejada, em vez de cristalizar um detalhe unsigned especifico da implementacao atual em Rust.
## Invariantes / Contrato
- `PreloadEntry` e normativamente `{ asset_id, slot }`.
- `asset_id` segue semantica de `java int`.
- `asset_name` nao tem autoridade operacional sobre preload.
- `preload` so pode referenciar assets existentes na `asset_table` do mesmo `assets.pa`.
- `preload` nao pode conter mais de uma ocupacao para o mesmo par `(bank_type, slot)`.
- falhas de validacao de preload sao erros estruturais de cart e devem abortar o bootstrap.
- `asset.load(name, kind, slot)` permanece fora do escopo desta decisao.
## Impactos
### Specs
- `15-asset-management.md` deve explicitar o shape normativo de `PreloadEntry`, a semantica de `asset_id` e as validacoes estruturais de preload.
- `13-cartridge.md` deve registrar que `assets.pa` invalido inclui preload com IDs ausentes ou clash de slot por tipo.
### Runtime
- `prometeu-hal/src/asset.rs` deve trocar `PreloadEntry.asset_name` por `asset_id`.
- tipos de `asset_id` expostos no contrato runtime-facing devem ser revisados para semantica de `java int`.
- o loader de `assets.pa` deve rejeitar preload invalido antes de iniciar preload.
- o `AssetManager` deve resolver preload diretamente por `asset_id`, sem ponte nome -> id.
### Tooling / Packer
- o produtor de `assets.pa` deve emitir preload apenas com `asset_id`.
- carts com preload legado por `asset_name` deixam de ser validos no contrato atual.
## Referencias
- `../agendas/024-assets-pa-preload-and-asset-table-id-based-contract.md`
- `../decisions/011-assets-pa-autocontained-runtime-contract.md`
- `../specs/13-cartridge.md`
- `../specs/15-asset-management.md`
- `../../crates/console/prometeu-hal/src/asset.rs`
- `../../crates/console/prometeu-hal/src/cartridge_loader.rs`
- `../../crates/console/prometeu-drivers/src/asset.rs`
## Propagacao Necessaria
- criar PR/plan para atualizar `spec 13` e `spec 15`;
- criar PR/plan de codigo para:
- trocar o shape de `PreloadEntry` para `asset_id`;
- validar preload durante parse/bootstrap de `assets.pa`;
- rejeitar `asset_id` ausente da `asset_table`;
- rejeitar clash de slot por tipo;
- revisar a tipagem de `asset_id` para semantica de `java int`;
- atualizar testes de loader e `AssetManager`;
- remover a `agenda 024` da lista de agendas ativas.

View File

@ -20,6 +20,7 @@ Decisoes ativas:
- `006-vm-owned-stateful-core-contract.md`
- `007-filesystem-fault-core-policy.md`
- `011-assets-pa-autocontained-runtime-contract.md`
- `012-assets-preload-asset-id-contract.md`
Decisoes implementadas e aposentadas (migradas para `learn/`):

View File

@ -0,0 +1,69 @@
# PR 012 - Propagacao da Decision 012 para Preload por `asset_id`
## Briefing
Esta PR executa a `decision 012`, que fecha a identidade normativa de preload em `assets.pa`.
O objetivo e remover preload por `asset_name`, adotar `asset_id` com semantica de `java int`, e validar preload como parte estrutural da formacao do cart.
## Decisions de Origem
- `../decisions/011-assets-pa-autocontained-runtime-contract.md`
- `../decisions/012-assets-preload-asset-id-contract.md`
## Alvo
Propagar a decisao em specs e codigo do runtime para que:
- `PreloadEntry` seja `{ asset_id, slot }`;
- preload invalido falhe no bootstrap;
- o runtime nao dependa de nome para resolver preload.
## Escopo
- atualizar `spec 13` e `spec 15`;
- atualizar structs e parse do header de `assets.pa`;
- validar preload contra `asset_table`;
- detectar clash de slot por tipo;
- ajustar `AssetManager` e testes para preload por `asset_id`;
- revisar tipos expostos de `asset_id` para semantica de `java int`.
## Fora de Escopo
- mudar a superficie VM-facing de `asset.load(name, kind, slot)`;
- redesenhar `asset_table` alem do necessario para suportar a decisao;
- introduzir compatibilidade legado por `asset_name`;
- alterar politica de bancos/slots fora da validacao de preload.
## Plano de Execucao
1. Atualizar specs `13` e `15` com o shape normativo novo e as regras de validacao estrutural.
2. Trocar `PreloadEntry` em `prometeu-hal` para `asset_id`.
3. Revisar os pontos runtime-facing onde `asset_id` ainda esta fixado como unsigned.
4. Validar, no parse/bootstrap de `assets.pa`, que:
- todo preload referencia `asset_id` existente;
- nao existe duplicidade de `(bank_type, slot)`.
5. Remover a resolucao nome -> id do caminho de preload no `AssetManager`.
6. Atualizar testes de loader, parse e preload em memoria.
## Criterios de Aceite
- `assets.pa` com preload valido por `asset_id` carrega normalmente.
- `assets.pa` com `asset_id` ausente da `asset_table` falha no bootstrap.
- `assets.pa` com clash de slot por tipo falha no bootstrap.
- preload deixa de depender de `asset_name`.
- nenhuma spec vigente continua descrevendo preload por nome.
## Tests / Validacao
- teste de parse do header com `PreloadEntry { asset_id, slot }`;
- teste de rejeicao para `preload.asset_id` inexistente;
- teste de rejeicao para duplicidade de `(bank_type, slot)`;
- teste de inicializacao do `AssetManager` preloading por `asset_id`;
- revisao textual das specs `13` e `15` contra a `decision 012`.
## Riscos
- a troca de semantica de `asset_id` para `java int` pode expor assumptos unsigned espalhados no runtime;
- validacao estrutural no loader pode exigir ajuste em fixtures de teste existentes;
- se houver produtor externo ainda emitindo preload por `asset_name`, ele quebrara imediatamente, como mandado pela decisao.

View File

@ -38,3 +38,4 @@ Uma PR deste diretório deve:
PRs propostas para execucao da rodada atual:
- `012-assets-preload-asset-id-propagation.md`

View File

@ -109,6 +109,13 @@ The JSON header carries:
`asset_table` and `preload` are therefore no longer the primary runtime contract of `manifest.json`.
`preload` is structurally validated as part of `assets.pa` bootstrap:
- each preload entry is `{ asset_id, slot }`;
- `asset_id` uses 32-bit signed integer semantics;
- each `preload.asset_id` must resolve to an entry in the same `asset_table`;
- no two preload entries may claim the same `(bank_type, slot)` pair once `bank_type` is resolved from `asset_table`.
Runtime lifecycle:
- `asset_table` is loaded from `assets.pa` and remains live while the cartridge is running;
@ -119,6 +126,7 @@ Runtime loading policy:
- cartridges without `assets.pa` remain valid when `Asset` capability is not declared;
- cartridges that declare `Asset` capability must provide valid `assets.pa`;
- `assets.pa` with invalid preload references or preload slot clashes is structurally invalid;
- failure must occur as early as cartridge bootstrap, before preload execution.
## 7 Runtime Forms

View File

@ -96,6 +96,8 @@ AssetEntry {
This table describes content identity and storage layout, not live residency.
`asset_id` is the stable runtime-facing asset identity and uses 32-bit signed integer semantics compatible with Java `int`.
`offset` is relative to the start of the payload region inside `assets.pa`.
## 5 Banks and Slots
@ -180,13 +182,33 @@ These metrics support debugging, telemetry, and certification-oriented inspectio
`preload` is stored in the JSON header of `assets.pa`.
The normative preload shape is:
```text
PreloadEntry {
asset_id
slot
}
```
These preload entries are consumed during cartridge initialization so the asset manager can establish initial residency before normal execution flow.
Validation rules:
- `preload` is resolved by `asset_id`, not by `asset_name`;
- every `preload.asset_id` must exist in the same `asset_table`;
- no two preload entries may resolve to the same `(bank_type, slot)` pair;
- legacy preload keyed by `asset_name` is invalid for the current contract.
Lifecycle rule:
- `preload` is boot-time input only;
- it does not need to remain live after initialization completes.
Bootstrap rule:
- invalid preload is a structural cartridge error and must fail cartridge bootstrap before normal execution begins.
## 10 Relationship to Other Specs
- [`13-cartridge.md`](13-cartridge.md) defines the cartridge package and the requirement that `assets.pa` carries its own asset header.