prometeu-runtime/docs/specs/runtime/15-asset-management.md

549 lines
17 KiB
Markdown

# Asset Management
Domain: asset runtime surface
Function: normative
This chapter defines the runtime-facing asset model of PROMETEU.
## 1 Scope
PROMETEU asset management is bank-centric.
Assets are:
- cold bytes stored in the cartridge;
- described by cartridge metadata;
- materialized into host-managed banks;
- separate from VM heap ownership.
This chapter describes the runtime contract currently visible in the codebase. It is not a full tooling pipeline specification.
## 2 Core Principles
1. asset residency is explicit;
2. asset memory belongs to the machine, not to the VM heap;
3. banks and slots are hardware/runtime concepts;
4. loading and activation are explicit operations;
5. asset memory does not participate in GC.
## 3 Cartridge Asset Artifact
The runtime currently consumes one primary cartridge asset artifact:
- `assets.pa`: autocontained asset artifact.
`assets.pa` carries, inside the same binary:
- fixed binary prelude;
- JSON header;
- payload bytes.
The JSON header carries:
- `asset_table`: metadata entries describing asset content;
- `preload`: optional initial residency requests consumed during cartridge initialization.
This chapter describes the runtime-facing asset contract. It does not define the Studio packer workflow or the shipper pipeline that publishes the cartridge.
### 3.1 `assets.pa` v1 envelope
`assets.pa` v1 is structured as:
```text
[fixed binary prelude]
[json header]
[binary payload region]
```
The fixed binary prelude contains, at minimum:
- `magic`
- `schema_version`
- `header_len`
- `payload_offset`
It may additionally include:
- `flags`
- `reserved`
- `header_checksum`
### 3.2 Header and payload contract
The runtime loads:
- `asset_table` from the JSON header and keeps it live during cartridge execution;
- `preload` from the JSON header and consumes it only during boot.
Payload bytes are addressed from the payload region using offsets relative to `payload_offset`, not relative to the start of the whole file.
## 4 Asset Table
Current runtime-facing asset metadata includes:
```text
AssetEntry {
asset_id
asset_name
bank_type
offset
size
decoded_size
codec
metadata
}
```
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`.
`size` is the serialized byte count stored in the payload region.
`decoded_size` is the byte count of the materialized runtime bank after decode, not necessarily the same as the serialized payload size.
`codec` identifies the generic transformation pipeline applied to the serialized payload slice before the asset becomes a resident bank.
`codec` does not define the bank-specific serialized layout itself. Specialized banks may still have normative decode rules even when `codec = NONE`.
### 4.1 `GLYPH` asset contract in v1
For `BankType::GLYPH`, the v1 runtime-facing contract is:
- `codec = NONE`
- serialized pixels use packed `u4` palette indices
- serialized palettes use `RGBA8888` with canonical RGBA channel order
- `palette_count = 64`
- runtime materialization may expand pixel indices to one `u8` per pixel
For `GLYPH`, `NONE` means there is no additional generic codec layer beyond the bank contract itself.
For the current transition window:
- `RAW` is a legacy deprecated alias of `NONE`
- newly published material must use `NONE` as the canonical value
Even with `codec = NONE`, `GLYPH` still requires bank-specific decode from its serialized payload. The serialized byte layout therefore does not need to match the in-memory layout.
#### 4.1.1 Metadata Normalization
Following `DEC-0004`, the `AssetEntry.metadata` field must be structured in segmented form to avoid ambiguity.
Required effective metadata fields for `GLYPH` at the root level:
- `tile_size`: tile edge in pixels; valid values are `8`, `16`, or `32`
- `width`: total sheet width in pixels
- `height`: total sheet height in pixels
- `palette_count`: number of serialized palettes for the bank
Optional informative subtrees:
- `metadata.codec`: codec/compressor-specific configuration (for example dictionaries or compression flags).
- `metadata.pipeline`: informative metadata from the Studio build process (for example source hashes, timestamps, or tool versions).
Validation rules for `GLYPH` v1:
- `palette_count` must be `64`
- `width * height` defines the number of logical indexed pixels in the decoded sheet
- extra metadata may exist, but the runtime contract must not depend on it to reconstruct the in-memory bank unless that data is defined at the root as an effective field.
#### 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 * 4` bytes.
The tile-bank payload therefore separates serialized storage form from runtime memory form:
- serialized pixel plane: packed `4bpp`
- decoded pixel plane: expanded `u8` indices, one entry per pixel
- palette table: `64 * 16` colors in RGBA8888 channel order
For `GLYPH` v1:
- `size` must match `ceil(width * height / 2) + (palette_count * 16 * 4)`
- `decoded_size` must match `(width * height) + (palette_count * 16 * 4)`
RGB565 palette payloads are not compatible runtime input. Existing assets with
two-byte RGB565 palette entries must be regenerated or converted by tooling
before they are loaded by the runtime.
Palette indices are ordinary indices. Transparency is represented by the alpha
channel of the resolved RGBA8888 palette entry, not by reserving index `0`.
### 4.2 `SCENE` asset contract in v1
For `BankType::SCENE`, the v1 runtime-facing contract is:
- `codec = NONE`
- payload magic is `SCNE`
- payload version is `1`
- layer count is fixed at `4`
- each layer carries its own glyph dependency
- the glyph dependency is serialized as canonical `AssetId` using signed 32-bit little-endian width
- the payload MUST describe cold asset dependencies only and MUST NOT encode runtime glyph slot topology
Even with `codec = NONE`, `SCENE` still has a bank-specific binary contract. The serialized layout is normative and distinct from any transient runtime residency state.
#### 4.2.1 Layer Header Layout
Each scene layer header is:
1. `flags: u8`
2. `glyph_asset_id: i32` (little-endian)
3. `tile_size: u8`
4. `reserved: u16`
5. `parallax_x: f32`
6. `parallax_y: f32`
7. `width: u32`
8. `height: u32`
9. `tile_count: u32`
10. `reserved: u32`
This makes the canonical per-layer header size `32` bytes.
Normative rules:
- `glyph_asset_id` MUST be interpreted as the cold identity of the glyph dependency for that layer;
- `glyph_asset_id` MUST NOT be interpreted as a runtime slot id;
- each layer MUST own exactly one glyph dependency reference;
- no shared dependency table exists in this contract;
- `tile_count` MUST equal `width * height`.
#### 4.2.2 Runtime Binding Semantics
`SCENE` assets do not own runtime residency.
Therefore:
- the runtime MUST resolve scene glyph dependencies through runtime-owned residency state;
- the runtime MUST maintain a runtime-owned mapping from committed glyph `AssetId` to glyph slot;
- scene bind and scene draw MUST consult runtime residency, not serialized slot topology;
- a missing scene glyph dependency is not a passive asset-status condition.
If a scene glyph dependency cannot be resolved:
- `bind_scene` MUST fail as a fatal machine error with clear logging;
- draw/composition MUST fail as a fatal machine error with clear logging;
- runtime MUST NOT continue scene composition after the missing dependency is detected.
## 5 Banks and Slots
The current runtime exposes bank types:
- `GLYPH`
- `SOUNDS`
- `SCENE`
Assets are loaded into explicit slots identified by slot index at the public ABI boundary.
The runtime resolves bank context from `asset_table` using `asset_id`.
Internally, the runtime may still use a bank-qualified slot reference such as:
```text
SlotRef { bank_type, index }
```
That internal representation is derived from the resolved `AssetEntry`, not supplied by the caller.
### 5.1 Canonical Bank Telemetry
The canonical visible bank telemetry contract is exposed by `AssetManager`.
The per-bank summary is slot-first and uses this shape:
```text
BankTelemetry {
bank_type
used_slots
total_slots
}
```
Rules:
- the visible contract MUST NOT expose byte-oriented bank occupancy as the canonical summary;
- canonical bank names are `GLYPH` and `SOUNDS`;
- detailed occupancy inspection remains host-owned and slot-based through slot references, not bank byte totals;
- public runtime specs MUST NOT treat `bank.slot_info` or JSON bank inspection payloads as the long-term guest ABI after `DEC-0009`;
- any residual byte accounting MAY exist internally, but it is not part of the visible bank telemetry contract.
## 6 Load Lifecycle
The runtime asset manager exposes a staged lifecycle:
- `PENDING`
- `LOADING`
- `READY`
- `COMMITTED`
- `CANCELED`
- `ERROR`
High-level flow:
1. request load of an asset into a slot;
2. resolve the asset entry from live `asset_table`;
3. open the payload slice in `assets.pa`;
4. perform read/decode/materialization work;
5. mark the load `READY`;
6. explicitly `commit`;
7. activate the resident asset in the slot.
The canonical payload paths are:
- `ROM -> open_slice -> CODEX/decode -> Bank`
- `ROM -> open_slice -> temporary in-memory blob -> CODEX/decode -> Bank`
`open_slice` is the runtime-facing concept for opening a limited view over a payload slice. The runtime must not require the whole `assets.pa` payload to remain resident in RAM as its baseline operating mode.
`OP_MODE` selects between direct slice consumption and temporary materialization in memory.
For v1:
- `OP_MODE` is derived from `codec`/CODEX;
- explicit per-asset hinting is not part of the baseline contract.
- `TILES` with `codec = NONE` may still stage in memory before bank installation because bank-specific decode expands packed pixel indices into the resident representation.
- during the migration window, runtime may accept legacy `RAW` as an alias of `NONE`.
The runtime does not treat asset installation as implicit side effect.
## 7 Residency and Ownership
Asset banks are host/runtime-owned memory.
Therefore:
- VM heap does not own asset residency;
- GC does not scan asset bank memory;
- shutting down a cartridge can release bank residency independently of VM heap behavior.
- the runtime must not keep the full `assets.pa` payload resident in RAM as a baseline requirement.
## 8 Bank Telemetry and Inspection Boundary
The runtime may maintain bank and slot statistics such as:
- total bytes;
- used bytes;
- free bytes;
- inflight bytes;
- slot occupancy;
- resident asset identity per slot.
These metrics support host-owned debugging, telemetry, and certification-oriented inspection.
Only bounded operational summaries belong to the public runtime contract.
Detailed per-slot inspection is valid for host tooling and runtime internals, but it must not be treated as a general guest-visible debug convenience API.
## 9 Preload
`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.
- [`16-host-abi-and-syscalls.md`](16-host-abi-and-syscalls.md) defines the syscall boundary used to manipulate assets.
- [`03-memory-stack-heap-and-allocation.md`](03-memory-stack-heap-and-allocation.md) defines the distinction between VM heap memory and host-owned memory.
## 11 Syscall Surface and Status Policy
`asset` follows status-first policy.
Fault boundary:
- `Trap`: structural ABI misuse (type/arity/capability/shape mismatch);
- `status`: operational failure;
- `Panic`: internal invariant break only.
### 11.1 MVP syscall shape
- `asset.load(asset_id, slot) -> (status:int, handle:int)`
- `asset.status(handle) -> status:int`
- `asset.commit(handle) -> status:int`
- `asset.cancel(handle) -> status:int`
Rules:
- `handle` is returned when `load` status is `OK`;
- `handle` represents a stable bank slot target, not a worker thread;
- a known handle remains queryable even when the slot is empty, has no active
request, or has a superseded request;
- failed `load` returns `handle = 0`;
- `commit` and `cancel` must not be silent no-op for unknown/invalid handle state.
- `asset.load` resolves the target bank type from `asset_table` using `asset_id`;
- public callers must not supply `asset_name` or `bank_type` to `asset.load`;
- slot validation and residency/lifecycle rejection remain in `asset` status space and are not delegated to `bank`.
### 11.2 Async work lane and backlog
Asset loading uses the runtime async work lane. This lane is separate from the
VM/main lane and separate from the render worker lane.
The asset async lane is serial:
- it executes at most one active asset job at a time;
- it keeps an ordered backlog of pending requests;
- it must not create one OS thread per `asset.load` request.
Asset requests are keyed by the target `bank_type/slot`.
Rules:
- each `bank_type/slot` has at most one current request;
- a newer request for the same `bank_type/slot` supersedes the previous request;
- if the previous request is queued, it is removed from the backlog;
- if the previous request is active, the lane should cancel cooperatively when
the current phase supports cheap cancellation;
- if active work cannot stop cheaply, it may finish, but the result must be
discarded when its generation no longer matches the target request generation;
- if the target already contains the requested `asset_id` as a valid resident
asset, `asset.load` returns a ready handle without adding a backlog entry.
The effective backlog size is bounded by the sum of targetable bank slots,
because only one current request can exist per `bank_type/slot`. The runtime
does not expose a guest-visible `queue_full` status for the normal asset
backlog path.
### 11.3 Handle state
An asset handle observes one stable bank slot target. Its observable state has
two parts:
```text
handle:
bank_type
slot
slot_state:
loaded_asset_id
resident_state
slot_generation
request_state:
requested_asset_id
request_generation
state
backlog_position
progress
```
`slot_state` describes what is currently resident in the target slot.
`request_state` describes the current or most recent request for that target.
Mutating operations such as commit, cancel, promote, demote, and move must act
on the current request generation. They must not accidentally mutate a newer
request through an older handle view.
### 11.4 Backlog inspection and ordering
The asset backlog surface may expose these status-first operations:
- `asset.backlog_info() -> (status, pending_count, active_handle, active_asset_id, active_bank_type, active_slot, active_progress)`
- `asset.backlog_position(handle) -> (status, state, position, progress)`
- `asset.backlog_move(handle, new_position) -> status`
- `asset.backlog_promote(handle) -> status`
- `asset.backlog_demote(handle) -> status`
- `asset.target_status(bank_type, slot) -> (status, asset_id, handle, state, position, progress)`
`asset.backlog_promote(handle)` is a shortcut for moving a queued request to
position `1`, the first pending position after the active job.
`asset.backlog_demote(handle)` is a shortcut for moving a queued request to the
end of the pending backlog.
### 11.5 Progress and telemetry
Asset progress uses integer progress, not floating point. The preferred scale is
`0..10000`.
The initial phase model is:
```text
queued -> 0
read -> 0..4000
decode -> 4000..9000
stage -> 9000..10000
ready -> 10000
```
If a phase cannot report internal progress, it keeps the previous progress mark
and advances at phase completion. The runtime must not invent false precision
for non-linear decode phases.
Minimum telemetry:
- current backlog depth;
- target/request position;
- active job progress;
- jobs submitted;
- jobs completed;
- jobs failed;
- jobs canceled;
- jobs superseded;
- job duration;
- percentiles by `bank_type`;
- lightweight percentiles or small-window samples by `asset_id`.
Percentiles are updated when a job closes, not inside the inner decode loop.
### 11.6 Minimum status tables
`asset.load` request statuses:
- `0` = `OK`
- `3` = `ASSET_NOT_FOUND`
- `5` = `SLOT_INDEX_INVALID`
- `6` = `BACKEND_ERROR`
`asset.status` lifecycle statuses:
- `0` = `PENDING`
- `1` = `LOADING`
- `2` = `READY`
- `3` = `COMMITTED`
- `4` = `CANCELED`
- `5` = `ERROR`
- `6` = `UNKNOWN_HANDLE`
- `7` = `QUEUED`
- `8` = `ACTIVE`
- `9` = `SUPERSEDED`
- `10` = `EMPTY`
- `11` = `INVALID`
- `12` = `BACKEND_UNAVAILABLE`
`asset.commit` and `asset.cancel` operation statuses:
- `0` = `OK`
- `1` = `UNKNOWN_HANDLE`
- `2` = `INVALID_STATE`
- `3` = `SUPERSEDED`