03 - SDF raymarch (compute, no geometry)
Tutorial 02 used the compute pipeline to iterate z = z*z + c per pixel
across the Mandelbrot set. This one uses the same compute pipeline to walk a
ray through a 3D scene defined by signed distance functions – no vertex
buffer, no triangles, no textures; the whole scene is math evaluated at every
pixel. The headline rails:
a storage image at
set=0/binding=0the compute shader writes to (same descriptor shape as Mandelbrot);a push constant carrying one float,
time, which drives the camera orbit, the torus rotation, the sphere bob, and the cosine palette;user-defined helpers (smin, sd_sphere, sd_torus, sd_plane, rot2, map, get_normal, march, soft_shadow, …) lowered to SPIR-V
OpFunctionCall– the raymarcher reads like CPU code;3-vector swizzle and arithmetic (
p.xz,p - float3(...),ro + rd * dist);a wide GLSL.std.450 breadth:
length/normalize/cross/dot/floor/lerp(lowered toFMix) /clamp/cos/sin/pow/exp– everything the IQ-style raymarch recipe needs. The smooth-unionsminis a private polynomial helper in the shader, not a GLSL.std.450 instruction; it lowers toOpFunctionCall.
Every line of the shader is daslang, lowered to SPIR-V at compile time by dasSpirv.
The clip above is the headless recording: 30 seconds, 30 fps, captured into an
APNG and ffmpeg-muxed with a daStrudel ambient pad bed.
The camera completes one full orbit around the scene (period chosen so the
loop closes seamlessly); the torus rotates around the sphere, the sphere
gently bobs, and the cosine-palette pulse cycles the blob hue across the orbit
(cream -> blue -> copper). See skills/recording.md. To watch the raymarch
update live on your own GPU, run the windowed viewer (see See it live below).
The shader
The whole pipeline lives in one [vulkan_compute_shader] function plus a
handful of private helpers. map(p, t) returns
(distance_to_nearest_surface, material_id) for any point in world space;
march walks the ray; get_normal does a 4-tap forward difference (1
baseline + 3 axis-displaced taps – the cheaper asymmetric cousin of the
classic 6-tap central difference, visually identical at this eps + lighting);
soft_shadow is IQ’s penumbra estimate. The [vulkan_compute_shader]
annotation also synthesises sdf_main_push_constants(cmd, layout) from
var @push_constant pc : Push – the host just writes pc.time = time
and calls it.
module sdf_tut_shaders public
require vulkan/vulkan_boost public // CommandBuffer + PipelineLayout for the generated push_constants function
require vulkan/spirv_vulkan_shader public
require spirv/spirv_builtins public
require math
// the output storage image (binding 0); the render path creates it Rgba8 and reads it back.
var @binding = 0 out_img : image2D
//! the square render resolution (multiple of 8 so the 8x8 compute local size tiles cleanly). The host
//! sizes the image off this constant; the shader sizes its pixel->ray mapping off the bound image via
//! imageSize so it does not reference SDF_DIM at runtime.
let public SDF_DIM = 768
let MAX_STEPS = 96 // raymarch iteration budget per pixel
let MAX_DIST = 60.0f // far plane: distance at which we declare a miss
let HIT_EPS = 0.001f // distance threshold that counts as a surface hit
let SHADOW_K = 32.0f // soft-shadow penumbra sharpness (IQ recipe)
// the single animation parameter, pushed each frame from wall-clock time. Drives camera orbit, torus
// rotation, sphere bob, and the blob's cosine-palette colour pulse.
struct Push {
time : float
}
var @push_constant pc : Push
// IQ's polynomial smooth union: blends two SDFs over a soft radius k. Reduces to min(a,b) when the
// two surfaces are far apart, but produces a curved meeting band where they overlap -- the visual
// signature of distance-field rendering.
def private smin(a, b, k : float) : float {
let h = max(k - abs(a - b), 0.0f) / k
return min(a, b) - h * h * h * k * (1.0f / 6.0f)
}
// SDF primitives. Each returns the signed distance from p to the surface (negative = inside).
def private sd_sphere(p : float3; r : float) : float {
return length(p) - r
}
def private sd_torus(p : float3; major, minor : float) : float {
let q = float2(length(p.xz) - major, p.y)
return length(q) - minor
}
def private sd_plane(p : float3; h : float) : float {
return p.y - h
}
// 2D rotation around the origin; used to spin the torus's local coordinate frame about Y.
def private rot2(p : float2; ang : float) : float2 {
let c = cos(ang)
let s = sin(ang)
return float2(c * p.x - s * p.y, s * p.x + c * p.y)
}
// The scene SDF. Returns (distance_to_nearest_surface, material_id) so the shader can branch on the
// material at the hit point. material 0 = blob (sphere smin torus), material 1 = ground.
def private map(p : float3; t : float) : float2 {
let d_plane = sd_plane(p, -0.5f)
let d_sphere = sd_sphere(p - float3(0.0f, 0.2f * sin(t * 1.5f), 0.0f), 0.55f)
// Torus orbits the sphere via its rotated local xz frame, rebuilt whole by constructor: dasSpirv has no write-swizzle (`pt.x = ...`) yet.
let xz = rot2(float2(p.x, p.z), t * 0.8f)
let pt = float3(xz.x, p.y, xz.y)
let d_torus = sd_torus(pt - float3(1.0f, 0.0f, 0.0f), 0.32f, 0.12f)
let d_blob = smin(d_sphere, d_torus, 0.3f)
if (d_plane < d_blob) {
return float2(d_plane, 1.0f)
}
return float2(d_blob, 0.0f)
}
// Scalar overload for the shadow march (no material needed).
def private map_d(p : float3; t : float) : float {
return map(p, t).x
}
// Forward-difference normal at p: 4 SDF taps (1 baseline + 3 axis-displaced), normalized. Cheaper
// than the 6-tap symmetric central difference; the slight +eps bias is invisible at this eps and at
// our distance threshold. Upgrade to central if normals look skewed under stronger lighting.
def private get_normal(p : float3; t : float) : float3 {
let eps = 0.001f
let d = map_d(p, t)
let nx = map_d(p + float3(eps, 0.0f, 0.0f), t) - d
let ny = map_d(p + float3(0.0f, eps, 0.0f), t) - d
let nz = map_d(p + float3(0.0f, 0.0f, eps), t) - d
return normalize(float3(nx, ny, nz))
}
// March from ro toward rd. Returns (t_hit, material); t_hit >= MAX_DIST means miss (sky). Three exits:
// HIT_EPS hit -> set mat + break; far-plane escape -> break; step budget exhausted -> force a miss,
// since otherwise dist could land just under MAX_DIST and the caller would shade garbage at material 0.
def private march(ro, rd : float3; t : float) : float2 {
var dist = 0.0f
var mat = 0.0f
var hit = 0 // 1 once HIT_EPS is satisfied; dasSpirv doesn't expose bool consts in shaders
for (_i in range(MAX_STEPS)) {
let p = ro + rd * dist
let m = map(p, t)
if (m.x < HIT_EPS) {
mat = m.y
hit = 1
break
}
dist = dist + m.x
if (dist > MAX_DIST) {
break
}
}
if (hit == 0) {
dist = MAX_DIST + 1.0f
}
return float2(dist, mat)
}
// Soft shadow via IQ's penumbra estimate: track min(h/dist) along the shadow ray; small ratios mean
// the ray grazed close to a surface (deep penumbra), larger ratios mean an unobstructed path.
def private soft_shadow(ro, rd : float3; t : float) : float {
var res = 1.0f
var dist = 0.05f // small offset to dodge self-shadowing from the surface we just hit
for (_i in range(48)) {
let p = ro + rd * dist
let h = map_d(p, t)
if (h < 0.001f) {
res = 0.0f
break
}
res = min(res, SHADOW_K * h / dist)
dist = dist + clamp(h, 0.02f, 1.0f)
if (dist > 10.0f) {
break
}
}
return clamp(res, 0.0f, 1.0f)
}
// Vertical sky gradient (warm horizon -> cool zenith) with a tiny solar disc along sun_dir.
def private sky_color(rd, sun_dir : float3) : float3 {
let h = clamp(rd.y * 0.5f + 0.5f, 0.0f, 1.0f)
let horizon = float3(0.94f, 0.78f, 0.62f) // warm peach
let zenith = float3(0.45f, 0.62f, 0.85f) // soft blue
var col = lerp(horizon, zenith, float3(h, h, h))
let sun = max(dot(rd, sun_dir), 0.0f)
col = col + float3(1.0f, 0.85f, 0.6f) * pow(sun, 64.0f)
return col
}
// Checkered ground albedo: parity of (floor(x) + floor(z)) selects between two warm-grey tones.
def private ground_color(p : float3) : float3 {
let cx = floor(p.x)
let cz = floor(p.z)
let sum = cx + cz
let parity = sum - 2.0f * floor(0.5f * sum) // (cx + cz) mod 2 as a float
let a = float3(0.95f, 0.92f, 0.85f)
let b = float3(0.30f, 0.34f, 0.38f)
return lerp(a, b, float3(parity, parity, parity))
}
// Blob albedo: IQ-style cosine palette pulsing with time, brightened slightly toward the top of the
// blob (p.y > 0) so the form reads better under flat lighting. The phase offsets between R/G/B give
// the palette its characteristic hue shift as time advances.
def private blob_color(p : float3; t : float) : float3 {
let phase = t * 0.6f
let r = 0.6f + 0.4f * cos(phase + 0.0f)
let g = 0.45f + 0.4f * cos(phase + 0.8f)
let b = 0.4f + 0.4f * cos(phase + 1.6f)
let lift = 0.85f + 0.15f * clamp(p.y + 0.5f, 0.0f, 1.0f)
return float3(r, g, b) * lift
}
// Reinhard tone map. Compresses any HDR overshoot (rim glow + sun disc) into [0,1]. Output stays
// LINEAR -- the linear->sRGB gamma encode happens at the sRGB-readback blit boundary (and the
// sRGB swapchain blit for the window), not here.
def private tonemap(c : float3) : float3 {
let one = float3(1.0f, 1.0f, 1.0f)
return c / (one + c)
}
[vulkan_compute_shader(local_size_x=8, local_size_y=8, name="sdf_spv")]
def sdf_main {
let gid = gl_GlobalInvocationID
let dim = imageSize(out_img)
let t = pc.time
// screen-normalized pixel coords, y in [-1,1] (sample at pixel center, +y up)
let px = (2.0f * (float(gid.x) + 0.5f) - float(dim.x)) / float(dim.y)
let py = -(2.0f * (float(gid.y) + 0.5f) - float(dim.y)) / float(dim.y)
// camera orbits the scene origin at fixed radius with a gentle vertical sway
let cam_ang = t * 0.4f
let cam_dist = 3.0f
let cam_y = 0.8f + 0.2f * sin(t * 0.3f)
let ro = float3(cos(cam_ang) * cam_dist, cam_y, sin(cam_ang) * cam_dist)
let target = float3(0.0f, 0.0f, 0.0f)
// build the camera basis: right = world_up x fwd, up = fwd x right (handedness is whatever the
// two cross-product orders below produce; flip either order to mirror the view if needed)
let fwd = normalize(target - ro)
let right = normalize(cross(float3(0.0f, 1.0f, 0.0f), fwd))
let up = cross(fwd, right)
let focal = 1.6f // pinhole focal length; larger = narrower FOV
let rd = normalize(fwd * focal + right * px + up * py)
let sun_dir = normalize(float3(-0.4f, 0.8f, -0.5f))
let hit = march(ro, rd, t)
var col = float3(0.0f, 0.0f, 0.0f)
if (hit.x >= MAX_DIST) {
col = sky_color(rd, sun_dir)
} else {
let p = ro + rd * hit.x
let n = get_normal(p, t)
var albedo = float3(0.5f, 0.5f, 0.5f)
if (hit.y > 0.5f) {
albedo = ground_color(p)
} else {
albedo = blob_color(p, t)
}
let key = max(dot(n, sun_dir), 0.0f)
let sh = soft_shadow(p + n * 0.001f, sun_dir, t)
let v = normalize(ro - p)
let rim = pow(1.0f - max(dot(n, v), 0.0f), 3.0f)
let amb = 0.25f + 0.25f * (n.y * 0.5f + 0.5f) // hemisphere ambient (sky/ground bounce)
let lit = albedo * (amb + key * sh * 0.85f) + float3(1.0f, 0.9f, 0.75f) * rim * 0.2f
// atmospheric fog: scene fades into the sky color with distance (exponential falloff)
let f = clamp(1.0f - exp(-0.02f * hit.x * hit.x), 0.0f, 1.0f)
col = lerp(lit, sky_color(rd, sun_dir), float3(f, f, f))
}
col = tonemap(col)
let coord = int2(int(gid.x), int(gid.y))
imageStore(out_img, coord, float4(col, 1.0f))
}
The render (headless)
render_sdf(time) is the one-shot path used by the test: build a fresh
SdfRenderer, dispatch one frame, tear down. The recording driver uses
build_sdf_renderer() once and calls render_sdf_frame(r, t) in a loop –
the long-lived state (instance, device, storage image + memory + view,
descriptor set + pipeline layout + compute pipeline, readback buffer) is
rebuilt once; the per-frame work is just pushing the new time, dispatching
the compute shader at one workgroup per 8x8 pixel tile, and copying the image
back to the host. No window required.
def public render_sdf_frame(var r : SdfRenderer; time : float) : array<uint8> {
var pixels : array<uint8>
run_cmd_sync(r.device, r.pool, r.queue) $(cmd) {
let none : VkAccessFlags
var shader_write : VkAccessFlags; shader_write.shader_write = true
var transfer_read : VkAccessFlags; transfer_read.transfer_read = true
var comp : VkPipelineStageFlags; comp.compute_shader = true
var xfer : VkPipelineStageFlags; xfer.transfer = true
// image enters this command buffer in TRANSFER_SRC_OPTIMAL (set up at create-time on the
// first frame, left there by the previous frame's final transition on every subsequent one)
transition_image(cmd, r.image, VkImageLayout.TRANSFER_SRC_OPTIMAL, VkImageLayout.GENERAL,
transfer_read, shader_write, xfer, comp)
cmd_bind_pipeline(cmd, r.pipeline, VkPipelineBindPoint.COMPUTE)
let sets <- [vk_value_to_boost(r.set)]
var no_dyn : array<uint>
cmd_bind_descriptor_sets(cmd, VkPipelineBindPoint.COMPUTE, r.pipe_layout, 0u, sets, no_dyn)
pc.time = time
sdf_main_push_constants(cmd, r.pipe_layout)
cmd_dispatch(cmd, uint(SDF_DIM / 8), uint(SDF_DIM / 8), 1u)
transition_image(cmd, r.image, VkImageLayout.GENERAL, VkImageLayout.TRANSFER_SRC_OPTIMAL,
shader_write, transfer_read, comp, xfer)
// r.image (linear UNORM) is now TRANSFER_SRC. Blit it into the R8G8B8A8_SRGB intermediate so the
// hardware applies the linear->sRGB encode at the format conversion; the recording then reads
// gamma-encoded bytes. Bring srgb_image UNDEFINED -> TRANSFER_DST first (we overwrite it whole).
var transfer_write : VkAccessFlags; transfer_write.transfer_write = true
var top : VkPipelineStageFlags; top.top_of_pipe = true
transition_image(cmd, r.srgb_image, VkImageLayout.UNDEFINED, VkImageLayout.TRANSFER_DST_OPTIMAL,
none, transfer_write, top, xfer)
var region : VkImageBlit
region.srcSubresource.aspectMask.color = true
region.srcSubresource.layerCount = 1u
region.srcOffsets[1].x = SDF_DIM
region.srcOffsets[1].y = SDF_DIM
region.srcOffsets[1].z = 1
region.dstSubresource.aspectMask.color = true
region.dstSubresource.layerCount = 1u
region.dstOffsets[1].x = SDF_DIM
region.dstOffsets[1].y = SDF_DIM
region.dstOffsets[1].z = 1
unsafe {
vkCmdBlitImage(boost_value_to_vk(cmd),
boost_value_to_vk(r.image), VkImageLayout.TRANSFER_SRC_OPTIMAL,
boost_value_to_vk(r.srgb_image), VkImageLayout.TRANSFER_DST_OPTIMAL,
1u, addr(region), VkFilter.NEAREST)
}
// srgb_image -> TRANSFER_SRC so copy_image_to_buffer can read it
transition_image(cmd, r.srgb_image, VkImageLayout.TRANSFER_DST_OPTIMAL, VkImageLayout.TRANSFER_SRC_OPTIMAL,
transfer_write, transfer_read, xfer, xfer)
copy_image_to_buffer(cmd, r.srgb_image, r.readback, SDF_DIM, SDF_DIM)
}
map_memory_to_array(r.device, r.readback.memory, r.buf_size) $(m) {
pixels := m
}
return <- pixels
}
Self-verifying
The test is the CI regression gate (lavapipe in CI, a real GPU locally). The
SDF scene is deterministic at a fixed time, so the oracle samples a few
pixels at known positions and asserts palette and structure: sky-blue at the
top corners, mid-tone non-saturated values along the bottom row (the
checkered ground hit), and a warm-toned hero blob in the central band.
[test]
def test_sdf(t : T?) {
var p <- render_sdf(TEST_TIME)
// ----- corners: top-left and top-right should be sky (always above horizon at this camera)
let tl = px(p, 4, 4)
t |> success(tl.x > 60 && tl.y > 100 && tl.z > 140, "top-left is sky (blue-dominant): {tl}")
let tr = px(p, SDF_DIM - 5, 4)
t |> success(tr.x > 60 && tr.y > 100 && tr.z > 140, "top-right is sky (blue-dominant): {tr}")
// ----- bottom row: ground checkerboard. The checkers vary, so just assert it's not black and
// not saturated -- a real hit shaded with key + ambient lands in the mid-tones.
var bottom_hits = 0
for (i in range(12)) {
let x = i * 64
let s = px(p, x, SDF_DIM - 8)
if (s.x > 20 && s.y > 20 && s.z > 20 && s.x < 250 && s.y < 250 && s.z < 250) {
bottom_hits++
}
}
t |> success(bottom_hits >= 8, "bottom row hits the ground (mid-tone, non-saturated): {bottom_hits}/12")
// ----- center band: the blob (sphere smin torus) lives near the screen center. Sample a 10x10
// grid centered at (DIM/2, DIM/2) and count pixels that look like the warm blob palette
// (red-dominant or yellow-dominant) rather than sky-blue or checker-grey.
var blob_hits = 0
let cx = SDF_DIM / 2
let cy = SDF_DIM / 2
for (j in range(10)) {
for (i in range(10)) {
let s = px(p, cx - 80 + i * 16, cy - 40 + j * 16)
// blob_color(p, TEST_TIME) = (0.6 + 0.4*cos(0.72), 0.45 + 0.4*cos(1.52), 0.4 + 0.4*cos(2.32))
// ~= (0.90, 0.47, 0.13) before lift/lighting -- warm/red-dominant; oracle asserts red exceeds blue.
if (s.x > s.z + 15 && s.x > 80) {
blob_hits++
}
}
}
t |> success(blob_hits >= 10, "central band hits the blob (warm-toned hero): {blob_hits}/100")
delete p
}
See it live
window/show_sdf.das opens a GLFW window with a Vulkan swapchain and
recomputes the raymarch every frame – the time push constant comes from
wall-clock, so the camera orbit and the cosine palette pulse update live as
the window stays open. It reuses the same sdf_spv blob the headless render
uses; the only additions are the surface, swapchain, and a per-frame blit of
the compute storage image onto the swap image. It needs a display and the
glfw module, so it lives in a window/ subfolder that the tutorial’s CI
gate skips (CI is headless and built without GLFW); it is the run-and-watch
companion to the headless oracle.
require glfw/glfw_boost
require vulkan
require vulkan/vulkan_boost
require vulkan/vulkan_window
require ../sdf_tut_shaders.das
require resident_compute
require daslib/defer
[export]
def main { // nolint:STYLE038 - flat viewer lifecycle scaffold
if (volkInitialize() != 0) {
panic("no vulkan loader")
}
// Hand GLFW the loader volk just found (Homebrew prefix on macOS). Must precede glfwInit.
glfwInitVulkanLoader(vk_get_instance_proc_addr())
if (glfwInit() == 0) {
panic("can't init glfw")
}
defer() { glfwTerminate() }
if (glfwVulkanSupported() == 0) {
panic("glfw reports no vulkan support")
}
glfwWindowHint(int(GLFW_CLIENT_API), GLFW_NO_API)
glfwWindowHint(int(GLFW_RESIZABLE), GLFW_TRUE)
var window = glfwCreateWindow(SDF_DIM, SDF_DIM, "dasVulkan tutorial 03 - SDF raymarch (blit)", null, null)
if (window == null) {
panic("can't create window")
}
defer() { glfwDestroyWindow(window) }
var ext_count = 0u
let glfw_exts = glfwGetRequiredInstanceExtensions(unsafe(addr(ext_count)))
var inst_exts : array<string>
inst_exts |> reserve(int(ext_count))
for (i in range(int(ext_count))) {
unsafe {
inst_exts |> push(glfw_exts[i])
}
}
var inscope instance <- create_instance("dasVulkan tutorial 03 (window)", make_api_version(1u, 3u, 0u), inst_exts)
volkLoadInstance(boost_value_to_vk(instance))
var inscope surface <- create_surface(instance, glfwGetNativeWindow(window), glfwGetNativeDisplay())
let phys = select_physical_device(instance)
let gfx = select_graphics_queue_family(phys)
if (!queue_family_supports_present(phys, gfx, surface)) {
panic("graphics queue family does not support presentation")
}
var inscope device <- create_device(phys, gfx, ["VK_KHR_swapchain"])
volkLoadDevice(boost_value_to_vk(device))
let queue = get_device_queue(device, gfx, 0u)
// resident compute resources -- recomputed every frame with a new time push constant
var inscope sc <- build_compute_image(device, phys, sdf_spv, SDF_DIM, SDF_DIM)
var inscope swap <- create_swapchain(device, phys, surface, SDF_DIM, SDF_DIM)
var poolci : CommandPoolCreateInfo
poolci.queueFamilyIndex = gfx
var inscope pool <- create_command_pool(device, poolci)
var inscope sync <- create_frame_sync(device)
while (glfwWindowShouldClose(window) == 0) {
glfwPollEvents()
var fbw = 0
var fbh = 0
glfwGetFramebufferSize(window, fbw, fbh)
if (fbw == 0 || fbh == 0) {
glfwWaitEvents()
continue
}
if (fbw != swap.width || fbh != swap.height) {
vkDeviceWaitIdle(boost_value_to_vk(device))
delete swap
swap <- create_swapchain(device, phys, surface, fbw, fbh) // nolint:PERF030 - deleted just above
}
let t = float(glfwGetTime())
let ok = present_frame(device, queue, swap, pool, sync) $(cmd; target; _idx) {
let none : VkAccessFlags
var shader_write : VkAccessFlags
shader_write.shader_write = true
var transfer_read : VkAccessFlags
transfer_read.transfer_read = true
var transfer_write : VkAccessFlags
transfer_write.transfer_write = true
var top : VkPipelineStageFlags
top.top_of_pipe = true
var comp : VkPipelineStageFlags
comp.compute_shader = true
var xfer : VkPipelineStageFlags
xfer.transfer = true
var bottom : VkPipelineStageFlags
bottom.bottom_of_pipe = true
transition_image(cmd, sc.image, VkImageLayout.UNDEFINED, VkImageLayout.GENERAL,
none, shader_write, top, comp)
record_compute(cmd, sc, t)
transition_image(cmd, sc.image, VkImageLayout.GENERAL, VkImageLayout.GENERAL,
shader_write, transfer_read, comp, xfer)
let dst = vk_value_to_boost(target)
transition_image(cmd, dst, VkImageLayout.UNDEFINED, VkImageLayout.TRANSFER_DST_OPTIMAL,
none, transfer_write, top, xfer)
var region : VkImageBlit
region.srcSubresource.aspectMask.color = true
region.srcSubresource.layerCount = 1u
region.srcOffsets[1].x = sc.width
region.srcOffsets[1].y = sc.height
region.srcOffsets[1].z = 1
region.dstSubresource.aspectMask.color = true
region.dstSubresource.layerCount = 1u
region.dstOffsets[1].x = swap.width
region.dstOffsets[1].y = swap.height
region.dstOffsets[1].z = 1
unsafe {
vkCmdBlitImage(boost_value_to_vk(cmd), boost_value_to_vk(sc.image), VkImageLayout.GENERAL,
target, VkImageLayout.TRANSFER_DST_OPTIMAL, 1u, addr(region), VkFilter.LINEAR)
}
transition_image(cmd, dst, VkImageLayout.TRANSFER_DST_OPTIMAL, VkImageLayout.PRESENT_SRC_KHR,
transfer_write, none, xfer, bottom)
}
if (!ok) {
vkDeviceWaitIdle(boost_value_to_vk(device))
delete swap
swap <- create_swapchain(device, phys, surface, fbw, fbh) // nolint:PERF030 - deleted just above
}
}
vkDeviceWaitIdle(boost_value_to_vk(device))
}
Running it
# the CI pixel-oracle gate (lavapipe in CI, real GPU locally)
daslang -load_module <dasVulkan> <daslang>/dastest/dastest.das -- \
--test <dasVulkan>/tutorials/03_sdf
# watch it live in a window (needs the glfw module + a display)
daslang -load_module <dasVulkan> \
<dasVulkan>/tutorials/03_sdf/window/show_sdf.das
# regenerate the recording (needs stbimage + audio + ffmpeg locally)
daslang -load_module <dasVulkan> \
<dasVulkan>/tutorials/03_sdf/recording/record_sdf.das
Next
04 - The Synthwave Cube (graphics + depth) switches back to the graphics pipeline and adds the rest of a “real” 3D scene: per-vertex attributes, a depth attachment, a procedurally generated texture, an MVP matrix carried in a UBO, and proper Blinn-Phong-ish lighting.