6. Writing GPU shaders in Daslang
The dasSpirv module (modules/dasSpirv) is a pure-daslang compile-time backend that lowers
annotated daslang functions directly to SPIR-V binary — no GLSL, no glslang, no LLVM. A
[compute_shader] / [vertex_shader] / [fragment_shader] function is read at compile time,
its AST is walked by an AstVisitor emitter, and the resulting SPIR-V words are captured into a
module global your host code feeds straight to vkCreateShaderModule. Just as [jit] lowers
daslang to native code via LLVM, [compute_shader] lowers daslang to GPU code via SPIR-V.
Drivers do all GPU optimization, so dasSpirv emits naive, valid SPIR-V (glslang does the same) and
leaves the optimizing to the driver. Validity is enforced by spirv-val over every blob the test
suite produces; every emitted opcode has a test (an opcode census plus the LCOV dispatch gate).
This page is the language/API reference. For runnable, screenshot-backed walkthroughs (compute, raymarch, textured mesh, shadow mapping, …) see the dasVulkan tutorials, which run the emitted blobs on a real driver.
6.1. Overview
Module:
require spirv/spirv_shaderbrings in the three stage annotations and re-exports the shader builtins (spirv_builtins). The reflection types live inspirv_reflect.What you write: an ordinary daslang function annotated with a stage, plus module-global
vars annotated with their resource role (@ssbo/@uniform/@push_constant/@in/@out/ a sampler type). The function body is never executed in daslang — only its AST is read and lowered.What you get: a module global
array<uint>holding the SPIR-V words (named<func>`spirvby default, or via thenameargument), plus a companion<name>_reflect : array<uint>holding the encoded reflection.
6.2. Quick start: a compute shader
require spirv/spirv_shader
// an SSBO at set 0, binding 0
var @ssbo @binding = 0 data : array<uint>
[compute_shader(local_size_x=64, name="square_spv")]
def square {
let i = gl_GlobalInvocationID.x
data[i] = i * i
}
After compilation, square_spv : array<uint> holds the SPIR-V. The host hands it to Vulkan and
decodes square_spv_reflect to build the descriptor-set layout — no hand-declared bindings:
require spirv/spirv_reflect
let refl = decode_reflection(square_spv_reflect)
// refl.bindings == [ {set=0, binding=0, kind=storage_buffer, count=1, stages=compute} ]
// refl.local_size == int3(64, 1, 1)
// feed square_spv words to vkCreateShaderModule (codeSize = 4*length, pCode = addr(words[0]))
6.3. Shader stages
Each stage is a function macro. [compute_shader] takes local_size_x / local_size_y /
local_size_z (default 1) for the workgroup size; all three take an optional name for the
captured global (default <func>`spirv). The entry point is always emitted as "main".
Annotation |
Execution model |
Notes |
|---|---|---|
|
|
|
|
|
reads |
|
|
reads |
6.4. Resources and stage I/O
Module-global vars are classified by their annotation. @set / @binding / @location
take an integer value (@binding = 0); @in / @out / @ssbo / @uniform /
@push_constant are flags. Set and binding default to 0 when omitted.
Declaration |
Lowers to |
Reflected kind |
|---|---|---|
|
StorageBuffer, std430, |
|
|
Uniform, std140, |
|
|
PushConstant, |
push-constant range |
|
Input, |
— (graphics I/O) |
|
Output, |
— (graphics I/O) |
@uniform / @push_constant globals must be a struct (32-bit scalar / vector / matrix
members); @ssbo must be an array<T>. Member access (ubo.field) lowers to OpAccessChain
+ OpLoad.
6.5. Built-in variables
Declared in spirv_builtins and recognized by name; available only in the stages shown.
Builtin |
Type |
SPIR-V BuiltIn |
Stage |
|---|---|---|---|
|
|
|
compute |
|
|
|
compute |
|
|
|
compute |
|
|
|
compute |
|
|
|
compute |
|
|
|
vertex |
|
|
|
vertex |
|
|
|
vertex |
|
|
|
fragment |
6.6. Textures and storage images
Sampler / image types are opaque marker structs; the sampling functions are recognized by name and
lower to the matching image op. Declare a sampler as a global with @set / @binding.
Type / call |
Lowers to |
Reflected kind |
|---|---|---|
|
combined image + sampler |
|
|
|
— (fragment) |
|
|
— (any stage) |
|
|
— (any stage) |
|
separate image / separate sampler |
|
|
|
— (fragment) |
|
read-write storage image |
|
|
|
— |
|
|
— |
6.7. Type and layout mapping
Daslang |
SPIR-V type |
Layout |
|---|---|---|
|
|
4 bytes |
|
|
no physical interface-block layout |
|
|
component-wise |
|
|
ColMajor, |
|
|
|
|
|
std430 |
|
|
std140 member |
Matrix · vector and matrix · matrix use SPIR-V’s default column-major convention, so a daslang
M * v is OpMatrixTimesVector with the matrix uploaded as-is.
6.8. Supported language surface
The emitter lowers the shader-legal subset of daslang. Anything outside it is rejected with a clean
compile error (never silently mis-emitted): no new / delete / variants / tuples / try /
string builders / goto — these are not shader constructs.
Control flow:
if/else(OpSelectionMerge),whileand range-for(OpLoopMerge),break/continue, earlyreturn, and the ternary?:(OpSelect, branchless).Operators: full scalar and vector arithmetic (
+ - * / %, unary-), comparisons (== != < > <= >=), logical&&/||, and matrix/vector products.Math:
dot(OpDot) plus the GLSL.std.450 set —sin/cos/tan/pow/exp/log/sqrt/rsqrt/floor/ceil/fract/abs/min/max/lerp/length/distance/normalize/cross/reflect/refractand more (the daslang name maps to its ext-inst opcode).
Note
This is a moving surface — additional rails (extra storage-image formats, depth-compare sampling,
derivatives, discard, shared memory + barriers + atomics, …) land per the dasSpirv roadmap.
The authoritative, never-drifting list of emitted opcodes is the test suite’s opcode census.
6.9. Reflection
dasSpirv emits, alongside each blob, an encoded SpirvReflection
describing the shader’s descriptor bindings and push-constant ranges. The host decodes it once
(decode_reflection) and builds descriptor-set / pipeline layouts from it, so the binding facts
live in exactly one place — the shader source — and never drift from a hand-written layout. The wire
form is a versioned array<uint> that rides the same module-global-capture rail as the blob.
6.10. Tutorials
Runnable, screenshot-backed tutorials — compute, fullscreen raymarching, textured meshes, shadow
mapping, compute particles, and more — live in the dasVulkan repository’s tutorials/
directory, where the emitted SPIR-V runs on a real Vulkan driver (and on lavapipe in CI).