15 - Hardware Ray Tracing
Everything so far rasterized. This tutorial fires real rays: the scene – a box
on a ground plane – is built into acceleration structures, a
VK_KHR_ray_tracing_pipeline traces one camera ray per pixel into it, and
each hit casts a second shadow ray toward the light. No render pass, no
vertex input, no rasterizer: the raygen shader writes the frame directly into a
storage image.
The headline rails:
Three new shader stages, all daslang.
[vulkan_raygen_shader]fires the pinhole-camera rays andimageStores the result;[vulkan_miss_shader]paints the sky;[vulkan_closest_hit_shader]flat-shades the hit. dasSpirv lowers all three to SPIR-V 1.4 with theRayTracingKHRcapability –@ray_payload/@incoming_ray_payloadglobals carry data between them, andtraceRayEXT(...)is the daslang spelling ofOpTraceRayKHR.BLAS + TLAS in two calls.
build_blasuploads the triangle soup and runs the GPU build;build_tlasplaces it withmake_accel_instance(the hand-packed stand-in forVkAccelerationStructureInstanceKHR’s C bitfields). Both block until built, so the resultingAccelStructures are immediately traceable.The shader binding table, assembled for you.
create_ray_tracing_pipelinelays the groups out as[raygen, miss..., hit]andbuild_shader_binding_tablefetches the opaque group handles, applies the device’s alignment rules, and returns the four strided regionscmd_trace_raysconsumes.Shadow without recursion into shading. The closest-hit’s shadow ray uses
terminate-on-first-hit + skip-closest-hitflags andmiss_index = 1: no hit shader ever runs for it – only the second miss shader, which flips the payload to “lit” when the ray reaches the light unobstructed.The descriptor side is reflection-driven. The TLAS binding reflects as
VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_KHRand the layouts come straight frombuild_descriptor_set_layouts/build_pipeline_layout– the same reflection path every raster tutorial uses, now RT-stage-aware.
The clip above is the headless recording: 30 seconds, 30 fps, one full camera
orbit – every frame is a real vkCmdTraceRaysKHR, and the traced shadow
swings around the box as the camera circles. The [test] pixel-oracle is the
strongest in the series: the same daslang scene tables and shading functions
also run in the interpreter as a CPU reference tracer, and the GPU frame must
match it pixel-for-pixel (up to a small silhouette budget where hardware
traversal and the CPU intersector round the boundary differently). One source.
Two execution tiers. Identical pixels.
Note
Hardware ray tracing requires VK_KHR_ray_tracing_pipeline +
VK_KHR_acceleration_structure with the bufferDeviceAddress feature
(rt_supported probes all of it). The tutorial soft-skips on devices
without RT support (lavapipe in CI, so the test passes as skipped) – the
recording here is from an RT-capable GPU.
The shaders
Four entry points: raygen (camera + imageStore), the sky miss, the shadow
miss (the “lit” flip), and the closest-hit (flat normal by gl_PrimitiveID,
lambert, one shadow ray). The scene tables and shading functions they call are
plain daslang functions – the CPU reference tracer in the test calls the very
same ones.
module raytracing_tut_shaders public
require vulkan/vulkan_boost public // Device + DeviceMemory used by the generated bind helpers
require vulkan/spirv_vulkan_shader public
require spirv/spirv_builtins public
require math public
// ===== the scene: a box on a ground plane =====
//! 12 vertices: ground quad [-30,30]^2 at y=0, then the 1.5-unit box resting on it.
def rt_scene_vertices : array<float3> {
return <- [
float3(-30.0, 0.0, -30.0), float3(30.0, 0.0, -30.0),
float3(30.0, 0.0, 30.0), float3(-30.0, 0.0, 30.0),
float3(-0.75, 0.0, -0.75), float3(0.75, 0.0, -0.75),
float3(0.75, 0.0, 0.75), float3(-0.75, 0.0, 0.75),
float3(-0.75, 1.5, -0.75), float3(0.75, 1.5, -0.75),
float3(0.75, 1.5, 0.75), float3(-0.75, 1.5, 0.75)
]
}
//! 14 triangles: prims 0-1 ground, then box faces front/back/left/right/top/bottom (2 each) —
//! the order rt_normal() keys off.
def rt_scene_indices : array<uint> {
return <- [
0u, 1u, 2u, 0u, 2u, 3u, // ground
4u, 5u, 9u, 4u, 9u, 8u, // front (-z)
7u, 6u, 10u, 7u, 10u, 11u, // back (+z)
4u, 7u, 11u, 4u, 11u, 8u, // left (-x)
5u, 6u, 10u, 5u, 10u, 9u, // right (+x)
8u, 9u, 10u, 8u, 10u, 11u, // top (+y)
4u, 5u, 6u, 4u, 6u, 7u // bottom (-y)
]
}
//! flat per-face normal by primitive index (the closest-hit shader's whole "vertex fetch")
def rt_normal(prim : int) : float3 {
let normals = fixed_array(
float3(0.0, 1.0, 0.0), float3(0.0, 1.0, 0.0),
float3(0.0, 0.0, -1.0), float3(0.0, 0.0, -1.0),
float3(0.0, 0.0, 1.0), float3(0.0, 0.0, 1.0),
float3(-1.0, 0.0, 0.0), float3(-1.0, 0.0, 0.0),
float3(1.0, 0.0, 0.0), float3(1.0, 0.0, 0.0),
float3(0.0, 1.0, 0.0), float3(0.0, 1.0, 0.0),
float3(0.0, -1.0, 0.0), float3(0.0, -1.0, 0.0))
return normals[prim]
}
def rt_ground_albedo : float3 => float3(0.42, 0.52, 0.42)
def rt_box_albedo : float3 => float3(0.85, 0.35, 0.18)
//! unit vector TOWARD the light: -normalize(float3(0.45, -1.0, 0.35)), pre-folded so the shader
//! reads plain literals
def rt_to_light : float3 => float3(-0.390936, 0.868747, -0.304062)
// ===== camera (orbits the box at CAM_HEIGHT, always looking at CAM_TARGET) =====
let CAM_DIST = 4.5
let CAM_HEIGHT = 2.2
let CAM_TAN = 0.5773503 // tan(30 deg): 60-degree vertical FOV
def rt_camera_origin(angle : float) : float3 => float3(sin(angle) * CAM_DIST, CAM_HEIGHT, -cos(angle) * CAM_DIST)
def rt_camera_dir(origin : float3; px, py : float; w, h : float) : float3 {
let fwd = normalize(float3(0.0, 0.6, 0.0) - origin)
let right = normalize(cross(fwd, float3(0.0, 1.0, 0.0)))
let up = cross(right, fwd)
let u = (px + 0.5) / w * 2.0 - 1.0
let v = 1.0 - (py + 0.5) / h * 2.0 // image row 0 is the top -> +v is up
return normalize(fwd + right * (u * CAM_TAN) + up * (v * CAM_TAN))
}
// ===== shading (shared GPU / CPU) =====
def sky_color(dir : float3) : float3 {
let t = clamp(dir.y, 0.0, 1.0)
return float3(0.75, 0.85, 0.95) + (float3(0.25, 0.45, 0.8) - float3(0.75, 0.85, 0.95)) * t
}
def shade_lambert(n, albedo : float3; lit : float) : float3 {
let ndl = max(dot(n, rt_to_light()), 0.0)
return albedo * (0.18 + 0.82 * ndl * lit)
}
// ===== the shaders =====
struct RtPush {
angle : float
}
var @push_constant pc : RtPush
var @set = 0 @binding = 0 rt_tlas : accelerationStructureEXT
var @set = 0 @binding = 1 rt_img : image2D
var @ray_payload prd : float3
[vulkan_raygen_shader(name="raytracing_rgen_spv")]
def raytracing_rgen {
let gid = gl_LaunchIDEXT
let dim = gl_LaunchSizeEXT
let origin = rt_camera_origin(pc.angle)
let dir = rt_camera_dir(origin, float(gid.x), float(gid.y), float(dim.x), float(dim.y))
prd = float3(0.0, 0.0, 0.0)
traceRayEXT(rt_tlas, 0x01u, 0xFFu, 0u, 0u, 0u, origin, 0.001, dir, 100.0, prd)
imageStore(rt_img, int2(int(gid.x), int(gid.y)), float4(prd, 1.0))
}
var @incoming_ray_payload miss_prd : float3
[vulkan_miss_shader(name="raytracing_miss_spv")]
def raytracing_miss {
miss_prd = sky_color(gl_WorldRayDirectionEXT)
}
var @incoming_ray_payload shadow_prd : float3
[vulkan_miss_shader(name="raytracing_shadow_miss_spv")]
def raytracing_shadow_miss {
shadow_prd = float3(1.0, 1.0, 1.0) // the shadow ray reached the light: lit
}
var @incoming_ray_payload hit_prd : float3
var @ray_payload @location = 1 shadow_ray : float3
[vulkan_closest_hit_shader(name="raytracing_chit_spv")]
def raytracing_chit {
let n = rt_normal(gl_PrimitiveID)
var albedo = rt_box_albedo()
if (gl_PrimitiveID < 2) {
albedo = rt_ground_albedo()
}
let p = gl_WorldRayOriginEXT + gl_WorldRayDirectionEXT * gl_HitTEXT
// shadow ray: opaque (0x1) + terminate-on-first-hit (0x4) + skip-closest-hit (0x8). No hit
// shader ever runs -- only the shadow miss (miss_index = 1) flips the payload to lit.
shadow_ray = float3(0.0, 0.0, 0.0)
traceRayEXT(rt_tlas, 0x0Du, 0xFFu, 0u, 0u, 1u,
p + n * 0.001, 0.001, rt_to_light(), 100.0, shadow_ray)
hit_prd = shade_lambert(n, albedo, shadow_ray.x)
}
The host side
build_rt_view assembles the resident RT stack – BLAS, TLAS,
reflection-built layouts, the pipeline, its shader binding table, and the
storage image – and record_trace re-fires the whole frame from one push
constant (the camera orbit angle).
struct RtView {
blas : AccelStructure
tlas : AccelStructure
set_layouts : array<DescriptorSetLayout>
pipe_layout : PipelineLayout
desc_pool : DescriptorPool
dsets : array<DescriptorSet>
pipeline : Pipeline
sbt : ShaderBindingTable
image : Image
memory : DeviceMemory
view : ImageView
width : int
height : int
}
def finalize(var rv : RtView) {
delete rv.sbt
delete rv.pipeline
delete rv.desc_pool // frees the sets allocated from it
delete rv.dsets
delete rv.pipe_layout
delete rv.set_layouts
delete rv.view
delete rv.image
delete rv.memory
delete rv.tlas
delete rv.blas
}
//! Build the resident RT stack. The device must come from create_device_ray_tracing; pool/queue
//! are used (and waited idle) for the BLAS/TLAS builds.
def build_rt_view(device : Device; phys : VkPhysicalDevice; pool : CommandPool; queue : VkQueue; // nolint:STYLE038 - flat one-call-per-item Vulkan setup run
w, h : int) : RtView {
let dev = boost_value_to_vk(device)
// acceleration structures over the scene tables
var verts <- rt_scene_vertices()
var idx <- rt_scene_indices()
var inscope blas <- build_blas(device, phys, pool, queue, verts, idx)
delete verts
delete idx
var insts <- [make_accel_instance(blas.address)]
var inscope tlas <- build_tlas(device, phys, pool, queue, insts)
delete insts
// layouts straight from shader reflection: set 0 = { binding 0 TLAS, binding 1 storage image },
// plus the raygen's `angle` push-constant range
var reflections <- [decode_reflection(raytracing_rgen_spv_reflect),
decode_reflection(raytracing_miss_spv_reflect),
decode_reflection(raytracing_shadow_miss_spv_reflect),
decode_reflection(raytracing_chit_spv_reflect)]
var inscope set_layouts <- build_descriptor_set_layouts(device, reflections)
var inscope pipe_layout <- build_pipeline_layout(device, set_layouts, reflections)
delete reflections
var dpci = DescriptorPoolCreateInfo(maxSets = 1u,
pPoolSizes <- [
DescriptorPoolSize(type_ = VkDescriptorType.ACCELERATION_STRUCTURE_KHR, descriptorCount = 1u),
DescriptorPoolSize(type_ = VkDescriptorType.STORAGE_IMAGE, descriptorCount = 1u)
])
var inscope desc_pool <- create_descriptor_pool(device, dpci)
var dsai : DescriptorSetAllocateInfo
dsai.descriptorPool = weak_copy(desc_pool)
dsai.pSetLayouts |> push(weak_copy(set_layouts[0]))
var inscope dsets <- allocate_descriptor_sets(device, dsai)
// the storage image the raygen writes (TRANSFER_SRC for both the readback and the blit path)
let fmt = VkFormat.R8G8B8A8_UNORM
var ici = ImageCreateInfo(imageType = VkImageType._2D, format = fmt, mipLevels = 1u,
arrayLayers = 1u, tiling = VkImageTiling.OPTIMAL, initialLayout = VkImageLayout.UNDEFINED)
ici.extent.width = uint(w)
ici.extent.height = uint(h)
ici.extent.depth = 1u
ici.samples._1 = true
ici.usage.storage = true
ici.usage.transfer_src = true
var inscope image <- create_image(device, ici)
var ireq : VkMemoryRequirements
vkGetImageMemoryRequirements(dev, boost_value_to_vk(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 imem <- allocate_memory(device, imai)
vk_check(vkBindImageMemory(dev, boost_value_to_vk(image), boost_value_to_vk(imem), 0ul), null)
var vci = ImageViewCreateInfo(image = weak_copy(image), viewType = VkImageViewType._2D, format = fmt)
vci.subresourceRange.aspectMask.color = true
vci.subresourceRange.levelCount = 1u
vci.subresourceRange.layerCount = 1u
var inscope view <- create_image_view(device, vci)
// descriptor writes: the TLAS through the pNext rail, the image through the boost path
write_descriptor_acceleration_structure(device, dsets[0], 0u, tlas)
var write = WriteDescriptorSet(dstSet = weak_copy(dsets[0]), dstBinding = 1u,
descriptorType = VkDescriptorType.STORAGE_IMAGE, descriptorCount = 1u)
write.pImageInfo |> push(DescriptorImageInfo(imageView = weak_copy(view), imageLayout = VkImageLayout.GENERAL))
var writes : array<WriteDescriptorSet>
writes |> emplace(write)
let no_copies : array<CopyDescriptorSet>
update_descriptor_sets(device, writes, no_copies)
// pipeline (raygen + [sky miss, shadow miss] + closest-hit) + its SBT; the shader modules may
// be destroyed once the pipeline exists, so they stay scope-local
var inscope rgen <- create_shader_module(device, raytracing_rgen_spv)
var inscope miss_sky <- create_shader_module(device, raytracing_miss_spv)
var inscope miss_shadow <- create_shader_module(device, raytracing_shadow_miss_spv)
var inscope chit <- create_shader_module(device, raytracing_chit_spv)
var miss_list <- [weak_copy(miss_sky), weak_copy(miss_shadow)]
var inscope pipeline <- create_ray_tracing_pipeline(device, pipe_layout, rgen, miss_list, chit, 2u)
delete miss_list
var inscope sbt <- build_shader_binding_table(device, phys, pipeline, 2u, 1u)
return <- RtView(blas <- blas, tlas <- tlas, set_layouts <- set_layouts,
pipe_layout <- pipe_layout, desc_pool <- desc_pool, dsets <- dsets,
pipeline <- pipeline, sbt <- sbt, image <- image, memory <- imem, view <- view,
width = w, height = h)
}
//! Record one full trace of the scene into `cmd` for the given camera orbit angle. The caller
//! owns the image layout: it must be GENERAL before this call, and the barriers around the
//! raygen's writes are the caller's (see render_raytracing / the windowed viewer).
def record_trace(cmd : CommandBuffer; rv : RtView; angle : float) {
cmd_bind_pipeline(cmd, rv.pipeline, VkPipelineBindPoint.RAY_TRACING_KHR)
var sets : array<DescriptorSet>
sets |> push(weak_copy(rv.dsets[0]))
var no_dyn : array<uint>
cmd_bind_descriptor_sets(cmd, VkPipelineBindPoint.RAY_TRACING_KHR, rv.pipe_layout, 0u, sets, no_dyn)
pc.angle = angle
raytracing_rgen_push_constants(cmd, rv.pipe_layout)
cmd_trace_rays(cmd, rv.sbt, uint(rv.width), uint(rv.height))
delete sets
}
//! Everything a headless offscreen trace needs, owned together: its own instance/device plus the
//! resident RtView and a readback buffer. Built once (build_rt_offscreen), traced per frame
//! (render_rt_frame) -- the shape both the pixel-oracle test and the recording driver share.
Self-verifying
The pixel-oracle traces the same frame twice – hardware RT on the GPU, the Moller-Trumbore reference in the interpreter – and asserts they agree pixel-for-pixel within a small silhouette budget, after first proving the CPU frame contains all four regimes (sky, lit ground, the cast shadow, the box).
[test]
def test_raytracing(t : T?) {
if (!rt_available()) {
feint("VK_KHR_ray_tracing_pipeline not supported by this device; skipping (CI lavapipe has no RT)\n")
return
}
var gpu <- render_raytracing(0.0, TEST_DIM, TEST_DIM)
var cpu <- render_raytracing_cpu(0.0, TEST_DIM, TEST_DIM)
t |> success(length(gpu) == TEST_DIM * TEST_DIM * 4, "GPU frame is {TEST_DIM}x{TEST_DIM} RGBA8")
t |> success(length(cpu) == length(gpu), "CPU reference frame has the same size")
// the CPU reference must contain all four regimes -- proves the scene really exercises the
// sky miss, plain lambert, the shadow ray, and the box hit group
var sky_px = 0
var lit_ground_px = 0
var shadow_ground_px = 0
var box_px = 0
let lit_ground = shade_lambert(float3(0.0, 1.0, 0.0), rt_ground_albedo(), 1.0)
let dark_ground = shade_lambert(float3(0.0, 1.0, 0.0), rt_ground_albedo(), 0.0)
for (py in range(TEST_DIM)) {
for (px in range(TEST_DIM)) {
let o = (py * TEST_DIM + px) * 4
let r = int(cpu[o])
let g = int(cpu[o + 1])
let b = int(cpu[o + 2])
if (b > r && b > g) {
sky_px++ // both sky gradient stops are blue-dominant
} elif (abs(r - int(255.0 * lit_ground.x)) <= 2 && abs(g - int(255.0 * lit_ground.y)) <= 2) {
lit_ground_px++
} elif (abs(r - int(255.0 * dark_ground.x)) <= 2 && abs(g - int(255.0 * dark_ground.y)) <= 2) {
shadow_ground_px++
} elif (r > g && r > b) {
box_px++ // the box albedo is red-dominant at any lambert level
}
}
}
t |> success(sky_px > 100, "CPU reference shows sky ({sky_px} px)")
t |> success(lit_ground_px > 100, "CPU reference shows lit ground ({lit_ground_px} px)")
t |> success(shadow_ground_px > 100, "CPU reference shows the cast shadow ({shadow_ground_px} px)")
t |> success(box_px > 100, "CPU reference shows the box ({box_px} px)")
// GPU == CPU up to the silhouette budget
var over_tol = 0
var first_bad = int2(-1, -1)
for (py in range(TEST_DIM)) {
for (px in range(TEST_DIM)) {
let o = (py * TEST_DIM + px) * 4
var worst = 0
for (c in range(3)) {
let d = abs(int(gpu[o + c]) - int(cpu[o + c]))
if (d > worst) {
worst = d
}
}
if (worst > TOL) {
over_tol++
if (first_bad.x < 0) {
first_bad = int2(px, py)
}
}
}
}
t |> success(over_tol <= EDGE_BUDGET,
"GPU matches the CPU reference: {over_tol} of {TEST_DIM * TEST_DIM} pixels beyond +-{TOL} (budget {EDGE_BUDGET}, first at {first_bad})")
delete gpu
delete cpu
}
See it live
window/show_raytracing.das opens a GLFW window and traces the orbit live:
build_rt_view once, then every frame is one push constant + one
vkCmdTraceRaysKHR + a blit onto the swapchain image.
require glfw/glfw_boost
require vulkan
require vulkan/vulkan_boost
require vulkan/vulkan_window
require ../raytracing_tut.das
require daslib/defer
let RT_DIM = 768
[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(RT_DIM, RT_DIM, "dasVulkan tutorial 15 - hardware ray tracing", null, null)
if (window == null) {
panic("can't create window")
}
defer() { glfwDestroyWindow(window) }
var ext_count = 0u
let glfw_exts = glfwGetRequiredInstanceExtensions(unsafe(addr(ext_count)))
var inst_exts : array<string>
inst_exts |> reserve(int(ext_count))
for (i in range(int(ext_count))) {
unsafe {
inst_exts |> push(glfw_exts[i])
}
}
var inscope instance <- create_instance("dasVulkan tutorial 15", 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)
if (!rt_supported(phys)) {
panic("this device does not support VK_KHR_ray_tracing_pipeline")
}
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_ray_tracing(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)
// the resident RT stack: BLAS/TLAS + pipeline + SBT + the storage image the raygen writes
var inscope rv <- build_rt_view(device, phys, pool, queue, RT_DIM, RT_DIM)
var inscope swap <- create_swapchain(device, phys, surface, RT_DIM, RT_DIM)
var inscope sync <- create_frame_sync(device)
while (glfwWindowShouldClose(window) == 0) {
glfwPollEvents()
var fbw = 0
var fbh = 0
glfwGetFramebufferSize(window, fbw, fbh)
if (fbw == 0 || fbh == 0) {
glfwWaitEvents()
continue
}
if (fbw != swap.width || fbh != swap.height) {
vkDeviceWaitIdle(boost_value_to_vk(device))
delete swap
swap <- create_swapchain(device, phys, surface, fbw, fbh) // nolint:PERF030 - deleted just above
}
let angle = float(glfwGetTime()) * 0.4
let ok = present_frame(device, queue, swap, pool, sync) $(cmd; target; _idx) {
let none : VkAccessFlags
var shader_write : VkAccessFlags
shader_write.shader_write = true
var transfer_read : VkAccessFlags
transfer_read.transfer_read = true
var transfer_write : VkAccessFlags
transfer_write.transfer_write = true
var top : VkPipelineStageFlags
top.top_of_pipe = true
var rt_stage : VkPipelineStageFlags
rt_stage.ray_tracing_shader_khr = true
var xfer : VkPipelineStageFlags
xfer.transfer = true
var bottom : VkPipelineStageFlags
bottom.bottom_of_pipe = true
// trace this frame's orbit angle into the storage image (GENERAL)
transition_image(cmd, rv.image, VkImageLayout.UNDEFINED, VkImageLayout.GENERAL,
none, shader_write, top, rt_stage)
record_trace(cmd, rv, angle)
// make the raygen writes available to the blit (GENERAL is a legal blit source)
transition_image(cmd, rv.image, VkImageLayout.GENERAL, VkImageLayout.GENERAL,
shader_write, transfer_read, rt_stage, xfer)
let dst = vk_value_to_boost(target)
transition_image(cmd, dst, VkImageLayout.UNDEFINED, VkImageLayout.TRANSFER_DST_OPTIMAL,
none, transfer_write, top, xfer)
var region : VkImageBlit
region.srcSubresource.aspectMask.color = true
region.srcSubresource.layerCount = 1u
region.srcOffsets[1].x = rv.width
region.srcOffsets[1].y = rv.height
region.srcOffsets[1].z = 1
region.dstSubresource.aspectMask.color = true
region.dstSubresource.layerCount = 1u
region.dstOffsets[1].x = swap.width
region.dstOffsets[1].y = swap.height
region.dstOffsets[1].z = 1
unsafe {
vkCmdBlitImage(boost_value_to_vk(cmd), boost_value_to_vk(rv.image), VkImageLayout.GENERAL,
target, VkImageLayout.TRANSFER_DST_OPTIMAL, 1u, addr(region), VkFilter.LINEAR)
}
transition_image(cmd, dst, VkImageLayout.TRANSFER_DST_OPTIMAL, VkImageLayout.PRESENT_SRC_KHR,
transfer_write, none, xfer, bottom)
}
if (!ok) {
vkDeviceWaitIdle(boost_value_to_vk(device))
delete swap
swap <- create_swapchain(device, phys, surface, fbw, fbh) // nolint:PERF030 - deleted just above
}
}
vkDeviceWaitIdle(boost_value_to_vk(device))
}
Running it
# the CI pixel-oracle gate (skips cleanly without RT support)
daslang -load_module <dasVulkan> <daslang>/dastest/dastest.das -- \
--test <dasVulkan>/tutorials/15_raytracing
# watch it live in a window (needs the glfw module + an RT-capable GPU)
daslang -load_module <dasVulkan> \
<dasVulkan>/tutorials/15_raytracing/window/show_raytracing.das
# regenerate the recording (needs stbimage + audio + ffmpeg locally)
daslang -load_module <dasVulkan> \
<dasVulkan>/tutorials/15_raytracing/recording/record_raytracing.das