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=0 the 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 to FMix) / clamp / cos / sin / pow / exp – everything the IQ-style raymarch recipe needs. The smooth-union smin is a private polynomial helper in the shader, not a GLSL.std.450 instruction; it lowers to OpFunctionCall.

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.