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/:
| Source | Processed 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/spec | platform 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:
- Game / engine — only
AssetStore/AssetRef/ scene_document (no packaging knowledge). Load isretain*/release*/pinPath;has*is a bool probe, never a borrowed pointer. - Asset facade —
AssetStorecaches and decodes; all raw IO goes throughassets/io.AssetIo. - Backend — default
assets/backends/shinra_io.zig(comptime) implements packs + loose/data trees. Wire formats live inassets/formats/;.shinbundleopen lives underassets/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:
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| Target | Texture compression |
|---|---|
windows-d3d12 | BC7 |
macos-metal | ASTC 6x6 |
linux-vulkan | BC7 |
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
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 shinsceneThe 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/*.jsonwithkind: 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/workoutput/scenes/*.shinscene(HSC1), same as other cooked assets — not next to authoring JSON. Product stages intodata/scenes/(then seal may pack). Watch mode re-cooks into the cache/work tree. - Runtime load: only via AssetStore (
scene_document.loadFromStore): HSC1.shinscenein non-Debug builds, with authoring.jsonfallback 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.jsonwithkind: com.hikari.prefab(editor always has UTF-8). - Cook: Shinra
--prefabs <project>/assets/prefabs(Kaji when the directory exists). Emits only under cook/workoutput/prefabs/**/*.shinprefab(same HSC1 wire as scenes; envelope validates prefab fields). Product stages intodata/prefabs/. Watch re-cooks into the cache/work tree. - Runtime load: AssetStore
retainBlobKind(…, .prefab)prefers.shinprefab;World.retainPrefabdecodes HSC1 without JSON. Authoring.prefab.jsononly 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:
| Form | Meaning | Pack selection |
|---|---|---|
asset://<pack>/<logical/path> | Absolute pack + path | Pack is in the URI |
asset://./<logical/path> | Unscoped absolute path under product data | Product 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)
- Explicit
asset://shared/models/cube→ use packsharedif retained, else fail (PackNotRetained). - Unscoped
asset://./models/cube→ look upmodels/cubein the product catalog (one path → one pack at seal). That pack must be retained, else fail. Does not search across open packs. - Uncatalogued path inside a
loosepack'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. - Anything else uncatalogued (content dirs, always-loose leftovers) resolves from the data plane as before.
- Seal dual-home is a build error: one logical path, one pack. Valid catalogs never need "first retained pack wins".
| Job | Mechanism |
|---|---|
| Which packs are open | Session system packs + each scene's derived closure + hi.host_api.packs() |
| Which pack owns a path | Explicit URI pack, or product catalog for ./ |
| What goes in a pack | configs/layout.json, resolved by the cook into packs.shinplan.json |
| What a retain opens | The 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:
closure(scene) = { packOf(ref) : ref ∈ scene refs } then + deps of each, transitivelyscene/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
packsarray 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.
| Launch | Who cooks | Asset plane | Layout |
|---|---|---|---|
kaji editor --project=… (+ optional --run / --clean) | Shinra watch (live) | <project>/.engine/cache/shinra | Always 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 project | Shinra watch (live) | same live work tree | same; product install supplies _engine/shaders into the live tree if missing |
| Editor project selector (no project) | n/a (chrome only) | product <exe>/data | data-only (_engine/shaders for device-init) |
kaji game --project=… --resources=bundles (+ optional --run / --clean) | Shinra batch + seal | product <exe>/data | domain packs under data/bundles/ + product.shinbundle.map.json |
kaji game … --resources=loose | Shinra batch + loose seal | product <exe>/data | loose files under data/; no catalog map |
kaji editor … (+ optional --package / --run) | Shinra watch only | project .engine/cache/shinra | No product packs. --resources= / --data-layout= are rejected. Host .app packaging is orthogonal (--package=). |
Game packaged as .app / zip | sealed at package time | product data/ next to exe | same as stage (bundles or loose) |
Runtime selection is self-describing (runtime_session/assets.zig):
loose_assets_rootset → editor live (cooked tree only; project/content FS is a separate plane for authoring/content://, not a second look-up for cooked binaries).- else
data/bundles/product.shinbundle.map.jsonpresent → product packs. - 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):
| Arg | Path |
|---|---|
--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):
| Path | Role |
|---|---|
.engine/build/shinra | Offline cook (kaji assets / product asset step) |
.engine/staged/shinra | Intermediate game pack for tooling |
.engine/staged/data | kaji assets product-data preview / seal (not for --run) |
.engine/cache/shinra | Editor live cooked plane (watch + engine ship shaders) |
.engine/cache/shaders | Shader 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:
| Line | Meaning |
|---|---|
@shinra hello v3 | Protocol handshake (v3) |
@shinra busy / @shinra idle | Compile 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 catalog | Manifest/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):
- 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).
- Each successful cook emits a terminal event for both the authoring JSON key and the cooked binary:
availableon a byte-identical cache hit, otherwiserebuilt. Soft-miss / pink error stand-ins upgrade without a full scene reload. @asset catalogat 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 status | Meaning |
|---|---|
pending | In-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. |
ready | Opened / decoded successfully. |
failed | Terminal: hard decode/parse or @asset failed. Sticky until markPending / rebuild / evict. Warn once; error cube / pink / hierarchy broken. |
missing | Terminal 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.
| Cause | Result |
|---|---|
| Mesh still processing / not on disk yet (loose) / soft not-ready | Store pending. Actor stays; geometry not bound; hidden. No warn log. |
Hard decode fail, or @asset failed / removed | Store failed. Magenta error cube; once warn. Hierarchy broken (editor). |
Mesh rebuilt (@asset rebuilt) after fail | Store ready → consumers re-instantiated → real mesh; once-log cache cleared for path. |
| Hot reload of mesh with actor refs | Terminal 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)
| Cause | Result |
|---|---|
| Material JSON missing, unreadable, or root unset | Soft retain installs pink engine stand-in (MaterialCache error entry, package _engine/error_material). Scene load continues. |
| Material JSON ok but project shader package unavailable | Renderer loads pink error_material for that material (see shaders). |
| Material rebuilt / file restored | reloadForAssetChange swaps the live material; error stand-in dropped when the entry reloads. No full scene re-instantiate. |
| Hot reload of material with actor refs | In-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)
| Cause | Result |
|---|---|
| Project shader not finished / file absent during load | Pink 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 failed | Store failed; materials reload in place and render pink. Once-log at err for the path. |
Shader @asset removed | Same as failed for dependents. |
Engine package (_engine/…) fails to load | Hard failure for that material load path (engine assets are required). |
| Shader rebuilt | Ready + 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)
| Cause | Result |
|---|---|
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 / removed | Slot null; once warn; materials stay otherwise ready. |
| Texture rebuilt | Live rebind (MaterialCache.refreshTextureAsset) + residency update + GPU ObjectData.map_mask upload on next render sync — no full scene re-instantiate. |
| Skybox cubemap pending/fail | Solid 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)
| Cause | Result |
|---|---|
| Bytecode not ready / soft miss | Entity spawns without script; soft attach skips. |
| Bytecode hard fail or load/run error | Entity stays scriptless; hard attach warns once (path-based). |
| Script rebuilt with actor refs | Scene 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)
| Cause | Result |
|---|---|
| Scene references archetype not in registry | Spawn _missing host (transform + render, error cube, pink-ish material path). Scene continues. |
| Recompile removes a type still in the open scene | Soft placeholders for those actors; audit logs missing types after successful recompile. |
| Recompile build fails | Keep previous dylib; no scene swap. |
| Recompile succeeds but register/load fails | Restore previous game module + scene; panic only if restore itself fails. |
| Recompile drops or renames a script native | Game-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 swap | job_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-compile | The child zig build is killed (BuildCancel); quit does not wait for the compile. |
| Host/game ABI or Entity/World layout mismatch | Reject 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)
| Cause | Result |
|---|---|
| Initial process-all / file not written yet | Open → pending (not missing). Quiet; soft bind. |
@asset pending | Store pending; log info with ref count; no scene re-instantiate yet; no missing/failed warn. |
@asset rebuilt | Evict 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 / removed | Mark failed; same rebind vs re-instantiate split as rebuilt; once-log err/warn; hierarchy/inspector broken via state_generation. |
| In-flight → failed | Prior quiet pending; then terminal failed + warn + error cube / empty texture as above. |
| In-flight → rebuilt | Quiet pending → ready; mesh/texture appear without ever logging “missing”. |
| Cook-cache hit → available | Ready edge only; byte-identical resident assets are not evicted, decoded, or rebound. |
| Asset event while Play active | Deferred (editor_state asset_reload work); applied after Stop / when allowed. |
| Shinra busy + many events | Coalesced 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 batch | Rebind under render-thread pause; no full scene thrash. |
| Mesh / model / script with authored actor refs | Serialize document UTF-8 → hotReloadAssets → re-instantiate (edit camera preserved). |
| Catalog / disconnect | Asset 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
| Situation | Result |
|---|---|
| Startup scene with mixed good/broken refs | Scene loads; broken actors use soft placeholders; good actors normal. |
| Corrupt / invalid scene JSON schema | Hard fail for that load op (not a soft content ref). |
| OOM during load | Hard fail (unrecoverable setup). |
| Play → Stop | Immediate 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)
| Concern | Editor | Packaged game |
|---|---|---|
| Asset source | Loose processed tree + live Shinra | Packaged bundles / staged data (typical) |
| Live reprocess | Yes (@asset …) | No |
| Hierarchy / inspector health | Yes (destructive styling + issue banner) | No chrome |
| Soft mesh / material / texture / script / archetype | Yes | Yes (same engine code) |
| Pink error shader / error cube | Yes | Yes |
| Game Zig recompile | Dynamic editor only | N/A |
| Apply while simulating | Asset reload deferred in Play | N/A |
Debug vs release config
Soft behavior (placeholders, continue scene, no crash on content) is the same in debug and release builds.
| Concern | Debug (--config=debug) | Release (--config=release) |
|---|---|---|
| Soft bind / pink / error cube | Same | Same |
| Log level (frontends) | Typically debug → pending/pipeline chatter and pink-fallback debug lines visible | Typically info+ → once warn/err still visible; many pending/debug lines suppressed |
| Once-log for failed/removed | warn/err | warn/err |
| Shinra tool build | Release 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 --config | Same |
| Leak-check smoke | --memory-leak-check + --run useful after hot reload / Play→Stop | Optional 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 / rebuilds | Ready event → rebind or re-instantiate → clear once-log for path |
| Material/shader fixed | Reload consumers (hot reload or Stop/Play or scene reload) |
| Archetype restored in game code | Recompile (editor) or restart game with new binary |
Sticky failed without event | Rebuild 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.