Akari (src/akari/) owns the shader product path end-to-end: shader package sources
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).
Product binary: flat bin/akari/akari (not platform-arch). Shinra invokes via --akari-sc. ABI packing: docs/reference/akari-abi.md.
There is no dual-native metal/ / d3d12/ / *_includes/ authoring tree.
Ray tracing uses first-class intrinsics (rt_visibility, rt_trace, bindless loads).
One authored package feeds each target's compiler:
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.
| Role | Package | PipelineKind | Layout |
|---|---|---|---|
| Opaque deferred geometry | _engine/gbuffer (or game package) | gbuffer_geometry | standard |
| Soft-ref / missing material | _engine/error_material | gbuffer_geometry | standard |
| Transparent forward | _engine/forward_lit | forward_transparent / forward_transparent_hdr | forward_lit |
| Shadow depth | _engine/shadow_depth | shadow_depth | standard |
| Shadow depth, alpha-cut casters | _engine/shadow_depth_masked | shadow_depth (same state; the material carries the PS) | standard |
| Deferred lighting | _engine/deferred_light | lighting / hdr_lighting | fullscreen_shadowed |
| Hybrid-shadow lighting | _engine/hybrid_light | lighting / hdr_lighting | fullscreen_hybrid_shadowed |
| RT-shadow lighting | _engine/raytraced_light | lighting / hdr_lighting | fullscreen_raytraced |
| Screen-space AO | _engine/screen_space_ambient_occlusion_compute | ambient_occlusion | fullscreen_reflections |
| Ray-traced AO | _engine/ray_traced_ambient_occlusion | ambient_occlusion | fullscreen_raytraced |
| Screen-space reflections | _engine/screen_space_reflections | hdr_lighting | fullscreen_reflections |
| Ray-traced reflections | _engine/ray_traced_reflections | hdr_lighting | fullscreen_raytraced |
| TAA resolve | _engine/temporal | hdr_lighting | fullscreen_reflections |
| Tonemap (ACES) | _engine/tonemap | tonemap (TONEMAP=aces) | fullscreen_texture_table |
| Tonemap (Reinhard) | _engine/tonemap_reinhard | tonemap (TONEMAP=reinhard, same source) | fullscreen_texture_table |
| Editor viewport blit | _engine/composite | tonemap | fullscreen_texture_table |
| Debug G-buffer grid | _engine/debug_visualizers | debug_visualizers | debug_visualizers |
| UI | _engine/ui | ui | ui / 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: hikari-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.
Compile-time choices are const parameters with named instances (e.g. the
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.
Source provenance and distribution notices
Record adapted shader code and its applicable permission in
src/hikari/shaders/PROVENANCE.md; retain complete upstream notices in
src/hikari/shaders/THIRD_PARTY_NOTICES.txt. A technique citation alone does not
establish permission to copy its reference implementation. Record equation-based
implementations separately from source adaptations, and preserve inherited notices.
Kaji includes the shader notices in game and editor products as
licenses/HIKARI-SHADER-NOTICES.txt (Contents/Resources/licenses/ inside a macOS
app bundle). The bundled editor SDK also carries the notices and provenance
registry beside its shader source modules. Notice changes invalidate the relevant
staging steps. Keep the notices when distributing shader sources separately.
Packaging fails if a registered product notice is missing from the staging tree;
rebuild package-host to restore it.
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 / …):
#shader composite
#layout fullscreen_texture_table
#import preamble, fullscreen_triangle
@vertex
fn composite_vertex(@vertex_id vid: uint) -> V { … }| Directive | Role |
|---|---|
#shader name / #module name | File kind + name |
#version N | Syntax version (default 2) |
#layout name | Binding layout contract (#shader only) |
#import a, b | Pure modules from akariImportPaths |
#require a, b | Package 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: docs/reference/akari-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:
HK_BUFFER(domain,name)
HK_TEXTURE(domain,name)
HK_TARGET(domain,name)@sampler(filter, address) (and the comparison form) is not a binding key:
it names a state of the engine's static sampler set (static_samplers, below).
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:
| Domain | Names | Who reads it |
|---|---|---|
frame / draw / geometry | uniforms, object, vertices | geometry packages (mesh geometry is meshlet-pulled from meshlet.vis_tris / meshlet.works plus the bindless geometry table; shadow depth uses the same pull) |
resources | texture_table, geometry_samplers | material maps via GpuMaterialRecord indices; content-compute heightfields land on the same heap (vertexTextureIndex / previousVertexTextureIndex) |
lighting | frame, records, clusters, shadows, probes, resource_indices | deferred / forward lighting, AO, GI, reflections. Scene textures are bindless through resource_indices (HikariLightingResources) |
post | hdr, lut, bloom_add, exposure | tonemap / composite family |
ui | vertices, atlas | UI packages |
debug | albedo … hdr (eight float slots the active visualizer assigns) plus material (Tex2D<uint>, its own slot) | debug visualizers |
Samplers
@sampler(...) states come from one engine-wide static sampler set, declared
once in bindings.kaji.json under static_samplers:
| Name | State | D3D12 | Vulkan (set 2) |
|---|---|---|---|
point_clamp | @sampler(point, clamp) | s0 | 0 |
linear_clamp | @sampler(linear, clamp) | s1 | 1 |
linear_repeat | @sampler(linear, repeat) | s2 | 2 |
comparison_greater_equal | @sampler(comparison, greater_equal, linear, clamp) (reverse-Z shadow compare) | s3 | 3 |
A sampler's register follows from its state alone — not from declaration order
or from which samplers an entry happens to use — and every D3D12 root signature
with space-0 samplers carries the whole set (binding_layout.static_samplers,
which a test keeps equal to the manifest). Metal bakes the state as a
constexpr sampler. A state outside the set is a compile error on every
backend; adding one is a manifest row plus a binding_layout row. Two samplers
with the same state in one entry share the register (HLSL aliases the second).
The material geometry sampler catalog is separate (space 1, geometry_sample).
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, notfloat)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.
float4 albedo [[color(HK_TARGET(gbuffer,albedo))]];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:
{
"version": 2,
"type": "AppleMetal",
"bindingManifest": "bindings.kaji.json",
"akariImportPaths": ["akari/modules/"],
"compilation": [
{
"name": "gbuffer",
"workingDirectory": "akari/packages/",
"optimizationLevel": 0,
"includeDebugSymbols": true,
"sources": ["gbuffer.akari"]
}
]
}Per-entry optimizationLevel / includeDebugSymbols are author defaults.
Product builds override them with Kaji --shaders:config=debug|release
(defaults to product --config, unless explicitly overridden). That maps to:
--shaders:config | optimization | debug symbols |
|---|---|---|
debug | 0 | on |
release | 2 | off |
Akari passes the optimization flag explicitly to every native compiler,
including -O0 for debug; compiler defaults do not choose the profile.
DXC uses explicit HLSL 2021 mode to preserve scalar short-circuit expressions.
akari compile accepts --config=debug|release. Shinra forwards the same
profile via --shaders-config= (hyphen; Clap cannot use : in long option
names).
Spec entries take only the keys above (unknown keys are an error). One pass
that the host loads as several packages — the tonemap operators — is one
module plus one tiny package per choice: modules/tonemap_pass.akari holds the
template entry with const operator: TonemapOperator, and packages/tonemap.akari,
tonemap_reinhard.akari, tonemap_agx.akari and tonemap_filmic.akari each
instantiate it as tonemap_fragment.
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:
{
"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:
| Set | Contents | Binding |
|---|---|---|
| 0 | Uniform / Buf / RwBuf / AtomicBuf / AccelStruct | the Metal buffer index |
| 1 | sampled and storage textures | the Metal texture index |
| 2 | samplers; @sampler(...) states bind at their static_samplers spirv binding | the Metal sampler index |
| 3 | renderer-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:
- Parses the
.akaripackage and transitive#importmodules. - Lowers AST → HIR → IR (specializing
const,fnand optional parameters). - Emits pure MSL, HLSL, or Vulkan HLSL (bindings resolved;
#line+ stage metadata). - Validates emit purity (no leftover
HK_*/#pragma hikari). - Invokes Metal, DXC, or DXC
-spirv. - On failure, prints
@akari diag {"sev","msg"}to stderr (machine), then humanerror:lines — metalc/DXC locations are remapped via structured line maps /#lineto.akariwhen 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.
Compile units own emitted source and line maps. Native compiler files are private
to each invocation, so concurrent builds never read shared generated files.
Native bytecode enters the cache from those private files before live artifact
publication, keeping fingerprints independent of concurrent output replacement.
Use akari transpile --out=<directory> to preserve source for inspection.
Native artifact names come from structured shader and entry metadata. Packages
with multiple entries of the same stage retain each entry's full name in the
artifact suffix, including when the package name differs from #shader.
Cache inventories record every artifact's name, size, and content hash. A hit
requires the complete verified set; a missing or corrupt member rebuilds the
package. Hits stream each blob once, checking its length and hash while writing
a private staging file; staging publishes only after the whole bundle verifies.
Bytecode blobs and inventories publish through unique temporary files
and atomic replacement, so readers never accept partially written cache data.
Shipping shader packages
Standalone game sealing uses the cooked asset manifest and pack plan to retain
shader packages referenced by shipped materials (including generated materials
and async_compute.package), plus explicit runtime roots and their transitive
dependencies. Editors keep the complete compiled set. Particle-specialized
programs remain embedded in their cooked particle asset.
An Akari compilation entry may declare:
{
"shipping": { "root": true, "requires": ["water_sim", "_engine/gbuffer"] }
}root: true retains shaders reached through runtime code or plugins even without
a material reference. root: false retains a package only through references or
another package's requires. Authored game packages default to root: true
when shipping metadata is omitted; material-generated specs explicitly use
root: false. Dependencies use the same package identities as
AssetRef of kind shader, with _engine/ for engine packages. Every engine
entry must declare its shipping policy; a test keeps policies aligned across
the three backend specs. Current renderer packages are runtime roots because
quality settings and fallback paths can change while the game runs. Do not mark
an existing renderer root unreferenced merely because a startup setting is off.
Akari writes exact per-package artifact ownership to shaders.akari.json.
Shinra caches typed material references with the existing manifest hashes and
resolves shader closure in packs.shinplan.json. Kaji validates all retained
artifacts before staging them from the cook tree, for both loose and bundled
output; unresolved references fail sealing. No stage-filename prefix matching
or scene-only reachability inference is used. The report at
<project>/.engine/build/shader-stripping.json records package reasons and sizes.
This pass strips whole packages at seal time; compilation still processes the
specs, and native entry-point stripping is not performed.
Validation
Akari can validate metadata, imports, bindings, stages, layouts, and specializations without invoking DXC:
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-onlyvalidate-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 absolute repository or SDK root:
bin/akari/akari compile \
--spec=../hikari-games/example/assets/shaders/build.akari.metal.json \
--engine-sdk="$PWD" \
--out=build/cache/shader-validation/out \
--cache=build/cache/shader-validation/cache \
--validate-onlyAlways 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).
Occlusion enters lighting as two separate factors. The light loops take
ambient_occlusion: float3, the diffuse occlusion of ambient-kind lights; build
it with hikari_ambient_diffuse_occlusion(ao_pass, ao_baked, base_color), or
pass float3(baked_ao) from a forward shader, which has no AO pass.
hikari_eval_ibl takes the AO pass's visibility and the baked map as separate
arguments (1.0 for the pass when none ran). Never pass their product: only the
pass's estimate takes the multi-bounce lift.
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.
bin/akari/akari fmt src/hikari/shaders/akari
bin/akari/akari fmt --check src/hikari/shaders/akari
bin/akari/akari fmt --stdin < foo.akariDirectories 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
.akarionly (#shader/#module,#layout,#import). - Bind with
@bind(group.name)/@target(group.name)— not raw registers.@sampler(...)picks a state of the engine's static sampler set; its register follows from the state. - Keep CPU/GPU structure layouts synchronized with their Zig definitions
(
akari transpile --dump-layout/ ABI rules indocs/reference/akari-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 compile-time permutations for material features;
keep
constinstances finite and named (tonemap operator, etc.). - Entry points that differ only by a constant (à-trous step, skinned/rigid,
RT/raster) are one template with
constparameters plusfn name = template(...)instances; the host still loads each instance by its name. Resources shared by a package's entries are declared once as package bindings (@bind(group.name) let name: T). See the language reference.