Skip to content
hikari
RenderingEditorAIToolchainDocumentation
Download

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsDownloadLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
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 governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
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 governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Systems31 min read

Assets and Shinra

On this page
On this pageSource and runtime assetsAsset pipeline (Shinra tool)Common command shapeScene documentsPrefab documentsAsset URIs and packsResolve rules (packs mode)Scenes do not declare packsOpening packs from game codeCooked dependency graphOne place resolves membershipAsset residency (refcount)Game and editor behaviorLaunch matrix (first-class; no dual-read fallback)Editor live protocolCompile requests (protocol v6)Watch schedulingSoft 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 rulesCPU-readable texture imports 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 hikari-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=<path>)
.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. The vtable returns an ABI-stable Status byte with out-params, never a Zig error union: the store is compiled into every backend dylib while the host implements the table, and error values are numbered per compilation. Implementers register through asset_io.adapt.* and keep writing Error! functions.
  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 editor/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 on PATH when invoking Shinra directly without an override.

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 in the Cargo child environment 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=../hikari-games/example --shinra:config=debug
# equivalent:
bin/kaji/kaji editor --project=../hikari-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 (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 (v4, 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 pack layout.

Scene documents

  • Authoring: any .json under assets/ whose top-level kind is com.hikari.scene (editor always has UTF-8). No folder is special — assets/scenes/main.json and assets/levels/act1/intro.json are both scenes, told apart from other documents by their format, never by location. The same rule classifies every authored document (.material.json, .prefab.json, …): one suffix table plus the kind tag for bare .json.
  • Cook: Shinra owns it as part of the ordinary authoring pass over --input. Emits <same relative path>.shinscene (the document as a schema-neutral value tree in the cooked container) under the cook/work output only — not next to authoring JSON. Kaji stages every cooked scene to the same relative path under data/ (then seal may pack). Watch mode re-cooks into the cache/work tree. A scene whose root partition is enabled additionally emits <stem>/cells/cell_<x>_<y>_<z>.shinscene per occupied cell and sweeps cell layers it no longer produces (removed events) — see World partition. The sweep removes only generated cell files and retains directories shared with concurrent material, model and other asset cooks.
  • Runtime load: only via AssetStore (scene_document.loadFromStore), only the cooked .shinscene, in every build. Loose / data_root / bundle — store decides. The editor opens its JSON documents itself and hands the decoded scene to the session.

Prefab documents

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

  • Authoring: assets/prefabs/**/*.prefab.json with kind: com.hikari.prefab (editor always has UTF-8).
  • Cook: part of Shinra's ordinary authoring pass over --input, like scenes: any *.prefab.json (or bare .json with kind: com.hikari.prefab) anywhere under assets/ cooks to <same relative path>.shinprefab (same scheme as scenes; envelope validates prefab fields) under cook/work only. Kaji stages it to the same relative path under data/. Watch re-cooks into the cache/work tree.
  • Runtime load: AssetStore, cooked .shinprefab only; World.retainPrefab decodes it without JSON. The editor reads authoring prefabs through its own PathLoader.

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 → AssetStore acquires pack shared automatically for the read.
  2. Unscoped asset://./models/cube → look up models/cube in the product catalog (one path → one pack at seal). AssetStore acquires that pack automatically for the read. Does not search across open packs.
  3. Uncatalogued path inside a loose pack's root → acquires the owning loose pack and resolves from disk. 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, scene/request dependency closures, automatic reads, and explicit hi.packs() holds
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) = packs(transitive asset dependencies(scene refs)) + explicit pack deps

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.
  • Runtime scenes and prefabs are cooked documents. Added content needs cooked artifacts and dependency metadata; the product never parses authoring JSON.
  • assets.shingraph supplies transitive, kind-specific dependencies before payload packs open. Model documents, materials, textures, prefabs, shaders and typed component references can live in different packs. Explicit pack deps remain available for content selected by code.
  • 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.

Scene jobs first dispatch cooked document IO and prefab expansion, then prepare the partition base and collect references from shared document storage. Preparation decodes component references once; pack discovery and sliced residency planning reuse that result. Cancelled document and decode workers retain their own storage until completion, so supersession and cell departures never join them on the tick thread. Detached decode batches and material descriptions use the asset store's heap, which survives Play retirement. Adoption transfers descriptions into the material cache's heap without parsing again. A failed submission stays queued for a later slice; only explicit callers without dispatch use synchronous preload.

Each decoded cache identity has one active decoder. Concurrent followers return pending and retry after publication; workers never wait on another decoder. Logical pack admission and mapping lifetime are separate: closing the last pack hold stops new reads, while decoded leases preserve existing bytes. A later open reuses that mapping until its final lease disappears. Integrity failures report IO failure, allocation failures report OOM, and only an absent asset reports an unknown path.

Editor live tree: the store opens with the cook plan as its catalog (packs.shinplan.json, parsed by BundleCatalog.parsePlan), so hasPack, packOf, packsForLabel and hi.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. A pack retained before the plan names it is held empty and is counted, with its dependencies, when a later cook's plan does — so its release always pairs with its own retain. Cook batches that leave the plan bytes unchanged do not reload it.

Opening packs from game code

hi.packs() — open(stem), openLabel(label), state(handle), close(handle), stat(stem), list, handlePacks. Handle-based so a retain is attributable and Play/Stop can drop what game code held. open registers an explicit storage hold and returns immediately. Cold mappings and dependency opens run on workers; state(handle) reports pending, ready, failed, or closed. An already-open root completes without disk work. Close cancels a pending handle without waiting and balances only successful retains. Label handles own a dynamically sized stem list. This is storage acquisition; use asset requests to load decoded payloads. AssetStore reads and SDK residency requests acquire storage automatically. Use explicit pack handles for grouping or longer storage lifetimes. 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.

Cooked dependency graph

Shinra publishes assets.shingraph alongside the manifest and pack plan. Kaji stages it in both loose and bundled products and records it as a required build output. The binary format is defined by the engine’s cooked_layout; reference descriptors are rendered for Shinra from the shared engine rules. Nodes use full cooked paths so a mesh, model document, and collision payload at one stem remain distinct. Edges carry source-field provenance.

Scene and prefab documents participate in manifest discovery, including generated scenes. Partition base-to-cell edges are deferred: inspecting the base reveals them, but loading it does not acquire every cell. Each admitted cell resolves its own asset closure. Scene jobs resolve and open dependency packs on workers; abandoned preparation owns its references and drains without borrowing a destroyed World.

Graph snapshots are immutable and retained across publication. Graph walks run outside the AssetStore lock. Pack closures grow dynamically and pin their dependency membership until the final root hold is released; a live layout edit cannot make a release decrement a different set. Public pack names remain stable for the store lifetime. Decoded byte leases protect mappings independently of logical pack holds.

The editor’s Asset References window defaults to the cooked graph. Turn off Cooked for the authored-document view. Both Uses and Used by include transitive navigation; tooltips show provenance, deferred cell edges, pack placement, cooked bytes and current CPU holds. The graph rebuilds when the cooked revision changes, even if the asset count is unchanged. The canvas reports truncation at its display limit; the underlying graph and runtime closure remain complete. Scene Packs uses the same transitive closure. The selection rail summarizes the complete graph independently of canvas limits, deduplicating shared dependencies and excluding deferred cells by default. Pack/missing/deferred/cross-pack filters preserve connecting paths. Cooked-byte totals describe stored content, not CPU/GPU residency; missing targets make them lower bounds. Queries refresh on index or selection changes, rather than traversing the graph each frame.

For an offline export, run shinra graph --root=<cook-tree-or-product-data> --output=<report.json>. The report includes unresolved targets for diagnosis; unresolved optional content is not silently erased.

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/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.

Kaji seeds the editor's .engine/cache/shaders/<config> from the engine build's Akari cache so the startup recompile is all hits. Only the content-addressed artifacts are portable. Clang's module cache (clang-module-cache/) records the absolute path it was built at, so the seed skips it. Akari also discards any module cache whose clang-module-cache.origin names another location, which covers copied or moved projects too.

Editor live protocol

Shinra watch mode speaks a line protocol on stdout (human logs on stderr). Handshake is @shinra hello v6; the editor refuses any other version:

LineMeaning
@shinra hello v6Protocol handshake (v6)
@shinra busy / @shinra idleCompile activity
@asset pending|available|rebuilt|failed|removed <key> [type=T] [size=N] [gen=N] [hash=H]Logical asset change (available is a byte-identical cache hit; type is stable AssetKind token; gen is process-monotonic; editors ignore stale gens; hash is the output's content identity)
@asset catalogManifest/catalog reshaped
@compiled <id> ok|error|busyAnswer to a compile request

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, @cmd rebuild <source_rel> (also used for portable Windows shutdown via quit + stdin EOF), and @cmd compile (below).

Compile requests (protocol v6)

The editor has no compiler of its own. A surface sends its current text, saved or not, with a JSON control frame:

text
@cmd compile {"id":42,"text_path":"/absolute/live-compile/42.json","source_rel":"ui/hud.ui.json"}

JSON escaping preserves whitespace and control characters in paths. Control frames are capped at 16 KiB and source text at 16 MiB. The editor becomes connected only after a matching hello. Outbound commands use a bounded queue and a separate pipe writer; commands from a previous child generation are discarded.

Shinra admits at most 32 preview requests across two lanes: ordinary documents and particles requiring native shader compilation. The document lane identifies and forwards particles without waiting for Akari. Native compiles share a process-wide permit with background shader work; waiting interactive requests get priority, with a background turn after eight foreground admissions. Running native compiles are not preempted to admit a preview.

Replies are @compiled <id> ok|error|busy. Successful bytes are published atomically at <text_path>.cooked; errors go to <text_path>.error. Busy replies have no payload and tell live surfaces to retain and retry their latest text. @cmd cancel <id> cancels queued work and running native subprocesses. A 20-second preview deadline includes queue wait; cancellation and deadline diagnostics use the ordinary error reply. Pure document compilers check cancellation before and after compilation. The client retires an unanswered ticket after 30 seconds and removes late reply files.

Diagnostics have a heading followed by one indented <path>: <message> line per problem (root/<id>.<prop>, state/<id>, transition/<from>/<to>, or node/<id>). editor/live_compile.zig retains the latest submitted text through queue saturation and reconnects. MCP asset edits defer their response while compiling, then recheck the project, source ETag and unsaved editor state before writing. Offline, timeout and busy results refuse the edit; no compiler wait runs on the editor main thread. Scenes compile only on save because they produce partition layers.

Watch scheduling

The filesystem watcher and control/preview services start before the initial cook; source events received during that cook are coalesced by path and processed afterward. This keeps startup edits without allowing initial and incremental cooks to write the same outputs concurrently. Rebuild-all commands set a scan flag rather than expanding the tree on the stdin reader.

Incremental work uses a deduplicated source scheduler. Changes outrank background rebuild jobs, with one background admission after eight changed jobs to prevent starvation. A save during an active cook marks that source dirty; completion requeues it directly in scheduler state and never blocks on a full worker channel. Deletions use the same scheduler, so an active cook finishes before its outputs are removed. Removal reads the source stamp to drop every companion and partition cell, invalidates the stamp, publishes removed, and refreshes the catalog. Directory removals also requeue active descendants.

Initial and incremental plugin work and catalog scans share a private Rayon pool, including nested texture encoding. The default watch budget reserves one logical CPU for the editor (minimum one worker); an explicit thread limit overrides it. The pinned ASTC wrapper invokes one native encoder index, so each context allocates one worker's scratch and asset-level concurrency comes from Rayon.

Model dependencies are indexed once at startup, using current stamp inputs where possible. A model edit replaces its dependency entries; texture, buffer and material-library events look up their owners without scanning the source tree. Shared dependencies retain all owners, and textures released by a model return to standalone cooking.

Cook-cache hits require the same target platform, cook flags, current layout and input stats. Import sidecars are checked against the asset-meta schema, including duplicate and unknown keys, before metadata is deserialized.

Asset results are published immediately. A separate catalog worker coalesces manifest/pack-plan regeneration with a 100 ms quiet period and a 500 ms maximum batching window; updates arriving during a scan trigger another pass. Manifest publication is atomic. Shutdown joins cook, preview and catalog workers and flushes the publication ledger.

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.

Every authored document cooks through one path. Batch, watch and the model cook all call the same companion cook: the file is read once, an object that repeats a key is refused (serde would silently keep the last copy), the document is checked against its rendered schema, and the checked value goes to the kind's compiler, which returns bytes. That compiler is where the real work lives: resolving names to indices, flattening graphs, lowering particle modules to IR, and packing fixed tables so the runtime reads them without allocating or validating. The output write is content-compared and atomic, so an unchanged recook leaves the file and its mtime alone and the engine never reads a half-written file. Format modules do not read or write authoring files themselves.

Model import preserves hand-authored model documents and their bindings. Generated and hand-authored model documents use the same companion compiler; part transparency follows the cooked material at load. Existing binaries never suppress an authored-document recook.

The runtime never falls back from a cooked file to its authoring document: every kind is read from its cooked form only, and a recook in flight simply means the previous cooked bytes are still what the store serves.

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 checks material documents against their rendered schema and lowers those carrying a com.hikari.material-graph payload. Source errors are accumulated independently, so valid graphs and unrelated assets still cook. 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, editor/actor_health/diagnostics.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 (either host linkage mode): 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. 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 recompileBoth host linkage modesN/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; override with --shaders:config=releaseRelease by default; override with --shaders:config=debug
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.

CPU-readable texture imports

Texture sidecars accept texture.uncompressed: true to retain lossless RGBA8 in the cooked .shintexture on every target. Compression remains enabled by default. For native/software cursor images, also disable mip generation and premultiplied alpha; see Game cursors. The authored PNG is the only image source; cook output and thumbnails remain pipeline-owned.

PreviousUI documentsNext Prefabs

Documentation follows the current engine checkout.

Snapshot 86f12d68Source docs/systems/assets-and-shinra.md
On this pageSource and runtime assetsAsset pipeline (Shinra tool)Common command shapeScene documentsPrefab documentsAsset URIs and packsResolve rules (packs mode)Scenes do not declare packsOpening packs from game codeCooked dependency graphOne place resolves membershipAsset residency (refcount)Game and editor behaviorLaunch matrix (first-class; no dual-read fallback)Editor live protocolCompile requests (protocol v6)Watch schedulingSoft 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 rulesCPU-readable texture imports Back to top