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
Guides12 min read

Shader authoring

On this page
On this pagePass roles and packagesPSO shareAkari file headersLanguage surface (authoring)Binding manifestDomainsSamplersRender targetsAuthored foliage normalsShader specificationsVulkan binding modelProcessing orderValidationFormatAuthoring rules Back to top

Akari (src/akari/) owns the shader product path end-to-end: pure package SoT under shaders/akari/packages/*.akari, shared modules under shaders/akari/modules/*.akari, transpile → MSL/HLSL (bindings resolved at emit), platform compile (xcrun metal / DXC / DXC -spirv). Metal metalc uses -std=metal4.0 and -mmacosx-version-min=26.0. Specs: build.akari.metal.json / build.akari.d3d12.json / build.akari.vulkan.json (type KhronosVulkan; same package list as the other two). Vulkan compiles through the vendored DXC in SPIR-V mode, so it cooks on any host — no Vulkan SDK.

Language reference (all syntax and features): Akari language. Grammar: src/akari/syntax.ebnf.
Editor highlighting: tools/vscode-extensions/akari (VS Code / Cursor); tools/nvim-plugins/akari (Neovim / Vim).
Tool notes: src/akari/AGENTS.md (Rust edition 2024). Product binary: flat bin/akari/akari (not platform-arch). Shinra invokes via $AKARI_SC / --akari-sc. ABI packing: src/akari/docs/abi.md.

There is no dual-native metal/ / d3d12/ / *_includes/ authoring tree. Ray tracing uses first-class intrinsics (rt_visibility, rt_trace, bindless loads).

Note

Vulkan shader cooking is available through Akari and DXC even though the Linux/Vulkan runtime is not yet a product peer. Compiling SPIR-V does not imply full runtime support.

One authored package feeds each target's compiler:

Diagram
Diagram source
flowchart TD
    source["Akari packages + shared modules"] --> akari["Akari — validate and emit resolved bindings"]
    spec["Target build specification"] --> akari
    akari --> msl["MSL"]
    akari --> hlsl["HLSL"]
    msl --> metal["Metal compiler — Metal 4 / macOS 26"]
    hlsl --> dxc["DXC — D3D12 target"]
    hlsl --> spirv["DXC -spirv — Vulkan target"]
    metal --> metalout["Metal shader artifacts"]
    dxc --> dxout["D3D12 shader artifacts"]
    spirv --> vkout["SPIR-V shader artifacts"]

Pass roles and packages

One package ≈ one pass role. Materials pick a package; they do not invent keyword sets. Graph pass order and gates: Rendering — Implemented pipeline.

RolePackagePipelineKindLayout
Opaque deferred geometry_engine/gbuffer (or game package)gbuffer_geometrystandard
Soft-ref / missing material_engine/error_materialgbuffer_geometrystandard
Transparent forward_engine/forward_litforward_transparent / forward_transparent_hdrforward_lit
Shadow depth_engine/shadow_depthshadow_depthstandard
Shadow depth, alpha-cut casters_engine/shadow_depth_maskedshadow_depth (same state; the material carries the PS)standard
Deferred lighting_engine/deferred_lightlighting / hdr_lightingfullscreen_shadowed
Hybrid-shadow lighting_engine/hybrid_lightlighting / hdr_lightingfullscreen_hybrid_shadowed
RT-shadow lighting_engine/raytraced_lightlighting / hdr_lightingfullscreen_raytraced
Screen-space AO_engine/screen_space_ambient_occlusion_computeambient_occlusionfullscreen_reflections
Ray-traced AO_engine/ray_traced_ambient_occlusionambient_occlusionfullscreen_raytraced
Screen-space reflections_engine/screen_space_reflectionshdr_lightingfullscreen_reflections
Ray-traced reflections_engine/ray_traced_reflectionshdr_lightingfullscreen_raytraced
TAA resolve_engine/temporalhdr_lightingfullscreen_reflections
Tonemap (ACES)_engine/tonemaptonemap (TONEMAP=aces)fullscreen_texture_table
Tonemap (Reinhard)_engine/tonemap_reinhardtonemap (TONEMAP=reinhard, same source)fullscreen_texture_table
Editor viewport blit_engine/compositetonemapfullscreen_texture_table
Debug G-buffer grid_engine/debug_visualizersdebug_visualizersdebug_visualizers
UI_engine/uiuiui / ui_atlas
Camera meshlet cull (compute)_engine/meshlet_cull(compute PSO, not mesh PipelineKind)standard
RT opacity micromap bake (compute)_engine/opacity_micromap_bake(lazy renderer-owned compute PSO)standard

Other engine packages (compute / post, not material roles) live beside these in src/hikari/shaders/akari/packages/: bloom, exposure, depth of field, motion blur, auto-focus, GI (screen-space, ray-traced, temporal/spatial/bounce), particles, volumetrics, atmosphere and clouds, light binning, skinning writeback, shadow meshlet cull, deferred decals.

Game-authored packages (sample project: src/games/example/assets/shaders/) stage under data/shaders/, not _engine/shaders/. Materials may set "binding_layout": "mesh_vertex_texture" and optional "async_compute": { "package", "entry", "resolution", "threads", "format", "lag_frames" } to drive content compute (producer key = package/entry/resolution/threads/format; lag_frames 0|1 only; prefer independent compute queue, fall back to graphics; format omit/r32f, rg16f, or rgba16f). Encode writes the selected ring textures onto GpuMaterialRecord.vertexTextureIndex / previousVertexTextureIndex in the bindless heap, so deforming vertex shaders can emit deformation-correct motion vectors. Producer kernels may sample their own prior ring result through content_compute.history; gate it with frame.historyValid and use simulation-clock frame.deltaTime for temporal evolution. Producers may also consume up to 64 game-published opaque records through content_compute.inputs / frame.inputCount; update them in one batch with hi.render().setContentComputeInputs. The engine tracks opaque vs transparent consumers so async-compute acquisition occurs at the first real sampling pass. Sample: packages water + water_sim.

Temporal geometry contract: opaque GBufferOutput.depth is float2(currentViewDepth, previousViewDepth); compute the previous depth from the same previous pose used for motion and write a nonpositive value when there is no correspondence. Forward/OIT entry points (including fallbacks) declare a third output float4(previousViewDepth, valid, 0, coverage); hikari_oit_encode weights it like colour. Particles without previous poses emit validity zero.

Composability: geometry packages (including content like water) write the deferred G-buffer only. They must not hardcode RT, deferred vs RT lighting, or other materials. Lighting / AO / reflection packages read the G-buffer (and optional TLAS) only. Content compute packages write a storage texture consumed through those bindless material heightfield indices. Full contracts: Rendering — Rendering contracts.

Shared forward BRDF/IBL helpers accept an optional trailing dielectric_f0 (float3(0.04) by default) and specular_occlusion (-1 = use the material AO). Engine forward_lit / forward_lit_rt derive F0 from IOR (hikari_dielectric_f0) and pass that same value to direct lighting, IBL, and the transmission mix. A transmitting interface is a dielectric: metalness is scaled by (1 - transmission) so it cannot steal F0, and IBL specular is not multiplied by baked AO (the Fresnel lobe is the environment, not a cavity). thickness == 0 is thin-walled: the raster path still walks the refracted ray by one pixel of world length at the surface depth (Snell in screen space, no Beer-Lambert). Volumes keep using authored thickness.

Formats, blend, and depth write live on PipelineKind, not shader #ifdef. Optional map presence uses a runtime map mask in the material row (HikariGpuMaterial.material.w, mirror of GpuMaterialRecord) (dynamic branches), not compile-time permutations.

Akari variant axes are only for finite, named build choices (e.g. tonemap operator). Never emit a Cartesian product of material features. Lighting-family scene textures (G-buffer, shadow atlas, skybox, captures, …) are bindless: index them through @bind(lighting.resource_indices) (HikariLightingResources), not fixed HK_TEXTURE(lighting,…) slots. The language still allows resource arrays (TexCube[N] / Tex2D[N]) — see the language reference.

PSO share

Mesh PSOs are keyed by (package, vertex entry, fragment entry, PipelineKind) and shared across materials. Residency still refcounts handles.

Native shader programs are independently cached by package, entry points, binding layout, shader model, and AssetStore content version. Material instances retain the immutable program while keeping per-material compute/culling state. The cache drops its strong reference on shader-source rebind or hot reload; old programs survive only while deferred GPU materials still reference them. Unused programs age out after 120 frames, so streamed chunks do not grow an immortal shader cache.

Akari file headers

One root per file, then optional headers, then items (struct / fn / …):

akari
#shader composite
#layout fullscreen_texture_table
#import preamble, fullscreen_triangle

@vertex
fn composite_vertex(@vertex_id vid: uint) -> V { … }
DirectiveRole
#shader name / #module nameFile kind + name
#version NSyntax version (default 2)
#layout nameBinding layout contract (#shader only)
#import a, bPure modules from akariImportPaths
#require a, bPackage capability declarations (optional; also inferred from use)

Stages are attributes on functions (@vertex / @fragment / @compute(threads=(x,y,z))), not text pragmas. D3D12 emit splits one entry per file because DXC produces one stage artifact per invocation.

Current layouts: standard, fullscreen_texture_table, fullscreen_shadowed, fullscreen_hybrid_shadowed, fullscreen_reflections, forward_lit, forward_lit_rt, fullscreen_raytraced, debug_visualizers, ui / ui_atlas, mesh_vertex_texture.

Language surface (authoring)

Full syntax, tables, and worked examples: Akari language. Grammar: src/akari/syntax.ebnf.
GPU packing: src/akari/docs/abi.md.

Array textures sample with a packed coord (e.g. Tex2DArray uses float3(uv, layer)); Metal emit splits layer from the coord. Prefer @bind over raw @slot except non-manifest compute experiments.

Binding manifest

Authoring uses @bind(group.name) / @target(group.name) / @sampler(…). @bind and @target resolve slots from bindings.kaji.json. Manifest keys keep the historical HK_* spelling:

text
HK_BUFFER(domain,name)
HK_TEXTURE(domain,name)
HK_TARGET(domain,name)

@sampler(filter, address) (and the comparison form) is not a manifest key. Metal emits a constexpr sampler; HLSL assigns s0, s1, … in declaration order. D3D12 static samplers come from the Zig root-signature / compute contract, not this JSON.

Unknown @bind / @target names are errors. Values come from the shared bindingManifest; they are not hardcoded in .akari source.

Domains

Slot numbers live only in bindings.kaji.json; author against the names. The domains you will meet:

DomainNamesWho reads it
frame / draw / geometryuniforms, object, verticesgeometry packages (mesh geometry is meshlet-pulled from meshlet.vis_tris / meshlet.works plus the bindless geometry table; shadow depth uses the same pull)
resourcestexture_table, geometry_samplersmaterial maps via GpuMaterialRecord indices; content-compute heightfields land on the same heap (vertexTextureIndex / previousVertexTextureIndex)
lightingframe, records, clusters, shadows, probes, resource_indicesdeferred / forward lighting, AO, GI, reflections. Scene textures are bindless through resource_indices (HikariLightingResources)
posthdr, lut, bloom_add, exposuretonemap / composite family
uivertices, atlasUI packages
debugalbedo … hdr (eight float slots the active visualizer assigns) plus material (Tex2D<uint>, its own slot)debug visualizers

Samplers

@sampler is a shader-local declaration, not a bindings.kaji.json row. Metal bakes a constexpr sampler; HLSL numbers sN in source order.

Most fullscreen and UI passes declare one @sampler(linear, clamp) (HLSL s0). Deferred and hybrid lighting declare three, in this order:

  • @sampler(point, clamp) → HLSL s0
  • @sampler(linear, clamp) (sky / capture) → HLSL s1
  • @sampler(comparison, greater_equal, linear, clamp) (reverse-Z shadow atlas) → HLSL s2

Render targets

G-buffer outputs use:

  • HK_TARGET(gbuffer,albedo)
  • HK_TARGET(gbuffer,normal)
  • HK_TARGET(gbuffer,arm)
  • HK_TARGET(gbuffer,depth)
  • HK_TARGET(gbuffer,material) (uint, not float)
  • HK_TARGET(gbuffer,motion)
  • HK_TARGET(gbuffer,anisotropy)
  • HK_TARGET(gbuffer,clearcoat)

They expand to Metal color indices 0 through 7 and HLSL semantics SV_TARGET0 through SV_TARGET7.

metal
float4 albedo [[color(HK_TARGET(gbuffer,albedo))]];
hlsl
float4 albedo : HK_TARGET(gbuffer,albedo);

Ordinary single-target HLSL fragment outputs continue to use native SV_TARGET.

Authored foliage normals

material.flags.preserve_authored_normals opts foliage out of the default two-sided policy (which mirrors the normal on back faces): G-buffer, forward, material-graph, and RT secondary hits keep the authored side of the tangent frame for both faces. Normal maps and clearcoat still perturb it. It is a normal policy only: alpha coverage, shadows, translucency, and the BRDF are unchanged, and double_sided is still set separately. Model sidecars set preserve_authored_normals: true in an exact-name model.material_overrides entry so recooks keep the choice; no name heuristic exists. No shader permutations or GPU record bytes are added (one flag bit in the material word and RT instance descriptor).

Shader specifications

The JSON specification version must be 2:

json
{
  "version": 2,
  "type": "AppleMetal",
  "bindingManifest": "bindings.kaji.json",
  "akariImportPaths": ["akari/modules/"],
  "compilation": [
    {
      "name": "gbuffer",
      "workingDirectory": "akari/packages/",
      "optimizationLevel": 0,
      "includeDebugSymbols": true,
      "sources": ["gbuffer.akari"],
      "variants": {}
    }
  ]
}

Per-entry optimizationLevel / includeDebugSymbols are author defaults. Product builds override them with Kaji --shaders:config=debug|release (default debug), which is independent of product --config. That maps to:

--shaders:configoptimizationdebug symbols
debug0on
release2off

akari compile accepts --config=debug|release. Shinra forwards the same profile via --shaders-config= (hyphen; Clap cannot use : in long option names) and/or env.

variants is optional and defaults to an empty object. Dual tonemap artifacts share one source with different selections:

json
{ "name": "tonemap", "sources": ["tonemap.akari"], "variants": { "TONEMAP": "aces" } }
{ "name": "tonemap_reinhard", "sources": ["tonemap.akari"], "variants": { "TONEMAP": "reinhard" } }

bindingManifest and akariImportPaths may be omitted when --engine-sdk can resolve bindings.kaji.json and engine akari/modules/ from the SDK tree.

The engine and game specifications both reference src/hikari/shaders/bindings.kaji.json. Each manifest entry maps one complete semantic expression to its Metal and D3D12 token, and optionally a Vulkan one:

json
{
  "version": 1,
  "bindings": {
    "HK_BUFFER(frame,uniforms)": {
      "metal": "1",
      "d3d12": "b0",
      "spirv": "0:1"
    }
  }
}

Keep one shared manifest for all packages so game-authored and built-in shaders cannot drift.

Vulkan binding model

Vulkan has no per-class register namespaces, so Akari fixes one descriptor-set layout (akari_emit_common::vulkan) that every Vulkan pipeline layout mirrors:

SetContentsBinding
0Uniform / Buf / RwBuf / AtomicBuf / AccelStructthe Metal buffer index
1sampled and storage texturesthe Metal texture index
2samplers; @sampler(...) descriptors take the next free binding in declaration orderthe Metal sampler index
3renderer-global: 0 bindless ByteAddressBuffer[], 1 Texture2D[], 2 TextureCube[], 3 Texture3D[], 4 Texture2D<float>[] (depth), 5 geometry sampler catalog SamplerState[18]fixed

An entry without a spirv value therefore needs no edit: its Metal index is the binding inside the class's set. "spirv": "set:binding" overrides that for one entry. A D3D12 register in the spirv column is rejected. Render targets use the Metal attachment index (SV_TargetN → Location N). @slot(N) keeps N in the class set. Storage textures carry [[vk::image_format]] for every format except float4, which stays Unknown (format-less writes). Compilation passes -fvk-use-dx-layout (D3D12 packing), -fvk-use-dx-position-w, and -fvk-invert-y on vertex stages, so CPU-side layouts and clip conventions are the ones D3D12 and Metal already use. Output is one <package>_<vs|ps|cs>.spv per stage entry.

Processing order

For each package, Akari:

  1. Parses the .akari package and transitive #import modules.
  2. Lowers AST → HIR → IR (variants selected from the spec entry).
  3. Emits pure MSL, HLSL, or Vulkan HLSL (bindings resolved; #line + stage metadata).
  4. Validates emit purity (no leftover HK_* / #pragma hikari).
  5. Invokes Metal, DXC, or DXC -spirv.
  6. On failure, prints @akari diag {"sev","msg"} to stderr (machine), then human error: lines — metalc/DXC locations are remapped via .linemap.json / #line to .akari when possible; Metal unknown-attribute / candidate-note noise is dropped.

Package failures are isolated. Akari completes every independent package in the spec and exits non-zero only after reporting the failed set. Successful packages publish atomically; a failed package does not replace its previous library. A shared manifest or imported-module error still fails every package that actually depends on that shared input.

Emitted sources for debug live under <shader-cache>/akari_emit/<package>/.

Validation

Akari can validate metadata, imports, bindings, stages, layouts, and variants without invoking DXC:

sh
bin/akari/akari compile \
  --spec=src/hikari/shaders/build.akari.d3d12.json \
  --out=build/cache/shader-validation/out \
  --cache=build/cache/shader-validation/cache \
  --engine-sdk=. \
  --validate-only

validate-only does not type-check native HLSL. The Windows build remains the authoritative DXC validation. The Vulkan spec has no such gap: without --validate-only it runs the vendored DXC in SPIR-V mode on every host, and DXC validates each module it writes, so the same command against build.akari.vulkan.json is a full compile check.

Validate the game's shaders against the engine's, not only the engine's. A game package that calls into an engine module is compiled against whatever that module currently declares, so an engine-side change can break a game shader that nobody touched — and the failure surfaces at cook time in the editor console, not in any Zig build. Point the same command at the game's spec with --engine-sdk naming the engine shader root:

sh
bin/akari/akari compile \
  --spec=src/games/example/assets/shaders/build.akari.metal.json \
  --engine-sdk=src/hikari/shaders \
  --out=build/cache/shader-validation/out \
  --cache=build/cache/shader-validation/cache \
  --validate-only

Always pass --out and --cache pointing somewhere disposable. Without --validate-only this recompiles every package and overwrites the staged metallibs for the given config.

HikariRasterLightCtx is a struct literal game shaders build, so a field added to it is a breaking change. Anything calling evaluate_lighting_forward and friends constructs one by naming every field, and Akari rejects a literal that misses one (missing field 'x' in 'HikariRasterLightCtx' literal) — there is no default, because the pack holds textures and samplers and neither can be zero-initialized. When the engine adds a resource to the light loops, every game shader that lights a surface needs the new field; mirror what akari/packages/forward_lit.akari does for it (the sample's water.akari is the worked example).

Format

akari fmt reprints .akari source in the canonical style (4-space indent, semicolons, name: Type spacing, wrapped parameter/argument lists). It parses and pretty-prints; it does not type-check or compile. Comments are preserved.

sh
bin/akari/akari fmt src/hikari/shaders/akari
bin/akari/akari fmt --check src/hikari/shaders/akari
bin/akari/akari fmt --stdin < foo.akari

Directories are walked recursively for *.akari. --check lists files that would change and exits 1. Default writes in place.

Authoring rules

  • Author packages and modules as pure .akari only (#shader / #module, #layout, #import).
  • Bind with @bind(group.name) / @target(group.name) — not raw registers. @sampler is shader-local (Metal constexpr / HLSL sN in declaration order) and does not take a manifest row.
  • Keep CPU/GPU structure layouts synchronized with their Zig definitions (akari transpile --dump-layout / ABI rules in src/akari/docs/abi.md).
  • Add a binding to the shared manifest and this document in the same change.
  • Treat missing imports and unknown bindings as errors; never substitute empty source.
  • Prefer runtime map masks over new variant axes for material features; keep variants finite and named (tonemap operator, etc.).
PreviousScripting with KawaNext Time of day

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/guides/shaders.md
On this pagePass roles and packagesPSO shareAkari file headersLanguage surface (authoring)Binding manifestDomainsSamplersRender targetsAuthored foliage normalsShader specificationsVulkan binding modelProcessing orderValidationFormatAuthoring rules Back to top