Asset loading
Usage guide of asset.preload, asset.cache and asset.meta: the loading plan in phases, the device cache, asset passports, settings, hooks, modding, engines.
Asset loading, cache and metadata (asset.* kits) #
How the files of a game reach the player: loaded in the order the game needs them, kept between two visits, and described by their passport. Readable offline: gripforge docs asset-preload, or the MCP tool gripforge_docs (topic asset-preload).
The map #
| Kit | Role |
|---|---|
asset.preload | loads each file once, in phases (first, background, next, lazy), and reports progress; draws nothing |
asset.cache | keeps the files on the player's device so a second visit downloads nothing that did not change |
asset.meta | hands the passport of each asset (real size, colliders, skeleton, per-kit sections) to the other kits as plain data |
All three require only game.engine and are services (role service). asset.preload reads asset.cache before downloading and writes to it afterwards. The loading view is another module: the title screen of ui.menu, ui.endscreen between two levels, the bar of hud.action (loadingBar: true), or the page.
Turn it on #
gripforge_gamekit_install { project_id: "gkp_…", id: "asset.preload" }
gripforge_gamekit_install { project_id: "gkp_…", id: "asset.cache" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "asset.preload", config: { "aheadScenes": 2, "concurrency": 4 } }
Only the keys that differ from the defaults need to be written. The full reference of a kit is gripforge kit api <id>.
asset.preload #
The plan #
Every file goes in one phase:
| Phase | What | When |
|---|---|---|
first | what the first playable frame needs: the files of the start scene; every file of a game without scenes | before the game starts |
background | the rest of the game | while the game is played |
next | the files of the scenes that may come next (nextScenes: "auto", aheadScenes) | after background, and again at each scene entered |
lazy | nothing ahead | only when something asks for the file |
Inside a phase, higher priority first, then the order of the game. One download per file: every caller gets the same promise while it downloads, then the same bytes (the page, the web player, several kits, several labels of one glTF). A file with a SHA-256 is read from asset.cache first, checked (size, SHA-256, then the host's format check) and kept for the next visit. A request jumps the queue.
Settings and defaults #
| Key | Default | Meaning |
|---|---|---|
enabled | true | off: the plan is empty; files are loaded only when asked (still once each) |
defaultPhase | first | the phase of a file the plan cannot place (first: the game starts once every file is in) |
nextScenes | auto | auto loads the scenes after the current one ahead; none |
aheadScenes | 1 | how many scenes ahead (0 to 8) |
concurrency | 3 | downloads at once (1 to 8) |
memoryMB | 512 | megabytes of downloaded files kept for late callers |
verify | true | check the SHA-256 of every file that has one, also from the cache |
progressEveryMs | 250 | at most one progress event per this many ms |
rules | [] | patterns placed by the game before the documents, e.g. { match: "assets/hud/*", phase: "first", priority: 5 } |
The game shapes the plan with documents of the collection asset_preload (moddable, so a mod does the same):
{ "id": "boss_early", "asset": "boss", "phase": "first", "priority": 10 }
{ "id": "intro_movie", "asset": "assets/intro.webm", "remove": true }
{ "id": "extra_music", "path": "assets/music/night.ogg", "sha256": "…", "bytes": 4200000, "phase": "next", "scene": "night" }
State, events, service #
Variables: asset.preload.phase (idle, first, background, next, done), asset.preload.progress (0 to 1), asset.preload.first_ready, asset.preload.source (cache, network, mixed). Events: asset.preload.planned, .phase, .progress (phase, ratio, first_ratio, downloaded_bytes, total_bytes, verified, count, from_cache, from_network, source), .loaded, .failed, .done, .ahead, .started, .complete, .released. Service: plan(), progress(), request(path, priority?), prioritize(path, priority, phase?), prefetch(scene), cancel(path?), ledger().
Hook asset.preload.plan { id, path, phase, priority, scene, bytes }: phase and priority can change; cancelled, the file leaves the plan and is loaded only when asked. A mod may override enabled, defaultPhase, nextScenes, aheadScenes, concurrency, memoryMB, rules and add documents to asset_preload.
asset.cache #
The store is IndexedDB in a page, else Cache Storage with a service worker scoped to the game, else the HTTP cache through the servers' headers, else memory or nothing. The key is the path and the SHA-256 (assets/scene.glb@<sha>): a new version has another key, so it is downloaded and the old entry of the path is removed. A file without a checksum is never kept. A browser that refuses storage gives mode: off and the game plays, each file still downloaded once.
| Key | Default | Meaning |
|---|---|---|
enabled | true | off: nothing is kept |
quotaMB | 512 | past it, entries are evicted (a file larger than the quota is not kept) |
version | 1 | changing it empties the store at the next visit |
store | auto | auto, indexeddb, cachestorage, http, memory, off |
fallbacks | indexeddb, cachestorage, http | stores a page may use, in order |
eviction | lru | lru (read longest ago), fifo, none |
serviceWorker | true | serves kept files to every request of the game |
minBytes | 0 | smaller files are not kept |
Service: stats() (mode, entries, bytes, quota, hits, misses, writes, evicted), entries(), invalidate(path), clear(), write, read. Variables asset.cache.mode, asset.cache.bytes, asset.cache.entries. Hooks asset.cache.store { path, sha, bytes } (cancel: not kept) and asset.cache.evict. A mod may override enabled, quotaMB, fallbacks, eviction, minBytes. The servers answer Cache-Control: immutable for a name that carries its version, and an ETag (the SHA-256) otherwise.
asset.meta #
Every asset carries a mesh passport (lib_….gfmesh.json, next to the file in the library): real size, orientation, skeleton, colliders, clips, and one section per game kit (kits.<section>, owned by the kit that reads it; the first is lod for render.lod: triangles, radius, levels, category, impostor, ratios, screenSizes, shadow levels). The measured values are auto, the user's corrections are manual and always win; the game receives the resolved passport.
Passports are documents of the project collection asset_passports, one per bound slot, filled from the library when the web bundle or an engine delivery is built:
{ "id": "tree", "item": "lib_…", "kind": "prop", "name": "Old oak",
"passport": { "etalon": { "height": 7.4 }, "kits": { "lod": { "category": "vegetation", "triangles": 18200, "radius": 4.1 } } } }
The kit has no config. Service: of(ref) (the resolved passport or null), section(ref, name), info(ref), has(ref), ids(), register(id, passport, info), bind(entity, id), reload(); ref is an entity id, a slot id or a library id. Event asset.meta.changed { id }. A kit that reads it must work without it: an asset without a passport answers null.
const lod = services.get('asset.meta')?.section(entityId, 'lod'); // null: use your own defaults
Nothing is moddable: a passport is the measured truth of an asset, so a mod that adds or patches an asset_passports document is refused (collection_not_moddable). A mod changes what the game does with an asset, not what the asset is.
Engines #
| Kit | Web and native player | Godot, Unity, Unreal |
|---|---|---|
asset.preload | the loader of the page and the Rust plan, hook and state | each keeps its own loading |
asset.cache | web and Rust | none |
asset.meta | all features | delivered natively, same passports |
When it does not do what you expect #
| Symptom | Cause and fix |
|---|---|
| The game waits for every file at the start | defaultPhase is first and no scenes split the plan: move files to background with asset_preload or rules |
| The second visit downloads again | the file has no SHA-256, asset.cache is off or its store is unavailable (stats().reason), or version changed |
| A file is loaded when asked only | its phase is lazy, or an asset.preload.plan tap cancelled it |
| The loading bar never shows | the view must read asset.preload.progress; hud.action needs loadingBar: true |
of(ref) answers null | the asset has no passport; the kit must use its defaults |