# Application lifecycle

## 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: world.ui beginFrame
            then tick groups / physics apply / swarms / LOD / fixed / animation
      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](/docs/tutorials/play-edit-and-scenes.md#pause-and-unpossess-editor-play-only).

### Dual view (unpossess / free-cam)

Publish resolves two PODs once per frame (`resolveFrameCameras`):

| | **Lens** (`DrawLens`) | **Game camera** (`GameCameraSnapshot`) |
|--|----------------------|----------------------------------------|
| **What** | Where *this image* is projected from | Game primary actor authority |
| **Possessed** | Game primary matrices | Same actor |
| **Unpossessed** | Editor fly-cam (`view_override`) | Still game primary (never swapped) |
| **Consumers** | VP, TAA, transparent sort, light clusters | **GPU frustum cull** (unpossess inspect), look volumes, shadow focus, HostApi primary, audio, game LOD |

Unpossess is an **inspection** tool: freecam for looking; game camera still owns cull + reaction (fog, shadow focus, audio). Fly outside the player frustum and missing draws show what cull dropped. Detach/repossess force temporal history invalidation. See [Rendering](/docs/systems/rendering.md#dual-view-lens-vs-game-camera).

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](/docs/systems/physics.md#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](/docs/guides/component-lifecycle.md) 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](/docs/systems/entity-messaging.md).

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

| Group          | When                                                                            |
|----------------|---------------------------------------------------------------------------------|
| `pre_physics`  | Before proxy apply. Rare: game-authoritative pose that must survive apply (dirty flags) before collision consumers — **not** “before the solver integrates.” |
| `post_physics` | After physics proxy apply + collision dispatch. Reads authoritative poses.      |
| `update`       | Default gameplay slot (Zig component `update` / Kawa `update`).                 |
| `post_update`  | After 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. Local transform setters and borrowed SDK `TransformData.markDirty()` enqueue stable component indices through a host-owned write sink. Joined phase boundaries drain only written rows, deduplicate forest roots, and retain writes if root admission fails. Preserve the sink when editing a borrowed pose; copied transforms must not invoke it.
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 {}
    },
});

pub const CameraRig = 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:

| Pointer | Valid through | Freed / invalidated |
|---------|---------------|---------------------|
| Entity script slots (`World.script_slots` range via `script_slot_first` / `script_slot_count`; each `instance` → Kawa `ScriptInstance`) | Until script destroy | Host 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` payload | Through Zig destroy hooks | Freed with entity teardown; do not stash across scene unload without generation. |
| `NativeUpdateEntry` payload pointer | While the entity is in a native update batch | Removed from batches before game-module unload (`clearEntityModules` / `destroyRegistered`). |
| Dirty callbacks (`*anyopaque` → `*Entity`) | Until hierarchy detach begins | Cleared 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](/docs/systems/ui-and-editor.md#profiler).

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](/docs/systems/scenes-and-gameplay.md#active--enable-trio). Details and authoring examples are in [Scenes and gameplay](/docs/systems/scenes-and-gameplay.md).

## 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](/docs/systems/session-services.md#global-settings-revert-on-stop). See [Session services](/docs/systems/session-services.md).

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.

```mermaid
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.
