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

Application lifecycle

On this page
On this pageStartup sequenceA frame in play modeDual view (unpossess / free-cam)Entity lifecycleTick groupsTeardown pointer rulesEditor lifecycleShutdown Back to top

Startup sequence

The native macOS (src/hikari/src/native_frontends/macOS/main.mm) and Windows (src/hikari/src/native_frontends/Windows/main.cpp) frontends create the process and receive OS events. They only call the opaque ABI declared by src/hikari/include/hikari_frontend.h.

The matching Zig application maps that ABI into frontend.Lifecycle and creates a session. A standalone game resolves packaged data beside its executable. An editor resolves the selected project's absolute assets root and must never author into the installed package's data/scenes copy.

SessionCore splits non-frame setup across three steps (runtime_session/session/lifecycle.zig):

  1. create — Own content/driver/world config copies; create input, optional audio (driver + worker; device start deferred), configs, runtime info, dispatch, physics, renderer, and the render-thread object (not yet running). No world and no game subsystem yet.
  2. start — dispatch.onLaunch() and mark started. Does not start the render thread (ABI still requires start before launch).
  3. launch — Create scripting + game-UI backends and World; bind HostApi; create/launch GameHost (onLaunch); wire physics/renderer to the world; register builtins + game content; bind AssetStore; attach the host to that World (onWorldAttach, with its WorldScope); load input actions; start the audio device; instantiate the startup scene (unless deferred); then startRenderThreadIfReady.

The editor opens a session in edit mode. The scene is instantiated and rendered, but entity updates, scripts, physics, and game updates remain frozen until Play. Standalone games launch in play mode.

A frame in play mode

SessionCore.tick = tickLogic then tickPublish (runtime_session/session/tick.zig). Drivers receive lifecycle and frame-granularity calls rather than per-object policy.

text
native event callbacks
  → InputSystem.syncFrame
  → editor chrome UI begin (if present; claims pointer before freecam/game)
  → pending scene ops applied (priority queue, up to 4 concurrent SceneLoadJobs; same-key serialize)
  → action bindings resolved (skipped while unpossessed)
  → World.onTick:
      if shouldAdvanceSim: tick groups / physics apply / swarms / LOD / fixed / animation
            then world.ui beginFrame
      else: early-out (host pause / load hold) — animation at dt==0 first, then UI begin, then return;
            no entity/script/swarm/fixed
  → GameSubsystem.onTick always in Play (sim uses deltaTime()==0 when held; chrome unscaled)
  → runtime commands + audio listener/sources tick
  → tickPublish: world.publishRenderState + scene readiness
  → render thread consumes the frame and presents

Host Pause sets World.sim_hold (does not write game time_scale). That and gameplay_suspended are the only sim gates — see Play / edit tutorial.

Dual view (unpossess / free-cam)

Publish resolves two PODs once per frame (resolveFrameCameras):

Lens (DrawLens)Game camera (GameCameraSnapshot)
WhatWhere this image is projected fromGame primary actor authority
PossessedGame primary matricesSame actor
UnpossessedEditor fly-cam (view_override)Still game primary (never swapped)
ConsumersVP, TAA, transparent sort, light clustersGPU frustum cull (unpossess inspect), look volumes, cascade focus, HostApi primary, audio, game LOD

Unpossess is an inspection tool: freecam for looking; game camera still owns cull + reaction (fog, cascades, audio). Fly outside the player frustum and missing draws show what cull dropped. Detach/repossess force temporal history invalidation. See Rendering.

In Edit, the same camera toolbar button is an explicit primary-camera preview: on binds the primary actor's transform and optics as one lens; off restores the parked editor fly-cam. Spatial Visual Zones follow whichever Edit lens is active. This is separate from Play unpossess and never mutates the actor.

Game-facing input while detached: exclusive empty input context (actions blocked) plus World.gameInputFrame scrub of freecam look signals (pointer_captured / mouse deltas) so primary-camera controllers cannot follow the editor freecam.

The engine keeps previous keyboard, mouse, and gamepad values alongside current values, so pressed/released/held queries are derived from a single synchronized input snapshot. Additive streaming and short edits use the cooperative scene physics barrier; full scene replace / reload / build hard-stop physics (see Physics — Scene integration).

Entity lifecycle

All actor/component callbacks, actor messages in either language, and Kawa session notifications enter scene/event_dispatcher.zig. World.events owns retained queues and recipient snapshots; the generated native component batches remain the execution layer. World tick scheduling and the worker pool remain separate from event delivery.

The actor and component lifecycle guide lists every hook, exact dispatch source, ordering, and component scope.

An archetype is defined with defineActor as a tuple of components (engine capabilities + game defineComponent types). Logic components may implement awake, start, onEnable, onDisable, update, lateUpdate, fixedUpdate, onCollision, onAnimationEvent, onMessage, onChanged, onEnvironmentPreview, onDestroy, and optional deinit. Kawa scripts attach as slots on the same actor: one per game component that declares .script, plus an optional entity-level .script. Entity-to-entity gameplay messages flush after entity updates in the same play tick; prefer hi.actors / Actors.* — see Actor communication.

Tick groups

Physics runs asynchronously on its own thread (~60 Hz). A TickGroup (hi.TickGroup) is a game-thread phase relative to when physics results are applied for the current frame — not a second physics clock. Enum is append-only (dense u8 index). lateUpdate / Kawa late_update always run in .post_update. Only .update and .post_update have Kawa hooks — pre_physics / post_physics are Zig only. Component .tick_rate (every frame / every N / distance LOD) further filters which instances run in the update group.

GroupWhen
pre_physicsBefore proxy apply. Rare: game-authoritative pose that must survive apply (dirty flags) before collision consumers — not “before the solver integrates.”
post_physicsAfter physics proxy apply + collision dispatch. Reads authoritative poses.
updateDefault gameplay slot (Zig component update / Kawa update).
post_updateAfter hierarchy refresh. Zig lateUpdate / Kawa late_update (fixed).

Within a group (and within the fixed-dt phase), batches sort by registration / priority; open-form dispatch tiles component kernels (tile_entries).

Engine-owned phases stay outside the enum (scene/world/world_tick.zig):

  1. Drain the termination queue + pre-hierarchy pass.
  2. pre_physics batches.
  3. physics_sync — drain replies → apply proxies → reconcile resources → hierarchy pass.
  4. Collision dispatch.
  5. post_physics batches.
  6. Swarms (if any) — after post_physics; integrate / per-row update then deferred retire drain.
  7. Distance LOD auto-distance sweep (when lod_auto_distance and a consumer needs significance) + rebucket (tick-rate bands no-op when every band is rate 1).
  8. Fixed-dt fixedUpdate (when hi.world().setFixedDeltaTime ≠ 0) — accumulator over scaled dt, capped steps/frame; TickContext.dt is the fixed step, is_fixed == true. Zig only (no Kawa hook).
  9. update batches + Kawa update scripts (LOD / every_n filtered).
  10. AnimationPlayer eval (after gameplay may start/stop clips; before late hierarchy / post_update / publish). Zero-dt ticks still evaluate so a pause/scrub write reaches the joint palette.
  11. Hierarchy refresh (parented world transforms written in update readable in post_update).
  12. post_update late batches + Kawa late_update scripts.
  13. Message flush.
  14. Purge terminated entities (+ seal physics publish commands).
zig
const CameraRigLogic = hi.defineComponent(.{
    .name = "camera_rig_logic",
    .storage = .embedded,
    .data = struct {
        pub fn update(_: *@This(), _: hi.ActorContext, _: *const hi.TickContext) void {}
    },
});

hi.defineActor(.{
    .archetype = "camera_rig",
    .components = .{ hi.ComponentCamera, CameraRigLogic },
});

// Opt-in fixed step (e.g. from GameSubsystem.onLaunch):
hi.world().setFixedDeltaTime(1.0 / 60.0);

Buckets fill at first batch creation (not registerArchetype). Profiler zones: world.tick_group.*, world.fixed_update.

Teardown pointer rules

Opaque and raw pointers during entity / scene unload are easy to UAF. Keep these lifetimes straight:

PointerValid throughFreed / invalidated
Entity script slots (World.script_slots range via script_slot_first / script_slot_count; each instance → Kawa ScriptInstance)Until script destroyHost entity nulled before instance free; Zig must not call into those instances afterward. Canonical order: all script slots destroyed before Zig terminate hooks.
Component blob / behaviour_data payloadThrough Zig destroy hooksFreed with entity teardown; do not stash across scene unload without generation.
NativeUpdateEntry payload pointerWhile the entity is in a native update batchRemoved from batches before game-module unload (clearEntityModules / destroyRegistered).
Dirty callbacks (*anyopaque → *Entity)Until hierarchy detach beginsCleared at the start of Entity.destroy (before detach / detachChildren).

Forbidden: holding a raw *Entity or bare *anyopaque behaviour pointer across a scene unload / despawn without EntityRef / ActorRef (or equivalent generation). After mark-for-termination, resolve refs again — do not cache the pointer.

--profiler-timing only compiles the profiler subsystem (default on in debug, off in release). At runtime, Profiler on (API: recording; default off) is the global sampling switch on the editor Profiler tab and in-game World.profiler_overlay. The editor tab works in Edit without Play so you can profile chrome/UI. Optional detail switches:

  • Editor timing (editor Profiler tab only) — retained UI composition and chrome phases under editor.
  • Entity timing — each play-frame update, script update, onCollision, and onMessage, grouped entities → name → method (EMA-smoothed; sticky ~3s then drop).
  • Message timing — flush cost aggregated by interned message name under messages.

Always-on nested zones also split publish (ui_finalize, lock, hierarchy, proxies, draw_lists, swap) and world.physics_sync (apply, reconcile, hierarchy). The sample game toggles the overlay with the toggle_profiler action (F4) via its debug entity; other projects must bind and draw the overlay themselves. See UI and editor.

The world does not rebuild a global actor list every tick. Generated archetype batches track active, scripted, and terminating entities incrementally. setActive(false) drops the entity from active/native-update batches (no update / script tick / message or collision delivery); physics and render still AND their own component switches with entity active — Active / enable trio. Details and authoring examples are in Scenes and gameplay.

Editor lifecycle

The editor keeps two different models:

  • SceneDocument is authored data, transactional history, stable actor IDs, deterministic serialization, and atomic saves.
  • World is the live preview/runtime projection.

Editor tools mutate through editor.Document; gestures open a transaction so repeated values become one undo step. The hierarchy reads the authored document, not the live world. In Play, docks and the Play/Stop toolbar stay; author gizmos and the world-grid overlay hide while possessed (game owns the lens) and return when unpossessed. Keyboard ownership moves to the game (RetainedUi.setAcceptsKeyboard(false)). Editor overlay decoration does not leak into the standalone game product.

SessionCore.mode is the sole simulation-mode truth. The editor owns two worlds during Play: the authoring world and its stopped physics backend are parked intact, while a disposable Play world with a separate physics backend is built from the in-memory SceneDocument UTF-8 (never the on-disk scene, so unsaved authoring is included). Hidden editor bodies therefore cannot collide with or inflate Play simulation.

Neither direction blocks the click. The Play copy is built across ticks on the normal scene-load budget while the authoring world keeps rendering and taking input; HostPolicy.isPlayPending is true and canAuthor false for that window (the document was captured at the click), and clicking again withdraws the request. Stop then atomically rebinds renderer, physics, HostApi, panels, and diagnostics to the retained authoring pair before returning — it never reconstructs the authored scene and never leaves the viewport showing Play-time state. Every renderer setWorld is an identity barrier: the incoming world forces a fresh publish and invalidates temporal history plus retained GPU residency (shadow/light caches, RT hit tables, bindless mappings, and BLAS/TLAS state) before its first frame. The invisible Play world/backend pair is destroyed in bounded slices afterwards, handing its queued GPU releases to the authoring world's command queue so no residency row is stranded. Its world, scripting driver, and physics backend share one disposable subsystem allocator; that allocator is released only after retirement completes, preventing Debug allocator page high-water from accumulating across otherwise clean Play/Stop cycles.

If Play navigated elsewhere (requestSceneLoad / Scene.load), Stop still returns to authoring scene A. In-game Quit in the editor stops Play the same way; standalone games close the process. Global session settings changed during Play are snapshotted on Play enter and replayed on Stop — see Global settings revert on Stop. See Session services.

The important boundary is world identity: Play gets a copy, while Stop returns to the retained authoring world even if Play has loaded another scene.

Diagram
Diagram source
sequenceDiagram
    participant E as Editor
    participant S as SessionCore
    participant A as Edit world
    participant P as Play world
    E->>S: Play + document snapshot
    S->>P: Build across budgeted ticks
    Note over E,A: Edit still renders · authoring disabled
    S->>A: Park world + physics
    S->>P: Bind and start simulation
    Note over A,P: Separate physics backends
    E->>S: Stop
    S->>A: Rebind all runtime / editor services
    S-->>E: Edit already active
    S->>P: Retire in bounded slices
    P->>A: Hand off GPU releases
    Note over P: Retire fully<br/>then free allocator

Shutdown

SessionCore.shutdown is idempotent. It stops the render thread and clears graphics providers first (so provider callbacks cannot outlive game/plugin state), then GameHost.onTerminate, stops physics/audio, destroys entities, and runs physics/renderer/world onTerminate while drivers are still mapped. destroy then tears down render-thread/renderer/physics/game-host/world/audio/store before backend modules unload. This ordering is mandatory: module-owned driver code must not be called after its library unloads.

PreviousArchitectureNext Frontends and drivers

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/lifecycle.md
On this pageStartup sequenceA frame in play modeDual view (unpossess / free-cam)Entity lifecycleTick groupsTeardown pointer rulesEditor lifecycleShutdown Back to top