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
.akari → parse → HIR → IR → pure MSL | HLSL | Vulkan HLSL → metalc | DXC | DXC -spirv → .metallib | .cso | .spvVulkan 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 frombindings.kaji.json, not hardcoded registers. - No dual native
metal//d3d12//vulkan/authoring trees. - Array lengths are integer constant expressions: literals or
constnames (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).
#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
| Directive | Where | Role |
|---|---|---|
#shader name | package root | Pass package |
#module name | module root | Shared import unit |
#version N | either | Syntax version (default 2) |
#layout name | #shader only | Binding layout contract (standard, ui, …) |
#import a, b | either | Load pure modules from akariImportPaths / engine SDK |
#require a, b | either | Explicit capabilities (optional; also inferred from use) |
Paths:
| Tree | Role |
|---|---|
src/hikari/shaders/akari/packages/*.akari | Engine packages |
src/hikari/shaders/akari/modules/*.akari | Engine 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
| Family | Forms |
|---|---|
| Bool | bool, bool2…bool4 |
| Signed int | int / i32, int2…int4 |
| Unsigned | uint / u32, uint2…uint4 |
| Float | float / f32, float2…float4 |
| Half | half / f16, half2…half4 |
| Wide int | uint64 / 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
let planes: float4[6] = …Length must be a constant integer. Uniform array stride follows ABI (std140-like); storage buffers pack tightly.
Resources
| Type | Meaning |
|---|---|
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 / TexCubeArray | Sampled 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 |
Sampler | Filtering sampler |
SamplerCmp | Comparison sampler (shadows) |
AccelStruct | TLAS handle (Metal IAS / DXR) |
Structs
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:
| Attr | Role |
|---|---|
@location(N) | Vertex input location / MRT index fallback |
@position | Clip position |
@flat | No interpolation |
@noperspective | Linear (no perspective) |
@target(group.name) | MRT color via HK_TARGET |
Literals and operators
let i: int = -3
let u: uint = 0x80000000u
let f: float = 1.5
let b: bool = true
let t: float = cond ? a : b // right-associativeArithmetic: + - * / %
Compare: == != < <= > >=
Logic: && \|\| !
Bitwise: & \| ^ ~ << >>
Inc/dec: ++x --x x++ x--
Assign: = and += -= *= /= %= &= \|= ^= <<= >>=
Cast / construct:
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.
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
let words = [0u, 1u, 2u, 3u]
let zeros = [0u; 4] // repeat; count is a constant integerCompile-time layout
Folded to uint literals from the GPU ABI (storage packing by default; sizeof(Uniform<T>) uses uniform packing):
const N: uint = sizeof(MeshletWork)
const O: uint = offsetof(MeshletWork, flags)Declarations
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
}| Form | Scope | Mutable | Init |
|---|---|---|---|
const (item) | module | no | required |
const (stmt) | local | no | required |
let | local | no | optional |
var | local | yes | optional |
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.
enum MapMask: uint {
Albedo = 1u,
Normal = 2u,
MetallicRoughness = 4u,
// omitted discriminant → previous + 1
}
fn sample_maps(mask: uint) {
if ((mask & MapMask.Albedo) != 0u) { /* … */ }
}| Rule | Detail |
|---|---|
| Underlying | Required: uint or int |
| Access | Name.Case (dot path) |
| Emit | Module consts Name_Case as underlying type (MSL constant / HLSL static const) |
| Typing | Type::Enum is transparent to its underlying for assign / bitwise / compare |
| Auto values | First 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).
#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
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.
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:
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):
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:
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.
struct Flags {
@bits packed: float
}
// `f.packed` is typed `uint` after lowerStages and system values
@vertex
fn vs(…) -> VsOut { … }
@fragment
fn ps(…) -> float4 { … }
@compute(threads = (8, 8, 1))
fn cs(…) { … }| Attribute | Typical type | Role |
|---|---|---|
@vertex_id | uint | Vertex index |
@instance_id | uint | Instance index |
@dispatch_id | uint / uint3 | Global thread id |
@group_id | uint / uint3 | Workgroup id |
@local_id | uint3 | Thread in group |
@local_index | uint | Flat index in group |
@front_facing | bool | Front face |
@vertex_in | struct | Mesh vertex (IA on D3D12, pull on Metal) |
@stage_in | struct | Interpolated 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:
@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| Form | Notes |
|---|---|
@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) |
@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:
struct GBufferOutput {
@target(gbuffer.albedo) albedo: float4
@target(gbuffer.normal) normal: float4
// …
}Control flow
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 killFlow hints
On if / for / while / do:
| Akari | HLSL | Metal |
|---|---|---|
@unroll / @unroll(N) | [unroll] / [unroll(N)] | [[unroll]] (N dropped) |
@loop | [loop] | [[dont_unroll]] |
@branch | [branch] | (no-op) |
@flatten | [flatten] | (no-op) |
@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:
| Op | Example | Notes |
|---|---|---|
| sample | tex.sample(samp, uv) | VS/CS force lod 0 on HLSL |
| sample_level | tex.sample_level(samp, uv, mip) | Explicit lod |
| sample_cmp | depth.sample_cmp(cmp_samp, uv, ref) | Returns float |
| sample_grad | tex.sample_grad(samp, uv, ddx, ddy) | Explicit gradients |
| load | tex.load(coord) / tex.load(coord, mip) | Integer coords; result follows the texel type |
| store | rw.store(coord, value) | RwTex2D (uint2) / RwTex3D (uint3) |
| dimensions | tex.dimensions() / dimensions(tex, mip) | uint2 or uint3 |
| mip_levels | mip_levels(tex) | Mip count |
Array / cube packing
Tex2DArray: UV + layer asfloat3(uv, layer)(Metal splits.xy+uint(.z)).TexCubeArray: direction + layer asfloat4where 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>.
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
@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)
}| Feature | Notes |
|---|---|
@groupshared var name: T | Threadgroup / groupshared storage |
barrier() | Group memory + execution barrier |
| Atomics | See below (compute entries) |
Atomics
On AtomicBuf / compatible UAV storage:
| Call | Args |
|---|---|
atomic_add | (buf, index, value) |
atomic_min / max / and / or / xor | same |
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.
| Call | Metal | HLSL |
|---|---|---|
wave_prefix_sum(uint) -> uint | simd_prefix_exclusive_sum | WavePrefixSum |
wave_sum(uint) -> uint | simd_sum | WaveActiveSum |
wave_is_first() -> bool | simd_is_first | WaveIsFirstLane |
wave_read_first(T) -> T | simd_broadcast_first | WaveReadLaneFirst |
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:
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.
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_queryis required (auto if the spec omitsshaderModel).
Variants (compile-time axes)
Finite named axes only — not material feature matrices.
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.
| Cap | Triggered by | Stage rule (entries) |
|---|---|---|
derivatives | ddx / ddy | @fragment only |
atomics | atomic_* | @compute only |
threadgroup | barrier, @groupshared | @compute only |
wave | wave_* | @compute / @fragment |
ray_query | rt_*, AccelStruct | any |
rw_texture | .store / RwTex2D / RwTex3D | any |
#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)
#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
#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)
#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)
# 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-layoutProduct 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)
- Pure
.akarionly — no hand-edited MSL/HLSL SoT. @bind/@target/@samplerover raw registers (except deliberate@slotexperiments).- Prefer
#importmodules over copy-paste. - Keep CPU struct layouts aligned with ABI (
--dump-layout). - Material features → runtime map masks, not Cartesian variants.
- Add new binds to
bindings.kaji.jsonand shaders.md together.
Related docs
| Doc | Contents |
|---|---|
src/akari/syntax.ebnf | Formal grammar (W3C EBNF; akari_syntax) |
| Shader authoring | Passes, layouts, manifests, specs, validation |
| Rendering | Graph order, composability contracts |
| ABI | Offsets, packing, @packed |
src/akari/AGENTS.md | Tool layout for agents / build |