Purpose and boundaries
This repository is a polyglot desktop game-engine workspace. The game owns its project configuration, entities, scene data, source assets, and optional global subsystem. The engine owns runtime orchestration, scene execution, graphics policy, input/action resolution, asset access, UI composition, and editor chrome.
The Zig product graph has a strict one-way boundary. src/hikari/src/hikari_api.zig
is the runtime/game root and cannot reach src/hikari/src/editor/. The editor uses the
separate src/hikari/src/hikari_editor_api.zig root, which composes the runtime
surface with editor-only code. Runtime and editor tests are built from those
separate roots as well; editor test coverage is never obtained by importing the
editor into the runtime module.
The design keeps stable authoring contracts above replaceable or platform-specific implementation:
Diagram source
flowchart TB
native["Native host"] -->|C ABI| app["Game / editor application"]
app --> session["SessionCore"]
session --> host["GameHost"]
session --> world["World"]
session --> services["Runtime services"]
host --> global["Cross-scene state"]
world --> scene["Actors · scripts · UI"]
services --> runtime["Drivers · threads · assets"]
class session accentSessionCore is the main live-runtime seam. It owns the world, dispatch system, physics subsystem, audio system (driver + command worker), renderer and render thread, input/action state, game host, and content paths. App and EditorApp are hosts around it; the native frontends retain process/event-loop ownership. Within World, domain-owned state is embedded by value (PhysicsState and the event dispatcher state in World.events) so ownership is visible without adding allocations or pointer indirection to hot paths. Cross-scene state and session scripts are documented in Session services. Audio threading is documented in Audio.
Native host domains
The macOS and Windows process layers use the same ownership shape under src/hikari/src/native/<platform>/src/:
| Domain | Owns |
|---|---|
application/ | Boot and ordered shutdown orchestration only |
host/ | Minimal process-lifetime state shared by native domains |
windowing/ | Primary/tool windows, registry/controllers, chrome, window events, and window exports |
frame/ | Display-driven scheduling, pacing policy application, and frame exports |
startup/ | Startup presentation and its completion export |
system/ | Appearance and power-state observation |
services/ | Dialog, clipboard, and file-manager integrations |
The dependency direction is inward toward host/; host/ does not import the feature domains. Native frontends use the public platform headers and provide renderer callbacks, but do not reach into HWND/NSWindow registries or own native-bridge globals. Tool-window creation is an atomic platform operation over the shared native/include/NativeWindowing.h contract: validation, OS-window creation, renderer-surface initialization, registration, input adoption, geometry publication, and rollback stay inside windowing/.
Kaji compiles each native domain into its own object directory and links only the object patterns for the currently declared domain set. Per-domain compile steps prune orphan objects, and static archives are recreated. A removed or renamed domain can therefore leave an inert cache directory, but it cannot leak stale code into a later link.
CPU jobs vs file streams vs audio: dispatch runs short non-blocking work on a worker pool. Long-lived append IO uses io_stream (one writer thread, per-stream SPSC slots, drop-if-busy) so the game thread never blocks on disk — profiler file traces and the engine log file tee are clients. Audio is a third pattern: game posts fixed-size commands; a dedicated audio worker owns decode and voices; the OS device callback only mixes (see Audio).
Repository map
| Area | Responsibility |
|---|---|
src/hikari | Zig engine, public API, native frontends, platform adapters, built-in shaders |
src/games/example | Sample game package, scenes, source assets, input actions, game shaders |
src/games/craft | Voxel dig/place sample game |
src/games/metropolis | Crowd / city streaming sample |
src/games/thomas | Narrative / level sample |
src/kaji | .NET build conductor and typed product/build-unit plans |
src/kawa | C scripting VM, compiler, archive support, examples, editor integrations |
src/shinra | Rust asset compiler, bundle builder, validators, watch mode |
src/akari | Rust shader language + end-to-end Metal/DXC compile |
src/tenkai3d | 3D physics middleware (wired into the engine) |
src/tenkai2d | 2D physics middleware (not in the engine 3D path; not product-registered) |
Naming glossary (easy to confuse)
These paths are intentionally different; do not merge them mentally or in PRs.
| Name | Means | Not to confuse with |
|---|---|---|
src/hikari/src/native_frontends/ | Native process entry (ObjC++/C++ main, event loop) | src/hikari/src/application/ (Zig application ABI/lifecycle) |
src/hikari/src/application/ | Zig game/editor application libraries over hikari_frontend.h | native_frontends/ (OS entrypoints) |
src/hikari/src/graphics/ | Shared renderer policy by feature (renderer/, rendergraph/, rhi/, shadow/, raytracing/, material/, residency/, effects/, gpu/, debug/) | Platform encode in platform/*/graphics/ |
src/hikari/src/scene/components/ | Actor component types + store | scene/world/ (world domain modules) |
src/hikari/src/editor/panels/ | Inspector, hierarchy, residency, … | editor/dialogs/, editor/viewport/ |
src/games/example/scenes/ | Authoring scene JSON documents (placeable actors) | src/games/example/assets/scenes/ (per-scene asset bags: models, materials, scripts for demos) |
src/games/example/assets/ | Cookable art tree (textures, models, materials, shaders, scripts) | src/games/example/resources/ (loose content dirs from content_dirs; often not Shinra-cooked the same way) |
src/games/example/resources/ | Project content roots (hikari.project.json → content_dirs) for ContentRef / staged loose data | assets/ (pipeline art) |
src/hikari/sdk/ | Thin game ABI pack (hikari_game); what games import | src/hikari/src/game_api/ (host-only bind; never packed into the SDK) |
src/hikari/src/backend/ | Driver contracts + registry.zig | Bodies in backend/modules/; thin build roots src/hikari/src/driver_module_*.zig (Zig package root must be src/) |
src/hikari/src/tools/ | Zig CLI / bench bodies (entity_bench, …) | Thin roots under src/ |
src/kawa/tools/kawac/ | Offline bytecode compiler (kawac) | Built by Kawa Kaji unit; staged to bin/game/tools/ for Shinra |
src/games/example (repo sample) | Full sample product project | src/hikari/standalone_game/ (editor stub when kaji editor has no --project) |
bin/hikari/ | Engine/tool staging from Zig build | bin/game/ (packaged product under Kaji) |
Human-facing change seams and validation: Development guide.
World space is right-handed, Y-up, −Z-forward with reverse-Z clip depth. See Coordinate space.
Public and private seams
Each seam answers the same four questions: why it exists, what crosses it, what guards it, and how to change it (including which version to bump).
Game SDK seam (hikari_game)
Why. Game modules reload independently of the host; they must not share engine memory layout.
What. Games import only hikari_game (src/hikari/sdk/src/). Product Zig uses const std = @import("hikari_std"); — only sdk/src/std/root.zig may import Zig std. The host installs a versioned HostApi (required subtables world / render / ui / animation / particle; optional debug / paths / jobs / audio / settings / config). Facades: hi.world(), hi.render(), hi.ui(), hi.debug(), hi.audio(), hi.animation(), hi.host_api.particle(), hi.settings, hi.config. Entities are generation-checked ActorRef handles (EntityId is an alias). The game exports config and content_manifest; each defineActor becomes one host archetype descriptor.
Optional editor chrome. With -Deditor-contrib=true (editor recompile always; standalone games default off), hi.editor and optional GameModule.register_editor register menus/panels/commands via C-ABI → host editor/game_contrib.zig. Not compiled into non-editor game modules — Game editor SDK.
Guard rails. C-ABI handshake (hikari_game_abi_version, hikari_game_zig_version) before reading GameModule; then abi_version, Zig/optimize/profiler gates; then per-table api_version at bindHost. Stale EntityId is a no-op. Editor contrib uses its own api_version_current and compile-time isolation.
Changing it.
| Change | Action |
|---|---|
Add/change a fn or field in a subtable (WorldApi, RenderApi, UiApi, DebugApi, AnimationApi, ParticleApi, AudioApi, JobsApi, PathsApi, SettingsApi, ConfigApi) | Bump that table's *_api_version_current; implement in host_bind.zig; add the facade method beside the table |
Add/remove a subtable, touch root HostApi fields | Bump host_api_version_current. Extract a new subtable only when a domain is its own subsystem (animation / particle / audio); do not split WorldApi to shrink the vtable — see src/hikari/AGENTS.md WorldApi policy |
Semantic change to GameModule loading/lifecycle | Bump abi_version_current (game_module_def.zig) |
editor_contrib.Api layout / semantics | Bump editor_contrib.api_version_current; update host game_contrib.zig |
| Facade methods / helper types over existing table entries | No bump — they compile into the game module |
Shared leaf types (motion, render_config, …) | Import the named build modules only, never by path — file identity must stay unique |
Session policy: Zig GameModule.config defaults + authored/staged solo configs/* overlays + writable per-user overlays (Config.sessionConfig / project_config.composeLayers). Load each layer once; compose in order without a second disk pass. Kind registry SSOT: project_config.kinds. hikari.project.json is identity only and its durable id scopes the platform user-data root. Editor chrome is engine-owned; game_ui is runtime UI / viewport only.
Driver seam (backend modules)
Why. Platform and middleware implementations (windowing, input, rendering, physics, scene scripting, game UI, audio) must be replaceable per product and platform — dynamic on desktop/tools, statically registered on consoles — without platform switches leaking into shared engine code.
What. Shared runtime code asks backend.Registry for type-erased driver implementations; dynamic driver dylibs export a DriverModule descriptor of factories. This is an engine-internal Zig contract, deliberately not a third-party binary ABI: engine types cross it freely because drivers rebuild with the engine.
Guard rails. Versioned entry symbol (hikari_driver_module_entry_v1 — an incompatible module simply fails symbol lookup), format_version, engine_contract_version, a LayoutFingerprint derived from every DriverModule field's name/offset/size/alignment (catches a forgotten version bump; derived rather than hand-listed so a new field cannot escape it), and Zig/optimize-mode checks. Process-wide singletons (log sink, geometry ledger) are installed into each loaded module through the single install_host_state hook — a driver dylib carries its own copy of every engine global, so adding one means adding a field to HostProcessState and a line to its adopt, never a per-driver edit. Drivers are destroyed before their module unloads. Shared code must not select platforms with builtin.os.tag or expose native graphics resources through the public graphics API.
Changing it. Factory, vtable, or cross-module type change → bump engine_contract_version (backend/module.zig). DriverModule descriptor layout change → bump module_format_version and the entry-symbol suffix. New driver kinds follow the checklist in Frontends and drivers.
Editor authoring seam
Why. The editor links the full engine, so its risk is not ABI but unmediated mutation: edits that bypass history break undo and desynchronize the authored document from the live preview.
What. SceneDocument is the authored source of truth; the running World is a live preview. Enter only via editor/authoring/scene_mutation.zig (Operation, max_operations, applyBatch / apply / applyJoin, txn helpers) — host chrome, MCP scene_edit / plane tools, and plugin hi.editor.scene() all share that plane. Each batch becomes document history with one undo label; live apply uses ActorMutation payloads (SceneEditAccess) so document and preview stay in sync. Mutations carry enough to invert — undo/redo and live sync fall out of one channel.
Guard rails. No front-end may call ad-hoc SceneDocument mutators for authoring. editor.Document remains the history boundary under the plane (component fields via ActorMutation.component_field / ComponentFieldEdit, entity active via ActorMutation.active); round-trip serialization tests plus comptime field-count tripwires catch a scene field added in one spot but not the others; retained widgets must not keep pointers into document actor storage.
Changing it. No version numbers — editor and engine are one binary. New op kinds land in scene_mutation first (MCP wire + SceneApi bridge consume them). An authorable scene/component field touches every serialization spot and the mutation path; the comptime tripwires fail the build if one is missed. New mutation kinds must carry their inverse.
Graphics boundary
Viewport output, active-render and allocation extents, plus the host presentation-list fallback, live in graphics/renderer/surface_policy.zig. Frame preparation and orchestration read this policy directly; dimensions are not backend hooks. Screenshot request handling and encoding live in graphics/renderer/screenshot.zig, with native pixel readback supplied by each backend. Ray-tracing residency rows come from the shared scene owner, and micromap arena growth follows the shared opacity-micromap limits.
On a macOS or Linux host the D3D12 half of platform/ is never analysed by an ordinary build — Zig is lazy and the platform arm is selected by switch (builtin.os.tag) — so its comptime contracts do not run. zig build check-windows builds a second module graph pinned to x86_64-windows-msvc and type-checks it (no link). The ABI must be msvc, not gnu: Clang types an unfixed C enum as int under the MS ABI and unsigned int under gnu, and the gnu choice turns every runtime enum passed to a D3D12 entry point into a false "expected c_uint, found c_int".
Parity between the two backends is held by comptime contracts checked in both directions: renderer_contract for renderer fields, pass_encoder.validate for the encode verbs (requiredVerbNames() is the contract; pass_encoder asserts its count so the list and the validator cannot drift), and rendergraph/backend.zig for the graph adapter. Each requires the shared set and rejects anything else unless the backend names it in its own platform_only_decls, so a one-sided hook is a recorded decision rather than an unnoticed difference. D3D12 root-parameter ordinals derive from the named layouts in graphics/gpu/binding_layout.zig, and each slot's register is checked against shaders/bindings.kaji.json by a test that runs on any host. The frame entry/exit sequence lives once in graphics/renderer/frame_entry.zig; backend differences are named hooks both backends declare, Metal's as empty bodies that record why.
Shared render code does not ask what a backend has. @hasDecl / @hasField probes are banned in src/graphics (zig build check-probes, a dependency of zig build test, fails on a new one): a hook is required and no-opped where it has nothing to do, a real capability difference is a constant both sides give a value to, and the few unavoidable questions have one named predicate each. A probe used as a @compileError assertion is the replacement, not the problem.
Target resolution is policy and runs once in pass_targets.Plan, a fully resolved description of which attachments a pass writes, what happens to them on entry, and what state they are left in. Each backend implements a single beginRenderPass over it. Because the plan is a value, fake_renderer.zig can run the real graph (assemble, compile, schedule, execute) against a recording encoder for every driver in its traced_kinds list, so the verb sequence a pass emits is a fact a Mac can assert without a GPU. Kinds that need more than a null backend can provide are listed in untraced_kinds with a reason, and a comptime check requires every registry kind to appear in exactly one list. call_trace (Debug, renderer.common.backend_trace_request) records one frame of verbs so the two backends can be diffed.
Shared cores hold what would otherwise be written twice: gpu/buffer_stream.zig, debug/pass_timing.zig, renderer/renderer_display.zig (reconfigure ordering), renderer/frame_entry.zig, and shadow/shadow_pass.zig. What remains mirrored is parallel control flow over different APIs; reducing it further means a command-recording vocabulary in rhi/, not more extraction.
src/hikari/src/graphics/rhi/ is the backend-neutral vocabulary for resources, state transitions, queues, capabilities, compute, and acceleration structures. Renderer policy (frame graph, PSO/resource tables, uniforms, residency/lifecycle) lives under src/hikari/src/graphics/; Metal/D3D12 implementations create native objects, encode commands, synchronize, and present. Backend identity (shader artifact format and reconstruction payload tag) and any render-thread scope are reported by the selected renderer driver; shared graphics never derives them from the host OS. See Rendering.
Ownership rules
- Native frontends own process entry and the stable frontend ABI callbacks; the native bridge owns OS windows and event-loop integration.
- Zig application libraries translate the C ABI and arrange host-specific behavior.
SessionCoreowns the running engine subsystems and destroys them in lifecycle order.GameSubsystemowns session-scoped services; scene unload must not destroy it.SceneDocument, not a runtimeWorld, is the editor's mutable authored source of truth.Worldowns live scene entities and retained runtime UI state (session HUD may draw intoworld.uiwithout owning an entity).- The render thread owns render-thread-sensitive backend reconfiguration.
- Platform/native folders are implementation details below engine-facing contracts.
Useful entry points
| Need | Start at |
|---|---|
| Game-facing API (thin ABI) | src/hikari/sdk/src/hikari_game.zig |
| Game editor chrome SDK | sdk/src/editor_contrib.zig (hi.editor, opt-in) → host editor/game_contrib.zig; docs |
| Engine/host API | src/hikari/src/hikari_api.zig |
| Runtime composition | src/hikari/src/runtime_session.zig |
| Project contract | src/hikari/sdk/src/project_config.zig and game_contract.zig |
| Backend selection | src/hikari/src/backend/registry.zig |
| Graphics front door | src/hikari/src/graphics/api.zig |
| Scene/actor model | src/hikari/src/scene/ |
| Editor state/events | src/hikari/src/editor/state.zig and docs/systems/ui-and-editor.md |
| Host editor contributions | src/hikari/src/editor/contrib.zig |
| Build product graph | src/kaji/src/HikariBuildPlans.cs and HikariGameConductor.cs |