Skip to content
hikari
RenderingEditorAIToolchainDocumentation
GitHub
hikari

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsLicenseBack to top
Documentation / Language reference
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
Language reference17 min read

Akari language reference

On this page
On this pageMental modelFile structureHeadersTypesScalars and vectorsMatricesArraysResourcesStructsLiterals and operatorsStruct literalsArray literalsCompile-time layoutDeclarationsEnumsVisibility (private)Default arguments, named arguments, overloadsref / inoutFunction values@bits field loadsStages and system valuesResources and bindingsControl flowFlow hintsTexturesComputeAtomicsLane groups (wave)Math and builtinsRay tracingVariants (compile-time axes)Emit hygiene (authors)Capabilities (#require)Type checkingComplete mini examplesVertex + fragment (geometry-style)Compute + atomics + groupsharedContent compute write (RwTex2D)Tooling (authors)Authoring rules (product)Related docs Back to top

Akari is the engine’s pure shader language: one .akari source emits Metal (MSL), D3D12 (HLSL) and Vulkan (HLSL in its Vulkan dialect, compiled to SPIR-V), then platform compilers produce bytecode. Product packaging and pass roles live in Shader authoring. GPU packing rules: src/akari/docs/abi.md. Tool layout: src/akari/. Editor highlighting: tools/vscode-extensions/akari (VS Code / Cursor); tools/nvim-plugins/akari (Neovim / Vim).

This page is the language surface for authors. It describes implemented syntax and checks only. Concrete syntax: src/akari/syntax.ebnf (W3C EBNF aligned with akari_syntax).


Mental model

text
.akari  →  parse → HIR → IR → pure MSL | HLSL | Vulkan HLSL  →  metalc | DXC | DXC -spirv  →  .metallib | .cso | .spv

Vulkan is the HLSL emitter in a second dialect, not a separate text backend: math, texture, ray-query and wave lowerings are shared with D3D12, and only the binding model differs ([[vk::binding(binding, set)]] from the manifest, runtime descriptor arrays for the bindless heaps, explicit [[vk::location]] stage I/O, [[vk::image_format]] on storage textures).

  • Packages (#shader) are one pass / PSO family (often multi-stage).
  • Modules (#module) are shared helpers imported by name.
  • Bindings use @bind(group.name); slots come from bindings.kaji.json, not hardcoded registers.
  • No dual native metal/ / d3d12/ / vulkan/ authoring trees.
  • Array lengths are integer constant expressions: literals or const names (uint[HIKARI_BIN_BUCKETS], float[N * 2u]). A length that does not evaluate, or evaluates to zero, is a compile error.

File structure

One root per file. Headers are #… directives. Body items are struct / enum / fn / const / variant (plus items implied by #require).

akari
#shader composite
#version 2
#layout fullscreen_texture_table
#import preamble, fullscreen_triangle
#require ray_query

struct V {
    @position position: float4
    uv: float2
}

const PI: float = 3.14159265

@vertex
fn composite_vertex(@vertex_id vid: uint) -> V {
    return V {
        position: hikari_full_screen_position(vid),
        uv: hikari_full_screen_uv(vid),
    }
}

@fragment
fn composite_fragment(
    @stage_in inn: V,
    @bind(post.hdr) hdr: Tex2D,
    @sampler(linear, clamp) samp,
) -> float4 {
    return hdr.sample(samp, inn.uv)
}

Headers

DirectiveWhereRole
#shader namepackage rootPass package
#module namemodule rootShared import unit
#version NeitherSyntax version (default 2)
#layout name#shader onlyBinding layout contract (standard, ui, …)
#import a, beitherLoad pure modules from akariImportPaths / engine SDK
#require a, beitherExplicit capabilities (optional; also inferred from use)

Paths:

TreeRole
src/hikari/shaders/akari/packages/*.akariEngine packages
src/hikari/shaders/akari/modules/*.akariEngine modules
<game>/assets/shaders/akari/packages/Game packages

Statement ; is optional. Engine/game sources usually keep ;. Separators inside for (init; cond; step) remain required. akari fmt always emits ;.


Types

Scalars and vectors

FamilyForms
Boolbool, bool2…bool4
Signed intint / i32, int2…int4
Unsigneduint / u32, uint2…uint4
Floatfloat / f32, float2…float4
Halfhalf / f16, half2…half4
Wide intuint64 / u64 (bindless addresses)

Aliases: vec2/vec3/vec4 → float vectors.

Matrices

Column-major. Named float{cols}x{rows}:

float2x2, float2x3, float2x4, float3x2, float3x3, float3x4, float4x2, float4x3, float4x4
(aliases: mat2/mat3/mat4 for square).

* between matrix and vector is matrix–vector mul. Prefer mul(m, v) when you want an explicit call. transpose(m) is first-class.

Arrays

akari
let planes: float4[6] = …

Length must be a constant integer. Uniform array stride follows ABI (std140-like); storage buffers pack tightly.

Resources

TypeMeaning
Uniform<T>Constant / cbuffer
Buf<T>Read-only structured / device buffer
RwBuf<T>Read–write structured buffer
AtomicBuf<T>Atomic buffer (Metal atomic_T*, HLSL UAV interlocked)
Tex2D / Tex2DArray / Tex3D / TexCube / TexCubeArraySampled textures (texel float4)
TexDepth2D / depth2d / Tex2D<depth>Depth texture for sample_cmp (MSL depth2d<float>, HLSL Texture2D<float>; load → float)
Tex2D<uint> / Tex2D<int> (and the same on the other four)Integer textures — load only, texel uint4 / int4
RwTex2D / RwTex2D<r16f|r32f|rg16f|r11g11b10f|rgba16f|r32u|r32i>Storage image (default texel float4)
RwTex3D / RwTex3D<r16f|r32f|rg16f|r11g11b10f|rgba16f|r32u|r32i>Storage volume — same formats; store takes a uint3 coord
SamplerFiltering sampler
SamplerCmpComparison sampler (shadows)
AccelStructTLAS handle (Metal IAS / DXR)

Structs

akari
struct MeshVertex {
    @location(0) position: float3
    @location(1) normal: float3
    @location(2) color: float4
    @location(3) uv: float2
}

@packed
struct Tight {
    a: float
    b: float3   // storage packing even under uniform-style rules
}

Field attributes:

AttrRole
@location(N)Vertex input location / MRT index fallback
@positionClip position
@flatNo interpolation
@noperspectiveLinear (no perspective)
@target(group.name)MRT color via HK_TARGET

Literals and operators

akari
let i: int = -3
let u: uint = 0x80000000u
let f: float = 1.5
let b: bool = true
let t: float = cond ? a : b   // right-associative

Arithmetic: + - * / %
Compare: == != < <= > >=
Logic: && \|\| !
Bitwise: & \| ^ ~ << >>
Inc/dec: ++x --x x++ x--
Assign: = and += -= *= /= %= &= \|= ^= <<= >>=

Cast / construct:

akari
let h: half = float(i) as half
let v: float3 = float3(1.0, 0.0, 0.0)
let z: float3 = float3(0.0)        // splat: one scalar fills every component
let u: uint = as_uint(f)            // bitcast
let f2: float = as_float(u)

Swizzles: .xyzw / .rgba (length 1–4).

Struct literals

Named fields. Missing fields are an error unless the literal ends with .. (zero-fill) or ..base (copy remaining fields from base). Zero-fill is scalars/vectors/arrays/nested structs; not resources.

akari
let s = VsOut { position: p, uv: uv }
let rest = VsOut { position: p, .. }
let skinned = GBufferVertex { lodFade: OPAQUE, ..common }

Positional VsOut(p, uv) still works (declaration order, all fields required).

Array literals

akari
let words = [0u, 1u, 2u, 3u]
let zeros = [0u; 4]            // repeat; count is a constant integer

Compile-time layout

Folded to uint literals from the GPU ABI (storage packing by default; sizeof(Uniform<T>) uses uniform packing):

akari
const N: uint = sizeof(MeshletWork)
const O: uint = offsetof(MeshletWork, flags)

Declarations

akari
const K: float = 16.0   // module-scope constant

fn helper(x: float3) -> float3 {
    const SCALE: float = 2.0   // immutable local (init required)
    let a: float = 1.0         // immutable local
    var b: float = 0.0         // mutable
    return normalize(x) * a * SCALE
}
FormScopeMutableInit
const (item)modulenorequired
const (stmt)localnorequired
letlocalnooptional
varlocalyesoptional

Assign / ++ / -- to const or let bindings (including fields/swizzles of them) is a type error. Module const and local const share the keyword; locals are not merged into the module constant table.

Enums

Typed closed sets of constants (bitflags and ordinals). Distinct from compile-time variant axes.

akari
enum MapMask: uint {
    Albedo = 1u,
    Normal = 2u,
    MetallicRoughness = 4u,
    // omitted discriminant → previous + 1
}

fn sample_maps(mask: uint) {
    if ((mask & MapMask.Albedo) != 0u) { /* … */ }
}
RuleDetail
UnderlyingRequired: uint or int
AccessName.Case (dot path)
EmitModule consts Name_Case as underlying type (MSL constant / HLSL static const)
TypingType::Enum is transparent to its underlying for assign / bitwise / compare
Auto valuesFirst omitted case is 0; each following omitted case is previous + 1

Prefer shared module enums over pasting local let/const flag bits in every package.

fn without a stage attribute is a free function (shared helper). Stage entries use attributes below.

Visibility (private)

Unmarked items stay import-visible (same as today). Prefix private to keep a symbol local to its defining #module / #shader — importers cannot call or name it; the defining file still can. Stored on struct / fn / enum / const. A leading private on variant is parsed and ignored (VariantDecl has no is_private).

akari
#module brdf

private fn schlick_weight(n_dot_v: float) -> float { … }

fn hikari_fresnel_schlick_nov(f0: float3, n_dot_v: float) -> float3 {
    return … schlick_weight(n_dot_v) …
}

Default arguments, named arguments, overloads

akari
fn eval_ibl(..., reflections_own_specular: bool = true) -> float3 { … }

// Trailing defaults may be omitted; names reorder before arity checks.
let a = eval_ibl(u, sky, …, N, V, …);
let b = eval_ibl(…, reflections_own_specular: false);

// Same source name, different parameter types → overload (emit uses distinct symbols).
fn fresnel(f0: float3, n_dot_v: float) -> float3 { … }
fn fresnel(f0: float, n_dot_v: float) -> float { … }

A scalar user function also lifts to vectors when no exact overload exists: srgb(float3) becomes float3(srgb(x), srgb(y), srgb(z)). An explicit vector overload still wins.

ref / inout

Passing convention, written after the colon. Handles (Buf, textures, samplers) ignore it — they are already cheap.

akari
fn bump(quota: inout uint) { quota = quota - 1u }
fn read_row(row: ref VsOut) -> float4 { return row.position }

inout is a mutable borrow (writes go back). ref is read-only. Both require an lvalue. Buffer elements (buf[i]) cannot be borrowed — pass Buf<T> and the index (a ref T of a float4x4[6] row would copy on HLSL).

A context is a struct of resource handles. It is a compile-time parameter pack — backends flatten it into per-handle arguments, because ordinary GPU structs cannot hold textures, samplers, or device pointers:

akari
struct LightCtx {
    lights: Buf<GpuLightRecord>
    atlas: TexDepth2D
    samp: SamplerCmp
}
fn evaluate(ctx: LightCtx, ...) { … ctx.lights[i] … }
// emits as evaluate(ctx_lights, ctx_atlas, ctx_samp, ...)

Function values

Compile-time only. Parameters of type fn(T) -> R specialize the callee at each call site (no GPU function pointers):

akari
fn apply(x: float, f: fn(float) -> float) -> float { return f(x) }
let y = apply(3.0, scale)

The argument must be a free-function name or a closure. Closures capture outer locals (including resource packs) and are specialized the same way — captures become extra parameters, not GPU function pointers:

akari
fn apply(x: float, f: fn(float) -> float) -> float { return f(x) }
let y = apply(3.0, |v| v * s)
let vis = evaluate_lights(ctx, |light, kind, L, pos, depth, dist, NoL, shadowed| {
    var v = 1.0
    if (shadowed) { v = raster_vis(light, kind, L, pos, depth) }
    return v
})

Parameter types may be omitted when the expected fn(...) type supplies them. |x: inout uint| { x = x - 1u } and || expr (no parameters) are valid.

@bits field loads

@bits on a float struct field means reads lower through as_uint (GPU bitcast). Do not use this to reshuffle CPU-mirrored packs such as HikariGpuMaterial.material.w — keep as_uint(.w) at those call sites so the wire layout stays stable. Prefer @bits on shader-local structs only.

akari
struct Flags {
    @bits packed: float
}
// `f.packed` is typed `uint` after lower

Stages and system values

akari
@vertex
fn vs(…) -> VsOut { … }

@fragment
fn ps(…) -> float4 { … }

@compute(threads = (8, 8, 1))
fn cs(…) { … }
AttributeTypical typeRole
@vertex_iduintVertex index
@instance_iduintInstance index
@dispatch_iduint / uint3Global thread id
@group_iduint / uint3Workgroup id
@local_iduint3Thread in group
@local_indexuintFlat index in group
@front_facingboolFront face
@vertex_instructMesh vertex (IA on D3D12, pull on Metal)
@stage_instructInterpolated inputs to fragment

Vertex packages with @vertex_in + @location fields lower to Metal float-stream pull or HLSL IA automatically.


Resources and bindings

Prefer semantic binds:

akari
@bind(frame.uniforms) uniforms: Uniform<HikariUniforms>
@bind(draw.object) objects: Buf<HikariObjectData>
@bind(debug.albedo) albedo: Tex2D
@bind(debug.shadow_depth) atlas: TexDepth2D
@sampler(linear, repeat) samp
@sampler(comparison, greater_equal, linear, clamp) shadow_s: SamplerCmp
FormNotes
@bind(group.name)Resolved via HK_BUFFER / HK_TEXTURE in the manifest
@bind(group.name) tex: TexCube[N] / Tex2D[N] / RwTex2D[N]Resource array: base key + N consecutive texture slots (Metal array<…, N> [[texture(base)]], HLSL Texture… name[N] : register(tBase)). No new host bind keys — declare only the base (e.g. depth_pyramid_compute.mips).
@slot(N)Raw slot (rare; compute experiments)
@sampler(filter, address)linear/point + clamp/repeat/mirror. Not a manifest key: Metal constexpr sampler, HLSL sN in declaration order.
@sampler(comparison, filter, address)Comparison sampler; compare func defaults to less_equal
@sampler(comparison, compare_func, filter, address)Explicit compare (greater_equal, less_equal, less, greater, equal, not_equal, always, never) — pick to match the host depth convention (Hikari reverse-Z → greater_equal)
akari
@bind(depth_pyramid_compute.mips) mips: RwTex2D<rg16f>[8],
// mips[i].store(px, value)

Unknown binds error when the manifest is loaded. Soft/unit tests may keep empty manifests.

MRT outputs:

akari
struct GBufferOutput {
    @target(gbuffer.albedo) albedo: float4
    @target(gbuffer.normal) normal: float4
    // …
}

Control flow

akari
if (cond) { … } else if (other) { … } else { … }

for (var i: uint = 0u; i < n; i = i + 1u) { … }
while (cond) { … }
do { … } while (cond)

switch (x) {
    case 0:
        …
        break
    case 1:
    case 2:
        …
        break
    default:
        …
        break
}

break
continue
discard    // fragment kill

Flow hints

On if / for / while / do:

AkariHLSLMetal
@unroll / @unroll(N)[unroll] / [unroll(N)][[unroll]] (N dropped)
@loop[loop][[dont_unroll]]
@branch[branch](no-op)
@flatten[flatten](no-op)
akari
@unroll
for (var i: uint = 0u; i < 6u; i = i + 1u) { … }

@branch
if ((mask & 1u) != 0u) { … }

Textures

Methods (preferred) or free functions with the same names:

OpExampleNotes
sampletex.sample(samp, uv)VS/CS force lod 0 on HLSL
sample_leveltex.sample_level(samp, uv, mip)Explicit lod
sample_cmpdepth.sample_cmp(cmp_samp, uv, ref)Returns float
sample_gradtex.sample_grad(samp, uv, ddx, ddy)Explicit gradients
loadtex.load(coord) / tex.load(coord, mip)Integer coords; result follows the texel type
storerw.store(coord, value)RwTex2D (uint2) / RwTex3D (uint3)
dimensionstex.dimensions() / dimensions(tex, mip)uint2 or uint3
mip_levelsmip_levels(tex)Mip count

Array / cube packing

  • Tex2DArray: UV + layer as float3(uv, layer) (Metal splits .xy + uint(.z)).
  • TexCubeArray: direction + layer as float4 where applicable.

Store arity

store must supply at least the target format's channel count — float2 for rg16f, float3 for r11g11b10f, a scalar for r16f/r32f/r32u/r32i, float4 otherwise. Extra components are dropped (HLSL swizzles the assignment down; Metal's hardware ignores them), so a float4 store into any format stays valid. A short store is a compile error at lowering rather than a backend error: HLSL rejects it while Metal's write — which only has a vec<T, 4> overload the emitter zero-fills to reach — would have accepted it, so the check keeps the two backends agreeing on what compiles.

Integer textures

No hardware filters integer texels, so the whole sample family is rejected on Tex2D<uint> / Tex2D<int> at compile time rather than surfacing as a DXC or Metal error against generated source. Read them with load and integer coordinates. Use them for bitfields — the G-buffer material target is one — where a filtered read would silently blend unrelated flags across an edge.

Depth textures

sample_cmp requires TexDepth2D (Metal only exposes sample_compare on depth2d). A colour Tex2D is a compile error. load, sample, sample_level, and sample_grad on a depth texture return a scalar float (not float4) — matching Metal depth2d and HLSL Texture2D<float>.

akari
let c = arr.sample(samp, float3(uv, float(layer)))
let shadow = atlas.sample_cmp(shadow_s, uv, ref_z)
let d = atlas.load(px)   // float
rw.store(uint2(id.xy), float4(h, 0.0, 0.0, 0.0))

Compute

akari
@compute(threads = (64, 1, 1))
fn meshlet_cull(
    @bind(meshlet_cull.frame) frame: Uniform<CullFrame>,
    @bind(meshlet_cull.arguments) arguments: AtomicBuf<uint>,
    @dispatch_id index: uint,
    @local_id lid: uint3,
    @local_index lix: uint,
) {
    @groupshared var tile: float[64]
    // …
    barrier()
    let slot = atomic_add(arguments, batch * 8u + 1u, 1u)
}
FeatureNotes
@groupshared var name: TThreadgroup / groupshared storage
barrier()Group memory + execution barrier
AtomicsSee below (compute entries)

Atomics

On AtomicBuf / compatible UAV storage:

CallArgs
atomic_add(buf, index, value)
atomic_min / max / and / or / xorsame
atomic_exchange(buf, index, value)
atomic_compare_exchange(buf, index, expected, desired) → previous

Return type is typically the previous value (uint path). Stage: compute entries only (free helpers allowed; stage checked on entries).


Lane groups (wave)

Ops across a SIMD-group / wave. The point is to replace N same-address atomics with one: have every lane vote, prefix-sum the votes for a local offset, and let a single lane take the group's base.

CallMetalHLSL
wave_prefix_sum(uint) -> uintsimd_prefix_exclusive_sumWavePrefixSum
wave_sum(uint) -> uintsimd_sumWaveActiveSum
wave_is_first() -> boolsimd_is_firstWaveIsFirstLane
wave_read_first(T) -> Tsimd_broadcast_firstWaveReadLaneFirst

Prefix sums are exclusive on both backends. Stage: compute and fragment — Metal has no SIMD-group functions in a vertex stage, so the language rejects it rather than emitting something only D3D12 can run. D3D12 needs SM 6.0, which is already the floor.

Both backends define these over the active lanes, so calling them inside divergent control flow gives the divergent answer. The portable idiom is to vote 0/1 from every lane instead of branching around the op:

akari
var vote = 0u
if (survived) { vote = 1u }
let offset = wave_prefix_sum(vote)
let total = wave_sum(vote)
var base = 0u
if (wave_is_first() && total > 0u) { base = atomic_add(counters, 0u, total) }
base = wave_read_first(base)
if (survived) { out_list[base + offset] = value }

Math and builtins

Portable math (renamed per backend as needed, e.g. mix↔lerp, fract↔frac):

min max clamp abs floor ceil round trunc sign fract/frac saturate
sin cos tan asin acos atan atan2
sqrt rsqrt exp log exp2 log2 pow fma
normalize length distance dot cross reflect refract
mix/lerp step smoothstep any all select
transpose mul
countbits firstbitlow reversebits (integer bit ops on uint/uintN; MSL popcount/ctz/reverse_bits)
ddx/dfdx ddy/dfdy (fragment entries)
remap_clip_z / clip_space_remap (engine clip-Z convention)

Engine helpers live in modules (hikari_*, lighting, etc.) and are normal functions after #import.


Ray tracing

Language intrinsics (rt_*, bindless_*). Engine helpers (hikari_rt_*, hikari_rt_candidate_accepts, …) live in modules and are normal functions after #import.

akari
let vis: float = rt_visibility(scene, origin, dir, t_min, t_max, mask)
let hit: RtTraceHit = rt_trace(scene, origin, dir, t_min, t_max, mask)
// hit.hit, hit.distance, hit.instance_id, hit.primitive_id, hit.barycentric

// Alpha-aware: non-opaque candidates run hikari_rt_candidate_accepts
// (engine helper; import raytraced_scene) inside the traversal.
let vis_m: float = rt_visibility_masked(
    scene, instances, materials, buffer_table, texture_table,
    instance_count, origin, dir, t_min, t_max, mask)
let hit_m: RtTraceHit = rt_trace_masked(
    scene, instances, materials, buffer_table, texture_table,
    instance_count, origin, dir, t_min, t_max, mask)

let f = bindless_load_f32(table, buf_index, elem)
let f2 = bindless_load_f32x2(table, buf_index, elem)
let f3 = bindless_load_f32x3(table, buf_index, elem)
let f4 = bindless_load_f32x4(table, buf_index, elem)
let u = bindless_load_u32(table, buf_index, elem)

let t2d = bindless_tex2d(texture_table, index)
let cube = bindless_texcube(texture_table, index)
let vol = bindless_tex3d(texture_table, index)
let depth = bindless_texdepth(texture_table, index)
  • Metal: #include <metal_raytracing> + helper bodies when RT is used.
  • D3D12: DXC shader model ≥ 6_5 when ray_query is required (auto if the spec omits shaderModel).

Variants (compile-time axes)

Finite named axes only — not material feature matrices.

akari
variant TONEMAP { aces, reinhard }

fn apply_tonemap(hdr: float3) -> float3 {
    if variant(TONEMAP) == .aces {
        return hikari_tonemap_aces(hdr)
    } else if variant(TONEMAP) == .reinhard {
        return reinhard(hdr)
    }
    return hdr
}

Selection comes from the compile spec / CLI (variants map). Inactive arms are eliminated; both arms are still typechecked.

Product example: tonemap.akari declares variant TONEMAP { aces, reinhard }. The Metal/D3D12 specs compile that source twice — package tonemap with TONEMAP=aces, package tonemap_reinhard with TONEMAP=reinhard — so Zig still loads two artifacts while authoring stays one file.

Emit hygiene (authors)

  • Metal: non-entry functions get forward prototypes, so helper order in a file does not matter.
  • HLSL: reserved identifiers (e.g. field sample) are renamed on both declaration and access (_sample). Prefer non-reserved names in new code.

Capabilities (#require)

Optional file header. Also inferred from use.

CapTriggered byStage rule (entries)
derivativesddx / ddy@fragment only
atomicsatomic_*@compute only
threadgroupbarrier, @groupshared@compute only
wavewave_*@compute / @fragment
ray_queryrt_*, AccelStructany
rw_texture.store / RwTex2D / RwTex3Dany
akari
#require ray_query, atomics
  • D3D12: ray_query ⇒ SM 6_5 minimum (auto or reject too-low explicit SM).
  • Metal: no flag matrix; RT packages emit raytracing includes. Stage gates still apply.
  • Unknown names → error: unknown capability in #require ….

You rarely need #require if you call the ops correctly; it documents intent and fails early on typos.


Type checking

At IR lower (before metalc/DXC):

  • Unknown symbols / functions / fields / methods
  • Wrong arity (user fns, math, texture ops)
  • Assign / init / return type mismatch
  • Capability stage violations

Errors are AkariError::Type with messages on the original .akari spans (CLI also prints @akari diag JSON to stderr for tooling).

When metalc/DXC fails, diagnostics are remapped through the emit LineMap (.linemap.json) to .akari path and line when possible. Toolchain “candidate function” notes and known Metal attribute noise are stripped so the first error is actionable.


Complete mini examples

Vertex + fragment (geometry-style)

akari
#shader gbuffer_mini
#layout standard
#import preamble

struct MeshVertex {
    @location(0) position: float3
    @location(1) uv: float2
}

struct VsOut {
    @position position: float4
    uv: float2
    @flat instanceId: uint
}

@vertex
fn gbuffer_vertex(
    @vertex_in vert: MeshVertex,
    @bind(frame.uniforms) uniforms: Uniform<HikariUniforms>,
    @bind(draw.object) objects: Buf<HikariObjectData>,
    @instance_id iid: uint,
) -> VsOut {
    let object = objects[iid]
    let world = object.modelMatrix * float4(vert.position, 1.0)
    return VsOut {
        position: remap_clip_z(uniforms.projectionMatrix * uniforms.viewMatrix * world),
        uv: vert.uv,
        instanceId: object.instanceId,
    }
}

@fragment
fn gbuffer_fragment(
    @stage_in inn: VsOut,
    @bind(debug.albedo) albedo: Tex2D,
    @sampler(linear, repeat) samp,
) -> float4 {
    return albedo.sample(samp, inn.uv)
}

Compute + atomics + groupshared

akari
#shader reduce
#layout standard
#require atomics, threadgroup

@compute(threads = (64, 1, 1))
fn reduce(
    @bind(meshlet_cull.source) source: Buf<float>,
    @bind(meshlet_cull.arguments) counters: AtomicBuf<uint>,
    @dispatch_id gid: uint,
    @local_index li: uint,
) {
    @groupshared var partial: float[64]
    partial[li] = source[gid]
    barrier()
    if (li == 0u) {
        var s: float = 0.0
        @unroll
        for (var i: uint = 0u; i < 64u; i = i + 1u) {
            s = s + partial[i]
        }
        if (s > 0.0) {
            let _ = atomic_add(counters, 0u, 1u)
        }
    }
}

Content compute write (RwTex2D)

akari
#shader water_sim
#layout standard
#import content_compute

@compute(threads = (8, 8, 1))
fn water_sim(
    @bind(content_compute.output) output: RwTex2D<r32f>,
    @bind(content_compute.frame) frame: Uniform<HikariContentComputeFrame>,
    @dispatch_id id: uint3,
) {
    let uv = float2(id.xy)
    let h = sin(uv.x * 0.1 + frame.simTime) * 0.5
    output.store(id.xy, float4(h, 0.0, 0.0, 0.0))
}

Tooling (authors)

sh
# Transpile one package (--target=metal | d3d12 | spirv)
bin/akari/akari transpile --input=path.akari --target=metal --out=/tmp/out \
  --import-dir=src/hikari/shaders/akari/modules --bindings=src/hikari/shaders/bindings.kaji.json

# Full spec compile (Metal, D3D12 or Vulkan — build.akari.{metal,d3d12,vulkan}.json)
bin/akari/akari compile \
  --spec=src/hikari/shaders/build.akari.metal.json \
  --out=/tmp/shaders --cache=/tmp/akari-cache \
  --engine-sdk=. --config=debug

# Layout dump (CPU mirror checks)
bin/akari/akari transpile … --dump-layout

Product builds go through Kaji / Shinra (--shaders:config=debug|release → Shinra --shaders-config= / env AKARI_SHADER_CONFIG → akari compile --config=). Binary: flat bin/akari/akari.


Authoring rules (product)

  1. Pure .akari only — no hand-edited MSL/HLSL SoT.
  2. @bind / @target / @sampler over raw registers (except deliberate @slot experiments).
  3. Prefer #import modules over copy-paste.
  4. Keep CPU struct layouts aligned with ABI (--dump-layout).
  5. Material features → runtime map masks, not Cartesian variants.
  6. Add new binds to bindings.kaji.json and shaders.md together.

Related docs

DocContents
src/akari/syntax.ebnfFormal grammar (W3C EBNF; akari_syntax)
Shader authoringPasses, layouts, manifests, specs, validation
RenderingGraph order, composability contracts
ABIOffsets, packing, @packed
src/akari/AGENTS.mdTool layout for agents / build
PreviousReferenceNext Kawa language reference

Documentation follows the current engine checkout.

Snapshot cc148c75Source docs/reference/akari-language.md
On this pageMental modelFile structureHeadersTypesScalars and vectorsMatricesArraysResourcesStructsLiterals and operatorsStruct literalsArray literalsCompile-time layoutDeclarationsEnumsVisibility (private)Default arguments, named arguments, overloadsref / inoutFunction values@bits field loadsStages and system valuesResources and bindingsControl flowFlow hintsTexturesComputeAtomicsLane groups (wave)Math and builtinsRay tracingVariants (compile-time axes)Emit hygiene (authors)Capabilities (#require)Type checkingComplete mini examplesVertex + fragment (geometry-style)Compute + atomics + groupsharedContent compute write (RwTex2D)Tooling (authors)Authoring rules (product)Related docs Back to top