# Project file

Hands-on walkthrough: [Tutorials — First project](/docs/tutorials/first-project.md).

Every game root contains `hikari.project.json`. It is the editor entry point for **project identity**: durable id, name, source and asset roots, engine SDK location, content dirs. Tunable session policy and packaging live as solo JSON files under `configs/`. Zig `ProjectConfig` (`GameModule.config`) supplies defaults; present `configs/*.json` fields win at session boot (`composeFromFiles` / `Config.sessionConfig`).

```json
{
  "kind": "com.hikari.project",
  "version": 5,
  "id": "b0d18223-4b45-4611-b17f-fcf4175a51b7",
  "name": "My Game",
  "source": "src",
  "assets": ".",
  "last_editor_version": "0.1.0",
  "content_dirs": [
    { "path": "resources", "bundled": true }
  ]
}
```

| Field | Role |
|---|---|
| `kind` | Must be `com.hikari.project`. |
| `version` | Descriptor schema version (`5` today). Not the engine/editor product version. |
| `id` | **Required** durable project UUID (`xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`, RFC 4122). Stable across renames and moves; shown read-only in Project Settings and exposed to editor/MCP as `project_id`. Generate once (editor mints UUID v4 when creating a descriptor); do not recycle or rewrite casually. |
| `name` / `source` / `assets` / `engine_sdk` | Discovery paths for the editor and Kaji. `engine_sdk` is the thin game-module SDK (`build.zig` + `hikari_game`): a path relative to the project root, or `"bundled"` / omitted → `<exe_dir>/sdk` if present (packaged / `--vendor:engine=bundle`), else walk ancestors for `hikari/sdk` (in-tree `src/hikari`) or `hikari/src/hikari/sdk` (games repo beside an engine clone). Omit it; games must not hard-code an engine path. |
| `last_editor_version` | Written by the editor on open. Internal engine/editor version number (`src/hikari/src/version.zig` `current.version.number`) that last opened the project. Absent until the first successful open. When present and different from the running editor, the project selector / boot path shows a warning before proceeding. |
| `content_dirs` | Loose content roots staged by Kaji into `data/<path>/` (not Shinra). See [Content dirs](/docs/guides/project-file.md#content-dirs-loose-data) and [Data, JSON, and modding](/docs/guides/data-and-modding.md). |

Legacy top-level `config` / `packaging` **objects** in the project file are **rejected** at open (`LegacyEmbeddedSessionConfig` / `LegacyEmbeddedPackaging`) so session policy is never silently dropped. Move them to `configs/*.json` before opening the project.

## Solo configs (`configs/`)

Same layout in the project and in the staged product `data/configs/`:

| File | Role |
|------|------|
| `configs/game.json` | Game window (mode, display, resizable, VSync / frame rate) + `startup.scene` + asset residency budget + world tuning |
| `configs/render.json` | Render pipeline (features / quality / look) for the game and editor viewport |
| `configs/audio.json` | Boot mix (bus gains, ducking, reverb), voice budgets, and audio device policy |
| `configs/drivers.json` | Runtime driver recipe and GPU adapter preference; driver implementations are inherited by the editor host |
| `configs/editor.json` | Editor window + host frame pacing; optional sparse `render` / `drivers` overlays on the game recipes |
| `configs/packaging.json` | Shipping identity (Kaji `--package`; not runtime) |
| `configs/layout.json` | Pack graph: membership, delivery, deps, labels (Kaji seal + Project Settings → Packs) |
| `configs/input_actions.json` | Authored action ids and default bindings |
| Writable `configs/input_bindings.json` | Sparse per-user binding overrides; never shipped |
| `configs/gameplay_tags.json` | Gameplay-tag catalog (`com.hikari.gameplay_tags`). Not session compose — File → Project Settings… → Gameplay Tags |
| `configs/plugin_settings.json` | Plugin package settings (`com.hikari.config.plugin_settings`), read by game code through `hi.settings`. Not session compose — File → Project Settings… → Plugins |

The editor does **not** keep a second full render or driver recipe. Compose copies composed `game.render` / `game.drivers` into the editor recipe, then applies only keys present under `editor.json` → `render` / `drivers`. GPU preference is the one target-local policy: editor defaults to `auto` instead of inheriting the runtime's adapter policy. Leave `editor.json` without other driver keys unless you need a rare host-only override (hand-edit).

### GPU adapter preference

GPU selection is a launch policy, not a persistent adapter id. The runtime value lives in `configs/drivers.json`; the independent editor value lives under `configs/editor.json` → `drivers`. Project Settings exposes both choices and marks them **Requires restart** because the device, queues, swapchains, and resident resources are created for one adapter.

`configs/drivers.json`:

```json
{
  "kind": "com.hikari.config.drivers",
  "version": 1,
  "gpu_preference": "high_performance"
}
```

`configs/editor.json` (sparse host override):

```json
{
  "kind": "com.hikari.config.editor",
  "version": 1,
  "drivers": {
    "gpu_preference": "auto"
  }
}
```

Accepted values are `auto` and `high_performance`. `auto` uses the platform's normal adapter ordering; `high_performance` asks for the discrete GPU and falls back when there is none. It reorders compatible adapters only — it never makes an otherwise unsupported GPU eligible.

There is deliberately no "prefer the low-power GPU" value: battery and thermal modes are player-facing runtime settings, not project constants. The knob is implemented on D3D12; on single-GPU platforms such as Apple silicon a non-`auto` value is logged and ignored.

Project Settings shows apply timing on every non-live row, repeats the strongest
pending boundary in the footer, and posts the same short reminder after Save:

| `Apply` | Meaning |
|---------|---------|
| `live` | Written to disk **and** pushed into the open editor session on Save (shadows, RT, reflections, AO, TAA, tonemap, …) |
| `restart` | Disk only until the editor restarts (GPU adapter preference, graphics/audio drivers, editor module linkage, plugin enablement, …) |
| `launch` | Disk only until the next standalone game launch. Play-in-editor uses the policy composed at editor startup, so restart the editor before testing the change in Play. |
| `package` | Packaging identity; next `--package` |

All Rendering and Quality rows are live. Feature gates, graph-shaping quality
choices, post-process look values, occlusion policy, and directional-shadow
tuning use one frame-boundary render-config transaction; the renderer ensures
newly needed pipelines and invalidates temporal history where required.

### Document header

Every solo config file starts with a document header (same shape as other Hikari JSON docs):

```json
{
  "kind": "com.hikari.config.game",
  "version": 1
}
```

| File | `kind` | Schema version |
|------|--------|----------------|
| `game.json` | `com.hikari.config.game` | 1 |
| `render.json` | `com.hikari.config.render` | 3 |
| `drivers.json` | `com.hikari.config.drivers` | 1 |
| `editor.json` | `com.hikari.config.editor` | 1 |
| `packaging.json` | `com.hikari.config.packaging` | 1 |
| `layout.json` | `com.hikari.config.layout` | 2 |
| `input_actions.json` | `com.hikari.config.input_actions` | 2 |
| `input_bindings.json` | `com.hikari.config.input_bindings` | 1 |
| `gameplay_tags.json` | `com.hikari.gameplay_tags` | 1 |
| `plugin_settings.json` | `com.hikari.config.plugin_settings` | 1 |

`version` is the document-domain schema, not one repository-wide number. For packaging, the marketing/store version is a separate field: `product_version` (e.g. `"0.1.0"`).

### Settings composition model

Runtime policy has three deliberate forms:

1. A resolved canonical value (`ProjectConfig`, `RenderPipelineConfig`) with no missing fields.
2. A sparse domain patch (`WorldPatch`, `DriverPatch`, `RenderPipelinePatch`) where omission means “inherit the lower layer”.
3. A document envelope carrying `kind` and that domain’s schema `version`.

Composition order is engine defaults, game-module Zig defaults, project JSON,
editor target overrides, then live runtime patches. The render document keeps
its domain body under `pipeline`; `editor.json` → `render` uses the same
`RenderPipelinePatch` directly. Render live changes resolve once in the session
and cross the render thread/backend as one complete pipeline value, so related
fields never arrive as transient half-configurations.

`pipeline.display` is shipped (`output`, `prefer`, `paper_white_nits`, `tone_target_nits`, `sdr_preview`). The example game sets HDR + HDR10 prefer. Contract: [Rendering — Display output](/docs/systems/rendering.md#display-output-hdr). `world.residency_bind_budget_ms` lives on `game.json` world — [asset residency](/docs/systems/asset-residency.md).

### Quality preset (`render.json`)

`quality.preset` sets every cost tier at once, so a project — or a player's
options screen — scales the whole frame without knowing what each knob costs.
Any other `quality` field in the same document overrides the preset on top of
it, whatever its position in the file. Presets never switch features on or off
(those are art direction under `features`) and never touch the governor,
authored directional-shadow geometry or the lens-flare preference.

```json
{
  "kind": "com.hikari.config.render",
  "version": 3,
  "pipeline": {
    "quality": {
      "preset": "low",
      "bloom": "off"
    }
  }
}
```

The default is `medium`; a project that authors nothing renders exactly the
medium preset.

| Tier | `low` | `medium` | `high` | `ultra` |
|---|---|---|---|---|
| `shadows` | low (16 MiB VSM) | medium (64 MiB) | high (144 MiB) | ultra (256 MiB) |
| `shadow_filter` | pcf | smrt_low | smrt_high | smrt_high |
| `contact_shadows` | low | medium | high | high |
| `ambient_occlusion` | low | medium | high | ultra |
| `reflections` | low | medium | high | ultra |
| `rt_hit` | performance | balanced | balanced | high |
| `gi` | low | medium | high | ultra |
| `rt_budget.tier` | low | medium | high | ultra |
| `volumetric_fog` | low | medium | high | high |
| `clouds` | low | medium | high | ultra |
| `environment` | low | medium | medium | high |
| `depth_of_field` | low | medium | high | high |
| `motion_blur` | low | medium | high | high |
| `bloom` | low | medium | high | high |
| `anisotropic_filtering` | x4 | x8 | x16 | x16 |
| `occlusion.mode` | single_phase | two_phase | two_phase | two_phase |
| `local_lights` | low | medium | high | ultra |

A preset also clears `shadow_lights` back to the shadow tier's derived budget.
Applied at runtime (`hi.render().setQualityPreset`, or a `setGraphics` patch
carrying `quality.preset`) it discards earlier explicit tiers; fields in the
same patch still win. Project Settings → Quality shows the preset first; every
tier row reads **Preset** until the project overrides it.

`lens_flare` (default `true`) is a player switch rather than a tier: `false`
removes the flare visibility pass and zeroes the flare uniforms, and a
flare-only camera no longer forces the camera lens composite or the HDR post
chain; vignette and chromatic aberration are unaffected. `bloom: "off"`
likewise disables bloom whatever scenes and Visual Zones author.

### Content capacity (`render.json`)

`pipeline.capacity` holds content ceilings: how much of each renderer-owned
content kind one frame may carry. They are project policy, not cost tiers —
the quality preset never changes them, so a player on Low still sees every fog
volume the level authors. Raise them for dense or open-world content; buffers
are sized from them at runtime and grow or shrink at a frame boundary. Past a
ceiling each system keeps its highest-priority content (nearest first) and logs
a warning once per budget value.

| Field | Default | Range | Cost per unit |
|---|---|---|---|
| `decals` | `256` | 16–8192 | 96 B GPU records per kept projector per frame in flight. The per-tile overlap cap (64 layers) does not scale |
| `fog_volumes` | `64` | 1–1024 | ~352 B GPU BVH nodes plus up to 768 B of program words per volume per frame in flight |
| `media_bricks` | `64` | 8–512 | Fog volumes whose material is cached as a 32³ density brick (384 KiB per frame in flight, twice that with spatial emission); particle shadow density takes bricks first. Fog past it evaluates its material per froxel (same image, higher GPU cost); particle density past it casts no volumetric shadow. The atlas, a grid 16 bricks wide, grows with the bricks in use, not with this ceiling |
| `shadow_views` | `0` | 0–4096 | `0` follows `quality.shadows` (32 / 128 / 256 / 512 views). One view per spot light, six per point light, a clipmap set per sun. ~94 KB GPU page table per view slot (capacity rounds up to a power of two of the live high-water mark) plus ~270 B CPU per budgeted slot |
| `light_grid` | `standard` | compact / standard / dense | 16×9×16 (2,304), 24×14×24 (8,064) or 24×14×48 (16,128) light clusters; the light-index pool is 2 × clusters × the `local_lights` cap words per frame in flight (standard at high: 8.3 MB) |

Project Settings → Rendering → Capacity edits them live; games read the active
values from `runtimeInfo().graphics.capacity` and change them with
`hi.render().setGraphics(.{ .render = .{ .capacity = .{ ... } } })`.

### Async-compute scheduling (`render.json`)

Async compute is a device-wide scheduling policy, not a visual feature. `auto`
uses a dedicated compute queue when the backend reports one usable; `off` runs
the same eligible work on the graphics queue. Unsupported devices always fall
back to graphics.

```json
{
  "kind": "com.hikari.config.render",
  "version": 3,
  "pipeline": {
    "scheduling": {
      "async_compute": "off"
    }
  }
}
```

The default is `auto`. Project Settings → Rendering → Platform and
`hi.render().setAsyncCompute` both apply the policy live at a frame boundary;
no process restart is required. Switching policy may recreate visible
content-compute producers once so their queue-owned resources remain coherent.

### Occlusion quality (`render.json`)

Occlusion separates graph shape from camera-motion policy. `mode` chooses the
algorithm; `profile` chooses when camera movement should conservatively fall
back to GPU frustum-only culling. The profile applies to `single_phase` only:
`two_phase` re-tests every predicted reject against the current frame's depth,
so motion costs it a few late draws, never a missing object, and it keeps Hi-Z
through motion whatever the profile says.

```json
{
  "kind": "com.hikari.config.render",
  "version": 3,
  "pipeline": {
    "quality": {
      "occlusion": {
        "mode": "two_phase",
        "profile": "stable"
      }
    }
  }
}
```

| Profile | Policy | Typical use |
|---|---|---|
| `stable` | Guard nearly all deliberate camera motion | Racing, fast traversal, fly cameras |
| `balanced` | Keep Hi-Z during ordinary movement; guard fast motion | General third-person / open world |
| `performance` | Keep Hi-Z active through motion | Constrained cameras, corridor-heavy games |

The thresholds are engine-owned and measured as camera velocity, so projects
do not accumulate resolution-, FOV-, or refresh-rate-specific shader constants.
Project Settings → Quality authors both fields as sparse JSON.

### Ray-tracing budget and frame governor (`render.json`)

The shared RT budget is a deterministic authored ceiling divided among the RT
effects that are enabled. The separate frame governor is adaptive by default:
its ray lever first reduces grants above each effect's useful floor, then its
indirect-resolution lever may reduce the stochastic input grids of RT GI and
reflections. RTAO keeps its fixed half-resolution producer/history lattice.
Temporal histories keep their authored grid and the bilateral resolve stays
full-resolution. Set `governor.mode` to `off` for deterministic captures or
fixed-tier benchmarks; an `unlimited` RT tier disables governing altogether.

```json
{
  "kind": "com.hikari.config.render",
  "version": 3,
  "pipeline": {
    "quality": {
      "rt_budget": {
        "tier": "high"
      },
      "governor": {
        "mode": "adaptive",
        "levers": {
          "rays": true,
          "indirect_resolution": true
        }
      }
    }
  }
}
```

Both policies are live in Project Settings → Quality.

### Example `configs/game.json`

```json
{
  "kind": "com.hikari.config.game",
  "version": 1,
  "window": { "title": "My Game", "width": 1280, "height": 720, "vsync": true, "frame_rate": "unlimited" },
  "startup": { "scene": "scenes/main" },
  "residency": {
    "keep_alive_mb": 512,
    "gpu_geometry_mb": 1024,
    "texture": {
      "gpu_budget_mb": 768,
      "initial_tail_mips": 4,
      "lod_bias": 0.0,
      "eviction_grace_frames": 30,
      "max_transitions_per_frame": 16
    }
  }
}
```

`residency.keep_alive_mb` bounds decoded, unreferenced keep-alive assets for
chunk streaming (default `512`; `0` destroys on last release). Referenced and
pinned assets are never evicted by this budget. `residency.gpu_geometry_mb`
ceilings live GPU mesh geometry; `0` is unlimited.

`residency.texture.gpu_budget_mb` controls projected-size mip streaming for
ordinary 2D material maps. It defaults to `768`, so projects stream without any
author setup; an explicit `0` keeps every authored mip fully GPU resident. A
streamed texture first uploads its `initial_tail_mips` smallest
mips, then moves toward the detail demanded by visible screen coverage while
staying under the shared budget. `lod_bias` is in mip levels (positive is
coarser), `eviction_grace_frames` prevents boundary churn, and
`max_transitions_per_frame` caps replacement uploads requested by one publish.
The budget is the usual "Texture quality" option: a running game changes it
live with `hi.render().setTextureBudget(mb)` (64 MiB–64 GiB; never turns
streaming on for a project authored at `0`). See [Asset residency](/docs/systems/asset-residency.md#gpu-texture-and-mip-streaming).

`window.vsync` (default `true`) and `window.frame_rate` (default `"unlimited"`) are **standalone game only**. Play-in-editor uses the editor host table below, never these keys. `frame_rate` is one of `"hz_10"` / `"hz_15"` / `"hz_30"` / `"hz_60"` / `"hz_120"` / `"unlimited"`. Unlimited with VSync is display-driven; unlimited without VSync is uncapped.

Input actions are **not** referenced from startup — authored actions are always
`config://input_actions.json`; runtime rebinding persists separately to the
writable `config://input_bindings.json` layer.

### Editor frame pacing (`editor.json` → `frame_pacing`)

Editor tick/present rates are independent of the game window. Defaults match the historical host table; omit the object to keep them. Every field accepts the same `frame_rate` tags, including `"unlimited"`.

```json
{
  "kind": "com.hikari.config.editor",
  "version": 1,
  "frame_pacing": {
    "ac_active": "hz_60",
    "ac_idle": "hz_30",
    "battery_active": "hz_30",
    "battery_idle": "hz_15",
    "saver_active": "hz_30",
    "saver_idle": "hz_10"
  }
}
```

| Field | Default | When it applies |
|-------|---------|-----------------|
| `ac_active` | `hz_60` | Play, viewport navigation, UI interaction, while on AC |
| `ac_idle` | `hz_30` | Settled Edit with no input, on AC |
| `battery_active` | `hz_30` | Active, unplugged |
| `battery_idle` | `hz_15` | Idle, unplugged |
| `saver_active` | `hz_30` | Active, OS Low Power Mode / battery saver |
| `saver_idle` | `hz_10` | Idle, OS Low Power Mode / battery saver |

A machine the OS cannot classify (typical desktop) stays on the AC rates. While unplugged, the toolbar shows a battery glyph; clicking it toggles **boost**, which uses the AC rows (ignoring battery saver too) and marks the glyph with fast-forward arrows. Boost lasts for the session and ends when the machine is plugged in. Project Settings → Editor authors this live; Project Settings → Game authors standalone `window.vsync` / `window.frame_rate` (next game launch). A running game changes them live with `hi.render().setVSync` / `setFrameRateLimit` or `hi.config.applyPlayerSettings`, which also saves them for the player.

### World tuning (`game.json` → `world`)

Gameplay-world knobs whose right value depends on the game rather than the
engine. Every default reproduces built-in behaviour, so omitting the section
changes nothing. The editor applies the **game's** `world` section during
play-in-editor, so it matches the shipped build.

```json
{
  "kind": "com.hikari.config.game",
  "version": 1,
  "world": {
    "lod_bands": [
      { "max_distance": 150, "rate": 1 },
      { "max_distance": 800, "rate": 4 },
      { "max_distance": 3.4e38, "rate": 0 }
    ],
    "lod_hysteresis": 5.0,
    "lod_budget": 4096,
    "fixed_hz": 60,
    "expected_entities": 100000
  }
}
```

| Field | Default | Meaning |
|-------|---------|---------|
| `lod_bands` | full rate everywhere | Tick-rate bands, nearest first; the last one catches everything beyond it. `rate` is `1` every frame, `N` every Nth, `0` asleep. Throttling is always an explicit project decision — with auto significance filling distances for every actor, a throttling default would silently stop far actors in games that never asked for LOD |
| `lod_hysteresis` | `1.0` | Distance past a band edge before an actor switches, so one loitering on a boundary does not flip rate every frame |
| `lod_budget` | `1024` | Actors re-bucketed per frame; trades how fast a rate change takes effect against frame cost. Also budgets the auto-distance sweep |
| `lod_auto_distance` | `true` | Engine fills each actor's LOD distance from the primary camera (budgeted round-robin), so distance LOD, animation rates, and particle sleep work with zero game code. `setLodDistance` overrides per actor |
| `mesh_lod_distance_scale` | `1` | Multiplier on the distance at which **mesh** LOD levels switch. `2` holds every level to twice the distance (more detail, more triangles); `0.5` switches at half. The main mesh-detail knob for a quality preset — per-mesh tuning belongs on the primitive's `lod_bias`. Clamped to `[0.05, 20]` |
| `mesh_lod_fade_time` | `0.25` | Seconds a **mesh** LOD switch spends dissolving between levels (unrelated to the tick-rate bands above). The pair draws as one widened vertex span, screen-door dithered, and the temporal resolve turns that into a cross-fade. `0` switches instantly — cheapest, and pops. A mesh mid-fade rasterises both levels, so this is a duration, not a quality tier |
| `fixed_hz` | `0` (off) | `fixedUpdate` rate. **While this is `0` the fixed phase never runs**, so components declaring `fixedUpdate` are never called — the engine warns at archetype registration if that combination appears |
| `max_fixed_steps` | `8` | Fixed steps per frame before the accumulator is clamped; the clamp is what stops a long frame spiralling |
| `job_workers` | `0` (auto) | Job-system threads. Auto leaves a core for the main thread and caps at 16 |
| `scene_preload_budget_ms` | `4` | Per-frame streaming residency budget |
| `scene_instantiate_budget_ms` | `4` | Per-frame actor spawn budget |
| `expected_entities` | `0` | Rough live actor count. Pre-sizes component stores so loading a large level does not regrow them repeatedly |

Bands must be strictly ascending; a non-ascending list is rejected at compose
time rather than asserting mid-frame. With `lod_auto_distance` (the default)
every actor gets a camera distance automatically; `world().setLodDistance`
overrides it per actor and `clearLodDistance` returns to auto. With auto off,
only actors the game publishes are throttled. To disable LOD outright, give a
single catch-all band at rate `1` rather than an empty list (empty means "keep
the engine default").

A game that needs to change policy at runtime — per region, or off a quality
setting — calls `world().setLodPolicy(bands, hysteresis, budget)` with the same
band type. Static policy belongs in this file.

`mesh_lod_distance_scale` and `mesh_lod_fade_time` are the two knobs here that
are *not* tick-rate LOD. Runtime levers: `world().setMeshLodDistanceScale` /
`setMeshLodFadeTime` (read back with `meshLodDistanceScale()` /
`meshLodFadeTime()`); both are snapshotted on Play and reverted on Stop like
every runtime-API global. Per-primitive overrides are `lod_bias` / `force_lod`
on `render().applyRender` / `applyRenderSlot` (the runtime form of scene JSON
`render.lod_bias` / `render.force_lod`; a negative `force_lod` returns to
automatic selection).

In the editor, **Project Settings → World** authors the same section: scalar
knobs as number fields, and `lod_bands` through a list editor (add / remove /
catch-all ∞). An empty band list means "engine default" and removes the key on
Save, matching every other unset convention in the dialog.

### Particle scalability (`game.json` → `particles`)

Global cost levers for the GPU particle pipeline. In a fixed-step deterministic
sim, "tick less often" is not a lever — batched catch-up runs the same steps —
so the honest knobs are fewer particles, sleeping far systems, and cheaper
shading. Omitting the section changes nothing; every value is also live-tunable
from the game through `hi.particle().setGlobal*`.

```json
{
  "kind": "com.hikari.config.game",
  "version": 1,
  "particles": {
    "spawn_scale": 0.75,
    "cull_distance": 150,
    "cull_fade_band": 20,
    "max_total_capacity": 65536,
    "lit": true,
    "soft_fade": true
  }
}
```

| Field | Default | Meaning |
|-------|---------|---------|
| `spawn_scale` | `1` | Multiplies every emitter's authored rate and bursts — the single knob quality presets hang off. Explicit `emitBurst` calls are gameplay and are never scaled |
| `max_steps_per_frame` | `8` | Fixed-step catch-up cap per system per frame (1–8). Lower drops sim time after a hitch instead of spiking the GPU |
| `max_frame_delta` | `0.25` | Frame delta clamp (seconds) fed to the fixed-step accumulator |
| `cull_distance` | `0` (never) | Systems farther than this from the primary camera sleep: no sim dispatches, no draw, GPU state frozen for seamless resume. Per-component `cull_distance` overrides (`-1` inherits, `0` never, `>0` explicit) |
| `cull_fade_band` | `5` | Width of the dim-out band just inside the cull distance, so systems fade instead of popping off |
| `max_total_capacity` | `0` (unlimited) | Budget over the summed asset capacities of live systems. When exceeded, the farthest non-`important` systems sleep until the total fits — mark the campfire `important`, not the footstep dust |
| `max_systems` | `64` | Particle systems simulated and drawn at once (1–4096). Past it the farthest non-`important` systems sleep, like the capacity budget. Live-tunable with `hi.particle().setGlobalSystemBudget` |
| `lit` | `true` | Master switch for scene-lit particles (the per-fragment cluster light walk is the most expensive thing in the pipeline). Off forces every emitter unlit |
| `soft_fade` | `true` | Master switch for soft depth-fade. Off skips the depth read |

In the editor, **Project Settings → Particles** authors the same section. The
per-actor overrides (`cull_distance`, `important`) live on the particle
component in the inspector and in scene JSON.

### One significance spine

`world`, `animation`, and `particles` all consume the **same per-actor
distance**: by default the engine fills it from the primary camera
(`world.lod_auto_distance`, budgeted round-robin), and `world().setLodDistance`
overrides it per actor — a game's custom significance (behind-camera bias,
gameplay weight) drives gameplay tick rates, animation evaluation rates, *and*
particle sleep coherently. `clearLodDistance` returns an actor to auto. This is
what makes the sections compose for open worlds: one distance, per-system
levers.

### Animation scalability (`game.json` → `animation`)

Animation is not a fixed-step sim, so — unlike particles — evaluation-rate
reduction is a real lever: a skipped character advances its clip/graph clock
but holds its pose, staying in sync and snapping to the correct frame when it
next evaluates. Live-tunable via `animation().setGlobal*`; per-actor
`important` (scene JSON on the `animation` component, or
`animation().setImportant`) exempts a character from the budget.

```json
{
  "kind": "com.hikari.config.game",
  "version": 1,
  "animation": {
    "cull_distance": 150,
    "lod_bands": [
      { "max_distance": 25, "rate": 1 },
      { "max_distance": 60, "rate": 2 },
      { "max_distance": 3.4e38, "rate": 4 }
    ],
    "max_evaluations_per_frame": 64
  }
}
```

| Field | Default | Meaning |
|-------|---------|---------|
| `cull_distance` | `0` (never) | Characters beyond this freeze entirely — no pose, no clock; the pose they slept with is the pose they wake with |
| `lod_bands` | empty | Animation-specific evaluation rates by distance. **Empty = follow the world tick-LOD bands** (one significance policy); non-empty decouples animation rates from gameplay rates |
| `lod_hysteresis` | `1.0` | Band-edge hysteresis for the animation-specific bands |
| `max_evaluations_per_frame` | `0` (off) | Hard cap on characters evaluated per frame, nearest first; `important` actors are exempt. Everything past the cap advances clocks only |

### Physics stepping (`game.json` → `physics`)

Launch-only — the backend consumes these at initialization, so there are no
runtime setters. Distance-based body sleeping is deliberately absent: the
backend's own sleep system owns that, and force-sleeping distant bodies breaks
gameplay.

| Field | Default | Meaning |
|-------|---------|---------|
| `gravity` | `[0, -9.81, 0]` | World gravity in m/s² |
| `substep_count` | `4` | Collision substeps per step (1–8) — the accuracy/cost knob |
| `max_substep_dt` | `1/240` | Largest dt one substep integrates; `substep_count × max_substep_dt` is the largest step absorbed without escalating work |
| `max_catchup_steps` | `4` | Whole catch-up steps per iteration after a hitch (1–8); the rest is repaid later or dropped — never a death spiral |
| `pace_hz` | `60` | Step-thread pacing target, and the step-size ceiling unless the substep budget is smaller |

### Example `configs/layout.json` (product pack layout)

Controls **what goes into which pack**, **how each pack is delivered**, and **what a retain pulls in with it**. The Shinra cook resolves it; **Project Settings → Packs** authors all of it — add and remove packs, reorder them, set role, delivery and root, and edit explicit members, include globs, dependencies and labels of the selected pack. **Absent file → one monolithic packed pack** holding everything the project ships — a working product with zero configuration.

```json
{
  "kind": "com.hikari.config.layout",
  "version": 2,
  "default_pack": "game",
  "packs": [
    {
      "stem": "damaged_helmet",
      "role": "assets",
      "include": ["scenes/damaged_helmet/**"],
      "members": ["models/helmet", "textures/helmet/"],
      "deps": ["shared"]
    },
    {
      "stem": "shared",
      "role": "assets",
      "labels": ["boot"],
      "include": ["models/**", "materials/**", "textures/**"]
    },
    { "stem": "scenes", "role": "scenes", "labels": ["boot"] },
    { "stem": "shaders", "role": "shaders", "labels": ["boot"] },
    { "stem": "content", "role": "content", "labels": ["boot"] },
    {
      "stem": "mods",
      "role": "files",
      "delivery": "loose",
      "root": "mods"
    }
  ],
  "ship_exclude": ["configs/packaging.json"]
}
```

| Field | Meaning |
|-------|---------|
| `role` | `assets` (cooked art, membership by `include` globs) · `scenes` · `shaders` · `content` (`content_dirs` with `bundled: true`) · `files` (a plain directory, named by `root`) · `all` (monolithic art plus system domains). Omitted: `assets` when `include` is present, `files` otherwise. |
| `delivery` | `packed` (default) → `data/bundles/<stem>.shinbundle` · `loose` → files under `root`. Any other value is a seal error. A packed pack with no members, a missing loose member, or a member outside its pack's `root` fails the seal. The stem and role `engine` are reserved. |
| `include` | Cook-tree globs, for role `assets` only. **After explicit membership, first match wins**, in declaration order — put specific packs above catch-alls. Globs on any other role are a cook error, not a silent no-op. |
| `members` | Explicit logical asset stems or folders ending in `/`, for cooked-art packs. An asset assignment wins over the most specific folder assignment, which wins over include rules. Asset suffixes normalize to a stem, so a model and its model document can stay together. Paths are case-sensitive and project-logical; no wildcards or `..`. Duplicate assignments are cook errors. |
| `deps` | Packs retained transitively with this one. For what no reference can express: a VO bank a script names by string. |
| `labels` | Retain-by-label groups. `hi.packs().openLabel("boot")` opens every pack carrying it. Packs with role `scenes`, `shaders`, `content` or `all` **must** carry `boot`: the session reads them before any scene can retain anything, and the cook refuses a layout where they do not. |
| `root` | Required for role `files` and for any `loose` pack. Bounds where uncatalogued (mod-added) files may resolve. |
| `default_pack` | Absorbs cooked art no pack claims. Reported, never a build failure. |
| `ship_exclude` | Globs over the product data root that never ship. Applied to pack members before packing and cataloguing, then to the remaining staged files; `configs/packaging.json` is always excluded and a project list adds to it. The seal's own outputs (`bundles/`, `assets.shingraph`) cannot be excluded. |

**Engine shaders** are not listed here. The product always seals them as stem `engine`.

#### Who resolves this

The **cook** does, once. Shinra reads this document, resolves explicit members and then globs against the cook tree, and writes `packs.shinplan.json` next to the asset manifest. Kaji seals what the plan says and the editor reads it; neither matches a glob. Editing this file re-resolves on the next cook, which watch mode triggers automatically.

The card counts are labelled **last cook**. The **Resolved contents** preview sends the unsaved layout to Shinra asynchronously and shows paths with their winning assignment, rule or fallback. **Default pack** includes fallback membership even when that pack is not declared explicitly; **Unmatched** lists assignments that currently match no cooked art. Preview does not save the draft or publish a new pack plan. A changed draft hides stale preview rows until the new answer arrives; **Refresh preview** also picks up a newer cooked inventory.

In the Asset Browser, right-click a cooked-art asset or a non-root folder and choose **Assign to pack…**. Select the destination pack in Project Settings and assign it, review the preview, then Save or Apply. **Use inherited rules** removes only that exact explicit assignment; ancestor folder assignments and include rules still apply. Assignment changes ownership, not the source file's location or its dependency references. Scene/prefab documents, shaders and directory/content domains retain their existing role-based placement; this preview resolves cooked art, not the full staged product tree.

#### Packed vs loose

A **packed** pack is a `.shinbundle` and the catalog is the whole truth of its membership. A **loose** pack is a directory, and the *directory* is the truth: a file dropped in after the build resolves exactly like a sealed one, **while the pack is open**. That is the only place an uncatalogued read is legal, which is what makes a partially moddable product expressible without weakening resolve anywhere else. Loose packs are additive — they never shadow a path a packed pack already owns.

#### Scenes do not declare packs

A scene names assets; the runtime derives which packs it needs from those references plus each pack's `deps`. There is no `packs` field in a scene document (one in an older file is ignored with a warning). A scene authored against a shipped product — by a modder, with no cook and no seal — resolves exactly like one that shipped with it. The editor's **Scene Packs** tab shows the resulting closure and which reference pulled each pack in.

**Runtime retain:** session open keeps the system packs (`engine`, `scenes`, `shaders`, `content`). Scene loads retain their derived closure. Game code opens the rest through `hi.packs()`. URI rules: [Assets and Shinra — Asset URIs and packs](/docs/systems/assets-and-shinra.md#asset-uris-and-packs).

Layout mode is still CLI `--resources=bundles|loose`, which now only decides whether *packed* packs are sealed at all.


### Config ownership

| Layer | Role |
|---|---|
| Zig `ProjectConfig` | Defaults for `.game` / `.editor` / `.startup` |
| Project/staged `configs/*.json` | Authored solo-file overlays; **JSON wins** when a field is present |
| Platform user-data `configs/*.json` | Sparse player overrides written by `hi.config.applyPlayerSettings` (or `save`); wins over the authored layer. Root: frontend `writable_path`, else the product folder named from the packaging identity ([Platforms](/docs/systems/platforms.md#per-user-writable-root)) |
| Engine `Config` facade | `load(T, file)` (read-only) / `edit(T, file)` (mutate + `save()`); shortcuts `sessionConfig()`, `input()`, … |

`startup.scene` is commonly set only in `configs/game.json`. When both Zig and JSON set it and they **differ**, compose logs a warning and JSON wins — so changing only the compiled recipe cannot silently fight a leftover overlay.

Hosts load authored configs from the project root (editor) or staged `data/`
(standalone), then load the writable layer scoped by the durable project id and
compose both with `composeLayers`. Absent files keep the previous layer.

Engine/editor authoring uses `Editable.save()` against the explicit project
root. Game code uses `hi.config.load/save/reset`: reads are user → authored,
writes are atomic and affect only the user layer. Known engine documents are
header-validated; custom nested `.json` documents are allowed under `configs/`.

### Content dirs (loose data)

Optional `content_dirs` lists project folders that Kaji copies into product `data/` for `content://` access. Policy lives **only** in this project file (no `hikari.content.json` per folder).

```json
{
  "content_dirs": [
    { "path": "resources", "bundled": true },
    "modding"
  ]
}
```

| Form | Local stage | `--package` |
|------|-------------|-------------|
| `"name"` or `{ "path": "name", "bundled": false }` | yes | yes — loose under `data/<path>/` |
| `{ "path": "name", "bundled": true }` | yes | yes — may pack into `content` when `--resources=bundles` (see [layout.json](/docs/guides/project-file.md#example-configslayoutjson-product-pack-layout)) |

Rules:

- `path` is a **single segment** under the project root (no `/`, `\`, `.`, `..`).
- Default `bundled` is **false** (string form is the same as `{ "path", "bundled": false }`).
- **Omitted** `content_dirs`: if `resources/` exists, stage it as loose-mod (ships naked).
- **Explicit** `"content_dirs": []`: stage nothing (disables the resources default).
- Duplicate paths, invalid entries, unknown properties, and a non-array value are build errors.
- Missing source directories are skipped quietly.

Prefer `"bundled": true` for tables you are fine packing; use `"bundled": false` for mod-friendly loose files (both ship when listed).

### Version axes (do not conflate)

These are independent; bumping one does not imply the others.

| Axis | Where | Role |
|---|---|---|
| Thin SDK `VERSION` | `src/hikari/sdk/VERSION` (copied into staged/packaged `sdk/VERSION`) | Packaging / human SDK identity. Kaji also writes `CONTENT_HASH` over staging inputs — **content edits restage even when `VERSION` is unchanged**. |
| Game ABI `abi_version` | `sdk/src/game_module_def.zig` `abi_version_current` (handshake symbols + `GameModule.abi_version`) | Editor ↔ `libgame` load gate. Bump on `GameModule` / thin ABI semantic breaks. HostApi subtable versions (`host_api_version_current`, `world_api_version_current`, …) are checked at `bindHost` and are separate from this integer. |
| Editor / engine version | `src/hikari/src/version.zig` `current.version.number` ↔ project `last_editor_version` | Product/editor identity for the project-open warning; the sibling `subtitle` and `slogan` are display identity, not compatibility keys. This is not the game-module ABI or thin SDK `VERSION`. |

### Packaging (game)

`configs/packaging.json` is the **game** shipping identity for `kaji game … --package` (see [Build and packaging](/docs/guides/build-and-packaging.md#distribution-packages)). It is not runtime session policy. Kaji requires this solo file — a top-level `packaging` block in `hikari.project.json` is rejected (same as the editor open path).

| Field | Role |
|---|---|
| `kind` / `version` | Document header: `com.hikari.config.packaging` / schema `1`. |
| `title` | Display name (Finder / Start menu / MSIX display name). Defaults to project `name`. |
| `product_version` | Marketing version (`1`–`4` numeric components). MSIX expands to four parts. |
| `identifier` | Reverse-DNS id (`com.studio.game`; letters, digits, hyphens). Defaults to `com.hikari.<slug>`. Also names the player's settings folder on macOS and Linux; `publisher` + `title` name it on Windows. |
| `authors` / `publisher` | Human credits; publisher seeds copyright and Windows display publisher. |
| `description` / `copyright` | Store / About strings. |
| `icon` | Project-relative `.png` (or platform-native icon). Required for `--package=bundle`. |
| `startup_background_color` | Optional `#RRGGBB`, default `#000000`. Author in **Project Settings → Packaging → Startup → Startup background**. Kaji compiles it into the macOS/Windows game frontend, including loose builds. Colors are opaque sRGB. Editor startup ignores it. |
| `macos.category` | `LSApplicationCategoryType` (default `public.app-category.games`). |
| `macos.minimum_system_version` | `LSMinimumSystemVersion` (default `26.0`; Metal 4.0 / macOS 26 product floor). |
| `macos.icon` | Optional macOS override: Icon Composer `.icon` (Liquid Glass via `actool` → `Assets.car` + fallback `.icns`), or `.icns`. Otherwise Kaji builds `.icns` from `icon` via `sips`/`iconutil`. |
| `windows.publisher` | MSIX identity publisher DN (e.g. `CN=Studio Name`). |
| `windows.publisher_display_name` | MSIX `PublisherDisplayName`. |
| `windows.icon` | Optional Windows logo override (PNG). |

Keep game icons under something like `packaging/` next to the game project root. The sample ships PNG icons under `hikari-games/example/packaging/`. For Liquid Glass on macOS 26+, author a real Icon Composer `.icon` and set `macos.icon` — do not wrap a finished PNG in a hand-rolled `.icon` (that produces nested frames or blank plates).

### Packaging (editor)

Two packaging identities:

| Product | Descriptor |
|---|---|
| `kaji editor` (no `--project`) | Engine-owned `src/hikari/editor.project.json` |
| `kaji editor --project=<game> --package` | Optional `editor` overlay in the game’s `configs/packaging.json` (falls back to shared packaging fields). Overlay marketing version is `version` (the solo-file root uses `product_version`). Paths relative to the game root. |
