One conductor: Kaji
Kaji is the only product build tool. Run it from the repository root:
# Game, monolithic debug build (staged under bin/game/)
bin/kaji/kaji game --workspace="$PWD" --project="$PWD/src/games/example" --type=monolithic --config=debug
# Project-bound editor: develop a game with Play/Edit, scene authoring, recompile
bin/kaji/kaji editor --workspace="$PWD" --project="$PWD/src/games/example" --type=dynamic --config=debug --run
# Standalone editor (no project; boots the project selector)
bin/kaji/kaji editor --workspace="$PWD" --type=dynamic --config=debug --run
# Shippable artifacts
bin/kaji/kaji game --workspace="$PWD" --project="$PWD/src/games/example" --type=monolithic --config=release --package=bundle
bin/kaji/kaji editor --workspace="$PWD" --type=monolithic --config=release --vendor:zig=fetch+bundle --package=bundleKaji builds vendor units, the native bridge, Opus, Kawa, Shinra, Akari, the engine core, shader packages, and the native frontends as one dependency graph. Units that do not read each other's outputs build concurrently. Zig emits application and plugin objects; Kaji owns the final executable link on every host. Build units are *.kaji.cs files stored beside their source; vendors enter the graph only through KajiUnitCatalog.RegisterVendor (Tenkai3D yes, Tenkai2D no).
The platform scripts under build/ (build-game-macos.sh, build-editor-windows.ps1, …) only bootstrap Kaji and forward options. Product policy lives in Kaji, never in those scripts.
Product options
| Option | Meaning |
|---|---|
game / editor | Product frontend |
--workspace=<repo> | Workspace root (auto-discovered inside an engine workspace) |
--project=<dir|hikari.project.json> | Game project. Required for game; optional for editor (omit = standalone host + project selector). Flag form only; bare positional paths are rejected |
--type=monolithic|dynamic | Driver/application/platform linkage (default monolithic). The editor always loads game code dynamically regardless of this flag |
--config=debug|release | Product configuration (default debug) |
--run | Launch the staged product from bin/game/ after success, with live stdio |
--log=<path> | With --run, forward as the product's engine log file path |
--clean | Wipe bin/* and build/cache/* (except Kaji) plus disposable <project>/.engine/* (keeps .engine/user/). Asks for confirmation unless --yes / --agent |
--yes / --silent / --agent | Skip the --clean confirmation; --silent and --agent also quiet conductor output |
--ui=auto|log|live | Console frontend. live redraws a status block in place; log prints one line per action (CI, pipes). auto picks live on an interactive terminal |
--trace=<path> | Write a Chrome trace-event file of every action and unit (open in Perfetto or chrome://tracing). The finish line also prints the critical path: the dependency chain that bounded the wall clock |
--until=<unit,…> | Build the named units and everything they depend on, nothing after them. Ids are what kaji plan prints: hikari, kawa, shaders, package-host, assets, … |
--only=<unit,…> | Build just the named units; what they depend on must already be built |
--events=<path> | Stream build events as NDJSON (one object per line) for the editor console and CI |
--parallel=<jobs> | Concurrent tool processes per unit (default: host CPU count) |
--dep-config=<unit>:debug|release | Override the configuration of a vendor unit, shinra, or akari |
--shinra:config=debug|release | Shinra tool configuration. Default release even under product debug; flip only when debugging Shinra |
--shaders:config=debug|release | Akari shader compile profile (default debug). Independent of product --config |
--resources=bundles|loose (alias --data-layout=) | game and kaji assets only. Product data layout under data/ (default bundles). Rejected on kaji editor, which never seals packs. Pack membership and delivery come from configs/layout.json (Project file) |
--package[=bundle|loose] | After staging, emit a distribution artifact under bin/packages/ (Distribution packages) |
--vendor:zig=fetch|bundle|fetch+bundle | Editor only. fetch downloads the pinned Zig archive into 3rd-party/zig/ when missing; bundle stages it into bin/game/tools/zig/ and fails on a missing archive or version mismatch. Without it the editor discovers a toolchain at runtime (Zig toolchain discovery) |
--vendor:engine=bundle | Editor only. Stage src/hikari/sdk/ into bin/game/sdk/ with the shader contract overlaid into sdk/shaders/. A leftover bin/game/sdk wins over the source tree, so restage or --clean after editing the SDK |
--memory:asan | AddressSanitizer for supported native builds. Rejected on macOS with Zig 0.16 (shadow-mapping conflict with Zig's allocator) |
--memory-leak-check | With --run, set HIKARI_FAIL_ON_LEAK=1 so a subsystem-allocator leak at shutdown fails the process (ReleaseSafe; Debug already panics unless HIKARI_ALLOW_LEAK=1; ReleaseFast does not track) |
Profiler and diagnostics switches
| Option | Meaning |
|---|---|
--profiler-timing[=true|false] | Compile the CPU profiler (default: on in debug, off in release). Runtime Recording is off until enabled; the editor Profiler tab works in Edit mode |
--profiler-timing:modules=markers,pix,xcode | Profiler modules: markers (GPU pass labels and debug names for encoders, buffers, textures, pipelines; default in debug), pix (Windows PIX), xcode (Instruments) |
--profiler-physics[=true|false] | Physics memory/state diagnostics for the editor Physics tab (default off) |
--profiler-audio[=true|false] | Audio bus/voice/worker diagnostics for the editor Audio tab (default off) |
--profiler-residency[=cpu|gpu|cpu+gpu|off] | Residency diagnostics (default off). cpu adds recent-free history and editor snapshots; gpu adds renderer snapshots. Refcounts, budgets, and eviction are always compiled (asset residency) |
Older spellings (--profiler, --profiler:modules, --residency) are rejected.
Plan and explain
Host packaging, editor shader staging, the asset toolchain, the game cook and seal, and the distribution package are nodes of the same graph with declared inputs and outputs, so a rebuild that changed nothing runs none of them and explain reports why one would.
kaji plan game|editor [options] prints the lowered action graph (every command, input, and output) without running anything. kaji explain … adds each action's cache verdict, so "why did that rebuild" is answered before the build. Both take the product options above plus --json.
Graphics diagnostics (D3D12 / Metal)
--d3d12-* and --metal-* are aliases for the same knobs; only the host backend emits defines. All are build-time, accept --flag, --flag=true, or --flag=false, and work in either configuration.
| Flag | Default | D3D12 | Metal |
|---|---|---|---|
--d3d12-debug-layer / --metal-debug-layer | on in debug, off in release | Debug layer + DRED; validation info queue drained into the log once per present | MTL_DEBUG_LAYER=1 set before Metal initializes |
--d3d12-trace / --metal-trace | off | Verbose native tracing (HIKARI_D3D12_LOG_TRACE) | Verbose native tracing |
--d3d12-gbv / --metal-gbv | off | GPU-Based Validation (very slow) | MTL_SHADER_VALIDATION=1 |
--metal-warning-mode=nslog|assert|ignore | ignore | — | MTL_DEBUG_LAYER_WARNING_MODE; implies --metal-debug-layer |
Notes:
- Metal validation is process-wide and also wraps AppKit/QuartzCore encoders. On macOS 26 those emit false
unused bindingwarnings at startup, so the warning channel defaults toignore. Validation errors always log. Use--metal-warning-mode=nslogfor an audit, orassertto halt inside the first offending call under a debugger. The mode is compiled in withsetenv(…, 0), soMTL_DEBUG_LAYER_WARNING_MODE=ignore ./hikari-editor …overrides it per run. - Two known false positives in Metal's own bookkeeping (acceleration-structure encoders and MetalFX-opened encoders) are corrected by shims in
src/hikari/src/native/macOS/src/MetalDebug.m. Each logs a one-shot "obsolete on this OS, delete it" line if an OS update fixes the layer itself. - Attribution help in a markers build: the once-a-second encoder census (encoders opened by kind, how many were empty, how many vendor encodes ran) and the per-pass empty-encoder report. If Metal prints an empty-encoder warning and the report does not name a pass, the encoder is not ours.
- On Windows the native logger also appends to
%TEMP%\hikari-d3d12-debug.logwhen the debug layer is compiled in, since a windowed process has no console. D3D12 INFO chatter is filtered at the info-queue storage filter. - Changing any of these flags rewrites the product-settings stamp and rebuilds the native bridge, so a toggle always takes effect.
.cpp/.medits are tracked build inputs and do not need--clean.
Windows: D3D12 Agility SDK
Windows stages copy the pinned Agility redistributable into bin/game/D3D12/ (D3D12Core.dll, d3d12SDKLayers.dll) and WinPixEventRuntime.dll beside the executables. The frontends export D3D12SDKVersion / D3D12SDKPath so the system loader picks them up. Pins: 3rd-party/d3d12-agility/, 3rd-party/winpix-event-runtime/.
Logging under --run
- Terminal: product stdio is inherited. Build steps capture their own output; only the final process streams live.
- Editor UI: the bottom console sink stays registered.
- File: NDJSON. The editor defaults to
<project>/.engine/logs/editor-latest.jsononce a project is open; the game needs--log=<path>.
Engine-side contract: Frontends and drivers.
Stage model
Typed plans in src/kaji/src/HikariBuildPlans.cs choose the host and build units. Engine build products install under bin/hikari/; the runnable stage is bin/game/; distribution artifacts land under bin/packages/.
- Dynamic stages separate application, platform, windowing, input, renderer, physics, scripting, and game-UI libraries. Monolithic links them into the executable. The two modes exercise different ownership and loading paths; a monolithic success is not evidence for a dynamic module lifecycle change.
- The editor always dlopens the project's game module after open/recompile.
--type=never bakes game code into the editor. ProjectConfigemits the driver package manifest from the same typed recipes runtime selection uses, so the stage list cannot drift from configuration.- Game modules built under the editor pass
-Deditor-contrib=true, exposinghi.editor.kaji gameomits it, so editor-only game code stays out of shipping binaries (Game editor SDK). - Plugins are source-composed into the game module from
hikari.plugins.json; native link and runtime staging inputs come only from the generatedhikari-plugin-artifacts.json(Plugins).
Distribution packages
Without --package, Kaji stops at the staged bin/game/ tree. With it, Kaji filters the stage into a shippable payload and asks the host packager for an artifact:
| Flag | macOS | Windows | Linux |
|---|---|---|---|
--package / --package=bundle | .app (ad-hoc signed) | .msix (Windows SDK MakeAppx) | not implemented |
--package=loose | .zip | .zip | .zip |
bin/packages/<slug>-<version>-<platform>-<arch>-<config>/
Sample.app | Editor.app | *.msix | *.zipGame slug is the packaging title; the editor slug is editor. A standalone editor package ships the host plus project selector. A project-bound editor package (--project=<game> --package) uses that game's packaging.editor block.
| Product | Keeps | Strips |
|---|---|---|
game | hikari, game application/module, sealed data/ (packs plus loose configs and identity; every content dir ships, packable ones inside the content pack, others loose for modding). configs/packaging.json never ships | editor frontend and application, tools/ |
editor | hikari-editor, editor application, backends, tools/ (Shinra, Zig SDK), loose data/_engine | game frontend, game application/module, game packs |
Product resource layout. After cook and shaders, Kaji seals packs under data/bundles/ with the catalog product.shinbundle.map.json (bundles mode) or stages the same paths loose. Without configs/layout.json there is one game pack plus product-sealed engine shaders. With one, each pack declares membership globs, delivery (packed or loose), deps, and labels; a loose pack stages as a directory and stays moddable. Cooked art no pack claims joins default_pack and is reported, never a build failure. Content dirs marked bundled: true may enter the content pack; bundled: false always stages loose under data/<path>/. Details: Data, JSON, and modding, Project file, design in resources-layout-and-logical-packs.
Zig bundling for self-contained editor packages that compile game modules offline:
--vendor:zig= | Effect |
|---|---|
| (omitted) | No download, no tools/zig/ staging; runtime discovery uses env/PATH |
fetch | Download the pinned host archive into 3rd-party/zig/ if missing |
bundle | Stage into tools/zig/; fail if missing or version mismatch |
fetch+bundle | Both |
Pin: 3rd-party/zig/VERSION, which must match src/hikari/build/Hikari.kaji.cs.
Host details. Loose and MSIX staging hardlink where the volume allows; the macOS .app copies because codesign rewrites Mach-O in place. Inside an .app, binaries live in Contents/MacOS/ and content in Contents/Resources/data/ with a MacOS/data symlink so exe_dir/data/… lookups keep working. A PNG or .icns icon becomes AppIcon.icns; an Icon Composer .icon (packaging.macos.icon) is compiled with actool into Assets.car plus a fallback .icns and CFBundleIconName, so macOS 26 renders Liquid Glass. Windows .msix requires the Windows 10/11 SDK; certificate selection is left to your release pipeline. Packagers live under src/kaji/src/Packaging/ (IPlatformPackager per host).
Packaging identity
Shipping identity is product-owned JSON that Kaji reads without compiling the game:
| Product | Descriptor |
|---|---|
kaji game … --package | Game configs/packaging.json (required) |
kaji editor … --package (no project) | src/hikari/editor.project.json → packaging |
kaji editor --project=<game> … --package | Game configs/packaging.json → editor (falls back to the shared fields) |
Fields: title, product_version, identifier, icon, authors/publisher, macos / windows blocks, optional startup_background_color (#RRGGBB, default black; the solid cover a standalone game shows while the first scene loads; embedded at build time, so rebuild after changing it). See Project file.
Host support
- macOS: Apple Silicon (
aarch64) only, Metal 4 MSL, product floor macOS 26. Tenkai3D compiles with Homebrew Odin plusllvm@22/lld@22. An unversionedbrew install llvmproduces a stublibtenkai3d.dylib(every export isbrk); Kaji fails that at the Odin step. Repair withbrew install llvm@22 && brew reinstall odin. - Windows:
x86_64, D3D12 with the Agility SDK staged beside the exe. - Linux:
x86_64build plumbing exists, but the runtime is incomplete and skips Akari shader compile. Not a product peer (Platforms).
Smoke matrices: build/test-build-macos.sh runs clean debug/release × dynamic/monolithic (dynamic via kaji game, monolithic via kaji editor, every cell with --project=). build/test-build-windows.ps1 runs the same grid but omits --project= on its game cells, so it is not yet a green peer. Both first run the Tenkai3D physics gate (python3 src/tenkai3d/test.py --soak; needs Python 3 and Odin on PATH) and stop on a physics failure (physics verification).
Local engine build tasks
From src/hikari, direct Zig tasks remain useful for focused validation: zig build test -Dgame-src=<abs game src>, zig build entity-bench …, zig build check-windows …. Use Kaji for real game and editor products; do not assemble ad hoc links. See Development → Validation.