Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

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

Frontends and drivers

On this page
On this pageNative frontend and Zig application splitDriver compositionDynamic and monolithic productsAdding or changing a driverLogging Back to top

Native frontend and Zig application split

The engine uses native desktop process frontends with a small opaque C ABI:

PlatformNative entry pointGraphics path
macOSsrc/hikari/src/native_frontends/macOS/main.mmMetal
Windowssrc/hikari/src/native_frontends/Windows/main.cppD3D12

The public C header is src/hikari/include/hikari_frontend.h (ABI version 9). Native code owns OS event loops. Zig App and EditorApp implementations own application/session state and translate callbacks through src/hikari/src/application/ (abi.zig lockstep with the header).

The platform bridge under src/hikari/src/native/{macOS,Windows}/src/ is split by ownership rather than by OS callback name:

DomainOwns
application/Boot/teardown sequencing, the thin AppKit/Win32 lifecycle facade, and application menu commands
startup/Splash state, drawing, compositor commit, and final-window handoff
windowing/Primary/tool window lifetime, native id registry, event routing, DPI/scale, and custom chrome
frame/Frame wake/coalescing, display cadence, vsync, and CPU pacing
system/Cached appearance, power-source, and power-saver state plus OS notifications
services/Dialogs, clipboard, and Finder/Explorer integration
root src/Renderer/input/audio/accessibility/secure-storage implementations that are already cohesive native subsystems

application/ is the only lifecycle orchestrator; it does not absorb implementations from the other domains. Frontends use platform include/Application.h and include/Windowing.h, not private Objective-C classes or Win32 registry state. Kaji compiles every domain into its own object-cache directory and links the recursive object set; compile-many removes orphaned objects and static archives are recreated so a source move or deletion cannot leave stale code in a product.

ABI v9 (product identity, window identity, durable services, frame clock, presentation readiness, frame admission) — required order: create → set_frontend_ops → window_config / product_identity / startup_presentation → start → launch → device-init → tick/render → shutdown → destroy. Product identity comes from src/hikari/src/version.zig; each release carries its number, subtitle, and slogan together, shared by logs, editor compatibility checks, About, and native startup chrome. A native editor shows a compact, theme-aware splash after start and commits it to the compositor (macOS: applicationWillFinishLaunching + CA flush / occlusion wait; Windows: RedrawWindow + DwmFlush) before launch blocks the UI thread on AssetStore / shader work. Without that commit, orderFront / ShowWindow only update the backing store and the plate appears after launch — then vanishes on the first present. Heavyweight editor/session creation stays deferred to launch, while the final editor window prepares invisibly. Launch must precede renderer device-init (shaders load only through AssetStore bound at launch). The splash uses the embedded About artwork and remains visible until is_presentation_ready reports that the render thread completed a primary present. macOS then grows/fades the splash into the ready editor; reduced-motion preference requests a direct handoff. Standalone on_tick may return HIKARI_RESULT_FRAME_DEFERRED: the frontend must skip on_renderer_render for that attempt and continue event/maintenance processing. Standalone durable services run while the application is running, including admission stalls; editor durable services also run while suspended. While suspended, tick/render remain invalid; editor frontends may call on_durable_service to pump bounded main-thread services such as MCP. Window notifications (on_resize, on_window_focus_changed) stay valid for as long as a window exists — started, running, or suspended — because the OS posts them from display reconfigures and monitor moves that land while the app is inactive.

Standalone games show their actual primary window with an opaque native cover before session launch/device initialization. The cover defaults to black; Kaji reads startup_background_color (#RRGGBB) from configs/packaging.json and embeds it in the game frontend. No artwork, extra scene, or separate splash window is required. macOS keeps the cover beside the layer-hosting Metal view; Windows uses a child cover. Resize keeps it filling the client area. Editor artwork and reveal behavior are unchanged.

The game App defers the initial scene and queues it through the ordinary budgeted scene loader. The host keeps processing events while scene planning, worker decoding, resource creation and instantiation advance. Native device setup remains synchronous and is timed separately. The cover waits for initial world GPU readiness, then for completion of a new primary submission after that readiness edge; an earlier empty frame cannot reveal the window. An initial scene failure is logged and closes the window. Games may choose a lightweight first scene and load further content with the existing scene APIs.

Standalone startup emits info-level milestones from application creation: native window committed, scene queued, render device ready, CPU assets ready, GPU ready, and the first ready scene frame. Work counters separate scene document IO/decode, planning, store adoption, spawn, asset worker activity, actual asset decodes, CPU GPU-upload staging/submission, and native graphics/compute pipeline creation. Counts and accumulated elapsed times cover completed calls at reveal, including cache-independent decode work; concurrent and nested stages overlap and must not be summed as total startup latency. Upload counters are not GPU timestamp measurements. Scene-job logs also report wall time by phase and pump advances (blocking advances are not frames). Blocking drains wait for both document and asset workgroups; interactive pumps never wait on those groups. Startup counters are disabled after reveal and in the editor, and dynamic drivers adopt the host sample through HostProcessState (driver contract 33).

Frame clock. on_frame_clock is optional and additive: a frontend that owns a vsync-aligned clock publishes the target present time of the frame it is about to request, and the sim uses that span instead of measuring the wall clock. It matters because a tick starts whenever the event loop reaches the coalesced frame, while the frame it produces is displayed on a vblank — feeding the sim the CPU span makes travel-per-frame disagree with duration-on-screen, which is visible as motion judder. macOS publishes CADisplayLink.targetTimestamp from the primary window's link only (a tool window on a second display has its own cadence and must not drive the sim). Windows does not publish yet and falls back to the monotonic clock. Linux has no product frontend. Either way a measured span is clamped to Time.max_measured_delta so a stalled frame cannot teleport the camera.

SymbolRole
HikariWindowId (PRIMARY=0, INVALID=0xFFFFFFFF)Native window system assigns ids
on_resize(app, window_id, w, h, layer_scale)Per-window physical drawable size + live display scale (PRIMARY on boot path); advisory — frontends drop zero-size frames and log rejections instead of aborting
on_renderer_device_initGPU device + primary surface only
on_renderer_surface_init(app, window_id, surface, scale)Secondary surfaces only; before device_init → INVALID_STATE; primary id → INVALID_ARGUMENT
on_window_close_requested → HikariCloseResponseCancelable close (CONTINUE / CANCEL)
on_window_focus_changedPer-window focus
HikariFrontendOps + set_frontend_opsApp→frontend vtable (create_window / destroy_window / set_window_title / window_command); registered after create, before start
on_durable_serviceEditor-only main-thread durable bus pump; valid while running or suspended; game returns UNSUPPORTED
on_frame_clock(app, target_timestamp)Optional vsync-aligned present stamp (seconds), main thread, before on_tick; unpublished → sim measures the wall clock

Hard cut — no legacy ABI support. Game product stays single-window (create_window / secondary surface → UNSUPPORTED). Editor Class 1 is shipped: native create_window + WindowId→SurfaceId map for chrome/tool OS windows (Settings, Project Settings, debug tools). Class 2 Asset Preview Viewport (PreviewHost) is shipped; a detached free-floating editor scene window remains deferred. Design: docs/design/multi-window-editor.md, preview-viewport.md.

This split lets the application compile as a dynamic library or as an LLVM object linked into the native executable without changing the process contract.

Driver composition

DriverRecipe selects implementations for:

  • windowing
  • input
  • renderer
  • physics
  • scripting
  • game_ui
  • audio
  • gpu_preference (auto, high_performance) — D3D12 only; see rendering.md

null selects the host default. A recipe may load default modules and list extra dynamic modules. Modules load before driver creation and remain loaded until all driver instances have been destroyed.

The registry is the one implementation-selection seam. Desktop/tool builds can load compatible engine modules; console configurations use compile-time registered SDK implementations only. Do not add platform switches to shared input, graphics, physics, audio, scene, or UI code.

Dynamic and monolithic products

ModeRuntime composition
dynamicNative executable plus shared Zig application, platform bridge, and driver dylibs (windowing, input, renderer, physics, scripting, game_ui, audio).
monolithicNative linker combines frontend, Zig application, engine, drivers, and platform bridge into one executable. Runtime driver loading is compiled out.

Editor game modules are always dynamic: opening a project compiles libgame against the thin hikari_game SDK and the editor dlopens it. Game modules talk to the host through HostApi — they do not share Entity/World layout with the editor. --type= only selects driver/application/platform linkage for the editor host. The standalone game product may still bake game code into a monolithic hikari executable.

Vendor middleware and packaged data may remain external in both modes. Monolithic builds reject non-empty dynamic driver module lists.

Adding or changing a driver

  1. Start from the shared type-erased contract in src/hikari/src/backend/.
  2. Keep native handles and API-specific types below src/hikari/src/platform/<platform>/ or src/hikari/src/native/<platform>/.
  3. Register selection only through backend.Registry.
  4. Put @export of hikari_driver_module_entry_v1 on the thin root (src/hikari/src/driver_module_*.zig, which must live under src/). PE only surfaces root-module exports; a nested @export produces empty stubs on Windows and the module fails symbol lookup.
  5. Preserve lifecycle ownership: destroy every driver before unloading its module.
  6. Bump backend/module.zig's contract version if a factory, vtable, or cross-module type changes. on_device_init returns POD DeviceInitStatus (ok / failed) — not anyerror; Zig error ints are not shared across dylib copies.
  7. Update Metal and D3D12 paths for shared graphics behavior, or document a real capability difference.

Audio follows the same driver pattern (Core Audio AudioQueue / WASAPI device factory via drivers.audio). The engine owns the software mixer, SPSC command queue, and audio worker thread; the device callback only pulls mixed f32. See audio.md.

Logging

Engine logging is centralized in src/hikari/src/log.zig. Executable and driver roots install std_options.logFn; native dependencies use their opt-in callback APIs. In dynamic builds, module-static logging state is not automatically shared with the host, so each dynamic root installs the sink explicitly.

Teeing: every enabled log record goes to:

  1. The optional process sink (editor bottom console via diagnostics.Store)
  2. stderr always (so kaji … --run and IDE terminals show live output)
  3. An optional NDJSON file via io_stream (log.configureFile / setFilePath):
    • Editor (project bound): <project>/.engine/logs/editor-latest.json by default (truncate/recreate each launch). Standalone selector: no file until a project is open.
    • Game: no file unless --log=<path> is set.
    • --log=<path> / HIKARI_LOG_FILE: redirects (or enables, for game) the file tee. Native frontends export the env before application create.

Application roots set std_options.log_level = .debug in Debug and .info in release. Both log.emit (including native callbacks and forwarded tool/game output) and the host/SDK logFn callbacks honor the root's log_level and log_scope_levels before sink delivery. Scope overrides can explicitly enable debug in release; there is no unconditional release-mode drop. Filtered records reach neither the UI sink, stderr, nor the NDJSON file.

Use std.log.scoped(...).debug for compile-time levels and scopes. Expensive debug-only arguments or timing capture need a comptime std.log.logEnabled guard as well; asset-load traces and mesh-decode timings follow this rule. log.emit is for already formatted records with runtime levels/scopes. Explicit --metal-trace and --d3d12-trace output remains opt-in and uses info-level records, so release's normal info threshold admits it without enabling ordinary debug logs. zig build test-logging checks filtering and scope overrides across the host, SDK, native bridge, console, and file sinks; run it in Debug and ReleaseFast.

kaji --run: the conductor launches the product with inherited stdout/stderr (no redirect/buffer-until-exit). Build-step tools still use redirected capture.

Frontend ABI v9 and driver engine contract v34 require rebuilding native frontends and dynamic drivers together. The input wire layout now carries latched keyboard/mouse press and release masks, and the game UI context carries explicit pointer edges.

PreviousApplication lifecycleNext Platforms and support

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/systems/frontends-and-drivers.md
On this pageNative frontend and Zig application splitDriver compositionDynamic and monolithic productsAdding or changing a driverLogging Back to top