Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials16
OverviewFirst game projectFirst entityAuthored user_data and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesPlay, Edit, and scenesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides16
OverviewDevelopment guideGameplay APIChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / UnrealActor and component lifecycle
Systems28
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)UI and editorEditor asset hot reloadEditor Project Selector
Language reference2
OverviewAkari language referenceKawa language reference
Engine overview
Systems23 min read

Assets and Shinra

On this page
On this pageSource and runtime assetsAsset pipeline (Shinra tool)Common command shapeScene documents (JSON + HSC1)Prefab documents (JSON + HSC1)Asset URIs and packsResolve rules (packs mode)Scenes do not declare packsOpening packs from game codeOne place resolves membershipAsset residency (refcount)Game and editor behaviorLaunch matrix (first-class; no dual-read fallback)Editor live protocolSoft refs and load/hot-reload recoveryStatus modelCause → result (by asset kind)Hot-reload pipeline edge cases (editor)Cold load vs hot reloadEditor vs game (summary)Debug vs release configRecovery cheatsheetAsset authoring rules Back to top

Hands-on walkthroughs: First mesh and material, Assets in Play.

Source and runtime assets

Games keep source assets under their package, normally src/games/example/assets/:

SourceProcessed output
PNG/JPEG/EXR/HDR/DDS/TGA.shintexture
glTF/GLB, FBX, OBJ, USDZ.shinmodel (+ companions: textures, .model.json / .shinmodeldoc, materials)
WAV/OGG.shinaudio
TTF.shinfont
Kawa scripts.kawabc (via kawac; --kawac / $KAWAC)
.material.json / .model.json.shinmaterial / .shinmodeldoc
.particle.json / .animgraph.json.shinparticle / .shinanimgraph
shader package/specplatform shader artifacts

Shinra also cooks scenes (.shinscene) and prefabs (.shinprefab). Wire details: Asset formats.

The engine's runtime asset APIs are in src/hikari/src/assets/. Three layers:

  1. Game / engine — only AssetStore / AssetRef / scene_document (no packaging knowledge). Load is retain* / release* / pinPath; has* is a bool probe, never a borrowed pointer.
  2. Asset facade — AssetStore caches and decodes; all raw IO goes through assets/io.AssetIo.
  3. Backend — default assets/backends/shinra_io.zig (comptime) implements packs + loose/data trees. Wire formats live in assets/formats/; .shinbundle open lives under assets/bundle/ and is only used by that backend.

The editor live loop uses assets/asset_pipeline_daemon.zig (tool protocol; brand isolated there).

Asset pipeline (Shinra tool)

Shinra (src/shinra) is the Rust asset compiler. Kaji builds it to bin/shinra/<platform-arch>/shinra. It processes inputs in parallel, supports sidecar metadata, watch mode, deterministic manifests, optional bundles, and validation. --target macos-metal|windows-d3d12|linux-vulkan drives texture compression (not runtime asset code). Kawa scripts cook via --kawac <path> (Kaji passes the staged kawac) or $KAWAC when invoking Shinra directly.

All independent asset work uses one batch-outcome contract, including plugin assets (audio, textures, models, scripts, fonts), authoring companions, particles, scenes, prefabs, and shader specifications. A source-local parse/import/compile failure records only that source as failed; sibling work and later phases continue, every successful output is published immediately, and the manifest is generated from the usable output tree before the batch reports an aggregate error. An outer pipeline error is reserved for infrastructure failures that prevent a phase from running, such as an unreadable input tree or unavailable required tool. Watch mode follows the same isolation rule: a failed change never invalidates sibling outputs, and atomic producers retain the failed source's last-known-good artifact.

On Windows, Kaji sets LIBCLANG_PATH from known LLVM / Visual Studio Clang install paths when building Shinra (bindgen needs libclang.dll). An existing valid LIBCLANG_PATH is left unchanged. The same pass sets CMAKE (and prepends its directory to PATH) from the Visual Studio CMake component or a standalone CMake install so opusic-sys can compile libopus. An existing valid CMAKE or a cmake already on PATH is left unchanged.

Shinra also needs a working C toolchain on every build host: ufbx is compiled through cc (same as intel_tex_2 / audiopus). The generated Rust bindings carry no rustdoc — read ufbx.h for semantics. audiopus is the Flakebi/audiopus git fork (pinned rev in Cargo.toml) for Windows-friendly linking; do not switch to crates.io-only without validating Windows builds. Ogg sources sniff OpusHead vs Vorbis ident: Vorbis decodes (lewton) then cooks; Opus + format: OpusOgg pass through into .shinaudio; Opus + PCM format is a hard error (use WAV/Vorbis). Do not feed Opus Ogg through lewton.

Kaji builds Shinra as release by default even when the product is --config=debug (host tool). To debug the pipeline itself:

bash
bin/kaji/kaji editor --project=src/games/example --shinra:config=debug
# equivalent:
bin/kaji/kaji editor --project=src/games/example --dep-config=shinra:debug
bin/kaji/kaji shinra --shinra:config=debug
TargetTexture compression
windows-d3d12BC7
macos-metalASTC 6x6
linux-vulkanBC7

Sidecar files use <filename>.shinmeta.json. They configure texture mipmaps/sRGB/premultiplication/compression quality, audio normalization/sample rate/format/streaming, and model LOD/optimization/compression behavior. Model allow_cpu_access defaults to false; enable it only when runtime code must inspect decoded render geometry. Static rendering and separately cooked collision do not need it.

Common command shape

bash
shinra --input assets/ --target macos-metal
shinra --input assets/ --output build/assets --target windows-d3d12 --validate
shinra --input assets/ --target macos-metal --watch
shinra --input assets/ --target windows-d3d12 --bundle game --compression zstd
# Domain pack (product seal / Kaji --resources=bundles): logical root ≠ output
shinra pack --name scenes --root data/ --output data/bundles/ --subdir scenes --include-ext shinscene

The default output mirrors source folders and emits assets.shinmanifest.json plus packs.shinplan.json (resolved pack membership). Kaji product cook always runs --bundle game, writing intermediate game.shinbundle + game.shinbundle.json under the cook work tree and .engine/staged/shinra (TOC membership only — no per-stem .map.json). That intermediate is not a product catalog stem; seal strips any copy that lands under data/bundles/. Editor watch (--watch) does not pass --bundle.

Product builds seal logical domain packs via Kaji --resources=bundles|loose (default bundles): configs/layout.json (v2) defines each pack's membership globs, delivery (packed or loose), deps and labels; Kaji always seals engine on full game builds. With no layout.json the whole project ships as one packed pack. Seal places what the plan says and writes {stem}.shinbundle (v3, TOC embedded; the .shinbundle.json beside it is informational), then writes the product.shinbundle.map.json v3 catalog: per pack delivery, deps, labels and loose root — a packed pack's membership is its own TOC, which the runtime maps at store open (not a mega-pack, not the cook game stem). See Product resource layout.

Scene documents (JSON + HSC1)

  • Authoring: scenes/*.json with kind: com.hikari.scene (editor always has UTF-8).
  • Cook: Shinra owns it — pass --scenes <project>/scenes (Kaji does this on product asset builds). Emits only under cook/work output/scenes/*.shinscene (HSC1), same as other cooked assets — not next to authoring JSON. Product stages into data/scenes/ (then seal may pack). Watch mode re-cooks into the cache/work tree.
  • Runtime load: only via AssetStore (scene_document.loadFromStore): HSC1 .shinscene in non-Debug builds, with authoring .json fallback only in Debug. Loose / data_root / bundle — store decides.

Prefab documents (JSON + HSC1)

Full expand/diff/variants/editor/runtime contract: Prefabs.

  • Authoring: assets/prefabs/**/*.prefab.json with kind: com.hikari.prefab (editor always has UTF-8).
  • Cook: Shinra --prefabs <project>/assets/prefabs (Kaji when the directory exists). Emits only under cook/work output/prefabs/**/*.shinprefab (same HSC1 wire as scenes; envelope validates prefab fields). Product stages into data/prefabs/. Watch re-cooks into the cache/work tree.
  • Runtime load: AssetStore retainBlobKind(…, .prefab) prefers .shinprefab; World.retainPrefab decodes HSC1 without JSON. Authoring .prefab.json only in Debug.

Asset URIs and packs

Game modules use typed hi.AssetRef for dynamic binds; scene JSON stores the same URI strings. See Game-facing refs.

Cooked assets are asset:// only. content:// (AssetStore tables under content_dirs), writable://, and project:// (filesystem) are ContentRef, not cooked-asset URIs.

Two asset:// prefixes (no pack-search / .../ form). Bare paths are an unscoped alias:

FormMeaningPack selection
asset://<pack>/<logical/path>Absolute pack + pathPack is in the URI
asset://./<logical/path>Unscoped absolute path under product dataProduct catalog path → pack (seal map)
bare <logical/path>Same as unscoped (catalog key)Same as ./

./ is not FS-relative (not “next to this model / this scene file”). It means no pack segment in the URI. The path is still absolute under the product data root (models/cube). Shinra bakes multi-mesh GLB companions this way (asset://./scenes/…/DamagedHelmet_img0) so pack membership can move without rewriting every companion ref.

Engine-owned meshes do not enter Shinra or scene preload residency. In addition to asset://_engine/meshes/unit_plane, scenes can request an indexed displacement grid as asset://_engine/meshes/grid_plane_<segments> (1–512). Shared vertices make a 256×256 surface roughly one sixth of the vertex upload and vertex-shader work of triangle soup.

Resolve rules (packs mode)

  1. Explicit asset://shared/models/cube → use pack shared if retained, else fail (PackNotRetained).
  2. Unscoped asset://./models/cube → look up models/cube in the product catalog (one path → one pack at seal). That pack must be retained, else fail. Does not search across open packs.
  3. Uncatalogued path inside a loose pack's root → resolves from disk while that pack is retained. This is the mod case: a file the seal never saw behaves exactly like a sealed member.
  4. Anything else uncatalogued (content dirs, always-loose leftovers) resolves from the data plane as before.
  5. Seal dual-home is a build error: one logical path, one pack. Valid catalogs never need "first retained pack wins".
JobMechanism
Which packs are openSession system packs + each scene's derived closure + hi.host_api.packs()
Which pack owns a pathExplicit URI pack, or product catalog for ./
What goes in a packconfigs/layout.json, resolved by the cook into packs.shinplan.json
What a retain opensThe pack plus its deps, transitively

Scenes do not declare packs

A scene document has no packs field. Which packs a scene needs is computed at load from the assets it references:

text
closure(scene) = { packOf(ref) : ref ∈ scene refs }  then + deps of each, transitively

scene/scene_pack_closure.zig does the first half; the backend's retainPack does the second. Consequences worth knowing:

  • A scene cannot name a pack that does not exist, or drift from configs/layout.json.
  • A scene authored after the build — by a modder, with no cook and no seal — resolves like one that shipped.
  • The walk is shallow: only the scene's own references are visible, because a model document's meshes live inside a pack and reading them would need it open already. Packs are directory-shaped in practice; where they genuinely are not, the owning pack declares a dep. Per-pack deps are O(packs) to author, where per-scene lists were O(scenes).
  • An older document's packs array is ignored with a validation warning.

The editor's Scene Packs tab (left dock) shows the closure and which reference pulled each pack in.

Shared art is referenced either as asset://shared/models/cube (pack explicit, greppable) or asset://./models/cube (catalog maps path → shared). Neither needs a scene-side declaration.

Editor live tree: the store opens with the cook plan as its catalog (packs.shinplan.json, read by BundleCatalog.loadPlan), so hasPack, packOf, packsForLabel and hi.host_api.packs() answer exactly as the product will. Retains are advisory there — nothing is mapped and no read is ever gated — but they are counted, so stat and the handle API behave the same in Play-in-editor as in a packed build. A tree that has not cooked yet has an empty catalog and stays fully readable.

Opening packs from game code

hi.host_api.packs() — open(stem), openLabel(label), close(handle), stat(stem), list, handlePacks. Handle-based so a retain is attributable and Play/Stop can drop what game code held. open makes assets resolvable; it is not a load — use the residency API or a scene load to wait for bytes. Delivery is invisible at this layer: a loose (moddable) pack and a packed one open, count and close identically. stat and list are two catalog lookups with no allocation (AssetIo.packRefs / packStems), so polling them per frame is fine.

Every scene load — sync, job, and the preload bridge — retains its closure through one function, scene_pack_closure.retainPacks, and every walk over a scene's references (closure, Scene Packs tab, tests) goes through scene/scene_asset_refs.zig. A reference added to an actor descriptor is added there and nowhere else.

One place resolves membership

configs/layout.json is authored; packs.shinplan.json is resolved. The Shinra cook matches the include globs against the cook tree once and writes the plan beside assets.shinmanifest.json. Kaji seals what the plan says; the editor reads it in Project Settings → Packs and in the Scene Packs tab. Neither matches a glob, so the partition the editor shows is the partition the product ships. Watch mode rewrites the plan when the cook tree changes and when layout.json is saved.

Implementation: assets/asset_ref.zig, assets/backends/shinra_io.zig, assets/bundle/catalog.zig, scene/scene_pack_closure.zig, game_api/host_bind/packs_api.zig, Shinra pipeline/pack_plan.rs, Kaji ProductResourcePacker + PackPlan.

Asset residency (refcount)

Shared CPU decode lives in AssetStore: retain/release, optional pin, immediate free at refs == 0 when unpinned. Materials, bound meshes, skybox/probes, audio, shaders, and scene loaders are the consumers.

Full overview (what is tracked, who holds what, refs → 0 policy, overlap/streaming, diagnostics): Asset residency.

Game and editor behavior

Launch matrix (first-class; no dual-read fallback)

One rule: the stage on disk is the product. Kaji (Shinra cook + Akari + seal) writes it; the runtime opens whatever layout is present. --run is optional — the same tree is what a manual launch or host package (.app) uses. --clean wipes stages; rebuild restages; open never invents a second cooked root at load time.

LaunchWho cooksAsset planeLayout
kaji editor --project=… (+ optional --run / --clean)Shinra watch (live)<project>/.engine/cache/shinraAlways loose. Engine ship shaders are staged into that tree after Kaji compiles them (and materialized from product install on cold project open).
Standalone editor opens a projectShinra watch (live)same live work treesame; product install supplies _engine/shaders into the live tree if missing
Editor project selector (no project)n/a (chrome only)product <exe>/datadata-only (_engine/shaders for device-init)
kaji game --project=… --resources=bundles (+ optional --run / --clean)Shinra batch + sealproduct <exe>/datadomain packs under data/bundles/ + product.shinbundle.map.json
kaji game … --resources=looseShinra batch + loose sealproduct <exe>/dataloose files under data/; no catalog map
kaji editor … (+ optional --package / --run)Shinra watch onlyproject .engine/cache/shinraNo product packs. --resources= / --data-layout= are rejected. Host .app packaging is orthogonal (--package=).
Game packaged as .app / zipsealed at package timeproduct data/ next to exesame as stage (bundles or loose)

Runtime selection is self-describing (runtime_session/assets.zig):

  1. loose_assets_root set → editor live (cooked tree only; project/content FS is a separate plane for authoring/content://, not a second look-up for cooked binaries).
  2. else data/bundles/product.shinbundle.map.json present → product packs.
  3. else → product loose (entire cooked tree under content_root = data/).

There is no “try packs then fall back to loose files for packed domains,” and no “read engine shaders from product data while the live tree is empty.” Missing engine metallibs in the live tree are materialized once into that tree from the product install, then opened as the single cooked root.

Editor watch layout (under the project .engine/ tree — do not commit):

ArgPath
--output / --cache-dir<project>/.engine/cache/shinra (editor live loose cook + staged engine shaders)
--shader-cache-dir<project>/.engine/cache/shaders (Akari / AIR intermediates)

All project-local engine artifacts live under <project>/.engine/ (clean cut — project root is source only: no data/ stage, no assets/.shinra, no cooked siblings):

PathRole
.engine/build/shinraOffline cook (kaji assets / product asset step)
.engine/staged/shinraIntermediate game pack for tooling
.engine/staged/datakaji assets product-data preview / seal (not for --run)
.engine/cache/shinraEditor live cooked plane (watch + engine ship shaders)
.engine/cache/shadersShader compile cache

Shipped / runnable product still stages under bin/…/data/ (runtime exe_dir/data), including packs in data/bundles/ when --resources=bundles.

Batch CLI runs still default shader cache to <output>/.shader-cache unless those flags are set.

Editor live protocol

Shinra watch mode speaks a line protocol on stdout (human logs on stderr). Handshake is @shinra hello v3:

LineMeaning
@shinra hello v3Protocol handshake (v3)
@shinra busy / @shinra idleCompile activity
@asset pending|available|rebuilt|failed|removed <key> [type=T] [size=N] [gen=N]Logical asset change (available is a byte-identical cache hit; type is stable AssetKind token; gen is process-monotonic; editors ignore stale gens)
@asset catalogManifest/catalog reshaped

The editor supervises the child over pipes, coalesces events per path (bounded, 8192 unique paths), defers apply while Play is active (@shinra busy is UI status only), and reloads the scene only when a terminal event references authored actors. Dropped queue notifications trigger one @cmd rebuild-all per overflow burst (debounced through busy→idle / catalog); daemon disconnect keeps soft-pending (child process_all on restart recovers). Stdin accepts @cmd quit, @cmd rebuild-all, and @cmd rebuild <source_rel> (also used for portable Windows shutdown via quit + stdin EOF).

Toolbar asset-pipeline pill hover opens a list tip (popup chrome): caption header, relative stem rows with trailing kind meta from AssetStore (Shinra type= / path suffix / explicit retain), capped with an overflow footer. That is the set the editor has observed via Shinra events or soft open misses — not Shinra’s full internal cook queue.

Invariant: every @asset pending is paired with a later available/rebuilt/failed/removed for that key (watch requeues mid-flight saves).

failed is source-scoped, never batch-scoped: Shinra must not fan one failed source out across unrelated cooked keys. A partial batch may therefore emit both rebuilt events for successful assets and failed for the bad source before returning a non-zero batch result. Shader packages additionally commit their native libraries atomically, so a rejected replacement cannot truncate the library that the running editor is using.

Authoring materials / model docs (.material.json → .shinmaterial, .model.json → .shinmodeldoc) are not plugin work items. On initial process_all (watch mode start, including kaji editor … --clean --run):

  1. They cook before the parallel model/texture pass so the live work tree has materials while the editor still binds the project scene (game-module compile splash often overlaps Shinra).
  2. Each successful cook emits a terminal event for both the authoring JSON key and the cooked binary: available on a byte-identical cache hit, otherwise rebuilt. Soft-miss / pink error stand-ins upgrade without a full scene reload.
  3. @asset catalog at end of the pass also triggers a soft rebind pass (notifyAssetsMayBeReady) so any residual stand-ins retry once the work tree is complete.

Graph-authored materials add an offline lowering step to that same material path. Shinra probes each .material.json for a com.hikari.material-graph payload before parsing it strictly — that ordering is what separates an ordinary hand-authored Akari material (skipped, correctly) from a graph this build cannot read (reported as a failure). It then validates node ids, slots, types, sampler-slot uniqueness and acyclicity, and writes generated Akari plus a target-specific transient spec under --shader-cache-dir/material-graphs/. Generated source is cache output, never project source; sources whose material was deleted or renamed are pruned on every pass. Batch builds merge the transient spec with the explicit project/engine specs.

Watch mode is scoped twice, and both matter for editor latency. A graph save that lowers to byte-identical Akari compiles nothing — every node drag is one of these, because positions live only in the authoring document and never reach the shader. When the lowering does differ, only the generated spec is recompiled: the engine and game specs cannot have changed, and compiling the merged set meant one compiler process per spec per debounced keystroke. Successful native libraries publish atomically; an invalid graph leaves the last-known-good library live and fails only that material source.

Soft refs and load/hot-reload recovery

Content references (mesh, material, texture, shader, script, archetype) are soft: a missing or failed asset must not crash the process or abort the whole scene. Authored paths stay in the scene document; runtime binds best-effort and recovers when the asset reappears.

Shader cooks follow the same rule per package. Shinra publishes every library Akari produced even when another package fails, and live watch retains the failed package's last-known-good library. Only the changed source is reported failed; unrelated shader-library keys are completed as rebuilt, so one bad material cannot invalidate the renderer's engine libraries. A clean cook has no previous binary for the failed package, so that package alone uses the normal soft fallback until fixed.

Shared runtime (editor preview world and packaged game use the same path): AssetStore states, soft material retain, soft mesh bind, soft script attach, soft archetype spawn, pink error shader, skybox color fallback.

Editor-only: Shinra watch / hot-reload apply, hierarchy/inspector health UI, game-module Recompile + archetype audit, deferred apply while Play is active.

Implementation: assets/ref_status.zig, ref_log.zig (once-per-issue logs), ref_consumers.zig, actor_health.zig, error_mesh.zig, material_cache.zig, scene/scene_loader.zig, editor/editor_app/hot_reload.zig.

Status model

AssetStore / ref statusMeaning
pendingIn-flight or soft miss — pipeline may still emit the file, bundle not retained, or first open hit UnknownAssetPath in loose/editor mode. Not an error. No warn log; mesh hidden until ready; texture slots empty with defaults.
readyOpened / decoded successfully.
failedTerminal: hard decode/parse or @asset failed. Sticky until markPending / rebuild / evict. Warn once; error cube / pink / hierarchy broken.
missingTerminal catalog absence — packaged UnknownAssetPath, or @asset removed. Loose/editor open misses stay pending until a terminal event. Warn once.

Editor startup: while Shinra is still processing the queue, absences are pending, not missing — console must not spam “texture/mesh missing”. Terminal only after @asset failed / removed or a hard decode failure.

state_generation bumps on state change so the editor can refresh health icons without thrashing every frame.

Cause → result (by asset kind)

Each row is cause → result. Viewport/runtime behavior is the same in editor and game unless noted.

Mesh (.shinmodel)

The mesh path cooks unique vertices plus meshlet topology. Import normalizes non-indexed source primitives to an index list, optimize_mesh welds identical attribute vertices and removes degenerate triangles, then SRM1 v5 writes vertices, 64-byte meshlet descriptors, remaps, and packed micro-triangles — no index buffer on disk. Hikari converts each unique vertex once into the engine layout, materializes a u32 index stream from the meshlets for BLAS and physics only, concatenates per-LOD slices, and uploads the meshlet topology used by every scene-mesh raster lane. Static CPU staging is released after the GPU snapshot unless the model explicitly sets allow_cpu_access; render residency never creates a triangle-corner copy.

CauseResult
Mesh still processing / not on disk yet (loose) / soft not-readyStore pending. Actor stays; geometry not bound; hidden. No warn log.
Hard decode fail, or @asset failed / removedStore failed. Magenta error cube; once warn. Hierarchy broken (editor).
Mesh rebuilt (@asset rebuilt) after failStore ready → consumers re-instantiated → real mesh; once-log cache cleared for path.
Hot reload of mesh with actor refsTerminal event + refs → full scene re-instantiate from document UTF-8 (edit view preserved).

Editor: hierarchy destructive only when mesh is failed (not while pending).
Game: same viewport; no hierarchy/inspector chrome.

Material (.material.json)

CauseResult
Material JSON missing, unreadable, or root unsetSoft retain installs pink engine stand-in (MaterialCache error entry, package _engine/error_material). Scene load continues.
Material JSON ok but project shader package unavailableRenderer loads pink error_material for that material (see shaders).
Material rebuilt / file restoredreloadForAssetChange swaps the live material; error stand-in dropped when the entry reloads. No full scene re-instantiate.
Hot reload of material with actor refsIn-place rebind under render-thread pause (same family as shaders).

Editor / game: same soft retain and pink stand-in.
Editor only: health banner + hierarchy warning when material is the error stand-in or path is missing.

Shader (compiled package / library)

CauseResult
Project shader not finished / file absent during loadPink error shader (Metal/D3D12 fallback). The stand-in preserves the authored G-buffer, direct-forward, or weighted-OIT attachment contract. Expected during pipeline churn.
Shader compile fail → @asset failedStore failed; materials reload in place and render pink. Once-log at err for the path.
Shader @asset removedSame as failed for dependents.
Engine package (_engine/…) fails to loadHard failure for that material load path (engine assets are required).
Shader rebuiltReady + in-place material/pipeline rebind.

Editor: pipeline events drive store + in-place material/pipeline rebind; console gets once-logs.
Game: no live shader recompile loop; missing packaged shader → pink at first material load.
Log noise: successful pink fallback for pending packages is often debug (see config section).

Texture (.shintexture)

CauseResult
Texture not processed yet / soft miss (pending)Slot null; pass defaults (white/normal/black). The material slot remains a residency-ticket requirement, including for actors spawned during Play. No warn log.
Texture hard fail or @asset failed / removedSlot null; once warn; materials stay otherwise ready.
Texture rebuiltLive rebind (MaterialCache.refreshTextureAsset) + residency update + GPU ObjectData.map_mask upload on next render sync — no full scene re-instantiate.
Skybox cubemap pending/failSolid skybox color (hot-reload safe).

Editor / game: same bind + fallback textures.
Editor: hierarchy broken only for failed slots, not pending in-flight.

Script (.kawa → .kawabc)

CauseResult
Bytecode not ready / soft missEntity spawns without script; soft attach skips.
Bytecode hard fail or load/run errorEntity stays scriptless; hard attach warns once (path-based).
Script rebuilt with actor refsScene re-instantiate → soft attach when bytecode available.

Editor / game: same soft attach.
Editor only: live Shinra recompiles scripts; health can show script pending/broken.

Zig archetype (game entity type)

CauseResult
Scene references archetype not in registrySpawn _missing host (transform + render, error cube, pink-ish material path). Scene continues.
Recompile removes a type still in the open sceneSoft placeholders for those actors; audit logs missing types after successful recompile.
Recompile build failsKeep previous dylib; no scene swap.
Recompile succeeds but register/load failsRestore previous game module + scene; panic only if restore itself fails.
Recompile drops or renames a script nativeGame-owned natives are forgotten with the module (clearGameNatives + trampoline clearSlots); the VM is rebuilt so no binding points into the old dylib.
Game left a job group open across the swapjob_groups.joinAll() waits for it before onTerminate; a non-zero count is logged as a game bug.
Old dylib stays mapped after close (Objective-C / TLS image, e.g. MetalFX plugin)Warned once per session (isImageResident probe). Each Recompile then leaks the module and stale pointers into it stay silent — treat "no crash after reload" as no evidence.
Editor quits mid-compileThe child zig build is killed (BuildCancel); quit does not wait for the compile.
Host/game ABI or Entity/World layout mismatchReject new dylib; keep previous; log “fully rebuild editor”.

Editor (dynamic): Recompile button + staged dylib + restore path. The compile and the swap each log their wall-clock time (game module gen-N built in … ms from the frontend, game module swap took … ms from the session under the game.module_swap profiler zone). Frame pacing drops to the idle cap while the compile runs so the editor does not compete with zig build for cores.
Editor (monolithic): no game-module hot reload.
Game: no recompile; unknown archetype at load still soft-spawns _missing if builtins registered (engine always registers core placeables empty / light / camera / visual_zone plus soft hosts _missing / _asset / _script / _audio).

Hot-reload pipeline edge cases (editor)

CauseResult
Initial process-all / file not written yetOpen → pending (not missing). Quiet; soft bind.
@asset pendingStore pending; log info with ref count; no scene re-instantiate yet; no missing/failed warn.
@asset rebuiltEvict that ReloadFamily + stem; mark ready. Texture / material / shader rebind in place. Mesh / model / script with authored actor refs → full scene re-instantiate from document UTF-8. Prefab / particle / animgraph / audio / font evict and rebind holders; they do not by themselves count as scene consumers.
@asset failed / removedMark failed; same rebind vs re-instantiate split as rebuilt; once-log err/warn; hierarchy/inspector broken via state_generation.
In-flight → failedPrior quiet pending; then terminal failed + warn + error cube / empty texture as above.
In-flight → rebuiltQuiet pending → ready; mesh/texture appear without ever logging “missing”.
Cook-cache hit → availableReady edge only; byte-identical resident assets are not evicted, decoded, or rebound.
Asset event while Play activeDeferred (editor_state asset_reload work); applied after Stop / when allowed.
Shinra busy + many eventsCoalesced per path; status bar shows queued / processing.
Dropped notifications (queue overflow)Warn with drop count; may miss until next rebuild-all / resave.
Texture / material / shader batchRebind under render-thread pause; no full scene thrash.
Mesh / model / script with authored actor refsSerialize document UTF-8 → hotReloadAssets → re-instantiate (edit camera preserved).
Catalog / disconnectAsset browser refresh; also retries soft-miss materials/meshes (notifyAssetsMayBeReady). No automatic full scene reload unless paths also fail.

Game: no Shinra watch daemon; assets are load-time only from bundle/loose config. No deferred Play queue.

Cold load vs hot reload

SituationResult
Startup scene with mixed good/broken refsScene loads; broken actors use soft placeholders; good actors normal.
Corrupt / invalid scene JSON schemaHard fail for that load op (not a soft content ref).
OOM during loadHard fail (unrecoverable setup).
Play → StopImmediate switch to the retained editor world. The disposable Play world is retired afterward in bounded slices; Stop does not decode or instantiate the authored scene again. See Session services — Play-world retirement.

Editor vs game (summary)

ConcernEditorPackaged game
Asset sourceLoose processed tree + live ShinraPackaged bundles / staged data (typical)
Live reprocessYes (@asset …)No
Hierarchy / inspector healthYes (destructive styling + issue banner)No chrome
Soft mesh / material / texture / script / archetypeYesYes (same engine code)
Pink error shader / error cubeYesYes
Game Zig recompileDynamic editor onlyN/A
Apply while simulatingAsset reload deferred in PlayN/A

Debug vs release config

Soft behavior (placeholders, continue scene, no crash on content) is the same in debug and release builds.

ConcernDebug (--config=debug)Release (--config=release)
Soft bind / pink / error cubeSameSame
Log level (frontends)Typically debug → pending/pipeline chatter and pink-fallback debug lines visibleTypically info+ → once warn/err still visible; many pending/debug lines suppressed
Once-log for failed/removedwarn/errwarn/err
Shinra tool buildRelease by default even under product debug (--shinra:config=debug to override)Same default
Shader compile (Akari)Debug by default (--shaders:config=release for opt + no symbols); independent of product --configSame
Leak-check smoke--memory-leak-check + --run useful after hot reload / Play→StopOptional CI; not product default

There is no separate “strict fail content in release” mode: shipping builds still soft-bind so a bad asset does not take down the process. Fix content and rebuild packages for clean shipping data.

Recovery cheatsheet

After…Recovery
Asset reappears / rebuildsReady event → rebind or re-instantiate → clear once-log for path
Material/shader fixedReload consumers (hot reload or Stop/Play or scene reload)
Archetype restored in game codeRecompile (editor) or restart game with new binary
Sticky failed without eventRebuild asset / rebuild-all / re-open project so store gets markPending/ready

Asset authoring rules

  • Keep raw assets and sidecars in the game package; do not write generated assets into engine source folders.
  • Treat Shinra output and bundles as generated artifacts.
  • Keep platform-specific compression in Shinra configuration, not in runtime asset consumers.
  • Validate output after format or bundle changes.
  • Use the engine's checked-in shader packages for required renderer passes; add game packages only for game materials/effects.
PreviousTemporal KitNext Prefabs

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/assets-and-shinra.md
On this pageSource and runtime assetsAsset pipeline (Shinra tool)Common command shapeScene documents (JSON + HSC1)Prefab documents (JSON + HSC1)Asset URIs and packsResolve rules (packs mode)Scenes do not declare packsOpening packs from game codeOne place resolves membershipAsset residency (refcount)Game and editor behaviorLaunch matrix (first-class; no dual-read fallback)Editor live protocolSoft refs and load/hot-reload recoveryStatus modelCause → result (by asset kind)Hot-reload pipeline edge cases (editor)Cold load vs hot reloadEditor vs game (summary)Debug vs release configRecovery cheatsheetAsset authoring rules Back to top