8.15.16. 16 - Vulkan 1.4: Push Descriptors, Host Image Copy, Local Read
Vulkan 1.4 adds no new way to draw a frame – dynamic rendering and synchronization2, the shape every earlier tutorial uses, are 1.3. What 1.4 promotes to core are three things that change how a frame is put together, and this tutorial uses all three on one small scene: three textured cubes lit through a two-pass G-buffer.
The headline rails:
Host image copy. Each cube’s texture goes from host memory straight into the device image – no staging buffer, no transfer command, no queue.
upload_image_from_hostis a host-side layout transition out ofUNDEFINEDplus onevkCopyMemoryToImage; the image is ready to sample when the call returns. The device says which layouts it copies into (host_copy_layoutasks forSHADER_READ_ONLY_OPTIMALand takesGENERALwhen that is refused), and the image is created with thehost_transferusage bit.Push descriptors. Every descriptor the frame binds – the camera UBO, each cube’s texture, the two input attachments – is pushed into the command buffer with
cmd_push_descriptor_setright before the draw that reads it. There is no descriptor pool and no set allocation anywhere; the set layouts come from the shaders’ reflection throughbuild_push_descriptor_set_layouts, which isbuild_descriptor_set_layoutswith thepush_descriptorflag.Dynamic rendering local read. The geometry pass writes albedo and the world normal into two attachments; the lighting pass reads both back at the same fragment through
subpassInput/subpassLoadand composes the lit color into a third attachment – inside onerecord_rendering, with no render pass and no subpass. Two maps make it work, set once at pipeline creation and again in the command buffer before each pass’s draws:vkCmdSetRenderingAttachmentLocationssays which fragment output location lands in which attachment,vkCmdSetRenderingInputAttachmentIndicessays which attachment asubpassInputindex reads. A by-region memory barrier between the passes orders the attachment writes before the input reads, and the G-buffer attachments stay in theRENDERING_LOCAL_READlayout, which is legal for both.
A 1.4 device comes from create_device_vulkan14, which turns the three
features on behind the 1.3 pair; vulkan14_supported is the probe, and the
oracle skips with a message where it fails. Lavapipe reports 1.4 from Mesa 25.0,
which the nightly lane installs; a local GPU with a current driver reports it.
8.15.16.1. See it live
window/show_vulkan14.das opens a GLFW window on a 1.4 instance and device and
blits the lit frame onto the swapchain every frame, the camera orbiting and the
light swinging with wall-clock time. It needs a display, the glfw module and
a Vulkan 1.4 driver, so it lives in the window/ subfolder the tutorial’s CI
gate skips.
require glfw/glfw_boost
require vulkan
require vulkan/vulkan_boost
require vulkan/vulkan_window
require ../vulkan14_tut.das
require daslib/defer
[export]
def main {
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(V14_W, V14_H, "dasVulkan tutorial 16 - Vulkan 1.4: push descriptors, host image copy, local read", 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))) {
inst_exts |> push(unsafe(glfw_exts[i]))
}
var inscope instance <- create_instance("dasVulkan tutorial 16 (window)", make_api_version(1u, 4u, 0u), inst_exts)
volkLoadInstance(boost_value_to_vk(instance))
var inscope surface <- create_surface(instance, glfwGetNativeWindow(window), glfwGetNativeDisplay())
let phys = select_physical_device(instance)
if (!vulkan14_supported(phys)) {
panic("this device or driver is below Vulkan 1.4, or lacks pushDescriptor / hostImageCopy / dynamicRenderingLocalRead")
}
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_vulkan14(phys, gfx, ["VK_KHR_swapchain"])
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_vulkan14_resources(device, phys)
var inscope swap <- create_swapchain(device, phys, surface, V14_W, V14_H)
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) {
recreate_swapchain(swap, device, phys, surface, fbw, fbh)
}
let t = float(glfwGetTime())
update_vulkan14_uniforms(res, device, t)
let ok = present_frame(device, queue, swap, pool, sync) $(cmd; target; _idx) {
record_vulkan14_frame(res, cmd, t)
blit_to_swapchain(cmd, res.color.image, VkImageLayout.TRANSFER_SRC_OPTIMAL, V14_W, V14_H, swap, target)
}
if (!ok) {
recreate_swapchain(swap, device, phys, surface, fbw, fbh)
}
}
vkDeviceWaitIdle(boost_value_to_vk(device))
}
8.15.16.2. The shaders
Four stages, all daslang. The geometry vertex stage transforms the cube and
passes the world normal and UV on; its fragment stage writes the texture’s color
to location 0 and the packed normal to location 1. The lighting vertex stage is
the fullscreen triangle; its fragment stage declares the two attachments as
subpassInput globals with @input_attachment_index and reads them with
subpassLoad. One @push_constant struct serves both passes.
// ===== shared =====
//! View + projection, std140: two column-major mat4, 128 bytes.
struct Camera {
view : float4x4
proj : float4x4
}
var @uniform @set = 0 @binding = 0 cam : Camera
//! One push-constant block for both passes: the geometry pass stamps `model` per cube, the lighting
//! pass reads `light_dir` (xyz = unit direction TOWARD the light, w unused). 80 bytes.
struct ScenePC {
model : float4x4
light_dir : float4
}
var @push_constant pc : ScenePC
// ===== geometry pass: cube -> albedo + normal =====
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_normal : float3
var @out @location = 1 v_uv : float2
[vulkan_vertex_shader(name="gbuf_vert_spv")]
def gbuf_vs {
gl_Position = cam.proj * cam.view * pc.model * float4(a_pos, 1.0)
// pc.model is rotation + translation only, so the normal transforms with the same matrix
v_normal = (pc.model * float4(a_normal, 0.0)).xyz
v_uv = a_uv
}
var @set = 0 @binding = 1 tex : sampler2D // this cube's texture, pushed before its draw
var @in @location = 0 f_normal : float3
var @in @location = 1 f_uv : float2
var @out @location = 0 g_albedo : float4 // attachment 0 of the rendering
var @out @location = 1 g_normal : float4 // attachment 1: world normal packed into [0,1]
[vulkan_fragment_shader(name="gbuf_frag_spv")]
def gbuf_fs {
g_albedo = float4(texture(tex, f_uv).rgb, 1.0) // alpha 1 marks "a cube is here" for the lighting pass
g_normal = float4(normalize(f_normal) * 0.5 + float3(0.5, 0.5, 0.5), 1.0)
}
// ===== lighting pass: albedo + normal -> lit color =====
//! Fullscreen triangle: (-1,-1), (3,-1), (-1,3) cover the whole viewport with three unindexed vertices.
[vulkan_vertex_shader(name="light_vert_spv")]
def light_vs {
let xs = fixed_array(-1.0, 3.0, -1.0)
let ys = fixed_array(-1.0, -1.0, 3.0)
let i = gl_VertexIndex
gl_Position = float4(xs[i], ys[i], 0.0, 1.0)
}
// The two G-buffer attachments, read at THIS fragment only. `@input_attachment_index` is the index
// the host maps attachment 0 and 1 to with vkCmdSetRenderingInputAttachmentIndices; the host binds
// them as INPUT_ATTACHMENT descriptors (pushed), in the RENDERING_LOCAL_READ layout.
var @set = 0 @binding = 0 @input_attachment_index = 0 in_albedo : subpassInput
var @set = 0 @binding = 1 @input_attachment_index = 1 in_normal : subpassInput
var @out @location = 0 out_color : float4 // location 0 is REMAPPED to attachment 2 by the host
[vulkan_fragment_shader(name="light_frag_spv")]
def light_fs {
let albedo = subpassLoad(in_albedo)
if (albedo.w < 0.5) {
// nothing drawn here: a sky gradient, brighter toward the top (y = 0 is the top row in Vulkan)
let t = gl_FragCoord.y / float(V14_H)
out_color = float4(0.05 + 0.10 * (1.0 - t), 0.06 + 0.12 * (1.0 - t), 0.12 + 0.20 * (1.0 - t), 1.0)
return
}
let n = normalize(subpassLoad(in_normal).xyz * 2.0 - float3(1.0, 1.0, 1.0))
let l = normalize(pc.light_dir.xyz)
let ambient = 0.15
let diffuse = max(dot(n, l), 0.0) * (1.0 - ambient) + ambient
out_color = float4(albedo.xyz * diffuse, 1.0)
}
8.15.16.3. The textures and the resources
create_host_copied_texture is the host-image-copy rail: an image with
sampled + host_transfer usage, device-local memory, and one
upload_image_from_host call that leaves it in the layout the device chose.
build_vulkan14_resources then builds everything else – the three
attachments and the depth buffer, the cube geometry, the camera UBO, the two
pipelines on push-descriptor set layouts. Each pipeline is built by hand because
it names three color attachments and carries its own location and input-index
maps on the create info’s pNext chain (build_pass_pipeline).
//! One TEX_DIM x TEX_DIM RGBA8 texture whose usage carries ``host_transfer`` beside ``sampled``, filled
//! from `pixels` through a host image copy into `layout` (the device's choice from `host_copy_layout`).
//! No staging buffer, no command buffer, no queue: the image is ready to sample when this returns.
def create_host_copied_texture(device : Device; phys : VkPhysicalDevice; pixels : array<uint8>; layout : VkImageLayout) : OffscreenTarget {
let usage = VkImageUsageFlags.sampled | VkImageUsageFlags.host_transfer // host_transfer: the 1.4 usage bit a host copy needs
var tex <- build_image(device, phys, TEX_DIM, TEX_DIM, VkFormat.R8G8B8A8_UNORM, VkSampleCountFlags._1, usage)
upload_image_from_host(device, tex.image, pixels, TEX_DIM, TEX_DIM, layout)
return <- tex
}
// ===== the two passes inside one rendering =====
//! Per pass, which attachment each fragment output location lands in and which attachment each ``subpassInput``
//! index reads: geometry writes attachments 0 and 1 and reads nothing, lighting writes 2 and reads 0 and 1.
//! Each pipeline is built with its pass's maps, and the command buffer sets the same maps before that pass's draws.
let GBUF_LOCATIONS <- [0u, 1u, VK_ATTACHMENT_UNUSED]
let GBUF_INPUTS <- [VK_ATTACHMENT_UNUSED, VK_ATTACHMENT_UNUSED, VK_ATTACHMENT_UNUSED]
let LIGHT_LOCATIONS <- [VK_ATTACHMENT_UNUSED, VK_ATTACHMENT_UNUSED, 0u]
let LIGHT_INPUTS <- [0u, 1u, VK_ATTACHMENT_UNUSED]
enum PassKind {
geometry
lighting
}
//! The cube vertex: pos (location 0), uv (1), normal (2).
def cube_vertex_attributes() : array<VkVertexInputAttributeDescription> {
return <- [
VkVertexInputAttributeDescription(location = 0u, format = VkFormat.R32G32B32_SFLOAT, offset = 0u),
VkVertexInputAttributeDescription(location = 1u, format = VkFormat.R32G32_SFLOAT, offset = 12u),
VkVertexInputAttributeDescription(location = 2u, format = VkFormat.R32G32B32_SFLOAT, offset = 20u)]
}
//! Built by hand: each pipeline names the rendering's three color attachments and carries its pass's maps on
//! the create info's pNext chain (``VkPipelineRenderingCreateInfo`` -> ``VkRenderingAttachmentLocationInfo`` ->
//! ``VkRenderingInputAttachmentIndexInfo``); the chain is linked here, in the frame the create info lives in.
def build_pass_pipeline(device : Device; layout : PipelineLayout; vert, frag : ShaderModule; kind : PassKind) : Pipeline { // nolint:STYLE038 - flat one-call-per-item Vulkan setup run
let is_geometry_pass = kind == PassKind.geometry
var stages : array<VkPipelineShaderStageCreateInfo>
stages |> resize(2)
stages[0] = VkPipelineShaderStageCreateInfo()
stages[0].stage.vertex = true
stages[0].module_ = boost_value_to_vk(vert)
stages[0].pName = "main"
stages[1] = VkPipelineShaderStageCreateInfo()
stages[1].stage.fragment = true
stages[1].module_ = boost_value_to_vk(frag)
stages[1].pName = "main"
var binding = VkVertexInputBindingDescription()
binding.binding = 0u
binding.stride = CUBE_VERTEX_STRIDE
binding.inputRate = VkVertexInputRate.VERTEX
var attrs <- cube_vertex_attributes()
var vinput = VkPipelineVertexInputStateCreateInfo()
if (is_geometry_pass) {
vinput.vertexBindingDescriptionCount = 1u
vinput.vertexAttributeDescriptionCount = uint(length(attrs))
}
var ia = VkPipelineInputAssemblyStateCreateInfo()
ia.topology = VkPrimitiveTopology.TRIANGLE_LIST
var viewport = VkViewport()
viewport.width = float(V14_W)
viewport.height = float(V14_H)
viewport.maxDepth = 1.0
var scissor : VkRect2D
scissor.extent.width = uint(V14_W)
scissor.extent.height = uint(V14_H)
var vp = VkPipelineViewportStateCreateInfo()
vp.viewportCount = 1u
vp.scissorCount = 1u
var rast = VkPipelineRasterizationStateCreateInfo()
rast.polygonMode = VkPolygonMode.FILL
rast.cullMode.back = is_geometry_pass
rast.frontFace = VkFrontFace.COUNTER_CLOCKWISE
rast.lineWidth = 1.0
var msaa = VkPipelineMultisampleStateCreateInfo()
msaa.rasterizationSamples._1 = true
// the geometry pass tests and writes depth; the lighting pass is a fullscreen triangle with depth off
var depthstate = VkPipelineDepthStencilStateCreateInfo()
depthstate.depthTestEnable = is_geometry_pass ? VK_TRUE : VK_FALSE
depthstate.depthWriteEnable = is_geometry_pass ? VK_TRUE : VK_FALSE
depthstate.depthCompareOp = VkCompareOp.LESS
// one blend attachment per attachment of the rendering, written by this pass or not
var blend_atts : array<VkPipelineColorBlendAttachmentState>
blend_atts |> resize(int(N_COLOR_ATTACHMENTS))
for (att in blend_atts) {
att.colorWriteMask = VkColorComponentFlags.r | VkColorComponentFlags.g | VkColorComponentFlags.b | VkColorComponentFlags.a
}
var blend = VkPipelineColorBlendStateCreateInfo()
blend.attachmentCount = N_COLOR_ATTACHMENTS
var color_fmts <- [ALBEDO_FMT, NORMAL_FMT, COLOR_FMT]
var locations := is_geometry_pass ? GBUF_LOCATIONS : LIGHT_LOCATIONS
var inputs := is_geometry_pass ? GBUF_INPUTS : LIGHT_INPUTS
var rendering = VkPipelineRenderingCreateInfo()
rendering.colorAttachmentCount = N_COLOR_ATTACHMENTS
rendering.depthAttachmentFormat = DEPTH_FMT
var loc_info = VkRenderingAttachmentLocationInfo()
loc_info.colorAttachmentCount = N_COLOR_ATTACHMENTS
var in_info = VkRenderingInputAttachmentIndexInfo()
in_info.colorAttachmentCount = N_COLOR_ATTACHMENTS
var h : VkPipeline
unsafe {
rendering.pColorAttachmentFormats = addr(color_fmts[0])
loc_info.pColorAttachmentLocations = addr(locations[0])
in_info.pColorAttachmentInputIndices = addr(inputs[0])
rendering.pNext = addr<void?>(loc_info)
loc_info.pNext = addr<void?>(in_info)
var gp = VkGraphicsPipelineCreateInfo()
gp.pNext = addr<void?>(rendering)
gp.stageCount = 2u
gp.pStages = addr(stages[0])
vinput.pVertexBindingDescriptions = addr(binding)
vinput.pVertexAttributeDescriptions = addr(attrs[0])
gp.pVertexInputState = addr(vinput)
gp.pInputAssemblyState = addr(ia)
vp.pViewports = addr(viewport)
vp.pScissors = addr(scissor)
gp.pViewportState = addr(vp)
gp.pRasterizationState = addr(rast)
gp.pMultisampleState = addr(msaa)
gp.pDepthStencilState = addr(depthstate)
blend.pAttachments = addr(blend_atts[0])
gp.pColorBlendState = addr(blend)
gp.layout = boost_value_to_vk(layout)
vk_check(vkCreateGraphicsPipelines(boost_value_to_vk(device), null, 1u, addr(gp), null, addr(h)), null)
}
delete stages
delete attrs
delete blend_atts
delete color_fmts
delete locations
delete inputs
var b = vk_value_to_boost(h)
b._needs_delete = true
b._device = unsafe(reinterpret<uint64>(boost_value_to_vk(device)))
return <- b
}
//! Everything the frame needs that depends only on (device, phys): the G-buffer and color targets, the
//! cube geometry, the camera UBO, the three host-copied textures, both pipelines on push-descriptor set
//! layouts, and a host-visible readback. No descriptor pool anywhere.
struct public Vulkan14Resources {
albedo : OffscreenTarget
normal : OffscreenTarget
color : OffscreenTarget
depth : OffscreenDepth
vb : HostBuffer
ib : HostBuffer
ubo : HostBuffer
textures : array<OffscreenTarget>
tex_layout : VkImageLayout // the layout host_copy_layout chose; the pushed descriptor names it
sampler : Sampler
gbuf_layouts : array<DescriptorSetLayout>
gbuf_pipe_layout : PipelineLayout
gbuf_pipeline : Pipeline
light_layouts : array<DescriptorSetLayout>
light_pipe_layout : PipelineLayout
light_pipeline : Pipeline
readback : HostBuffer
buf_size : uint64
}
def public finalize(var r : Vulkan14Resources) {
delete r.readback
delete r.light_pipeline
delete r.light_pipe_layout
delete r.light_layouts
delete r.gbuf_pipeline
delete r.gbuf_pipe_layout
delete r.gbuf_layouts
delete r.sampler
delete r.textures
delete r.ubo
delete r.ib
delete r.vb
delete r.depth
delete r.color
delete r.normal
delete r.albedo
}
//! The scene's resources on a caller-owned device from `create_device_vulkan14` (the context's or the viewer's).
def public build_vulkan14_resources(device : Device; phys : VkPhysicalDevice) : Vulkan14Resources {
let gbuf_usage = VkImageUsageFlags.input_attachment // read back inside the rendering by the lighting pass
var inscope albedo <- build_offscreen_target(device, phys, V14_W, V14_H, ALBEDO_FMT, VkSampleCountFlags._1, gbuf_usage)
var inscope normal <- build_offscreen_target(device, phys, V14_W, V14_H, NORMAL_FMT, VkSampleCountFlags._1, gbuf_usage)
var inscope color <- build_offscreen_target(device, phys, V14_W, V14_H, COLOR_FMT)
var inscope depth <- build_offscreen_depth(device, phys, V14_W, V14_H, DEPTH_FMT)
var vb_usage : VkBufferUsageFlags
vb_usage.vertex_buffer = true
var verts_copy := cube_vertices
var inscope vb <- create_host_buffer_from_bytes(device, phys, vb_usage, verts_copy)
delete verts_copy
var ib_usage : VkBufferUsageFlags
ib_usage.index_buffer = true
var indices_copy := cube_indices
var inscope ib <- create_host_buffer_from_bytes(device, phys, ib_usage, indices_copy)
delete indices_copy
var ubo_usage : VkBufferUsageFlags
ubo_usage.uniform_buffer = true
var inscope ubo <- create_host_buffer(device, phys, CAMERA_UBO_BYTES, ubo_usage)
// host image copy lands in SHADER_READ_ONLY_OPTIMAL where the device lists it, else GENERAL - both sample legally
let tex_layout = host_copy_layout(phys, VkImageLayout.SHADER_READ_ONLY_OPTIMAL)
var inscope textures : array<OffscreenTarget>
textures |> reserve(N_CUBES)
for (pattern in each(CubePattern.red_checker)) {
var pixels <- gen_cube_texture(pattern)
textures |> emplace(create_host_copied_texture(device, phys, pixels, tex_layout))
delete pixels
}
let sci = SamplerCreateInfo(
magFilter = VkFilter.NEAREST,
minFilter = VkFilter.LINEAR,
mipmapMode = VkSamplerMipmapMode.NEAREST,
addressModeU = VkSamplerAddressMode.REPEAT,
addressModeV = VkSamplerAddressMode.REPEAT,
addressModeW = VkSamplerAddressMode.REPEAT,
maxLod = 1.0f)
var inscope sampler <- create_sampler(device, sci)
// push-descriptor set layouts: the reflected bindings, flagged so the sets are never allocated
var gbuf_refl <- [decode_reflection(gbuf_vert_spv_reflect), decode_reflection(gbuf_frag_spv_reflect)]
var inscope gbuf_layouts <- build_push_descriptor_set_layouts(device, gbuf_refl)
var inscope gbuf_pipe_layout <- build_pipeline_layout(device, gbuf_layouts, gbuf_refl)
delete gbuf_refl
var light_refl <- [decode_reflection(light_vert_spv_reflect), decode_reflection(light_frag_spv_reflect)]
var inscope light_layouts <- build_push_descriptor_set_layouts(device, light_refl)
var inscope light_pipe_layout <- build_pipeline_layout(device, light_layouts, light_refl)
delete light_refl
var inscope gbuf_vert <- create_shader_module(device, gbuf_vert_spv)
var inscope gbuf_frag <- create_shader_module(device, gbuf_frag_spv)
var inscope light_vert <- create_shader_module(device, light_vert_spv)
var inscope light_frag <- create_shader_module(device, light_frag_spv)
var inscope gbuf_pipeline <- build_pass_pipeline(device, gbuf_pipe_layout, gbuf_vert, gbuf_frag, PassKind.geometry)
var inscope light_pipeline <- build_pass_pipeline(device, light_pipe_layout, light_vert, light_frag, PassKind.lighting)
let buf_size = uint64(V14_W * V14_H * 4)
var inscope readback <- create_host_buffer(device, phys, buf_size)
return <- Vulkan14Resources(
albedo <- albedo, normal <- normal, color <- color, depth <- depth,
vb <- vb, ib <- ib, ubo <- ubo,
textures <- textures, tex_layout = tex_layout, sampler <- sampler,
gbuf_layouts <- gbuf_layouts, gbuf_pipe_layout <- gbuf_pipe_layout, gbuf_pipeline <- gbuf_pipeline,
light_layouts <- light_layouts, light_pipe_layout <- light_pipe_layout, light_pipeline <- light_pipeline,
readback <- readback, buf_size = buf_size)
}
8.15.16.4. The frame
record_vulkan14_frame moves the two G-buffer attachments into
RENDERING_LOCAL_READ and the color target into COLOR_ATTACHMENT_OPTIMAL,
then opens one rendering over all three plus depth. Inside it: the attachment
maps for the geometry pass, the three cube draws (a push-constant model matrix
and two pushed descriptors each), the by-region barrier, the maps for the
lighting pass, two pushed input attachments, and the fullscreen draw.
//! Cube `i`'s camera UBO and texture, pushed into the command buffer - there is no set to allocate or update.
def push_cube_descriptors(res : Vulkan14Resources; cmd : CommandBuffer; i : int) {
var w0 = WriteDescriptorSet(dstBinding = 0u, descriptorType = VkDescriptorType.UNIFORM_BUFFER, descriptorCount = 1u)
w0.pBufferInfo |> push(DescriptorBufferInfo(buffer = weak_copy(res.ubo.buffer), range_ = CAMERA_UBO_BYTES))
var w1 = WriteDescriptorSet(dstBinding = 1u, descriptorType = VkDescriptorType.COMBINED_IMAGE_SAMPLER, descriptorCount = 1u)
w1.pImageInfo |> push(DescriptorImageInfo(sampler = weak_copy(res.sampler), imageView = weak_copy(res.textures[i].view),
imageLayout = res.tex_layout))
var writes <- [ <- w0, <- w1]
cmd_push_descriptor_set(cmd, VkPipelineBindPoint.GRAPHICS, res.gbuf_pipe_layout, 0u, writes)
unsafe { delete writes }
}
//! Albedo and normal as input attachments in RENDERING_LOCAL_READ, the layout the rendering holds them in.
def push_light_descriptors(res : Vulkan14Resources; cmd : CommandBuffer) {
var writes : array<WriteDescriptorSet>
writes |> reserve(2)
for (binding, target in [0u, 1u], [weak_copy(res.albedo.view), weak_copy(res.normal.view)]) {
var w = WriteDescriptorSet(dstBinding = binding, descriptorType = VkDescriptorType.INPUT_ATTACHMENT, descriptorCount = 1u)
w.pImageInfo |> push(DescriptorImageInfo(imageView = weak_copy(target), imageLayout = VkImageLayout.RENDERING_LOCAL_READ))
writes |> emplace(w)
}
cmd_push_descriptor_set(cmd, VkPipelineBindPoint.GRAPHICS, res.light_pipe_layout, 0u, writes)
unsafe { delete writes }
}
//! The command-buffer twin of the pipeline's maps: fragment location -> attachment, attachment -> ``subpassInput`` index.
def set_pass_maps(cmd : CommandBuffer; locations, inputs : array<uint>) {
var loc : RenderingAttachmentLocationInfo
loc.pColorAttachmentLocations := locations
cmd_set_rendering_attachment_locations(cmd, loc)
delete loc
var inp : RenderingInputAttachmentIndexInfo
inp.pColorAttachmentInputIndices := inputs
cmd_set_rendering_input_attachment_indices(cmd, inp)
delete inp
}
//! Record the frame: the G-buffer attachments move to RENDERING_LOCAL_READ (written as attachments and read
//! as input attachments in that one layout), then ONE rendering holds both passes - geometry into attachments
//! 0 and 1, a by-region barrier, lighting into attachment 2. Ends with the lit color in TRANSFER_SRC_OPTIMAL.
def public record_vulkan14_frame(res : Vulkan14Resources; cmd : CommandBuffer; time : float) {
let local_read_stages = VkPipelineStageFlags2.color_attachment_output | VkPipelineStageFlags2.fragment_shader
let local_read_access = VkAccessFlags2.color_attachment_write | VkAccessFlags2.input_attachment_read
// every attachment starts the frame UNDEFINED: the previous frame's reads finished at its queue-idle wait
transition_image2(cmd, res.albedo.image, VkImageLayout.UNDEFINED, VkImageLayout.RENDERING_LOCAL_READ,
PIPELINE_STAGE_2_NONE, ACCESS_2_NONE, local_read_stages, local_read_access)
transition_image2(cmd, res.normal.image, VkImageLayout.UNDEFINED, VkImageLayout.RENDERING_LOCAL_READ,
PIPELINE_STAGE_2_NONE, ACCESS_2_NONE, local_read_stages, local_read_access)
transition_image2(cmd, res.color.image, VkImageLayout.UNDEFINED, VkImageLayout.COLOR_ATTACHMENT_OPTIMAL,
PIPELINE_STAGE_2_NONE, ACCESS_2_NONE,
VkPipelineStageFlags2.color_attachment_output, VkAccessFlags2.color_attachment_write)
transition_depth_image2(cmd, res.depth.image, VkImageLayout.UNDEFINED, VkImageLayout.DEPTH_ATTACHMENT_OPTIMAL,
PIPELINE_STAGE_2_NONE, ACCESS_2_NONE,
VkPipelineStageFlags2.early_fragment_tests | VkPipelineStageFlags2.late_fragment_tests,
VkAccessFlags2.depth_stencil_attachment_write)
// the G-buffer attachments are transient: cleared on entry, read inside, never stored
var atts <- [
<- RenderingAttachmentInfo(imageView = weak_copy(res.albedo.view), imageLayout = VkImageLayout.RENDERING_LOCAL_READ,
loadOp = VkAttachmentLoadOp.CLEAR, storeOp = VkAttachmentStoreOp.DONT_CARE, clearValue = clear_color(0.0f, 0.0f, 0.0f, 0.0f)),
<- RenderingAttachmentInfo(imageView = weak_copy(res.normal.view), imageLayout = VkImageLayout.RENDERING_LOCAL_READ,
loadOp = VkAttachmentLoadOp.CLEAR, storeOp = VkAttachmentStoreOp.DONT_CARE, clearValue = clear_color(0.5f, 0.5f, 0.5f, 0.0f)),
<- rendering_color_attachment(res.color.view, clear_color(0.0f, 0.0f, 0.0f, 1.0f))]
var depth_att <- rendering_depth_attachment(res.depth.view, clear_depth(1.0f))
record_rendering(cmd, full_area(V14_W, V14_H), 1u, atts, depth_att) {
set_pass_maps(cmd, GBUF_LOCATIONS, GBUF_INPUTS)
cmd_bind_pipeline(cmd, res.gbuf_pipeline)
var vbufs <- [weak_copy(res.vb.buffer)]
var voffs <- [0ul]
cmd_bind_vertex_buffers(cmd, 0u, vbufs, voffs)
delete vbufs
delete voffs
cmd_bind_index_buffer(cmd, weak_copy(res.ib.buffer), 0ul, VkIndexType.UINT16)
for (i in range(N_CUBES)) {
pc.model = model_matrix(time * (0.6f + 0.3f * float(i)), float3(1.7f * float(i - 1), 0.0f, 0.0f))
gbuf_vs_push_constants(cmd, res.gbuf_pipe_layout)
push_cube_descriptors(res, cmd, i)
cmd_draw_indexed(cmd, uint(N_INDICES), 1u, 0u, 0, 0u)
}
// the hand-off: attachment writes -> input-attachment reads, per region, inside the rendering
var dep : DependencyInfo
dep.dependencyFlags.by_region = true
dep.pMemoryBarriers |> emplace(MemoryBarrier2(
srcStageMask = VkPipelineStageFlags2.color_attachment_output, srcAccessMask = VkAccessFlags2.color_attachment_write,
dstStageMask = VkPipelineStageFlags2.fragment_shader, dstAccessMask = VkAccessFlags2.input_attachment_read))
cmd_pipeline_barrier2(cmd, dep)
delete dep
set_pass_maps(cmd, LIGHT_LOCATIONS, LIGHT_INPUTS)
cmd_bind_pipeline(cmd, res.light_pipeline)
light_fs_push_constants(cmd, res.light_pipe_layout)
push_light_descriptors(res, cmd)
cmd_draw(cmd, 3u)
}
delete atts
delete depth_att
transition_image2(cmd, res.color.image, VkImageLayout.COLOR_ATTACHMENT_OPTIMAL, VkImageLayout.TRANSFER_SRC_OPTIMAL,
VkPipelineStageFlags2.color_attachment_output, VkAccessFlags2.color_attachment_write,
VkPipelineStageFlags2.copy | VkPipelineStageFlags2.blit, VkAccessFlags2.transfer_read)
}
8.15.16.5. Self-verifying
The oracle renders one frame at time 0 and samples it: the sky at the frame’s
edge is the lighting pass’s gradient (not the cleared black of the third
attachment), each cube shows the hue of the texture host image copy uploaded,
and the top face under the light is brighter than the side face turned away
from it – which only the normal attachment, read back through local read, can
produce. It skips where vulkan14_supported is false.
[test]
def test_vulkan14_scene(t : T?) {
if (!vulkan14_available()) {
feint("Vulkan 1.4 with pushDescriptor + hostImageCopy + dynamicRenderingLocalRead not available; skipping (lavapipe reports 1.4 from Mesa 25.0)\n")
return
}
var p <- render_vulkan14_scene(0.0f)
t |> success(length(p) == V14_W * V14_H * 4, "one RGBA8 frame came back")
// sky: the lighting pass wrote its gradient where no cube is - lighter at the top than the bottom
let sky_top = px(p, 20, 10)
let sky_bottom = px(p, 20, V14_H - 10)
t |> success(sky_top.z > sky_top.x && sky_top.z > 40, "sky is the lighting pass's blue gradient, not the cleared black")
t |> success(sky_top.z > sky_bottom.z, "sky is brighter at the top")
// the three cubes sit left, center, right at time 0 (camera on +Z looking at the origin); each block
// sits on a front face, and the block mean carries the texture's hue
let left = avg_px(p, 180, 257)
let mid = avg_px(p, 384, 257)
let right = avg_px(p, 570, 257)
t |> success(left.x > left.y + 30 && left.x > left.z + 30, "left cube carries the red checker texture")
t |> success(mid.y > mid.x + 30 && mid.y > mid.z + 30, "center cube carries the green stripes texture")
t |> success(right.z > right.x + 20 && right.z > right.y + 20, "right cube carries the blue dots texture")
// lighting read the normal attachment: on the right cube, the top face under the light is brighter
// than the -X face the time-0 light direction leaves in ambient
let top_face = avg_px(p, 592, 195)
let away_face = avg_px(p, 508, 245)
t |> success(top_face.x + top_face.y + top_face.z > away_face.x + away_face.y + away_face.z + 100,
"the face under the light is lit brighter than the one turned away from it")
delete p
}
8.15.16.6. Running it
# the CI pixel-oracle gate (lavapipe from Mesa 25.0 in CI, a 1.4 GPU locally)
daslang.exe dastest/dastest.das -- --test modules/dasVulkan/tutorials/16_vulkan14
# watch it live in a window (needs the glfw module, a display and a 1.4 driver)
daslang.exe modules/dasVulkan/tutorials/16_vulkan14/window/show_vulkan14.das
# regenerate the recording (needs stbimage + audio + ffmpeg + dasHV, and Kokoro TTS at :8880, locally)
daslang.exe modules/dasVulkan/tutorials/16_vulkan14/recording/record_vulkan14.das
8.15.16.7. Beside the tutorials
examples/shader_objects.das draws the hello triangle with no pipeline at
all: VK_EXT_shader_object binds a shader object per stage and takes every
piece of fixed-function state from the command buffer
(cmd_set_shader_object_baseline sets what a device from
create_device_shader_object requires). It is an opt-in beside the
pipeline path – an extension in every Vulkan version, absent on Android and
partial on MoltenVK – and tests/integration/test_shader_objects.das checks
it draws the same triangle.