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
VkPipelineRenderingCreateInfovia pNext (one color format, one depth format, sample count). Per-frame,record_renderingopens an inlineVkRenderingInfowith attachment views + clear values and the body records draws.create_graphics_pipeline_dynis the new boost helper; the existingcreate_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). TheRenderingAttachmentInfocarriesresolveMode = AVERAGEand aresolveImageViewpointing 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_deviceoverload that takesVkPhysicalDeviceVulkan13Featuresand threads it via thepNextchain. Caller setsf13.dynamicRendering = 1uand 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 secondrecord_renderingpass (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.