Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Guides
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
Guides21 min read

Project file

On this page
On this pageSolo configs (configs/)GPU adapter preferenceDocument headerSettings composition modelAsync-compute scheduling (render.json)Occlusion quality (render.json)Ray-tracing budget and frame governor (render.json)Example configs/game.jsonEditor frame pacing (editor.json → frame_pacing)World tuning (game.json → world)Particle scalability (game.json → particles)One significance spineAnimation scalability (game.json → animation)Physics stepping (game.json → physics)Example configs/layout.json (product pack layout)Config ownershipContent dirs (loose data)Version axes (do not conflate)Packaging (game)Packaging (editor) Back to top

Hands-on walkthrough: Tutorials — First project.

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 }
  ]
}
FieldRole
kindMust be com.hikari.project.
versionDescriptor schema version ("5" today). Not the engine/editor product version.
idRequired 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_sdkDiscovery 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 project parents for a sibling hikari/sdk (e.g. src/games/craft → src/hikari/sdk).
last_editor_versionWritten 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_dirsLoose content roots staged by Kaji into data/<path>/ (not Shinra). See Content dirs and Data, JSON, and modding.

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/:

FileRole
configs/game.jsonGame window (mode, display, resizable, VSync / frame rate) + startup.scene + asset residency budget + world tuning
configs/render.jsonRender pipeline (features / quality / look) for the game and editor viewport
configs/audio.jsonBoot mix (bus gains, ducking, reverb), voice budgets, and audio device policy
configs/drivers.jsonRuntime driver recipe and GPU adapter preference; driver implementations are inherited by the editor host
configs/editor.jsonEditor window + host frame pacing; optional sparse render / drivers overlays on the game recipes
configs/packaging.jsonShipping identity (Kaji --package; not runtime)
configs/layout.jsonPack graph: membership, delivery, deps, labels (Kaji seal + Project Settings → Packs)
configs/input_actions.jsonAuthored action ids and default bindings
Writable configs/input_bindings.jsonSparse per-user binding overrides; never shipped
configs/gameplay_tags.jsonGameplay-tag catalog (com.hikari.gameplay_tags). Not session compose — File → Project Settings… → Gameplay Tags

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:

ApplyMeaning
liveWritten to disk and pushed into the open editor session on Save (shadows, RT, reflections, AO, TAA, tonemap, …)
restartDisk only until the editor restarts (GPU adapter preference, graphics/audio drivers, editor module linkage, plugin enablement, …)
launchDisk 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.
packagePackaging 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,
  ...
}
FilekindSchema version
game.jsoncom.hikari.config.game1
render.jsoncom.hikari.config.render3
drivers.jsoncom.hikari.config.drivers1
editor.jsoncom.hikari.config.editor1
packaging.jsoncom.hikari.config.packaging1
layout.jsoncom.hikari.config.layout2
input_actions.jsoncom.hikari.config.input_actions2
input_bindings.jsoncom.hikari.config.input_bindings1
gameplay_tags.jsoncom.hikari.gameplay_tags1

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: HDR display output. world.residency_bind_budget_ms lives on game.json world — asset residency.

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.

json
{
  "kind": "com.hikari.config.render",
  "version": 3,
  "pipeline": {
    "quality": {
      "occlusion": {
        "mode": "two_phase",
        "profile": "stable"
      }
    }
  }
}
ProfilePolicyTypical use
stableGuard nearly all deliberate camera motionRacing, fast traversal, fly cameras
balancedKeep Hi-Z during ordinary movement; guard fast motionGeneral third-person / open world
performanceKeep Hi-Z active through motionConstrained 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. See Asset residency.

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"
  }
}
FieldDefaultWhen it applies
ac_activehz_60Play, viewport navigation, UI interaction, while on AC
ac_idlehz_30Settled Edit with no input, on AC
battery_activehz_30Active, unplugged
battery_idlehz_15Idle, unplugged
saver_activehz_30Active, OS Low Power Mode / battery saver
saver_idlehz_10Idle, OS Low Power Mode / battery saver

A machine the OS cannot classify (typical desktop) stays on the AC rates. Project Settings → Editor authors this live; Project Settings → Game authors standalone window.vsync / window.frame_rate (next game launch).

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
  }
}
FieldDefaultMeaning
lod_bandsfull rate everywhereTick-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_hysteresis1.0Distance past a band edge before an actor switches, so one loitering on a boundary does not flip rate every frame
lod_budget1024Actors re-bucketed per frame; trades how fast a rate change takes effect against frame cost. Also budgets the auto-distance sweep
lod_auto_distancetrueEngine 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_scale1Multiplier 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_time0.25Seconds 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_hz0 (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_steps8Fixed steps per frame before the accumulator is clamped; the clamp is what stops a long frame spiralling
job_workers0 (auto)Job-system threads. Auto leaves a core for the main thread and caps at 16
scene_preload_budget_ms4Per-frame streaming residency budget
scene_instantiate_budget_ms4Per-frame actor spawn budget
expected_entities0Rough 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.host_api.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
  }
}
FieldDefaultMeaning
spawn_scale1Multiplies 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_frame8Fixed-step catch-up cap per system per frame (1–8). Lower drops sim time after a hitch instead of spiking the GPU
max_frame_delta0.25Frame delta clamp (seconds) fed to the fixed-step accumulator
cull_distance0 (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_band5Width of the dim-out band just inside the cull distance, so systems fade instead of popping off
max_total_capacity0 (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
littrueMaster 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_fadetrueMaster 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
  }
}
FieldDefaultMeaning
cull_distance0 (never)Characters beyond this freeze entirely — no pose, no clock; the pose they slept with is the pose they wake with
lod_bandsemptyAnimation-specific evaluation rates by distance. Empty = follow the world tick-LOD bands (one significance policy); non-empty decouples animation rates from gameplay rates
lod_hysteresis1.0Band-edge hysteresis for the animation-specific bands
max_evaluations_per_frame0 (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.

FieldDefaultMeaning
gravity[0, -9.81, 0]World gravity in m/s²
substep_count4Collision substeps per step (1–8) — the accuracy/cost knob
max_substep_dt1/240Largest dt one substep integrates; substep_count × max_substep_dt is the largest step absorbed without escalating work
max_catchup_steps4Whole catch-up steps per iteration after a hitch (1–8); the rest is repaid later or dropped — never a death spiral
pace_hz60Step-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 the 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/**"],
      "deps": ["shared"]
    },
    {
      "stem": "shared",
      "role": "assets",
      "labels": ["boot"],
      "include": ["models/**", "materials/**", "textures/**"]
    },
    { "stem": "scenes", "role": "scenes" },
    { "stem": "shaders", "role": "shaders" },
    { "stem": "content", "role": "content" },
    {
      "stem": "mods",
      "role": "files",
      "delivery": "loose",
      "root": "mods"
    }
  ],
  "ship_exclude": ["configs/packaging.json"]
}
FieldMeaning
roleassets (cooked art, membership by include globs) · scenes · shaders · content (content_dirs with bundled: true) · files (a plain directory, named by root). Omitted: assets when include is present, files otherwise.
deliverypacked (default) → data/bundles/<stem>.shinbundle · loose → files under root
includeCook-tree globs, for role assets only. 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.
depsPacks retained transitively with this one. For what no reference can express: a VO bank a script names by string.
labelsRetain-by-label groups. hi.packs().openLabel("boot") opens every pack carrying it. Packs with role scenes, shaders or content must carry boot: the session reads them before any scene can retain anything, and the cook refuses a layout where they do not.
rootRequired for role files and for any loose pack. Bounds where uncatalogued (mod-added) files may resolve.
default_packAbsorbs cooked art no pack claims. Reported, never a build failure.

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, matches the 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.

Between a save here and that cook, the editor shows the previous partition and says so.

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.host_api.packs(). URI rules: Assets and Shinra — Asset URIs and packs.

Full design: resources-layout-and-logical-packs.md. Layout mode is still CLI --resources=bundles|loose, which now only decides whether packed packs are sealed at all.

Config ownership

LayerRole
Zig ProjectConfigDefaults for .game / .editor / .startup
Project/staged configs/*.jsonAuthored solo-file overlays; JSON wins when a field is present
Platform user-data configs/*.jsonSparse player overrides written by hi.config; wins over the authored layer
Engine Config facadeload(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"
]
FormLocal stage--package
"name" or { "path": "name", "bundled": false }yesyes — loose under data/<path>/
{ "path": "name", "bundled": true }yesyes — may pack into content when --resources=bundles (see layout.json)

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: first wins (warning). Invalid entries are skipped (warning).
  • 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.

AxisWhereRole
Thin SDK VERSIONsrc/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_versionsdk/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 versionsrc/hikari/src/version.zig current.version.number ↔ project last_editor_versionProduct/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). 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).

FieldRole
kind / versionDocument header: com.hikari.config.packaging / schema 1.
titleDisplay name (Finder / Start menu / MSIX display name). Defaults to project name.
product_versionMarketing version (1–4 numeric components). MSIX expands to four parts.
identifierReverse-DNS id (com.studio.game). Defaults to com.hikari.<slug>.
authors / publisherHuman credits; publisher seeds copyright and Windows display publisher.
description / copyrightStore / About strings.
iconProject-relative .png (or platform-native icon). Required for --package=bundle.
startup_background_colorOptional #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.categoryLSApplicationCategoryType (default public.app-category.games).
macos.minimum_system_versionLSMinimumSystemVersion (default 26.0; Metal 4.0 / macOS 26 product floor).
macos.iconOptional 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.publisherMSIX identity publisher DN (e.g. CN=Studio Name).
windows.publisher_display_nameMSIX PublisherDisplayName.
windows.iconOptional Windows logo override (PNG).

Keep game icons under something like packaging/ next to the game project root. The sample ships PNG icons under src/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:

ProductDescriptor
kaji editor (no --project)Engine-owned src/hikari/editor.project.json
kaji editor --project=<game> --packageOptional 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.
PreviousBuild and packagingNext Plugins

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/guides/project-file.md
On this pageSolo configs (configs/)GPU adapter preferenceDocument headerSettings composition modelAsync-compute scheduling (render.json)Occlusion quality (render.json)Ray-tracing budget and frame governor (render.json)Example configs/game.jsonEditor frame pacing (editor.json → frame_pacing)World tuning (game.json → world)Particle scalability (game.json → particles)One significance spineAnimation scalability (game.json → animation)Physics stepping (game.json → physics)Example configs/layout.json (product pack layout)Config ownershipContent dirs (loose data)Version axes (do not conflate)Packaging (game)Packaging (editor) Back to top