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/../hikari-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/../hikari-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/../hikari-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).
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 |
--leak-policy=default|fail|report | With --run, choose leak handling: Debug failure by default, explicit failure, or reporting without failure. Tracking remains build-dependent |
--deep-validate | With --run, enable deep mesh validation |
--break-on-scene-load-error | With --run, break on scene-load errors in Debug |
--d3d12-gbv-pass=<label> | With --run, enable GPU validation for one exact pass label (requires the D3D12 debug layer) |
--zig=<path> | Editor with --run: explicit compiler for game-module builds |
--trust-project=<digest> | Editor with --run: approve the reviewed project/plugin digest; changes invalidate the grant |
--source-revision=<revision> / --source-vcs=<label> | Explicit build provenance; VCS defaults to external when a revision is supplied |
--stacktrace | Include exception stacks in Kaji diagnostics; available on all command option tables |
--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> | Job slots shared by every action in the build (default: host CPU count). Cargo joins the same pool through the GNU make jobserver; Zig and Odin take one slot plus whatever is free when they start |
--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 (defaults to product --config; explicit value overrides it) |
--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 (shadow-mapping conflict with Zig's allocator) |
--memory-leak-check | With --run, forward --leak-policy=fail so tracked subsystem leaks fail the process (Debug/ReleaseSafe; 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 localization [--project=<dir>] runs only the localization unit: it compiles assets/localization/<domain>/<locale>.localization.json into .engine/localization/ for the editor and, with --project, the game. zig build roots editor_text/game_text at those generated files and fails configure when a generated Id enum no longer matches its source catalog, naming this command; it never regenerates them itself.
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. The flags in this table 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.
Configuration and environment
Product behavior uses explicit options. Native frontends parse launch arguments, copy them into validated startup configuration, and share the host-owned configuration with dynamic drivers. They reject unknown arguments. The same runtime flags above work when launching the executable directly; omit Kaji's --run. The game frontend's --write-package-manifest is a standalone packaging command and cannot be combined with launch flags.
Shinra carries compiler paths in pipeline configuration (--akari-sc=<path>, --kawac=<path>) and passes shader profiles explicitly (--shaders-config=debug|release). Akari accepts compile --dxc=<path> and --config=debug|release; its cache includes the Akari executable identity and native compiler version. Compiler overrides and profiles no longer use project-specific environment aliases.
Child processes do not inherit your shell. Each receives only host plumbing (PATH, home and temp directories, proxies, CARGO_HOME/RUSTUP_HOME, DEVELOPER_DIR, Windows system variables) plus the overrides its toolchain declares, and only those overrides enter the action cache digest. Variables such as CPATH, SDKROOT, RUSTFLAGS, or a Developer prompt's INCLUDE never reach a tool, so they cannot change an output without a rebuild. Toolchains are keyed by their resolved executable and full version line, so a patch release, a dev build, or a different install of the same version rebuilds. Parallel actions never mutate the conductor's environment.
The remaining environment interfaces serve the operating system, external tools, or isolated test harnesses:
| Interface | Purpose |
|---|---|
PATH, HOME, USERPROFILE, APPDATA, LOCALAPPDATA, XDG_DATA_HOME, XDG_CONFIG_HOME, TMPDIR, TEMP / TMP, COMSPEC, ProgramFiles / ProgramFiles(x86) | Host paths, executable discovery, and shell integration |
DEVELOPER_DIR, TOOLCHAINS, CLANG_MODULE_CACHE_PATH, Visual Studio's toolchain environment, LIBCLANG_PATH, CMAKE | Native toolchain discovery and child-process configuration |
ZIG_LOCAL_CACHE_DIR, ZIG_GLOBAL_CACHE_DIR | Zig 0.17 build caches, set by Kaji per child process |
MTL_DEBUG_LAYER, MTL_DEBUG_LAYER_ERROR_MODE, MTL_DEBUG_LAYER_WARNING_MODE, MTL_SHADER_VALIDATION | Apple's process-level graphics diagnostics |
P4PORT, P4CLIENT, P4CONFIG | Perforce provenance discovery |
NO_COLOR | Standard terminal color preference |
NVIM_APPNAME | Neovim plugin installer application directory |
Cargo's CARGO_MANIFEST_DIR, OUT_DIR and compile-time Cargo metadata | Rust build protocol |
ODIN | Physics harness compiler default; --odin=<path> overrides it |
HIKARI_AREA_TEST_OUTPUT, HIKARI_VSM_TEST_OUTPUT | Optional shader-test artifacts; no effect on product behavior |
DYLD_FRAMEWORK_PATH, HIKARI_GPUTRACE_INSPECT_REEXEC | GPU-trace inspection tool framework loading and internal re-exec guard |
Source generation is explicit: zig build generate-document-schemas and zig build generate-cooked-layouts from src/hikari; Shinra fixture regeneration uses its named ignored tests. Ordinary freshness tests never rewrite source fixtures. Build smoke scripts accept --games=<path>, native-style wrappers accept a leading --zig=<path> and --llvm=<root>, and the shader formatter harness accepts its shader tree as an argument.
Stage model
Typed plans in src/kaji/src/Conductors/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, the project descriptor and product.json — the resolved identifier/title/publisher the engine names the player's settings folder from; 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, Product pack layout.
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 the Zig minimum in src/kaji/src/Build/KajiVocabulary.cs (0.17.0).
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; Vulkan shader cooking is available. Not a product peer (Platforms).
Smoke matrices: build/test-build-macos.sh and build/test-build-windows.ps1 run clean game/editor × debug/release × dynamic/monolithic builds, always passing the Example project explicitly. Pass --games=<path> to select a different games checkout. Game cells verify the cooked bundle catalog; editor cells verify the staged toolchain and reloadable game module without starting the editor. Both first run the Tenkai3D physics gate (python3 src/tenkai3d/tools/test_physics.py --soak; needs Python 3 and Odin on PATH) and stop on a physics failure (physics verification).
Local engine build tasks
Zig 0.17 uses -Doptimize=debug|safe|fast|small for direct build tasks. Windows/MSVC bindings use explicit C preprocessing in src/hikari/build/c_bindings.zig before translation to preserve pointer qualifiers; included headers remain tracked dependencies.
From src/hikari, direct Zig tasks remain useful for focused validation: zig build test -Dgame-src=<abs game src>, zig build test-graphics …, 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.
Rust dependency resolution
Akari and Shinra keep their Cargo.lock files in version control. Kaji invokes Cargo with --locked, so a build fails if a manifest change requires a different dependency resolution. Update the affected lockfile explicitly with the dependency change and include it in the same commit.