# Threads, jobs, events, and routines

| Tool | Use it for | Execution |
|---|---|---|
| `hi.std.Thread` | Explicit OS threads and long-lived native services | Actual parallelism; you own synchronization and shutdown |
| `hi.jobs` | Bounded CPU work on game-owned data | Existing worker pool; no World/render/UI access from jobs; join within the owning frame |
| Actor events/messages | Notify another actor or component | World dispatcher delivery; messaging itself is not concurrency |
| `hi.Routine` | Straight-line gameplay spread over simulation ticks | Cooperative fibers on the World thread; no worker thread is occupied while waiting |

Use [Flow/Timer](/docs/systems/temporal-kit.md) for tiny caller-driven state and delays.
Use routines when keeping ordinary locals, loops, and nested calls makes gameplay
code easier to write. Events can update state that a routine observes; they do not
create threads or fibers. Thread/job APIs retain their existing contracts.

## Start, wait, cancel

```zig
const hi = @import("hikari_game");

fn pulse(routine: *hi.Routine) !void {
    const actor = routine.actor(); // Weak handle; safe to keep across waits.
    const original = hi.world().scale(actor);
    defer hi.world().setScale(actor, original);

    hi.world().setScale(actor, .{ 2, 2, 2 });
    try routine.wait(0.5);
    hi.world().setScale(actor, .{ 0.5, 0.5, 0.5 });
    try routine.wait(0.5);
}

// Inside component Data:
state: struct { pulse: hi.RoutineHandle = .invalid } = .{},

pub fn onEnable(self: *@This(), actor: hi.ActorContext) void {
    self.state.pulse = actor.startRoutine(pulse) catch |err| {
        hi.std.log.warn("pulse could not start: {s}", .{@errorName(err)});
        return;
    };
}
```

`try routine.nextTick()` suspends until a later simulation tick. Conditions use
ordinary loops: `while (!ready(routine.actor())) try routine.nextTick();`.
Nested helpers can take `*hi.Routine` and wait too. Coordinate exclusive ownership
when multiple routines write the same property; the pulse example assumes that
it owns scale until its cleanup runs.

`hi.world().cancelRoutine(handle)` requests cancellation; `routineStatus(handle)`
reports queued/running/sleeping/cancelling/completed/cancelled/failed/expired.
Handles are weak: a completed status remains readable until its pool slot is
reused, then the old handle expires. No retained result allocation or release is
required. `hi.world().startRoutine(.invalid, function)` explicitly starts work
without an actor owner; full scene unload still cancels it.

## Timing and lifetime

- Start queues work; it never calls the function inline. Execution is after the
  World's `.update` group and before animation/`.post_update`.
- `wait(seconds)` arms a deadline at suspension. `wait(0)` means next tick;
  negative/non-finite values return `error.InvalidWait`. Sim pause freezes both
  time and execution. Routine cadence is independent of component tick LOD.
- Cancellation wakes a sleeping routine; its wait returns `error.Cancelled`.
  **Propagate that error with `try`** so `defer` runs. Do not catch and retry it.
  An unstarted routine can be cancelled without entering its function.
- Actor disable/destruction cancels actor-owned routines. Starts are actor-owned,
  even from a component: cancel the stored handle in `onDestroy` if removal of
  that component should also stop its routine. Re-enable does not restart work
  automatically; the example authors that behavior in `onEnable`.
- Full scene unload, World teardown, and module reload drain cleanup before
  releasing the affected state/code. Play Stop drains in bounded retirement
  passes. An unload cannot safely discard a suspended stack containing `defer`.
- Never retain `ActorContext`, sibling pointers, tick-arena memory, or held locks
  across a wait. Keep copied values, owned resources, and weak actor handles.
  Cancelled cleanup must tolerate a missing actor. Do not switch threads, block
  on I/O, or wait for a job while holding a gameplay borrow.

Routines are cooperative: a long loop without a wait still stalls the World
thread, and swallowing cancellation can prevent teardown. Fixed-update waits,
unscaled waits, child joins, and direct event/operation subscriptions are not
part of this initial API. Running stacks are transient, not scene/save data.

## Capacity and scaling

Configure on the World owner thread before starting work (for example in
`onWorldAttach`, with that World bound):

```zig
try hi.world().configureRoutines(.{
    .capacity = 10_000,
    .stack_bytes = 64 * 1024,
    .resumes_per_tick = 1024,
});
```

The default pool is created lazily on first start: 256 slots, 64 KiB stack budget
per slot, 1,024 resumptions per tick. Explicit configuration avoids that setup
cost in a gameplay callback. Pools cannot be reconfigured while active. Starts
beyond capacity return `error.RoutineCapacity`; they never silently grow the pool.

After reservation, start/wait/cancel use pooled slots and intrusive ready queues
without heap allocation. An indexed deadline heap parks sleepers and removes
cancelled timers without leaving tombstones. Ready work rotates under the budget;
100,000 continuously ready routines with a 1,024-resume budget need roughly 98
simulation ticks to service everyone once. This is not 100,000 updates per frame.

`routineStats()` exposes active/sleeping/ready counts, last-pass resumptions,
capacity, and configured stack bytes. Stack bytes exclude guard pages, metadata,
and platform rounding; they are not resident RAM. Each suspended fiber retains
its stack. macOS arm64 uses guarded virtual mappings; Windows x86-64 uses native
Win32 fibers. Linux routines currently report unavailable.

Validation from `src/hikari`: `zig build test-routines -Dgame-src=<abs>`,
`zig build check-routines-windows -Dgame-src=<abs>`, and
`zig build routines-bench -Dgame-src=<abs> -Doptimize=fast`.
See the [benchmark results](/docs/reports/routines-benchmark.md) for measured limits.
