09 - MSAA + Dynamic Rendering: No Render Pass, Smooth Edges

08 - Shadow Mapping: Two Passes, One Depth Image was the last tutorial built on VkRenderPass + VkFramebuffer. This one drops both: the pipeline carries a VkPipelineRenderingCreateInfo in its pNext chain (color + depth format + sample count), and each frame opens a VkRenderingInfo inline with cmd_begin_rendering / cmd_end_rendering. Vulkan 1.3 core. While we’re re-doing the cube scene from 04 - The Synthwave Cube (graphics + depth), we also turn on 4× MSAA with an auto-resolve target – the GPU averages 4 samples per pixel into a 1× image at the end of the render. The headline rails:

  • No more ``VkRenderPass`` / ``VkFramebuffer``. The pipeline carries VkPipelineRenderingCreateInfo via pNext (one color format, one depth format, sample count). Per-frame, record_rendering opens an inline VkRenderingInfo with attachment views + clear values and the body records draws. create_graphics_pipeline_dyn is the new boost helper; the existing create_graphics_pipeline_v3d (render-pass-bound) stays available for legacy code.

  • 4× MSAA + auto-resolve. The color attachment is built at samples = 4 with build_offscreen_target(..., samples, extra_usage). The RenderingAttachmentInfo carries resolveMode = AVERAGE and a resolveImageView pointing at a separate 1× image – the driver does the box filter for free at end-of-render. The 1× target is what gets blitted into the readback buffer or swapchain target. The depth attachment matches at samples = 4 (the spec requires color + depth to agree).

  • Vulkan 1.3 feature opt-in. Dynamic rendering became core in 1.3 but is still a feature toggle; the device-create call goes through a new create_device overload that takes VkPhysicalDeviceVulkan13Features and threads it via the pNext chain. Caller sets f13.dynamicRendering = 1u and we’re done.

  • Built-in side-by-side comparison. The cube uses two pipelines built from the same shader modules – one 4× MSAA, one 1× – each with matching color + depth attachments. record_msaa_render(res, cmd, use_msaa) picks which to use per frame. A 16-row mode-indicator strip cleared via a second record_rendering pass (no shader, no draw – the CLEAR loadOp does all the work) sits at the top of every frame: bright green = 4× MSAA, bright red = 1× rasterization. The recording driver defaults to AUTO mode (flip every 2 s) so the comparison shows up automatically; the windowed viewer adds an M-key cycle (AUTO → OFF → ON).

  • Pixel oracle that proves MSAA actually averages. The test scans 21 horizontal lines across the cube’s vertical extent and counts pixels in the partial-coverage gradient band (B ∈ [18, 200]). 1× rasterization produces zero such pixels by construction; 4× MSAA produces dozens. Floor on the count is the structural assert.

Every line of every shader is daslang, lowered to SPIR-V at compile time. The shaders themselves are unchanged from 04 - The Synthwave Cube (graphics + depth) – MSAA is a pipeline + attachment concern, the fragment program doesn’t know it’s being sampled four times.

The clip above is the headless recording: 30 seconds, 30 fps, captured into an APNG and ffmpeg-muxed with a daStrudel music bed. The 16-row mode strip at the top flips every 2 s – watch the cube’s edges go from jaggy (red strip) to smooth (green strip) and back. The [test] checks the structural MSAA signal at a fixed frame – the CI regression gate. To watch the same scene live on your own GPU with M-key control, run the windowed viewer (see See it live below).

The shaders

Reused from tutorial 04 – a vertex shader pulling (pos, uv, normal) per-vertex and writing world-space outputs, plus a fragment shader doing texture + key+rim lighting. The MSAA happens at the pipeline + attachment level, the fragment program doesn’t see it.

module msaa_tut_shaders public

require vulkan/vulkan_boost public
require vulkan/spirv_vulkan_shader public
require spirv/spirv_builtins public
require math

// ===== shared UBO =====

struct Camera {
    view     : float4x4
    proj     : float4x4
    cam_time : float4     // xyz = camera world position, w = time in seconds
}
var @uniform @set = 0 @binding = 0 cam : Camera

struct ModelPC {
    model : float4x4
}
var @push_constant pc : ModelPC

// ===== vertex shader =====

var @in @location = 0 a_pos    : float3
var @in @location = 1 a_uv     : float2
var @in @location = 2 a_normal : float3

var @out @location = 0 v_world_pos    : float3
var @out @location = 1 v_world_normal : float3
var @out @location = 2 v_uv           : float2

[vulkan_vertex_shader(name="msaa_vert_spv")]
def msaa_vs {
    let world = pc.model * float4(a_pos, 1.0)
    gl_Position = cam.proj * cam.view * world
    v_world_pos = world.xyz
    v_world_normal = (pc.model * float4(a_normal, 0.0)).xyz
    v_uv = a_uv
}

// ===== fragment shader =====

var @set = 0 @binding = 1 tex : sampler2D

var @in @location = 0 f_world_pos    : float3
var @in @location = 1 f_world_normal : float3
var @in @location = 2 f_uv           : float2
var @out @location = 0 frag_color    : float4

[vulkan_fragment_shader(name="msaa_frag_spv")]
def msaa_fs {
    let n = normalize(f_world_normal)
    let v = normalize(cam.cam_time.xyz - f_world_pos)
    let l = normalize(float3(0.5, 1.0, 0.3))
    let key = max(dot(n, l), 0.0) * 0.7 + 0.3
    let rim = pow(1.0 - max(dot(n, v), 0.0), 2.0)
    let rim_color = float3(0.4, 0.6, 1.0)
    let scrolled = float2(f_uv.x, f_uv.y + cam.cam_time.w * 0.05)
    let albedo = texture(tex, scrolled).rgb
    let lit = albedo * key + rim * rim_color
    frag_color = float4(lit, 1.0)
}

The render (headless)

The host builds two pipelines (4× MSAA + 1×), three color/depth attachment sets (MSAA + resolve + 1×), and one descriptor set; the per-frame call record_msaa_render(res, cmd, use_msaa) picks the path. No VkRenderPass and no VkFramebuffer ever – both rails replaced by VkPipelineRenderingCreateInfo on the pipeline and cmd_begin_rendering in the command buffer.

def public build_msaa_resources(device : Device; phys : VkPhysicalDevice; queue : VkQueue; pool : CommandPool) : MsaaResources {   // nolint:STYLE038 - flat one-call-per-item Vulkan setup run
    let dev = boost_value_to_vk(device)

    // 4x MSAA color target: rendered into, never blitted from (cannot carry transfer_src).
    var samples4 : VkSampleCountFlags
    samples4._4 = true
    let msaa_color_extra : VkImageUsageFlags    // no extras; just color_attachment
    var inscope color_msaa <- build_offscreen_target(device, phys, MSAA_W, MSAA_H, MSAA_COLOR_FMT, samples4, msaa_color_extra)

    // 1x resolve target: the rendering attachment resolves MSAA into this image at end-of-pass.
    // Marked transfer_src so the same image flows into the readback buffer (or swapchain blit).
    var samples1 : VkSampleCountFlags
    samples1._1 = true
    var resolve_extra : VkImageUsageFlags
    resolve_extra.transfer_src = true
    var inscope color_resolve <- build_offscreen_target(device, phys, MSAA_W, MSAA_H, MSAA_COLOR_FMT, samples1, resolve_extra)

    // 4x MSAA depth: must match color sample count (VUID-VkSubpassDescription-pDepthStencilAttachment-01418).
    let depth_extra : VkImageUsageFlags
    var inscope depth_msaa <- build_offscreen_depth(device, phys, MSAA_W, MSAA_H, MSAA_DEPTH_FMT, depth_extra, samples4)

    // 1x rasterization path (no MSAA): standalone color target (blittable) + matching depth.
    // Used by the runtime toggle in the window viewer / recording so the same scene can be
    // compared with and without MSAA without a rebuild.
    var color_1x_extra : VkImageUsageFlags
    color_1x_extra.transfer_src = true
    var inscope color_1x <- build_offscreen_target(device, phys, MSAA_W, MSAA_H, MSAA_COLOR_FMT, samples1, color_1x_extra)
    var inscope depth_1x <- build_offscreen_depth(device, phys, MSAA_W, MSAA_H, MSAA_DEPTH_FMT, depth_extra, samples1)

    var vb_usage : VkBufferUsageFlags
    vb_usage.vertex_buffer = true
    var sverts : array<float>
    var sindices : array<uint16>
    build_spiky_geometry(sverts, sindices)
    let spiky_index_count = uint(length(sindices))
    var inscope vb <- create_host_buffer_from_bytes(device, phys, vb_usage, sverts)
    delete sverts
    var ib_usage : VkBufferUsageFlags
    ib_usage.index_buffer = true
    var inscope ib <- create_host_buffer_from_bytes(device, phys, ib_usage, sindices)
    delete sindices

    var ubo_usage : VkBufferUsageFlags
    ubo_usage.uniform_buffer = true
    var inscope ubo <- create_host_buffer(device, phys, 144ul, ubo_usage)

    var tex_pixels <- gen_synthwave_texture(TEX_DIM, TEX_DIM)
    var staging_usage : VkBufferUsageFlags
    staging_usage.transfer_src = true
    var inscope staging <- create_host_buffer_from_bytes(device, phys, staging_usage, tex_pixels)
    delete tex_pixels

    var ici = ImageCreateInfo(imageType = VkImageType._2D, format = MSAA_COLOR_FMT)
    ici.extent.width = uint(TEX_DIM)
    ici.extent.height = uint(TEX_DIM)
    ici.extent.depth = 1u
    ici.mipLevels = 1u
    ici.arrayLayers = 1u
    ici.samples._1 = true
    ici.tiling = VkImageTiling.OPTIMAL
    ici.usage.sampled = true
    ici.usage.transfer_dst = true
    ici.initialLayout = VkImageLayout.UNDEFINED
    var inscope tex_image <- create_image(device, ici)
    var ireq : VkMemoryRequirements
    vkGetImageMemoryRequirements(dev, boost_value_to_vk(tex_image), ireq)
    var iwant : VkMemoryPropertyFlags
    iwant.device_local = true
    let imai = MemoryAllocateInfo(allocationSize = ireq.size,
        memoryTypeIndex = find_memory_type(phys, ireq.memoryTypeBits, iwant))
    var inscope tex_memory <- allocate_memory(device, imai)
    vk_check(vkBindImageMemory(dev, boost_value_to_vk(tex_image), boost_value_to_vk(tex_memory), 0ul), null)

    var tvci = ImageViewCreateInfo(image = weak_copy(tex_image),
        viewType = VkImageViewType._2D, format = MSAA_COLOR_FMT)
    tvci.subresourceRange.aspectMask.color = true
    tvci.subresourceRange.levelCount = 1u
    tvci.subresourceRange.layerCount = 1u
    var inscope tex_view <- create_image_view(device, tvci)

    let sci = SamplerCreateInfo(
        magFilter = VkFilter.LINEAR,
        minFilter = VkFilter.LINEAR,
        mipmapMode = VkSamplerMipmapMode.LINEAR,
        addressModeU = VkSamplerAddressMode.REPEAT,
        addressModeV = VkSamplerAddressMode.REPEAT,
        addressModeW = VkSamplerAddressMode.REPEAT,
        maxLod = 1.0f)
    var inscope sampler <- create_sampler(device, sci)

    var reflections <- [decode_reflection(msaa_vert_spv_reflect), decode_reflection(msaa_frag_spv_reflect)]
    var inscope set_layouts <- build_descriptor_set_layouts(device, reflections)

    var dpci : DescriptorPoolCreateInfo
    dpci.maxSets = 1u
    let ps0 = DescriptorPoolSize(type_ = VkDescriptorType.UNIFORM_BUFFER, descriptorCount = 1u)
    let ps1 = DescriptorPoolSize(type_ = VkDescriptorType.COMBINED_IMAGE_SAMPLER, descriptorCount = 1u)
    dpci.pPoolSizes <- [ps0, ps1]
    var inscope desc_pool <- create_descriptor_pool(device, dpci)

    var dsai = VkDescriptorSetAllocateInfo()
    dsai.descriptorPool = boost_value_to_vk(desc_pool)
    dsai.descriptorSetCount = 1u
    var raw_set_layout = boost_value_to_vk(set_layouts[0])
    var raw_set : VkDescriptorSet
    unsafe {
        dsai.pSetLayouts = addr(raw_set_layout)
        vk_check(vkAllocateDescriptorSets(dev, dsai, addr(raw_set)), null)
    }

    var writes : array<WriteDescriptorSet>
    var w0 = WriteDescriptorSet(dstSet = vk_value_to_boost(raw_set), dstBinding = 0u,
        descriptorType = VkDescriptorType.UNIFORM_BUFFER, descriptorCount = 1u)
    let binfo = DescriptorBufferInfo(buffer = weak_copy(ubo.buffer), range_ = 144ul)
    w0.pBufferInfo |> push(binfo)
    writes |> emplace(w0)
    var w1 = WriteDescriptorSet(dstSet = vk_value_to_boost(raw_set), dstBinding = 1u,
        descriptorType = VkDescriptorType.COMBINED_IMAGE_SAMPLER, descriptorCount = 1u)
    let iinfo = DescriptorImageInfo(sampler = weak_copy(sampler),
        imageView = weak_copy(tex_view), imageLayout = VkImageLayout.SHADER_READ_ONLY_OPTIMAL)
    w1.pImageInfo |> push(iinfo)
    writes |> emplace(w1)
    let no_copies : array<CopyDescriptorSet>
    update_descriptor_sets(device, writes, no_copies)

    var inscope pipe_layout <- build_pipeline_layout(device, set_layouts, reflections)
    var push_size = 0u
    for (refl in reflections) {
        for (p in refl.push_constants) {
            push_size = uint(p.size)
        }
    }
    assert(push_size == 64u, "expected reflected push constant size 64 (model float4x4)")
    delete reflections

    var inscope vert <- create_shader_module(device, msaa_vert_spv)
    var inscope frag <- create_shader_module(device, msaa_frag_spv)

    var vbindings : array<VkVertexInputBindingDescription>
    var vb_desc : VkVertexInputBindingDescription
    vb_desc.binding = 0u
    vb_desc.stride = 32u
    vb_desc.inputRate = VkVertexInputRate.VERTEX
    vbindings |> push(vb_desc)
    var vattrs : array<VkVertexInputAttributeDescription>
    var attr_pos : VkVertexInputAttributeDescription
    attr_pos.location = 0u
    attr_pos.binding = 0u
    attr_pos.format = VkFormat.R32G32B32_SFLOAT
    attr_pos.offset = 0u
    vattrs |> push(attr_pos)
    var attr_uv : VkVertexInputAttributeDescription
    attr_uv.location = 1u
    attr_uv.binding = 0u
    attr_uv.format = VkFormat.R32G32_SFLOAT
    attr_uv.offset = 12u
    vattrs |> push(attr_uv)
    var attr_n : VkVertexInputAttributeDescription
    attr_n.location = 2u
    attr_n.binding = 0u
    attr_n.format = VkFormat.R32G32B32_SFLOAT
    attr_n.offset = 20u
    vattrs |> push(attr_n)
    // Two pipelines from the same shader modules, differing only in rasterizationSamples. The format +
    // samples go into VkPipelineRenderingCreateInfo on pNext (no VkRenderPass). The bindings/attrs arrays
    // are cloned for the second build because create_graphics_pipeline_dyn takes them as `var array<>`.
    var vbindings_copy := vbindings
    var vattrs_copy := vattrs
    var inscope pipeline_msaa <- create_graphics_pipeline_dyn(device,
        MSAA_COLOR_FMT, MSAA_DEPTH_FMT, samples4,
        pipe_layout, vert, frag, MSAA_W, MSAA_H, vbindings, vattrs)
    var inscope pipeline_1x <- create_graphics_pipeline_dyn(device,
        MSAA_COLOR_FMT, MSAA_DEPTH_FMT, samples1,
        pipe_layout, vert, frag, MSAA_W, MSAA_H, vbindings_copy, vattrs_copy)
    delete vbindings
    delete vattrs
    delete vbindings_copy
    delete vattrs_copy

    let buf_size = uint64(MSAA_W * MSAA_H * 4)
    var inscope readback <- create_host_buffer(device, phys, buf_size)

    // one-shot texture upload: staging -> tex_image, UNDEFINED -> TRANSFER_DST -> SHADER_READ_ONLY
    run_cmd_sync(device, pool, queue) $(cmd) {
        var to_dst : VkAccessFlags
        to_dst.transfer_write = true
        let none : VkAccessFlags
        var top : VkPipelineStageFlags
        top.top_of_pipe = true
        var xfer : VkPipelineStageFlags
        xfer.transfer = true
        var frag_stage : VkPipelineStageFlags
        frag_stage.fragment_shader = true
        var to_read : VkAccessFlags
        to_read.shader_read = true
        transition_image(cmd, tex_image, VkImageLayout.UNDEFINED, VkImageLayout.TRANSFER_DST_OPTIMAL,
            none, to_dst, top, xfer)
        var region : BufferImageCopy
        region.imageSubresource.aspectMask.color = true
        region.imageSubresource.layerCount = 1u
        region.imageExtent.width = uint(TEX_DIM)
        region.imageExtent.height = uint(TEX_DIM)
        region.imageExtent.depth = 1u
        var regions : array<BufferImageCopy>
        regions |> emplace(region)
        cmd_copy_buffer_to_image(cmd, staging.buffer, tex_image, VkImageLayout.TRANSFER_DST_OPTIMAL, regions)
        transition_image(cmd, tex_image, VkImageLayout.TRANSFER_DST_OPTIMAL, VkImageLayout.SHADER_READ_ONLY_OPTIMAL,
            to_dst, to_read, xfer, frag_stage)
    }

    return <- MsaaResources(
        color_msaa <- color_msaa, color_resolve <- color_resolve, depth_msaa <- depth_msaa,
        color_1x <- color_1x, depth_1x <- depth_1x,
        vb <- vb, ib <- ib, ubo <- ubo,
        tex_image <- tex_image, tex_memory <- tex_memory, tex_view <- tex_view, sampler <- sampler,
        set_layouts <- set_layouts, desc_pool <- desc_pool, desc_set = raw_set,
        pipe_layout <- pipe_layout,
        pipeline_msaa <- pipeline_msaa, pipeline_1x <- pipeline_1x,
        readback <- readback,
        buf_size = buf_size, push_size = push_size, index_count = spiky_index_count)
}

//! Build the offscreen-self-contained context. The instance asks for Vulkan 1.3 API; the device
//! opts in to the dynamicRendering feature via VkPhysicalDeviceVulkan13Features.

record_msaa_render is the per-frame work: it picks the 4× MSAA path or the 1× path based on use_msaa, then draws the indicator strip via a second record_rendering block (CLEAR loadOp – no draws inside).

def public record_msaa_render(res : MsaaResources; cmd : CommandBuffer; use_msaa : bool = true) {   // nolint:STYLE038 - flat command-record run
    var to_color : VkAccessFlags
    to_color.color_attachment_write = true
    var to_depth : VkAccessFlags
    to_depth.depth_stencil_attachment_write = true
    let none : VkAccessFlags
    var top : VkPipelineStageFlags
    top.top_of_pipe = true
    var color_out : VkPipelineStageFlags
    color_out.color_attachment_output = true
    var depth_tests : VkPipelineStageFlags
    depth_tests.early_fragment_tests = true

    var color_att = RenderingAttachmentInfo(imageLayout = VkImageLayout.COLOR_ATTACHMENT_OPTIMAL,
        loadOp = VkAttachmentLoadOp.CLEAR)
    color_att.clearValue.color.float32[0] = 0.02f
    color_att.clearValue.color.float32[1] = 0.0f
    color_att.clearValue.color.float32[2] = 0.06f
    color_att.clearValue.color.float32[3] = 1.0f
    var depth_att = RenderingAttachmentInfo(loadOp = VkAttachmentLoadOp.CLEAR,
        storeOp = VkAttachmentStoreOp.DONT_CARE)
    depth_att.clearValue.depthStencil.depth = 1.0f
    depth_att.imageLayout = VkImageLayout.DEPTH_ATTACHMENT_OPTIMAL

    // The "final image" -- the one that ends up holding the rendered pixels.
    let final_img = final_image(res, use_msaa)

    if (use_msaa) {
        transition_image(cmd, res.color_msaa.image, VkImageLayout.UNDEFINED, VkImageLayout.COLOR_ATTACHMENT_OPTIMAL,
            none, to_color, top, color_out)
        transition_image(cmd, res.color_resolve.image, VkImageLayout.UNDEFINED, VkImageLayout.COLOR_ATTACHMENT_OPTIMAL,
            none, to_color, top, color_out)
        transition_depth_image(cmd, res.depth_msaa.image, VkImageLayout.UNDEFINED, VkImageLayout.DEPTH_ATTACHMENT_OPTIMAL,
            none, to_depth, top, depth_tests)
        // Color attachment: 4x MSAA source + 1x resolve target. resolveMode=AVERAGE is the
        // standard box filter; SAMPLE_ZERO would be the no-AA fast path.
        color_att.imageView = weak_copy(res.color_msaa.view)
        color_att.storeOp = VkAttachmentStoreOp.DONT_CARE   // MSAA buffer discarded after resolve
        color_att.resolveMode.average = true
        color_att.resolveImageView = weak_copy(res.color_resolve.view)
        color_att.resolveImageLayout = VkImageLayout.COLOR_ATTACHMENT_OPTIMAL
        depth_att.imageView = weak_copy(res.depth_msaa.view)
    } else {
        transition_image(cmd, res.color_1x.image, VkImageLayout.UNDEFINED, VkImageLayout.COLOR_ATTACHMENT_OPTIMAL,
            none, to_color, top, color_out)
        transition_depth_image(cmd, res.depth_1x.image, VkImageLayout.UNDEFINED, VkImageLayout.DEPTH_ATTACHMENT_OPTIMAL,
            none, to_depth, top, depth_tests)
        // 1x path: no resolve attachment, store the rasterized pixels directly.
        color_att.imageView = weak_copy(res.color_1x.view)
        color_att.storeOp = VkAttachmentStoreOp.STORE
        depth_att.imageView = weak_copy(res.depth_1x.view)
    }

    var color_atts : array<RenderingAttachmentInfo>
    color_atts |> emplace(color_att)
    let pipeline = use_msaa ? res.pipeline_msaa : res.pipeline_1x
    record_rendering(cmd, full_area(MSAA_W, MSAA_H), 1u, color_atts, depth_att) {
        cmd_bind_pipeline(cmd, pipeline)
        let sets <- [vk_value_to_boost(res.desc_set)]
        var no_dyn : array<uint>
        cmd_bind_descriptor_sets(cmd, VkPipelineBindPoint.GRAPHICS, res.pipe_layout, 0u, sets, no_dyn)
        msaa_vs_push_constants(cmd, res.pipe_layout)
        let vbufs <- [weak_copy(res.vb.buffer)]
        var voffs : array<uint64>
        voffs |> push(0ul)
        cmd_bind_vertex_buffers(cmd, 0u, vbufs, voffs)
        cmd_bind_index_buffer(cmd, weak_copy(res.ib.buffer), 0ul, VkIndexType.UINT16)
        cmd_draw_indexed(cmd, res.index_count, 1u, 0u, 0, 0u)
    }

    // Mode indicator strip: a second dynamic-rendering "pass" with renderArea narrowed to the top
    // INDICATOR_H rows, loadOp=CLEAR with a mode-coloured value. No draw calls -- the clear at begin
    // time IS the indicator. Green = 4x, red = 1x; works in window and recording with no font texture.
    var indicator_att = RenderingAttachmentInfo(
        imageView = use_msaa ? weak_copy(res.color_resolve.view) : weak_copy(res.color_1x.view),
        imageLayout = VkImageLayout.COLOR_ATTACHMENT_OPTIMAL,
        loadOp = VkAttachmentLoadOp.CLEAR,
        storeOp = VkAttachmentStoreOp.STORE)
    indicator_att.clearValue.color.float32[0] = use_msaa ? 0.10f : 0.95f
    indicator_att.clearValue.color.float32[1] = use_msaa ? 0.85f : 0.18f
    indicator_att.clearValue.color.float32[2] = use_msaa ? 0.32f : 0.18f
    indicator_att.clearValue.color.float32[3] = 1.0f
    var indicator_atts : array<RenderingAttachmentInfo>
    indicator_atts |> emplace(indicator_att)
    var indicator_area : VkRect2D
    indicator_area.extent.width = uint(MSAA_W)
    indicator_area.extent.height = uint(INDICATOR_H)
    record_rendering(cmd, indicator_area, 1u, indicator_atts) {
        pass    // clear-only -- no draws needed for the indicator
    }

    // Final image -> TRANSFER_SRC for the readback / blit-out.
    var to_xfer : VkAccessFlags
    to_xfer.transfer_read = true
    var xfer : VkPipelineStageFlags
    xfer.transfer = true
    transition_image(cmd, final_img, VkImageLayout.COLOR_ATTACHMENT_OPTIMAL, VkImageLayout.TRANSFER_SRC_OPTIMAL,
        to_color, to_xfer, color_out, xfer)
}

Self-verifying

The test is the CI regression gate (lavapipe in CI, real GPU locally). It renders one frame at fixed time = 1.0 + camera_t = 0.18 and checks the four basic structural properties (corner background, central cube hits, magenta + cyan texture sampling) plus the MSAA-specific edge-mix count – the load-bearing visual element of this tutorial.

[test]
def test_msaa_oracle(t : T?) {
    var inscope ctx <- build_msaa_context()
    var on  <- render_msaa_frame(ctx, TEST_TIME, TEST_CAM, true)
    var off <- render_msaa_frame(ctx, TEST_TIME, TEST_CAM, false)

    // four corners are the clear color -- the ball does not fill the frame. Top corners are sampled
    // BELOW the 16-row mode-indicator strip (24px margin) so the indicator's colour doesn't trip bg().
    t |> success(bg(px(on, 8, 24)),                  "top-left corner is the clear color")
    t |> success(bg(px(on, MSAA_W - 8, 24)),         "top-right corner is the clear color")
    t |> success(bg(px(on, 8, MSAA_H - 8)),          "bottom-left corner is the clear color")
    t |> success(bg(px(on, MSAA_W - 8, MSAA_H - 8)), "bottom-right corner is the clear color")

    // central box is dominantly the ball (its solid core fills the centre)
    var ball_hits = 0
    let cx = MSAA_W / 2
    let cy = MSAA_H / 2
    for (j in range(20)) {
        for (i in range(20)) {
            if (!bg(px(on, cx - 100 + i * 10, cy - 100 + j * 10))) {
                ball_hits ++
            }
        }
    }
    t |> success(ball_hits >= 360, "central 200x200 box is dominantly the ball ({ball_hits}/400)")

    // synthwave palette from the procedural texture wrapped on the core (magenta sky, cyan grid)
    var saw_magenta = false
    var saw_cyan = false
    for (j in range(60)) {
        for (i in range(60)) {
            let p = px(on, cx - 120 + i * 4, cy - 120 + j * 4)
            if (p.x > 140 && p.z > 120 && p.y < 120) {
                saw_magenta = true
            }
            if (p.z > 120 && p.y > 100 && p.x < 40) {
                saw_cyan = true
            }
        }
    }
    t |> success(saw_magenta, "found a magenta pixel on the ball (synthwave sky band sampled)")
    t |> success(saw_cyan,    "found a cyan pixel on the ball (perspective-grid sampled)")

    // THE MSAA assertion: 1x rasterization speckles the thin spikes with aliased partial-coverage
    // pixels; 4x MSAA averages them away. So the 1x image has far more faint near-background pixels
    // (measured ~1.9x). A broken/no-op MSAA path would leave the two equal and fail this.
    let faint_on = count_faint(on)
    let faint_off = count_faint(off)
    t |> success(faint_off > faint_on * 3 / 2,
        "4x MSAA antialiases the spikes: 1x speckle {faint_off} >> 4x {faint_on}")

    delete on
    delete off
}

See it live

window/show_msaa.das opens a GLFW window with a Vulkan swapchain and runs the scene per frame with time derived from wall-clock. The window title shows the current toggle state (“M = AUTO/OFF/ON | drawing: MSAA 4x / 1x (no AA)”); pressing M cycles between the three states. Default is AUTO, which flips MSAA on and off every 2 seconds. The indicator strip at the top of the frame is the same one the recording uses.

require glfw/glfw_boost
require vulkan
require vulkan/vulkan_boost
require vulkan/vulkan_window
require ../msaa_tut.das
require daslib/defer
require math

[export]
def main {   // nolint:STYLE038 - flat viewer lifecycle scaffold
    if (volkInitialize() != 0) {
        panic("no vulkan loader")
    }
    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(MSAA_W, MSAA_H, "dasVulkan tutorial 09 - MSAA + dynamic rendering", 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])
        }
    }
    // Vulkan 1.3 API: dynamic_rendering is core there
    var inscope instance <- create_instance("dasVulkan tutorial 09 (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")
    }
    // Swapchain extension + dynamicRendering Vulkan 1.3 feature (pNext chain)
    var features13 = VkPhysicalDeviceVulkan13Features()
    features13.dynamicRendering = 1u
    var inscope device <- create_device(phys, gfx, ["VK_KHR_swapchain"], features13)
    volkLoadDevice(boost_value_to_vk(device))
    let queue = get_device_queue(device, gfx, 0u)

    var poolci : CommandPoolCreateInfo
    poolci.queueFamilyIndex = gfx
    var inscope pool <- create_command_pool(device, poolci)

    var inscope res <- build_msaa_resources(device, phys, queue, pool)
    var inscope swap <- create_swapchain(device, phys, surface, MSAA_W, MSAA_H)
    var inscope sync <- create_frame_sync(device)

    // Three-state MSAA cycle: AUTO (flips every AUTO_PERIOD sec) -> FORCE_OFF -> FORCE_ON ->
    // AUTO. Default AUTO so the recording driver gets the comparison for free.
    var toggle = MsaaToggle.auto_toggle
    var m_was_down = false
    var title_label = ""

    while (glfwWindowShouldClose(window) == 0) {
        glfwPollEvents()

        // M-key edge-detect cycles the toggle. GLFW returns the current state every poll, so we
        // remember the prior state and only fire on the down-edge.
        let m_down = glfwGetKey(window, GLFW_KEY_M) == GLFW_PRESS
        if (m_down && !m_was_down) {
            toggle = next_msaa_toggle(toggle)
        }
        m_was_down = m_down

        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())
        // 24-second orbit (camera_t in [0, 1))
        let raw_cam = t * 0.042f
        let camera_t = raw_cam - floor(raw_cam)
        update_msaa_uniforms(res, device, t, camera_t)

        let use_msaa = msaa_active(toggle, t)
        // Update window title when the active mode label changes -- the on-image strip is the
        // primary indicator, but the title shows the toggle state ("AUTO/OFF/ON") explicitly.
        let new_label = "M = {msaa_toggle_label(toggle)} | drawing: {use_msaa ? "MSAA 4x" : "1x (no AA)"} - press M to cycle"
        if (new_label != title_label) {
            glfwSetWindowTitle(window, "dasVulkan tutorial 09 - {new_label}")
            title_label = new_label
        }

        let ok = present_frame(device, queue, swap, pool, sync) $(cmd; target; _idx) {
            record_msaa_render(res, cmd, use_msaa)

            let none : VkAccessFlags
            var transfer_write : VkAccessFlags
            transfer_write.transfer_write = true
            var top : VkPipelineStageFlags
            top.top_of_pipe = true
            var xfer : VkPipelineStageFlags
            xfer.transfer = true
            var bottom : VkPipelineStageFlags
            bottom.bottom_of_pipe = true
            let dst = vk_value_to_boost(target)
            transition_image(cmd, dst, VkImageLayout.UNDEFINED, VkImageLayout.TRANSFER_DST_OPTIMAL,
                none, transfer_write, top, xfer)
            let src_img = final_image(res, use_msaa)
            var region : VkImageBlit
            region.srcSubresource.aspectMask.color = true
            region.srcSubresource.layerCount = 1u
            region.srcOffsets[1].x = MSAA_W
            region.srcOffsets[1].y = MSAA_H
            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(src_img), VkImageLayout.TRANSFER_SRC_OPTIMAL,
                    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/09_msaa

# watch it live in a window with the M-key cycle (needs the glfw module + a display)
daslang -load_module <dasVulkan> \
    <dasVulkan>/tutorials/09_msaa/window/show_msaa.das

# regenerate the recording (needs stbimage + audio + ffmpeg locally)
daslang -load_module <dasVulkan> \
    <dasVulkan>/tutorials/09_msaa/recording/record_msaa.das

Next

10 - Deferred Shading: Putting It All Together is the putting-it-all-together payoff scene. ONE multi-subpass render pass with SIX attachments and THREE subpasses: G-buffer (MRT writes albedo + normal + world-pos), SSAO (reads subpassInputs, writes AO), and lighting (composes everything plus the 08 - Shadow Mapping: Two Passes, One Depth Image shadow rail and three orbiting coloured point lights). Per-pixel cost stays constant in light count – the deferred payoff. The M-key cycles 7 toggle states to decompose the final pixel into its G-buffer, SSAO, and lighting-only components.