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
Systems6 min read

Editor asset hot reload

On this page
On this pageDaemon-to-editor event queueEditor tick processingRuntime hot reloadRender-thread synchronizationScene reload behaviorFirst cook is not a changeInvariants for maintainers Back to top

The editor implements live visual asset swapping through a thread-safe, daemon-to-edit event queue and a render-thread-paused re-instantiation path. This page describes the current flow and the invariants that keep it safe.

The queue separates background cooking from world mutation. GPU-visible changes happen only inside the frame-boundary pause:

Diagram
Diagram source
sequenceDiagram
    participant D as Shinra
    participant E as Editor / session
    participant R as Renderer
    D->>E: Queue asset events
    Note over E: Drain in Edit only<br/>Play keeps events queued
    E->>R: Pause at frame boundary
    R-->>E: Paused
    Note over E: Release holds → evict<br/>Rebind → flush GPU releases
    Note over E: Reload document snapshot<br/>only if reconstruction is needed
    E->>R: Resume

Daemon-to-editor event queue

  • assets/asset_pipeline_daemon.zig runs Shinra in watch mode and emits Event values: pending, available, rebuilt, failed, removed, catalog, disconnect.
  • editor/editor_app/app.zig owns the asset_pipeline_daemon and receives events through onAssetPipelineEvent, which pushes them into EditorApp.asset_events.
  • editor/editor_app/asset_events.zig defines the Queue: path-coalesced, capacity-bounded (max_paths = 8192), and generation-aware. It keeps the last terminal state per path and ignores stale generations. catalog and disconnect are flags, not per-path events.
  • Pipeline callbacks wake the durable main-thread bus so backgrounded editors can drain events. During Play, reload work is deferred: the durable pump parks the non-empty queue without re-waking, and the editor-state coordinator releases it when the mode returns to Edit.

Editor tick processing

  • editor/editor_app/hot_reload.zig:processPending runs each edit tick when editor_state.allows(.asset_reload).
  • It drains asset_events into asset_event_drain using Queue.drainInto.
  • If events were dropped, it requests a rebuild-all from the daemon once per overflow burst (debounced until the daemon has gone busy→idle, or a catalog/disconnect arrives). Stacking rebuild-all while the queue is still overflowing re-floods Shinra.
  • For each per-path event it:
    • computes affectedEntityCount by matching authored actor script, model, mesh, and material fields;
    • classifies the path as texture / material / shader / mesh / script;
    • decides whether a full scene reload is needed (requiresSceneReload is false only for texture / material / shader);
    • invalidates the editor thumbnail cache for that path;
    • tracks .shinparticle rebuilds for preview rebind;
    • builds a Session.AssetChange list.
  • When a scene reload is required and the boot game-module compile has finished, it serializes SceneDocument to UTF-8.
  • It calls session.hotReloadAssets(applied, scene).

Runtime hot reload

  • runtime_session/scene_ops/hot_reload.zig:hotReloadAssets is edit-mode-only.
  • It pauses the render thread with session.render_thread.pauseAtFrameBoundary() and resumes it in a defer with resumeFromFrameBoundary().
  • For each change:
    • it deduplicates by asset_deps.ReloadFamily + stem (texture / model / audio / font / blob);
    • stops audio streamers before eviction for .audio families;
    • calls forceEvictFamily(world, store, stem, family), which abandons world holders (skybox, reflection-probe cubemaps, visual-zone LUTs, bound meshes, animation skeleton/clip binds, material texture slots; marks animation-graph actors stale) then calls store.evictFamilyForReload(stem, family). Abandoning releases each hold rather than nulling it: eviction defers to the last borrower, so a hold dropped without its release strands the entry reload_pending and the reload never arrives;
    • if the store returns a GPU texture handle, enqueues RenderCommand.release_texture on world.render_command_queue;
    • updates the AssetStore state to pending, failed, or ready;
    • refreshes texture bindings in world.materials for texture assets;
    • reloads materials/shaders via world.materials.reloadForAssetChange and patches primitive references;
    • invalidates compiled Kawa scripts for .kawa/.kawabc changes;
    • re-arms looping audio beds for .audio changes.
  • After all changes it drains pending texture frees, flushes the render command queue, and (if a scene snapshot was provided) reloads the scene from UTF-8 with loadSceneFromUtf8(session, scene, .preserve).

Render-thread synchronization

  • graphics/renderer/render_thread.zig implements pauseAtFrameBoundary/resumeFromFrameBoundary using pause_requested and paused flags protected by the existing mutex/cond.
  • graphics/renderer/render_command_queue.zig has a release_texture: RenderHandle command variant.
  • graphics/renderer/renderer_shared/commands.zig:applyRenderCommands drains release_texture via resources.deferRemoveTexture(handle), which ages the GPU resource before freeing.

Scene reload behavior

  • Full scene re-instantiation runs only when requiresSceneReload is true and affectedEntityCount > 0 (authored script / model / mesh / material, or a material that references a changed shader). The SceneDocument is serialized to UTF-8 and reloaded through runtime_session/scene_ops/load.zig:loadSceneFromUtf8 with EditViewSeed.preserve.
  • loadSceneFromUtf8 → instantiateScene starts a replace SceneLoadJob and drains it (live-A swap when the world is occupied; world_precleared when empty). World.unloadScene / unloadSceneForSwap do not clear render_command_queue, so release_primitive commands emitted during entity teardown are preserved and drained before the new scene renders.
  • Texture, material, and shader changes rebind in place. Prefab / particle / animgraph / audio / font evict and rebind holders but do not increment affectedEntityCount, so they do not by themselves serialize the document.

First cook is not a change

A cold start cooks the whole project while the scene is already open. Every .rebuilt event for a model with live references would ask for a whole-scene re-instantiate; those requests coalesce while the daemon is busy (scene_reload_deferred) into one rebuild as the cooker goes quiet, and that rebuild would tear down and rebuild an identical scene (visible as the scene appearing piece by piece twice). It is redundant because actors waiting on an uncooked model are soft-pending and the residency wake binds the model in place when it lands (render.pending_model → bindModelParts), so firstCookBindsInPlace suppresses the reload request for a model going .pending → .rebuilt.

The narrowing is deliberately tight, because "binds in place" is a property of the consumer rather than of the event:

CaseReload?Why
mesh, .pending → .rebuiltnoactors are soft-pending; the wake binds it
mesh, .ready → .rebuiltyesgenuine change, and nothing is soft-pending to rebind
mesh, .failed → .rebuiltyesthe failure already resolved to an error mesh and cleared the pending flag
script / prefab, anyyesno in-place late-bind path exists

Authoring JSON mirrors (*.material.json, *.model.json, …) publish as available, not rebuilt: the cooked binary is the runtime change, and treating the JSON copy as a change would evict and re-parse the same material twice per cook.

Fog-volume materials parse through loadDesc, which pins the blob. Five lanterns sharing lantern_glow decode once. They cannot MaterialCache.retain — that path rejects non-surface domains and would install a pink stand-in.

Engine shader acquire pins the blob after the first decode, so PSOs sharing one metallib do not re-read it on every acquire/release.

Cook progress logs one line per source (relative path, duration). Extracted companions with no actor field still hot-reload; they log at debug, with an info summary when a batch has eight or more.

Invariants for maintainers

  • Route asset change notifications through the path-coalesced queue; do not call hotReloadAssets directly from daemon threads.
  • A durable pump may re-wake only for work that is runnable now. Play-deferred asset events stay queued until the Edit transition publishes work_ready; treating a merely non-empty queue as runnable can starve the native message loop.
  • Pause the render thread at a frame boundary before evicting or reloading assets; resume only after flushing the render command queue.
  • Preserve the render command queue across scene unload/load so GPU release commands are not lost.
  • Force-evict by ReloadFamily only; a same stem with a different kind (for example, texture vs model) must not be wiped by a single event.
  • Abandon world holders before AssetStore eviction to avoid use-after-free on same-stem rebind.
PreviousUI and editorNext Editor Project Selector

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/editor-asset-hot-reload.md
On this pageDaemon-to-editor event queueEditor tick processingRuntime hot reloadRender-thread synchronizationScene reload behaviorFirst cook is not a changeInvariants for maintainers Back to top