GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/vk/rt.rs (98.2K)
1 //! The tier-1 RT engine (RT-renderer phase 2): an internal triangle+material
2 //! scene, a CPU-built binned-SAH BVH uploaded as storage buffers, and the
3 //! `rt.wgsl` compute path tracer with progressive accumulation.
4 //!
5 //! The stage renders into its own pane-sized storage image and blits it into
6 //! the backdrop image's viewport-pane region — exactly the slot the raster
7 //! `SceneStage` fills — so the swapchain copy, blur plates, and the 2D UI pass
8 //! are untouched. Runs on plain Vulkan compute (no `VK_KHR_ray_*`), which is
9 //! the point: it works on the integrated GPU; a tier-2 ray-query backend can
10 //! later swap out just the traversal.
11 //!
12 //! Scene schema is internal by design — importers (OBJ/glTF) belong in a
13 //! future loader that converts *into* [`RtTriangle`]/[`RtMaterial`].
14
15 use ash::vk;
16 use gpu_allocator::vulkan::{Allocation, AllocationCreateDesc, AllocationScheme, Allocator};
17 use gpu_allocator::MemoryLocation;
18
19 use super::renderer::{
20 compile_wgsl, compile_wgsl_ray_query, create_cpu_buffer, destroy_cpu_buffer, AllocatedBuffer,
21 };
22 pub use crate::draw::rt::{PreparedRtScene, RtCamera, RtEnvironment, RtImage, RtImagePixels, RtMaterial, RtTriangle};
23 use crate::draw::rt::{
24 denoise_params, pack_scene, rt_params, DenoiseParams, PackedScene, ParamImage, RtParams, DENOISE_ITERATIONS, MAX_SAMPLES,
25 WORKGROUP,
26 };
27
28 /// Where the stage's image comes from.
29 pub(crate) enum RtImageSource<'a> {
30 /// The 2D pass's image of this id, looked up each frame: its upload may
31 /// not have landed when the scene is set, and it may be replaced after.
32 Shared(u32),
33 /// Pixels for the stage to upload and own.
34 Pixels { pixels: &'a [u8], width: u32, height: u32 },
35 }
36
37 /// The two trace backends. They share every shader line except
38 /// `intersect_scene` (rt_bvh.wgsl vs rt_query.wgsl) and binding 1.
39 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
40 enum RtTier {
41 /// CPU-built BVH traversed in compute — runs on any device.
42 Compute,
43 /// Driver acceleration structures + VK_KHR_ray_query — RT cores.
44 RayQuery,
45 }
46
47 impl RtTier {
48 /// The tier a stage on a device with (`has_ray_query`) or without the
49 /// ray-query stack runs: tier 2 when it can, unless `CCE_VK_RT=compute`
50 /// forces the BVH tier.
51 fn choose(has_ray_query: bool) -> RtTier {
52 let force_compute = std::env::var("CCE_VK_RT").is_ok_and(|v| v == "compute");
53 if has_ray_query && !force_compute {
54 RtTier::RayQuery
55 } else {
56 RtTier::Compute
57 }
58 }
59 }
60
61 /// Whether a stage made on a device with (`has_ray_query`) or without the
62 /// ray-query stack traverses a CPU-built BVH: what `Stage3D::rt_needs_bvh`
63 /// answers before the stage exists.
64 pub(crate) fn needs_bvh(has_ray_query: bool) -> bool {
65 RtTier::choose(has_ray_query) == RtTier::Compute
66 }
67
68 /// Tier-2 GPU objects: one BLAS over the triangle buffer, a one-instance
69 /// TLAS over it. Rebuilt wholesale on every scene replacement.
70 struct Accel {
71 blas: vk::AccelerationStructureKHR,
72 blas_buffer: AllocatedBuffer,
73 tlas: vk::AccelerationStructureKHR,
74 tlas_buffer: AllocatedBuffer,
75 instances: AllocatedBuffer,
76 }
77
78 struct RtFrame {
79 uniforms: AllocatedBuffer,
80 descriptor_set: vk::DescriptorSet,
81 }
82
83 /// A texture the stage made and owns: the 1x1 stand-in bound while the
84 /// scene has no image, and the headless tracer's image.
85 struct OwnedTexture {
86 image: vk::Image,
87 view: vk::ImageView,
88 allocation: Option<Allocation>,
89 size: (u32, u32),
90 }
91
92 impl OwnedTexture {
93 /// Upload sRGB RGBA8 pixels as a one-level texture, with a blocking
94 /// one-time submit. One level: the headless tracer renders a still, and
95 /// its samples average what a mip chain would have.
96 fn new(
97 device: &ash::Device,
98 allocator: &mut Allocator,
99 queue: vk::Queue,
100 command_pool: vk::CommandPool,
101 pixels: &[u8],
102 width: u32,
103 height: u32,
104 ) -> Self {
105 assert_eq!(pixels.len(), (width * height * 4) as usize, "8888 size mismatch");
106 let range = vk::ImageSubresourceRange::default()
107 .aspect_mask(vk::ImageAspectFlags::COLOR)
108 .level_count(1)
109 .layer_count(1);
110 unsafe {
111 let image = device
112 .create_image(
113 &vk::ImageCreateInfo::default()
114 .image_type(vk::ImageType::TYPE_2D)
115 .format(vk::Format::R8G8B8A8_SRGB)
116 .extent(vk::Extent3D { width, height, depth: 1 })
117 .mip_levels(1)
118 .array_layers(1)
119 .samples(vk::SampleCountFlags::TYPE_1)
120 .tiling(vk::ImageTiling::OPTIMAL)
121 .usage(vk::ImageUsageFlags::SAMPLED | vk::ImageUsageFlags::TRANSFER_DST)
122 .initial_layout(vk::ImageLayout::UNDEFINED),
123 None,
124 )
125 .expect("Failed to create RT texture");
126 let requirements = device.get_image_memory_requirements(image);
127 let allocation = allocator
128 .allocate(&AllocationCreateDesc {
129 name: "rt-texture",
130 requirements,
131 location: MemoryLocation::GpuOnly,
132 linear: false,
133 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
134 })
135 .expect("Failed to allocate RT texture memory");
136 device
137 .bind_image_memory(image, allocation.memory(), allocation.offset())
138 .expect("Failed to bind RT texture memory");
139 let mut staging = create_cpu_buffer(
140 device,
141 allocator,
142 pixels.len() as vk::DeviceSize,
143 vk::BufferUsageFlags::TRANSFER_SRC,
144 "rt-texture-staging",
145 );
146 staging.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..pixels.len()]
147 .copy_from_slice(pixels);
148
149 let cmd = device
150 .allocate_command_buffers(
151 &vk::CommandBufferAllocateInfo::default()
152 .command_pool(command_pool)
153 .level(vk::CommandBufferLevel::PRIMARY)
154 .command_buffer_count(1),
155 )
156 .expect("Failed to allocate RT texture command buffer")[0];
157 device
158 .begin_command_buffer(
159 cmd,
160 &vk::CommandBufferBeginInfo::default()
161 .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
162 )
163 .unwrap();
164 let barrier = |from_access, to_access, from_layout, to_layout, from_stage, to_stage| {
165 device.cmd_pipeline_barrier(
166 cmd,
167 from_stage,
168 to_stage,
169 vk::DependencyFlags::empty(),
170 &[],
171 &[],
172 &[vk::ImageMemoryBarrier::default()
173 .src_access_mask(from_access)
174 .dst_access_mask(to_access)
175 .old_layout(from_layout)
176 .new_layout(to_layout)
177 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
178 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
179 .image(image)
180 .subresource_range(range)],
181 );
182 };
183 barrier(
184 vk::AccessFlags::empty(),
185 vk::AccessFlags::TRANSFER_WRITE,
186 vk::ImageLayout::UNDEFINED,
187 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
188 vk::PipelineStageFlags::TOP_OF_PIPE,
189 vk::PipelineStageFlags::TRANSFER,
190 );
191 device.cmd_copy_buffer_to_image(
192 cmd,
193 staging.buffer,
194 image,
195 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
196 &[vk::BufferImageCopy::default()
197 .buffer_row_length(width)
198 .buffer_image_height(height)
199 .image_subresource(
200 vk::ImageSubresourceLayers::default()
201 .aspect_mask(vk::ImageAspectFlags::COLOR)
202 .layer_count(1),
203 )
204 .image_extent(vk::Extent3D { width, height, depth: 1 })],
205 );
206 barrier(
207 vk::AccessFlags::TRANSFER_WRITE,
208 vk::AccessFlags::SHADER_READ,
209 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
210 vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
211 vk::PipelineStageFlags::TRANSFER,
212 vk::PipelineStageFlags::COMPUTE_SHADER,
213 );
214 device.end_command_buffer(cmd).unwrap();
215 let cmds = [cmd];
216 device
217 .queue_submit(
218 queue,
219 &[vk::SubmitInfo::default().command_buffers(&cmds)],
220 vk::Fence::null(),
221 )
222 .expect("RT texture upload submit failed");
223 device.queue_wait_idle(queue).expect("RT texture upload wait failed");
224 device.free_command_buffers(command_pool, &cmds);
225 destroy_cpu_buffer(device, allocator, &mut staging);
226
227 let view = device
228 .create_image_view(
229 &vk::ImageViewCreateInfo::default()
230 .image(image)
231 .view_type(vk::ImageViewType::TYPE_2D)
232 .format(vk::Format::R8G8B8A8_SRGB)
233 .subresource_range(range),
234 None,
235 )
236 .expect("Failed to create RT texture view");
237 OwnedTexture { image, view, allocation: Some(allocation), size: (width, height) }
238 }
239 }
240
241 /// Caller must have the device idle.
242 fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
243 unsafe {
244 device.destroy_image_view(self.view, None);
245 device.destroy_image(self.image, None);
246 }
247 if let Some(a) = self.allocation.take() {
248 let _ = allocator.free(a);
249 }
250 }
251 }
252
253 /// The scene's image as the stage holds it.
254 struct StagedImage {
255 /// The 2D pass's image of this id, or None for one the stage owns.
256 shared: Option<u32>,
257 owned: Option<OwnedTexture>,
258 corners: [[f32; 3]; 4],
259 opacity: f32,
260 }
261
262 struct DenoiserFrame {
263 /// DENOISE_ITERATIONS dynamic-offset slices of [`DenoiseParams`].
264 uniforms: AllocatedBuffer,
265 /// src = ping, dst = pong.
266 set_a: vk::DescriptorSet,
267 /// src = pong, dst = ping.
268 set_b: vk::DescriptorSet,
269 }
270
271 /// The à-trous denoise pipeline (rt_denoise.wgsl). Owned by [`RtStage`];
272 /// its buffers (features/ping/pong) live on the stage with the other
273 /// pane-sized targets.
274 struct Denoiser {
275 pipeline: vk::Pipeline,
276 pipeline_layout: vk::PipelineLayout,
277 descriptor_set_layout: vk::DescriptorSetLayout,
278 descriptor_pool: vk::DescriptorPool,
279 shader_module: vk::ShaderModule,
280 uniform_stride: vk::DeviceSize,
281 frames: Vec<DenoiserFrame>,
282 }
283
284 impl Denoiser {
285 fn new(
286 device: &ash::Device,
287 allocator: &mut Allocator,
288 frames_in_flight: usize,
289 min_uniform_align: vk::DeviceSize,
290 ) -> Self {
291 unsafe {
292 let bindings = [
293 vk::DescriptorSetLayoutBinding::default()
294 .binding(0)
295 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
296 .descriptor_count(1)
297 .stage_flags(vk::ShaderStageFlags::COMPUTE),
298 vk::DescriptorSetLayoutBinding::default()
299 .binding(1)
300 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
301 .descriptor_count(1)
302 .stage_flags(vk::ShaderStageFlags::COMPUTE),
303 vk::DescriptorSetLayoutBinding::default()
304 .binding(2)
305 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
306 .descriptor_count(1)
307 .stage_flags(vk::ShaderStageFlags::COMPUTE),
308 vk::DescriptorSetLayoutBinding::default()
309 .binding(3)
310 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
311 .descriptor_count(1)
312 .stage_flags(vk::ShaderStageFlags::COMPUTE),
313 vk::DescriptorSetLayoutBinding::default()
314 .binding(4)
315 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
316 .descriptor_count(1)
317 .stage_flags(vk::ShaderStageFlags::COMPUTE),
318 vk::DescriptorSetLayoutBinding::default()
319 .binding(5)
320 .descriptor_type(vk::DescriptorType::STORAGE_IMAGE)
321 .descriptor_count(1)
322 .stage_flags(vk::ShaderStageFlags::COMPUTE),
323 ];
324 let descriptor_set_layout = device
325 .create_descriptor_set_layout(
326 &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
327 None,
328 )
329 .expect("Failed to create denoise descriptor set layout");
330 let set_layouts_one = [descriptor_set_layout];
331 let pipeline_layout = device
332 .create_pipeline_layout(
333 &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts_one),
334 None,
335 )
336 .expect("Failed to create denoise pipeline layout");
337 let spirv = compile_wgsl(crate::draw::shaders::RT_DENOISE);
338 let shader_module = device
339 .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spirv), None)
340 .expect("Failed to create denoise shader module");
341 let pipeline = device
342 .create_compute_pipelines(
343 vk::PipelineCache::null(),
344 &[vk::ComputePipelineCreateInfo::default()
345 .stage(
346 vk::PipelineShaderStageCreateInfo::default()
347 .stage(vk::ShaderStageFlags::COMPUTE)
348 .module(shader_module)
349 .name(c"cs_denoise"),
350 )
351 .layout(pipeline_layout)],
352 None,
353 )
354 .expect("Failed to create denoise pipeline")[0];
355
356 let n = frames_in_flight as u32;
357 let pool_sizes = [
358 vk::DescriptorPoolSize::default()
359 .ty(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
360 .descriptor_count(2 * n),
361 vk::DescriptorPoolSize::default()
362 .ty(vk::DescriptorType::STORAGE_BUFFER)
363 .descriptor_count(8 * n),
364 vk::DescriptorPoolSize::default()
365 .ty(vk::DescriptorType::STORAGE_IMAGE)
366 .descriptor_count(2 * n),
367 ];
368 let descriptor_pool = device
369 .create_descriptor_pool(
370 &vk::DescriptorPoolCreateInfo::default()
371 .max_sets(2 * n)
372 .pool_sizes(&pool_sizes),
373 None,
374 )
375 .expect("Failed to create denoise descriptor pool");
376 let set_layouts: Vec<vk::DescriptorSetLayout> =
377 vec![descriptor_set_layout; frames_in_flight * 2];
378 let sets = device
379 .allocate_descriptor_sets(
380 &vk::DescriptorSetAllocateInfo::default()
381 .descriptor_pool(descriptor_pool)
382 .set_layouts(&set_layouts),
383 )
384 .expect("Failed to allocate denoise descriptor sets");
385
386 let uniform_stride = (std::mem::size_of::<DenoiseParams>() as vk::DeviceSize)
387 .next_multiple_of(min_uniform_align.max(1));
388 let frames: Vec<DenoiserFrame> = (0..frames_in_flight)
389 .map(|i| {
390 let uniforms = create_cpu_buffer(
391 device,
392 allocator,
393 uniform_stride * DENOISE_ITERATIONS as vk::DeviceSize,
394 vk::BufferUsageFlags::UNIFORM_BUFFER,
395 "rt-denoise-uniforms",
396 );
397 let (set_a, set_b) = (sets[2 * i], sets[2 * i + 1]);
398 for set in [set_a, set_b] {
399 let infos = [vk::DescriptorBufferInfo::default()
400 .buffer(uniforms.buffer)
401 .range(std::mem::size_of::<DenoiseParams>() as vk::DeviceSize)];
402 device.update_descriptor_sets(
403 &[vk::WriteDescriptorSet::default()
404 .dst_set(set)
405 .dst_binding(0)
406 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC)
407 .buffer_info(&infos)],
408 &[],
409 );
410 }
411 DenoiserFrame { uniforms, set_a, set_b }
412 })
413 .collect();
414
415 Denoiser {
416 pipeline,
417 pipeline_layout,
418 descriptor_set_layout,
419 descriptor_pool,
420 shader_module,
421 uniform_stride,
422 frames,
423 }
424 }
425 }
426
427 /// Re-point the per-target bindings after the pane-sized buffers are
428 /// (re)created. Device is idle (target recreation contract).
429 fn write_target_descriptors(
430 &self,
431 device: &ash::Device,
432 accum: vk::Buffer,
433 features: vk::Buffer,
434 ping: vk::Buffer,
435 pong: vk::Buffer,
436 output_view: vk::ImageView,
437 ) {
438 for frame in &self.frames {
439 for (set, src, dst) in
440 [(frame.set_a, ping, pong), (frame.set_b, pong, ping)]
441 {
442 let buf_infos = [
443 vk::DescriptorBufferInfo::default().buffer(accum).range(vk::WHOLE_SIZE),
444 vk::DescriptorBufferInfo::default().buffer(features).range(vk::WHOLE_SIZE),
445 vk::DescriptorBufferInfo::default().buffer(src).range(vk::WHOLE_SIZE),
446 vk::DescriptorBufferInfo::default().buffer(dst).range(vk::WHOLE_SIZE),
447 ];
448 let image_infos = [vk::DescriptorImageInfo::default()
449 .image_view(output_view)
450 .image_layout(vk::ImageLayout::GENERAL)];
451 let writes: Vec<vk::WriteDescriptorSet> = buf_infos
452 .iter()
453 .enumerate()
454 .map(|(i, info)| {
455 vk::WriteDescriptorSet::default()
456 .dst_set(set)
457 .dst_binding(1 + i as u32)
458 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
459 .buffer_info(std::slice::from_ref(info))
460 })
461 .chain(std::iter::once(
462 vk::WriteDescriptorSet::default()
463 .dst_set(set)
464 .dst_binding(5)
465 .descriptor_type(vk::DescriptorType::STORAGE_IMAGE)
466 .image_info(&image_infos),
467 ))
468 .collect();
469 unsafe { device.update_descriptor_sets(&writes, &[]) };
470 }
471 }
472 }
473
474 /// After the frame fence: the per-iteration params. `n_after` is the
475 /// sample count the accumulation will hold once this frame's dispatch
476 /// lands — the color sigma tightens as it grows.
477 fn write_frame_uniforms(&mut self, frame_index: usize, width: u32, height: u32, n_after: u32) {
478 let frame = &mut self.frames[frame_index];
479 let mapped = frame.uniforms.allocation.as_mut().unwrap().mapped_slice_mut().unwrap();
480 for (i, params) in denoise_params(width, height, n_after).iter().enumerate() {
481 let offset = self.uniform_stride as usize * i;
482 mapped[offset..offset + std::mem::size_of::<DenoiseParams>()]
483 .copy_from_slice(bytemuck::bytes_of(params));
484 }
485 }
486
487 /// Record the à-trous iterations. The tracer's dispatch has already run
488 /// in this command buffer; the last iteration rewrites `out_img` (still
489 /// in GENERAL). Iteration parity: 0 → set_b (writes ping), 1 → set_a,
490 /// 2 → set_b.
491 fn record(&self, device: &ash::Device, cmd: vk::CommandBuffer, frame_index: usize, w: u32, h: u32) {
492 let frame = &self.frames[frame_index];
493 unsafe {
494 device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::COMPUTE, self.pipeline);
495 for i in 0..DENOISE_ITERATIONS {
496 // Order this iteration's reads after the previous compute
497 // writes (tracer or prior iteration).
498 device.cmd_pipeline_barrier(
499 cmd,
500 vk::PipelineStageFlags::COMPUTE_SHADER,
501 vk::PipelineStageFlags::COMPUTE_SHADER,
502 vk::DependencyFlags::empty(),
503 &[vk::MemoryBarrier::default()
504 .src_access_mask(vk::AccessFlags::SHADER_WRITE)
505 .dst_access_mask(
506 vk::AccessFlags::SHADER_READ | vk::AccessFlags::SHADER_WRITE,
507 )],
508 &[],
509 &[],
510 );
511 let set = if i % 2 == 0 { frame.set_b } else { frame.set_a };
512 device.cmd_bind_descriptor_sets(
513 cmd,
514 vk::PipelineBindPoint::COMPUTE,
515 self.pipeline_layout,
516 0,
517 &[set],
518 &[(self.uniform_stride as u32) * i as u32],
519 );
520 device.cmd_dispatch(cmd, w.div_ceil(WORKGROUP), h.div_ceil(WORKGROUP), 1);
521 }
522 }
523 }
524
525 fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
526 unsafe {
527 for frame in &mut self.frames {
528 let mut uniforms = std::mem::replace(&mut frame.uniforms, AllocatedBuffer::null());
529 destroy_cpu_buffer(device, allocator, &mut uniforms);
530 }
531 device.destroy_descriptor_pool(self.descriptor_pool, None);
532 device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
533 device.destroy_pipeline(self.pipeline, None);
534 device.destroy_pipeline_layout(self.pipeline_layout, None);
535 device.destroy_shader_module(self.shader_module, None);
536 }
537 }
538 }
539
540 pub(crate) struct RtStage {
541 tier: RtTier,
542 accel_loader: Option<ash::khr::acceleration_structure::Device>,
543 as_scratch_align: vk::DeviceSize,
544 accel: Option<Accel>,
545
546 pipeline: vk::Pipeline,
547 pipeline_layout: vk::PipelineLayout,
548 descriptor_set_layout: vk::DescriptorSetLayout,
549 descriptor_pool: vk::DescriptorPool,
550 shader_module: vk::ShaderModule,
551 frames: Vec<RtFrame>,
552
553 nodes: AllocatedBuffer,
554 tris: AllocatedBuffer,
555 materials: AllocatedBuffer,
556 tri_count: u32,
557
558 /// Bound at the image bindings while the scene has no image, or has one
559 /// whose upload has not landed: a shader's bindings are never empty.
560 stand_in: OwnedTexture,
561 image_sampler: vk::Sampler,
562 image: Option<StagedImage>,
563
564 accum: AllocatedBuffer,
565 /// Primary-hit features (2 vec4 per pixel) written by the tracer, read
566 /// by the denoiser.
567 features: AllocatedBuffer,
568 /// À-trous ping-pong color buffers (1 vec4 per pixel each).
569 ping: AllocatedBuffer,
570 pong: AllocatedBuffer,
571 denoiser: Option<Denoiser>,
572 output_image: vk::Image,
573 output_view: vk::ImageView,
574 output_allocation: Option<Allocation>,
575 output_size: (u32, u32),
576 output_initialized: bool,
577
578 pane: (u32, u32, u32, u32),
579 pane_moved: bool,
580 camera: Option<RtCamera>,
581 sample_index: u32,
582 /// Samples per dispatch: 1 interactive, higher for offscreen rendering.
583 spp: u32,
584 staged: bool,
585 /// What a camera ray that meets nothing shows, linear RGB; None is the
586 /// sky. See [`RtStage::set_background`].
587 background: Option<[f32; 3]>,
588 /// The sky and sun. See [`RtStage::set_environment`].
589 environment: RtEnvironment,
590 }
591
592 impl RtStage {
593 /// `accel_loader` present means the device has the ray-query stack; the
594 /// stage then runs tier 2 unless `CCE_VK_RT=compute` forces the BVH tier.
595 pub(crate) fn new(
596 device: &ash::Device,
597 allocator: &mut Allocator,
598 frames_in_flight: usize,
599 accel_loader: Option<&ash::khr::acceleration_structure::Device>,
600 as_scratch_align: vk::DeviceSize,
601 min_uniform_align: vk::DeviceSize,
602 queue: vk::Queue,
603 command_pool: vk::CommandPool,
604 ) -> Self {
605 let denoise_on = !std::env::var("CCE_VK_RT_DENOISE")
606 .is_ok_and(|v| v == "off" || v == "0" || v == "false");
607 let tier = RtTier::choose(accel_loader.is_some());
608 log::info!(
609 "RT stage: {} tier",
610 match tier {
611 RtTier::Compute => "compute (BVH)",
612 RtTier::RayQuery => "ray-query (hardware)",
613 }
614 );
615 let binding1_type = match tier {
616 RtTier::Compute => vk::DescriptorType::STORAGE_BUFFER,
617 RtTier::RayQuery => vk::DescriptorType::ACCELERATION_STRUCTURE_KHR,
618 };
619 unsafe {
620 let bindings = [
621 vk::DescriptorSetLayoutBinding::default()
622 .binding(0)
623 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
624 .descriptor_count(1)
625 .stage_flags(vk::ShaderStageFlags::COMPUTE),
626 vk::DescriptorSetLayoutBinding::default()
627 .binding(1)
628 .descriptor_type(binding1_type)
629 .descriptor_count(1)
630 .stage_flags(vk::ShaderStageFlags::COMPUTE),
631 vk::DescriptorSetLayoutBinding::default()
632 .binding(2)
633 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
634 .descriptor_count(1)
635 .stage_flags(vk::ShaderStageFlags::COMPUTE),
636 vk::DescriptorSetLayoutBinding::default()
637 .binding(3)
638 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
639 .descriptor_count(1)
640 .stage_flags(vk::ShaderStageFlags::COMPUTE),
641 vk::DescriptorSetLayoutBinding::default()
642 .binding(4)
643 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
644 .descriptor_count(1)
645 .stage_flags(vk::ShaderStageFlags::COMPUTE),
646 vk::DescriptorSetLayoutBinding::default()
647 .binding(5)
648 .descriptor_type(vk::DescriptorType::STORAGE_IMAGE)
649 .descriptor_count(1)
650 .stage_flags(vk::ShaderStageFlags::COMPUTE),
651 vk::DescriptorSetLayoutBinding::default()
652 .binding(6)
653 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
654 .descriptor_count(1)
655 .stage_flags(vk::ShaderStageFlags::COMPUTE),
656 vk::DescriptorSetLayoutBinding::default()
657 .binding(7)
658 .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
659 .descriptor_count(1)
660 .stage_flags(vk::ShaderStageFlags::COMPUTE),
661 vk::DescriptorSetLayoutBinding::default()
662 .binding(8)
663 .descriptor_type(vk::DescriptorType::SAMPLER)
664 .descriptor_count(1)
665 .stage_flags(vk::ShaderStageFlags::COMPUTE),
666 ];
667 let descriptor_set_layout = device
668 .create_descriptor_set_layout(
669 &vk::DescriptorSetLayoutCreateInfo::default().bindings(&bindings),
670 None,
671 )
672 .expect("Failed to create RT descriptor set layout");
673 let set_layouts_one = [descriptor_set_layout];
674 let pipeline_layout = device
675 .create_pipeline_layout(
676 &vk::PipelineLayoutCreateInfo::default().set_layouts(&set_layouts_one),
677 None,
678 )
679 .expect("Failed to create RT pipeline layout");
680
681 let spirv = match tier {
682 RtTier::Compute => compile_wgsl(&crate::draw::shaders::rt_bvh_source()),
683 RtTier::RayQuery => compile_wgsl_ray_query(&format!("{}\n{}", crate::draw::shaders::RT_COMMON, crate::draw::shaders::RT_QUERY)),
684 };
685 let shader_module = device
686 .create_shader_module(&vk::ShaderModuleCreateInfo::default().code(&spirv), None)
687 .expect("Failed to create RT shader module");
688 let pipeline = device
689 .create_compute_pipelines(
690 vk::PipelineCache::null(),
691 &[vk::ComputePipelineCreateInfo::default()
692 .stage(
693 vk::PipelineShaderStageCreateInfo::default()
694 .stage(vk::ShaderStageFlags::COMPUTE)
695 .module(shader_module)
696 .name(c"cs_main"),
697 )
698 .layout(pipeline_layout)],
699 None,
700 )
701 .expect("Failed to create RT compute pipeline")[0];
702
703 let n = frames_in_flight as u32;
704 let mut pool_sizes = vec![
705 vk::DescriptorPoolSize::default()
706 .ty(vk::DescriptorType::UNIFORM_BUFFER)
707 .descriptor_count(n),
708 vk::DescriptorPoolSize::default()
709 .ty(vk::DescriptorType::STORAGE_BUFFER)
710 .descriptor_count(5 * n),
711 vk::DescriptorPoolSize::default()
712 .ty(vk::DescriptorType::STORAGE_IMAGE)
713 .descriptor_count(n),
714 vk::DescriptorPoolSize::default()
715 .ty(vk::DescriptorType::SAMPLED_IMAGE)
716 .descriptor_count(n),
717 vk::DescriptorPoolSize::default()
718 .ty(vk::DescriptorType::SAMPLER)
719 .descriptor_count(n),
720 ];
721 if tier == RtTier::RayQuery {
722 pool_sizes.push(
723 vk::DescriptorPoolSize::default()
724 .ty(vk::DescriptorType::ACCELERATION_STRUCTURE_KHR)
725 .descriptor_count(n),
726 );
727 }
728 let descriptor_pool = device
729 .create_descriptor_pool(
730 &vk::DescriptorPoolCreateInfo::default()
731 .max_sets(n)
732 .pool_sizes(&pool_sizes),
733 None,
734 )
735 .expect("Failed to create RT descriptor pool");
736 let set_layouts: Vec<vk::DescriptorSetLayout> =
737 vec![descriptor_set_layout; frames_in_flight];
738 let sets = device
739 .allocate_descriptor_sets(
740 &vk::DescriptorSetAllocateInfo::default()
741 .descriptor_pool(descriptor_pool)
742 .set_layouts(&set_layouts),
743 )
744 .expect("Failed to allocate RT descriptor sets");
745 let frames: Vec<RtFrame> = sets
746 .into_iter()
747 .map(|descriptor_set| {
748 let uniforms = create_cpu_buffer(
749 device,
750 allocator,
751 std::mem::size_of::<RtParams>() as vk::DeviceSize,
752 vk::BufferUsageFlags::UNIFORM_BUFFER,
753 "rt-uniforms",
754 );
755 let buffer_infos = [vk::DescriptorBufferInfo::default()
756 .buffer(uniforms.buffer)
757 .offset(0)
758 .range(std::mem::size_of::<RtParams>() as vk::DeviceSize)];
759 device.update_descriptor_sets(
760 &[vk::WriteDescriptorSet::default()
761 .dst_set(descriptor_set)
762 .dst_binding(0)
763 .descriptor_type(vk::DescriptorType::UNIFORM_BUFFER)
764 .buffer_info(&buffer_infos)],
765 &[],
766 );
767 RtFrame { uniforms, descriptor_set }
768 })
769 .collect();
770
771 let denoiser = denoise_on
772 .then(|| Denoiser::new(device, allocator, frames_in_flight, min_uniform_align));
773
774 let stand_in =
775 OwnedTexture::new(device, allocator, queue, command_pool, &[255; 4], 1, 1);
776 // Linear within a level and between levels; the shader names
777 // the level, since a compute shader has no derivatives to
778 // choose one by.
779 let image_sampler = device
780 .create_sampler(
781 &vk::SamplerCreateInfo::default()
782 .mag_filter(vk::Filter::LINEAR)
783 .min_filter(vk::Filter::LINEAR)
784 .mipmap_mode(vk::SamplerMipmapMode::LINEAR)
785 .min_lod(0.0)
786 .max_lod(vk::LOD_CLAMP_NONE)
787 .address_mode_u(vk::SamplerAddressMode::CLAMP_TO_EDGE)
788 .address_mode_v(vk::SamplerAddressMode::CLAMP_TO_EDGE)
789 .address_mode_w(vk::SamplerAddressMode::CLAMP_TO_EDGE),
790 None,
791 )
792 .expect("Failed to create RT image sampler");
793 for frame in &frames {
794 Self::write_image_descriptor(
795 device,
796 frame.descriptor_set,
797 stand_in.view,
798 image_sampler,
799 );
800 }
801
802 RtStage {
803 tier,
804 accel_loader: accel_loader.cloned(),
805 as_scratch_align,
806 accel: None,
807 pipeline,
808 pipeline_layout,
809 descriptor_set_layout,
810 descriptor_pool,
811 shader_module,
812 frames,
813 nodes: AllocatedBuffer::null(),
814 tris: AllocatedBuffer::null(),
815 materials: AllocatedBuffer::null(),
816 tri_count: 0,
817 stand_in,
818 image_sampler,
819 image: None,
820 accum: AllocatedBuffer::null(),
821 features: AllocatedBuffer::null(),
822 ping: AllocatedBuffer::null(),
823 pong: AllocatedBuffer::null(),
824 denoiser,
825 output_image: vk::Image::null(),
826 output_view: vk::ImageView::null(),
827 output_allocation: None,
828 output_size: (0, 0),
829 output_initialized: false,
830 pane: (0, 0, 0, 0),
831 pane_moved: false,
832 camera: None,
833 sample_index: 0,
834 spp: 1,
835 staged: false,
836 background: None,
837 environment: RtEnvironment::default(),
838 }
839 }
840 }
841
842 /// What a camera ray that meets nothing shows: a colour (linear RGB),
843 /// or None for the sky. The sky stays the light either way — a bounce
844 /// that leaves the scene still meets it — so this is the backdrop and
845 /// not the lighting, as an app's raster background colour is. A change
846 /// restarts the accumulation.
847 pub(crate) fn set_background(&mut self, background: Option<[f32; 3]>) {
848 if self.background != background {
849 self.background = background;
850 self.sample_index = 0;
851 }
852 }
853
854 /// The sky and sun the scene is lit by. A change restarts the
855 /// accumulation.
856 pub(crate) fn set_environment(&mut self, environment: RtEnvironment) {
857 if self.environment != environment {
858 self.environment = environment;
859 self.sample_index = 0;
860 }
861 }
862
863 fn write_image_descriptor(
864 device: &ash::Device,
865 set: vk::DescriptorSet,
866 view: vk::ImageView,
867 sampler: vk::Sampler,
868 ) {
869 let image_infos = [vk::DescriptorImageInfo::default()
870 .image_view(view)
871 .image_layout(vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL)];
872 let sampler_infos = [vk::DescriptorImageInfo::default().sampler(sampler)];
873 unsafe {
874 device.update_descriptor_sets(
875 &[
876 vk::WriteDescriptorSet::default()
877 .dst_set(set)
878 .dst_binding(7)
879 .descriptor_type(vk::DescriptorType::SAMPLED_IMAGE)
880 .image_info(&image_infos),
881 vk::WriteDescriptorSet::default()
882 .dst_set(set)
883 .dst_binding(8)
884 .descriptor_type(vk::DescriptorType::SAMPLER)
885 .image_info(&sampler_infos),
886 ],
887 &[],
888 );
889 }
890 }
891
892 /// Whether this stage traverses a CPU-built BVH (tier 1) rather than
893 /// building driver acceleration structures (tier 2).
894 pub(crate) fn needs_bvh(&self) -> bool {
895 self.tier == RtTier::Compute
896 }
897
898 /// Replace the scene with one already packed (`draw::rt::pack_scene`,
899 /// whose image quad must be the one `image` describes). Tier 1 uploads
900 /// its BVH, building it here only if it was packed without one; tier 2
901 /// builds driver acceleration structures on the given queue instead,
902 /// ignoring any BVH. Caller must have the device idle.
903 pub(crate) fn set_scene(
904 &mut self,
905 device: &ash::Device,
906 allocator: &mut Allocator,
907 queue: vk::Queue,
908 command_pool: vk::CommandPool,
909 scene: &PackedScene,
910 image: Option<(RtImageSource, [[f32; 3]; 4], f32)>,
911 ) {
912 if let Some(mut old) = self.image.take().and_then(|i| i.owned) {
913 old.destroy(device, allocator);
914 }
915 self.image = image.map(|(source, corners, opacity)| match source {
916 RtImageSource::Shared(id) => {
917 StagedImage { shared: Some(id), owned: None, corners, opacity }
918 }
919 RtImageSource::Pixels { pixels, width, height } => StagedImage {
920 shared: None,
921 owned: Some(OwnedTexture::new(
922 device,
923 allocator,
924 queue,
925 command_pool,
926 pixels,
927 width,
928 height,
929 )),
930 corners,
931 opacity,
932 },
933 });
934 let scene = match self.tier {
935 RtTier::Compute => scene.with_bvh(),
936 RtTier::RayQuery => std::borrow::Cow::Borrowed(scene),
937 };
938 let (gpu_tris, gpu_mats, nodes) = (&scene.tris, &scene.materials, &scene.nodes);
939
940 self.destroy_accel(device, allocator);
941 for buf in [&mut self.nodes, &mut self.tris, &mut self.materials] {
942 destroy_cpu_buffer(device, allocator, buf);
943 }
944 let upload = |allocator: &mut Allocator,
945 bytes: &[u8],
946 usage: vk::BufferUsageFlags,
947 name: &str|
948 -> AllocatedBuffer {
949 let mut buf = create_cpu_buffer(
950 device,
951 allocator,
952 (bytes.len() as vk::DeviceSize).max(64),
953 usage,
954 name,
955 );
956 if !bytes.is_empty() {
957 buf.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()[..bytes.len()]
958 .copy_from_slice(bytes);
959 }
960 buf
961 };
962 // Tier 2 reads the same triangle buffer as BLAS build input (the
963 // shading data still comes through the storage binding).
964 let tri_usage = match self.tier {
965 RtTier::Compute => vk::BufferUsageFlags::STORAGE_BUFFER,
966 RtTier::RayQuery => {
967 vk::BufferUsageFlags::STORAGE_BUFFER
968 | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS
969 | vk::BufferUsageFlags::ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_KHR
970 }
971 };
972 self.tris = upload(allocator, bytemuck::cast_slice(gpu_tris), tri_usage, "rt-tris");
973 self.materials = upload(
974 allocator,
975 bytemuck::cast_slice(gpu_mats),
976 vk::BufferUsageFlags::STORAGE_BUFFER,
977 "rt-materials",
978 );
979 self.tri_count = gpu_tris.len() as u32;
980 self.sample_index = 0;
981
982 // Binding 1 (per tier), then the shared 2/3.
983 match self.tier {
984 RtTier::Compute => {
985 self.nodes = upload(
986 allocator,
987 bytemuck::cast_slice(nodes),
988 vk::BufferUsageFlags::STORAGE_BUFFER,
989 "rt-nodes",
990 );
991 for frame in &self.frames {
992 let infos = [vk::DescriptorBufferInfo::default()
993 .buffer(self.nodes.buffer)
994 .range(vk::WHOLE_SIZE)];
995 unsafe {
996 device.update_descriptor_sets(
997 &[vk::WriteDescriptorSet::default()
998 .dst_set(frame.descriptor_set)
999 .dst_binding(1)
1000 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
1001 .buffer_info(&infos)],
1002 &[],
1003 );
1004 }
1005 }
1006 }
1007 RtTier::RayQuery => {
1008 if self.tri_count > 0 {
1009 self.build_accel(device, allocator, queue, command_pool);
1010 let accel = self.accel.as_ref().unwrap();
1011 let handles = [accel.tlas];
1012 for frame in &self.frames {
1013 let mut as_info =
1014 vk::WriteDescriptorSetAccelerationStructureKHR::default()
1015 .acceleration_structures(&handles);
1016 let mut write = vk::WriteDescriptorSet::default()
1017 .dst_set(frame.descriptor_set)
1018 .dst_binding(1)
1019 .descriptor_type(vk::DescriptorType::ACCELERATION_STRUCTURE_KHR)
1020 .push_next(&mut as_info);
1021 write.descriptor_count = 1;
1022 unsafe { device.update_descriptor_sets(&[write], &[]) };
1023 }
1024 }
1025 }
1026 }
1027
1028 for frame in &self.frames {
1029 let infos = [
1030 vk::DescriptorBufferInfo::default().buffer(self.tris.buffer).range(vk::WHOLE_SIZE),
1031 vk::DescriptorBufferInfo::default()
1032 .buffer(self.materials.buffer)
1033 .range(vk::WHOLE_SIZE),
1034 ];
1035 let writes: Vec<vk::WriteDescriptorSet> = infos
1036 .iter()
1037 .enumerate()
1038 .map(|(i, info)| {
1039 vk::WriteDescriptorSet::default()
1040 .dst_set(frame.descriptor_set)
1041 .dst_binding(2 + i as u32)
1042 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
1043 .buffer_info(std::slice::from_ref(info))
1044 })
1045 .collect();
1046 unsafe { device.update_descriptor_sets(&writes, &[]) };
1047 }
1048 }
1049
1050 /// Build the BLAS (over `self.tris`, opaque triangles) and a one-instance
1051 /// TLAS, on the given queue with a blocking one-time submit. Device is
1052 /// idle (set_scene contract), so replacing old structures is safe.
1053 fn build_accel(
1054 &mut self,
1055 device: &ash::Device,
1056 allocator: &mut Allocator,
1057 queue: vk::Queue,
1058 command_pool: vk::CommandPool,
1059 ) {
1060 let loader = self.accel_loader.clone().expect("tier 2 without accel loader");
1061 let create_as_buffer = |allocator: &mut Allocator,
1062 size: vk::DeviceSize,
1063 usage: vk::BufferUsageFlags,
1064 name: &str|
1065 -> AllocatedBuffer {
1066 unsafe {
1067 let buffer = device
1068 .create_buffer(
1069 &vk::BufferCreateInfo::default()
1070 .size(size)
1071 .usage(usage | vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS)
1072 .sharing_mode(vk::SharingMode::EXCLUSIVE),
1073 None,
1074 )
1075 .expect("Failed to create AS buffer");
1076 let requirements = device.get_buffer_memory_requirements(buffer);
1077 let allocation = allocator
1078 .allocate(&AllocationCreateDesc {
1079 name,
1080 requirements,
1081 location: MemoryLocation::GpuOnly,
1082 linear: true,
1083 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
1084 })
1085 .expect("Failed to allocate AS memory");
1086 device
1087 .bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
1088 .expect("Failed to bind AS memory");
1089 AllocatedBuffer { buffer, allocation: Some(allocation), size }
1090 }
1091 };
1092 let addr_of = |buffer: vk::Buffer| unsafe {
1093 device.get_buffer_device_address(&vk::BufferDeviceAddressInfo::default().buffer(buffer))
1094 };
1095
1096 unsafe {
1097 // --- BLAS over the triangle buffer (stride 16: p0/p1/p2 vec4s).
1098 let tri_addr = addr_of(self.tris.buffer);
1099 let blas_geometry = vk::AccelerationStructureGeometryKHR::default()
1100 .geometry_type(vk::GeometryTypeKHR::TRIANGLES)
1101 .flags(vk::GeometryFlagsKHR::OPAQUE)
1102 .geometry(vk::AccelerationStructureGeometryDataKHR {
1103 triangles: vk::AccelerationStructureGeometryTrianglesDataKHR::default()
1104 .vertex_format(vk::Format::R32G32B32_SFLOAT)
1105 .vertex_data(vk::DeviceOrHostAddressConstKHR { device_address: tri_addr })
1106 .vertex_stride(16)
1107 .max_vertex(self.tri_count * 3 - 1)
1108 .index_type(vk::IndexType::NONE_KHR),
1109 });
1110 let blas_geometries = [blas_geometry];
1111 let mut blas_build = vk::AccelerationStructureBuildGeometryInfoKHR::default()
1112 .ty(vk::AccelerationStructureTypeKHR::BOTTOM_LEVEL)
1113 .flags(vk::BuildAccelerationStructureFlagsKHR::PREFER_FAST_TRACE)
1114 .mode(vk::BuildAccelerationStructureModeKHR::BUILD)
1115 .geometries(&blas_geometries);
1116 let blas_sizes = {
1117 let mut sizes = vk::AccelerationStructureBuildSizesInfoKHR::default();
1118 loader.get_acceleration_structure_build_sizes(
1119 vk::AccelerationStructureBuildTypeKHR::DEVICE,
1120 &blas_build,
1121 &[self.tri_count],
1122 &mut sizes,
1123 );
1124 sizes
1125 };
1126 let blas_buffer = create_as_buffer(
1127 allocator,
1128 blas_sizes.acceleration_structure_size,
1129 vk::BufferUsageFlags::ACCELERATION_STRUCTURE_STORAGE_KHR,
1130 "rt-blas",
1131 );
1132 let blas = loader
1133 .create_acceleration_structure(
1134 &vk::AccelerationStructureCreateInfoKHR::default()
1135 .buffer(blas_buffer.buffer)
1136 .size(blas_sizes.acceleration_structure_size)
1137 .ty(vk::AccelerationStructureTypeKHR::BOTTOM_LEVEL),
1138 None,
1139 )
1140 .expect("Failed to create BLAS");
1141
1142 // --- One-instance TLAS.
1143 let blas_addr = loader.get_acceleration_structure_device_address(
1144 &vk::AccelerationStructureDeviceAddressInfoKHR::default()
1145 .acceleration_structure(blas),
1146 );
1147 let instance = vk::AccelerationStructureInstanceKHR {
1148 transform: vk::TransformMatrixKHR {
1149 matrix: [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0],
1150 },
1151 instance_custom_index_and_mask: vk::Packed24_8::new(0, 0xff),
1152 instance_shader_binding_table_record_offset_and_flags: vk::Packed24_8::new(0, 0),
1153 acceleration_structure_reference: vk::AccelerationStructureReferenceKHR {
1154 device_handle: blas_addr,
1155 },
1156 };
1157 let instance_bytes = std::slice::from_raw_parts(
1158 (&instance as *const vk::AccelerationStructureInstanceKHR).cast::<u8>(),
1159 std::mem::size_of::<vk::AccelerationStructureInstanceKHR>(),
1160 );
1161 let mut instances = create_cpu_buffer(
1162 device,
1163 allocator,
1164 instance_bytes.len() as vk::DeviceSize,
1165 vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS
1166 | vk::BufferUsageFlags::ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_KHR,
1167 "rt-tlas-instances",
1168 );
1169 instances.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
1170 [..instance_bytes.len()]
1171 .copy_from_slice(instance_bytes);
1172
1173 let tlas_geometry = vk::AccelerationStructureGeometryKHR::default()
1174 .geometry_type(vk::GeometryTypeKHR::INSTANCES)
1175 .geometry(vk::AccelerationStructureGeometryDataKHR {
1176 instances: vk::AccelerationStructureGeometryInstancesDataKHR::default()
1177 .array_of_pointers(false)
1178 .data(vk::DeviceOrHostAddressConstKHR {
1179 device_address: addr_of(instances.buffer),
1180 }),
1181 });
1182 let tlas_geometries = [tlas_geometry];
1183 let mut tlas_build = vk::AccelerationStructureBuildGeometryInfoKHR::default()
1184 .ty(vk::AccelerationStructureTypeKHR::TOP_LEVEL)
1185 .flags(vk::BuildAccelerationStructureFlagsKHR::PREFER_FAST_TRACE)
1186 .mode(vk::BuildAccelerationStructureModeKHR::BUILD)
1187 .geometries(&tlas_geometries);
1188 let tlas_sizes = {
1189 let mut sizes = vk::AccelerationStructureBuildSizesInfoKHR::default();
1190 loader.get_acceleration_structure_build_sizes(
1191 vk::AccelerationStructureBuildTypeKHR::DEVICE,
1192 &tlas_build,
1193 &[1],
1194 &mut sizes,
1195 );
1196 sizes
1197 };
1198 let tlas_buffer = create_as_buffer(
1199 allocator,
1200 tlas_sizes.acceleration_structure_size,
1201 vk::BufferUsageFlags::ACCELERATION_STRUCTURE_STORAGE_KHR,
1202 "rt-tlas",
1203 );
1204 let tlas = loader
1205 .create_acceleration_structure(
1206 &vk::AccelerationStructureCreateInfoKHR::default()
1207 .buffer(tlas_buffer.buffer)
1208 .size(tlas_sizes.acceleration_structure_size)
1209 .ty(vk::AccelerationStructureTypeKHR::TOP_LEVEL),
1210 None,
1211 )
1212 .expect("Failed to create TLAS");
1213
1214 // Shared scratch, aligned to the device's scratch requirement
1215 // (buffer device addresses only guarantee allocation alignment).
1216 let scratch_size =
1217 blas_sizes.build_scratch_size.max(tlas_sizes.build_scratch_size);
1218 let mut scratch = create_as_buffer(
1219 allocator,
1220 scratch_size + self.as_scratch_align,
1221 vk::BufferUsageFlags::STORAGE_BUFFER,
1222 "rt-as-scratch",
1223 );
1224 let scratch_addr =
1225 addr_of(scratch.buffer).next_multiple_of(self.as_scratch_align.max(1));
1226
1227 blas_build = blas_build
1228 .dst_acceleration_structure(blas)
1229 .scratch_data(vk::DeviceOrHostAddressKHR { device_address: scratch_addr });
1230 tlas_build = tlas_build
1231 .dst_acceleration_structure(tlas)
1232 .scratch_data(vk::DeviceOrHostAddressKHR { device_address: scratch_addr });
1233
1234 // One-time submit: BLAS build → barrier → TLAS build.
1235 let cmd = device
1236 .allocate_command_buffers(
1237 &vk::CommandBufferAllocateInfo::default()
1238 .command_pool(command_pool)
1239 .level(vk::CommandBufferLevel::PRIMARY)
1240 .command_buffer_count(1),
1241 )
1242 .expect("Failed to allocate AS build command buffer")[0];
1243 device
1244 .begin_command_buffer(
1245 cmd,
1246 &vk::CommandBufferBeginInfo::default()
1247 .flags(vk::CommandBufferUsageFlags::ONE_TIME_SUBMIT),
1248 )
1249 .unwrap();
1250 let blas_range = [vk::AccelerationStructureBuildRangeInfoKHR::default()
1251 .primitive_count(self.tri_count)];
1252 loader.cmd_build_acceleration_structures(cmd, &[blas_build], &[&blas_range]);
1253 device.cmd_pipeline_barrier(
1254 cmd,
1255 vk::PipelineStageFlags::ACCELERATION_STRUCTURE_BUILD_KHR,
1256 vk::PipelineStageFlags::ACCELERATION_STRUCTURE_BUILD_KHR,
1257 vk::DependencyFlags::empty(),
1258 &[vk::MemoryBarrier::default()
1259 .src_access_mask(vk::AccessFlags::ACCELERATION_STRUCTURE_WRITE_KHR)
1260 .dst_access_mask(
1261 vk::AccessFlags::ACCELERATION_STRUCTURE_READ_KHR
1262 | vk::AccessFlags::ACCELERATION_STRUCTURE_WRITE_KHR,
1263 )],
1264 &[],
1265 &[],
1266 );
1267 let tlas_range =
1268 [vk::AccelerationStructureBuildRangeInfoKHR::default().primitive_count(1)];
1269 loader.cmd_build_acceleration_structures(cmd, &[tlas_build], &[&tlas_range]);
1270 device.end_command_buffer(cmd).unwrap();
1271 let cmds = [cmd];
1272 device
1273 .queue_submit(
1274 queue,
1275 &[vk::SubmitInfo::default().command_buffers(&cmds)],
1276 vk::Fence::null(),
1277 )
1278 .expect("AS build submit failed");
1279 let _ = device.queue_wait_idle(queue);
1280 device.free_command_buffers(command_pool, &cmds);
1281 destroy_cpu_buffer(device, allocator, &mut scratch);
1282
1283 self.accel = Some(Accel { blas, blas_buffer, tlas, tlas_buffer, instances });
1284 }
1285 }
1286
1287 fn destroy_accel(&mut self, device: &ash::Device, allocator: &mut Allocator) {
1288 if let Some(mut accel) = self.accel.take() {
1289 let loader = self.accel_loader.as_ref().expect("accel without loader");
1290 unsafe {
1291 loader.destroy_acceleration_structure(accel.tlas, None);
1292 loader.destroy_acceleration_structure(accel.blas, None);
1293 }
1294 destroy_cpu_buffer(device, allocator, &mut accel.tlas_buffer);
1295 destroy_cpu_buffer(device, allocator, &mut accel.blas_buffer);
1296 destroy_cpu_buffer(device, allocator, &mut accel.instances);
1297 }
1298 }
1299
1300 /// Stage an RT frame for the viewport pane (physical pixels). Recreates the
1301 /// pane-sized targets on size change (waits for device idle) and resets the
1302 /// accumulation when the camera, size, or scene changed.
1303 pub(crate) fn stage(
1304 &mut self,
1305 device: &ash::Device,
1306 allocator: &mut Allocator,
1307 pane: (u32, u32, u32, u32),
1308 camera: RtCamera,
1309 ) {
1310 let (_, _, w, h) = pane;
1311 if w == 0 || h == 0 {
1312 self.staged = false;
1313 return;
1314 }
1315 if (w, h) != self.output_size {
1316 unsafe {
1317 let _ = device.device_wait_idle();
1318 }
1319 self.recreate_targets(device, allocator, w, h);
1320 self.sample_index = 0;
1321 }
1322 if pane != self.pane && self.pane != (0, 0, 0, 0) {
1323 // Pane moved or shrank: stale RT pixels sit outside the new
1324 // region; clear the backdrop once before the next blit.
1325 self.pane_moved = true;
1326 }
1327 if self.camera != Some(camera) {
1328 self.camera = Some(camera);
1329 self.sample_index = 0;
1330 }
1331 self.pane = pane;
1332 self.staged = true;
1333 }
1334
1335 fn recreate_targets(&mut self, device: &ash::Device, allocator: &mut Allocator, w: u32, h: u32) {
1336 self.destroy_targets(device, allocator);
1337 self.output_size = (w, h);
1338 self.output_initialized = false;
1339 let gpu_buffer = |allocator: &mut Allocator,
1340 bytes_per_px: vk::DeviceSize,
1341 name: &'static str|
1342 -> AllocatedBuffer {
1343 let size = (w as vk::DeviceSize) * (h as vk::DeviceSize) * bytes_per_px;
1344 unsafe {
1345 let buffer = device
1346 .create_buffer(
1347 &vk::BufferCreateInfo::default()
1348 .size(size)
1349 .usage(vk::BufferUsageFlags::STORAGE_BUFFER)
1350 .sharing_mode(vk::SharingMode::EXCLUSIVE),
1351 None,
1352 )
1353 .expect("Failed to create RT target buffer");
1354 let requirements = device.get_buffer_memory_requirements(buffer);
1355 let allocation = allocator
1356 .allocate(&AllocationCreateDesc {
1357 name,
1358 requirements,
1359 location: MemoryLocation::GpuOnly,
1360 linear: true,
1361 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
1362 })
1363 .expect("Failed to allocate RT target memory");
1364 device
1365 .bind_buffer_memory(buffer, allocation.memory(), allocation.offset())
1366 .expect("Failed to bind RT target memory");
1367 AllocatedBuffer { buffer, allocation: Some(allocation), size }
1368 }
1369 };
1370 self.accum = gpu_buffer(allocator, 16, "rt-accum");
1371 self.features = gpu_buffer(allocator, 32, "rt-features");
1372 if self.denoiser.is_some() {
1373 self.ping = gpu_buffer(allocator, 16, "rt-denoise-ping");
1374 self.pong = gpu_buffer(allocator, 16, "rt-denoise-pong");
1375 }
1376 unsafe {
1377 let image = device
1378 .create_image(
1379 &vk::ImageCreateInfo::default()
1380 .image_type(vk::ImageType::TYPE_2D)
1381 .format(vk::Format::R8G8B8A8_UNORM)
1382 .extent(vk::Extent3D { width: w, height: h, depth: 1 })
1383 .mip_levels(1)
1384 .array_layers(1)
1385 .samples(vk::SampleCountFlags::TYPE_1)
1386 .tiling(vk::ImageTiling::OPTIMAL)
1387 .usage(vk::ImageUsageFlags::STORAGE | vk::ImageUsageFlags::TRANSFER_SRC)
1388 .initial_layout(vk::ImageLayout::UNDEFINED),
1389 None,
1390 )
1391 .expect("Failed to create RT output image");
1392 let requirements = device.get_image_memory_requirements(image);
1393 let allocation = allocator
1394 .allocate(&AllocationCreateDesc {
1395 name: "rt-output",
1396 requirements,
1397 location: MemoryLocation::GpuOnly,
1398 linear: false,
1399 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
1400 })
1401 .expect("Failed to allocate RT output memory");
1402 device
1403 .bind_image_memory(image, allocation.memory(), allocation.offset())
1404 .expect("Failed to bind RT output memory");
1405 let view = device
1406 .create_image_view(
1407 &vk::ImageViewCreateInfo::default()
1408 .image(image)
1409 .view_type(vk::ImageViewType::TYPE_2D)
1410 .format(vk::Format::R8G8B8A8_UNORM)
1411 .subresource_range(
1412 vk::ImageSubresourceRange::default()
1413 .aspect_mask(vk::ImageAspectFlags::COLOR)
1414 .level_count(1)
1415 .layer_count(1),
1416 ),
1417 None,
1418 )
1419 .expect("Failed to create RT output view");
1420 self.output_image = image;
1421 self.output_view = view;
1422 self.output_allocation = Some(allocation);
1423
1424 for frame in &self.frames {
1425 let accum_infos = [vk::DescriptorBufferInfo::default()
1426 .buffer(self.accum.buffer)
1427 .range(vk::WHOLE_SIZE)];
1428 let feature_infos = [vk::DescriptorBufferInfo::default()
1429 .buffer(self.features.buffer)
1430 .range(vk::WHOLE_SIZE)];
1431 let image_infos = [vk::DescriptorImageInfo::default()
1432 .image_view(view)
1433 .image_layout(vk::ImageLayout::GENERAL)];
1434 device.update_descriptor_sets(
1435 &[
1436 vk::WriteDescriptorSet::default()
1437 .dst_set(frame.descriptor_set)
1438 .dst_binding(4)
1439 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
1440 .buffer_info(&accum_infos),
1441 vk::WriteDescriptorSet::default()
1442 .dst_set(frame.descriptor_set)
1443 .dst_binding(5)
1444 .descriptor_type(vk::DescriptorType::STORAGE_IMAGE)
1445 .image_info(&image_infos),
1446 vk::WriteDescriptorSet::default()
1447 .dst_set(frame.descriptor_set)
1448 .dst_binding(6)
1449 .descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
1450 .buffer_info(&feature_infos),
1451 ],
1452 &[],
1453 );
1454 }
1455 if let Some(denoiser) = &self.denoiser {
1456 denoiser.write_target_descriptors(
1457 device,
1458 self.accum.buffer,
1459 self.features.buffer,
1460 self.ping.buffer,
1461 self.pong.buffer,
1462 view,
1463 );
1464 }
1465 }
1466 }
1467
1468 fn destroy_targets(&mut self, device: &ash::Device, allocator: &mut Allocator) {
1469 unsafe {
1470 if self.output_view != vk::ImageView::null() {
1471 device.destroy_image_view(self.output_view, None);
1472 device.destroy_image(self.output_image, None);
1473 self.output_view = vk::ImageView::null();
1474 self.output_image = vk::Image::null();
1475 }
1476 }
1477 if let Some(a) = self.output_allocation.take() {
1478 let _ = allocator.free(a);
1479 }
1480 destroy_cpu_buffer(device, allocator, &mut self.accum);
1481 destroy_cpu_buffer(device, allocator, &mut self.features);
1482 destroy_cpu_buffer(device, allocator, &mut self.ping);
1483 destroy_cpu_buffer(device, allocator, &mut self.pong);
1484 self.output_size = (0, 0);
1485 }
1486
1487 /// True while another dispatch would still refine the image.
1488 pub(crate) fn accumulating(&self) -> bool {
1489 self.tri_count > 0 && self.sample_index < MAX_SAMPLES
1490 }
1491
1492 /// After the frame fence: write this frame's params, and point this
1493 /// frame's image bindings at the scene's image as it is NOW.
1494 ///
1495 /// Each frame, because a shared image is the 2D pass's to replace: its
1496 /// upload may land after the scene was set, and freeing it destroys the
1497 /// view. This frame's set is past its fence, so it is safe to rewrite,
1498 /// and it is rewritten before every use — a view that is gone is never
1499 /// one a dispatch reads. `shared` looks an id up: its view and size.
1500 pub(crate) fn write_frame_uniforms(
1501 &mut self,
1502 device: &ash::Device,
1503 frame_index: usize,
1504 shared: &dyn Fn(u32) -> Option<(vk::ImageView, u32, u32)>,
1505 ) {
1506 if !self.staged || self.tri_count == 0 {
1507 return;
1508 }
1509 let Some(camera) = self.camera else { return };
1510 let bound = self.image.as_ref().and_then(|image| {
1511 let (view, w, h) = match (&image.owned, image.shared) {
1512 (Some(owned), _) => (owned.view, owned.size.0, owned.size.1),
1513 (None, Some(id)) => shared(id)?,
1514 (None, None) => return None,
1515 };
1516 Some((view, ParamImage { width: w, height: h, corners: image.corners, opacity: image.opacity }))
1517 });
1518 // No image, or one not there yet: opacity 0 lets every ray through
1519 // the quad, and the stand-in is never sampled for it.
1520 let (view, param_image) = match bound {
1521 Some((view, p)) => (view, Some(p)),
1522 None => (self.stand_in.view, None),
1523 };
1524 Self::write_image_descriptor(
1525 device,
1526 self.frames[frame_index].descriptor_set,
1527 view,
1528 self.image_sampler,
1529 );
1530 let params = rt_params(
1531 camera,
1532 self.output_size,
1533 self.sample_index,
1534 self.spp,
1535 param_image,
1536 self.background,
1537 &self.environment,
1538 );
1539 let frame = &mut self.frames[frame_index];
1540 frame.uniforms.allocation.as_mut().unwrap().mapped_slice_mut().unwrap()
1541 [..std::mem::size_of::<RtParams>()]
1542 .copy_from_slice(bytemuck::bytes_of(¶ms));
1543 if let Some(denoiser) = &mut self.denoiser {
1544 denoiser.write_frame_uniforms(
1545 frame_index,
1546 self.output_size.0,
1547 self.output_size.1,
1548 self.sample_index + self.spp,
1549 );
1550 }
1551 }
1552
1553 /// Record one accumulation dispatch + the blit into the backdrop's pane
1554 /// region. Returns false when there is nothing to do (not staged, empty
1555 /// scene, or converged) — the backdrop then simply keeps its content.
1556 /// On true, the backdrop ends in TRANSFER_SRC (like `SceneStage::record`).
1557 ///
1558 /// `backdrop_in_transfer_src` says the raster scene pass already ran this
1559 /// frame (backdrop in TRANSFER_SRC); otherwise it is in SHADER_READ_ONLY.
1560 pub(crate) fn record(
1561 &mut self,
1562 device: &ash::Device,
1563 cmd: vk::CommandBuffer,
1564 frame_index: usize,
1565 backdrop_image: vk::Image,
1566 backdrop_extent: vk::Extent2D,
1567 backdrop_in_transfer_src: bool,
1568 ) -> bool {
1569 if !self.staged || self.tri_count == 0 || self.output_image == vk::Image::null() {
1570 self.staged = false;
1571 return false;
1572 }
1573 self.staged = false;
1574 if self.sample_index >= MAX_SAMPLES {
1575 return false;
1576 }
1577 let (w, h) = self.output_size;
1578 let color_range = vk::ImageSubresourceRange::default()
1579 .aspect_mask(vk::ImageAspectFlags::COLOR)
1580 .level_count(1)
1581 .layer_count(1);
1582 unsafe {
1583 // Output image to GENERAL for the compute write; order this
1584 // dispatch's accum access after the previous frame's. The src
1585 // stage always includes COMPUTE_SHADER — the accum buffer
1586 // barrier's access flags must be legal for it even on the first
1587 // dispatch, when the image side is still UNDEFINED.
1588 let (old_layout, src_access, src_stage) = if self.output_initialized {
1589 (
1590 vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1591 vk::AccessFlags::TRANSFER_READ,
1592 vk::PipelineStageFlags::TRANSFER | vk::PipelineStageFlags::COMPUTE_SHADER,
1593 )
1594 } else {
1595 (
1596 vk::ImageLayout::UNDEFINED,
1597 vk::AccessFlags::empty(),
1598 vk::PipelineStageFlags::COMPUTE_SHADER,
1599 )
1600 };
1601 device.cmd_pipeline_barrier(
1602 cmd,
1603 src_stage,
1604 vk::PipelineStageFlags::COMPUTE_SHADER,
1605 vk::DependencyFlags::empty(),
1606 &[],
1607 &[vk::BufferMemoryBarrier::default()
1608 .src_access_mask(vk::AccessFlags::SHADER_READ | vk::AccessFlags::SHADER_WRITE)
1609 .dst_access_mask(vk::AccessFlags::SHADER_READ | vk::AccessFlags::SHADER_WRITE)
1610 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1611 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1612 .buffer(self.accum.buffer)
1613 .size(vk::WHOLE_SIZE)],
1614 &[vk::ImageMemoryBarrier::default()
1615 .src_access_mask(src_access)
1616 .dst_access_mask(vk::AccessFlags::SHADER_WRITE)
1617 .old_layout(old_layout)
1618 .new_layout(vk::ImageLayout::GENERAL)
1619 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1620 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1621 .image(self.output_image)
1622 .subresource_range(color_range)],
1623 );
1624 self.output_initialized = true;
1625
1626 device.cmd_bind_pipeline(cmd, vk::PipelineBindPoint::COMPUTE, self.pipeline);
1627 device.cmd_bind_descriptor_sets(
1628 cmd,
1629 vk::PipelineBindPoint::COMPUTE,
1630 self.pipeline_layout,
1631 0,
1632 &[self.frames[frame_index].descriptor_set],
1633 &[],
1634 );
1635 device.cmd_dispatch(cmd, w.div_ceil(WORKGROUP), h.div_ceil(WORKGROUP), 1);
1636
1637 // À-trous denoise passes; the last one rewrites out_img (still
1638 // GENERAL), so the transfer barrier below covers either writer.
1639 if let Some(denoiser) = &self.denoiser {
1640 denoiser.record(device, cmd, frame_index, w, h);
1641 }
1642
1643 // Output to TRANSFER_SRC, backdrop to TRANSFER_DST for the blit.
1644 let (bd_old, bd_access, bd_stage) = if backdrop_in_transfer_src {
1645 (
1646 vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1647 vk::AccessFlags::TRANSFER_READ,
1648 vk::PipelineStageFlags::TRANSFER,
1649 )
1650 } else {
1651 (
1652 vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
1653 vk::AccessFlags::SHADER_READ,
1654 vk::PipelineStageFlags::FRAGMENT_SHADER,
1655 )
1656 };
1657 device.cmd_pipeline_barrier(
1658 cmd,
1659 vk::PipelineStageFlags::COMPUTE_SHADER | bd_stage,
1660 vk::PipelineStageFlags::TRANSFER,
1661 vk::DependencyFlags::empty(),
1662 &[],
1663 &[],
1664 &[
1665 vk::ImageMemoryBarrier::default()
1666 .src_access_mask(vk::AccessFlags::SHADER_WRITE)
1667 .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
1668 .old_layout(vk::ImageLayout::GENERAL)
1669 .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1670 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1671 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1672 .image(self.output_image)
1673 .subresource_range(color_range),
1674 vk::ImageMemoryBarrier::default()
1675 .src_access_mask(bd_access)
1676 .dst_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1677 .old_layout(bd_old)
1678 .new_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1679 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1680 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1681 .image(backdrop_image)
1682 .subresource_range(color_range),
1683 ],
1684 );
1685
1686 if self.pane_moved {
1687 self.pane_moved = false;
1688 device.cmd_clear_color_image(
1689 cmd,
1690 backdrop_image,
1691 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1692 &vk::ClearColorValue { float32: [0.0; 4] },
1693 &[color_range],
1694 );
1695 }
1696
1697 // Blit (not copy): converts UNORM → the backdrop's sRGB format.
1698 let (px, py, _, _) = self.pane;
1699 let dst_x0 = px.min(backdrop_extent.width);
1700 let dst_y0 = py.min(backdrop_extent.height);
1701 let bw = w.min(backdrop_extent.width - dst_x0);
1702 let bh = h.min(backdrop_extent.height - dst_y0);
1703 if bw > 0 && bh > 0 {
1704 let layers = vk::ImageSubresourceLayers::default()
1705 .aspect_mask(vk::ImageAspectFlags::COLOR)
1706 .layer_count(1);
1707 device.cmd_blit_image(
1708 cmd,
1709 self.output_image,
1710 vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1711 backdrop_image,
1712 vk::ImageLayout::TRANSFER_DST_OPTIMAL,
1713 &[vk::ImageBlit::default()
1714 .src_subresource(layers)
1715 .src_offsets([
1716 vk::Offset3D { x: 0, y: 0, z: 0 },
1717 vk::Offset3D { x: bw as i32, y: bh as i32, z: 1 },
1718 ])
1719 .dst_subresource(layers)
1720 .dst_offsets([
1721 vk::Offset3D { x: dst_x0 as i32, y: dst_y0 as i32, z: 0 },
1722 vk::Offset3D {
1723 x: (dst_x0 + bw) as i32,
1724 y: (dst_y0 + bh) as i32,
1725 z: 1,
1726 },
1727 ])],
1728 vk::Filter::NEAREST,
1729 );
1730 }
1731
1732 // Backdrop to TRANSFER_SRC: the swapchain copy path expects it
1733 // exactly as SceneStage::record leaves it.
1734 device.cmd_pipeline_barrier(
1735 cmd,
1736 vk::PipelineStageFlags::TRANSFER,
1737 vk::PipelineStageFlags::TRANSFER,
1738 vk::DependencyFlags::empty(),
1739 &[],
1740 &[],
1741 &[vk::ImageMemoryBarrier::default()
1742 .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1743 .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
1744 .old_layout(vk::ImageLayout::TRANSFER_DST_OPTIMAL)
1745 .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
1746 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1747 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1748 .image(backdrop_image)
1749 .subresource_range(color_range)],
1750 );
1751 }
1752 self.sample_index += self.spp;
1753 true
1754 }
1755
1756 pub(crate) fn destroy(&mut self, device: &ash::Device, allocator: &mut Allocator) {
1757 self.destroy_targets(device, allocator);
1758 self.destroy_accel(device, allocator);
1759 if let Some(mut denoiser) = self.denoiser.take() {
1760 denoiser.destroy(device, allocator);
1761 }
1762 for buf in [&mut self.nodes, &mut self.tris, &mut self.materials] {
1763 destroy_cpu_buffer(device, allocator, buf);
1764 }
1765 if let Some(mut owned) = self.image.take().and_then(|i| i.owned) {
1766 owned.destroy(device, allocator);
1767 }
1768 self.stand_in.destroy(device, allocator);
1769 unsafe {
1770 device.destroy_sampler(self.image_sampler, None);
1771 for frame in &mut self.frames {
1772 let mut uniforms = std::mem::replace(&mut frame.uniforms, AllocatedBuffer::null());
1773 destroy_cpu_buffer(device, allocator, &mut uniforms);
1774 }
1775 device.destroy_descriptor_pool(self.descriptor_pool, None);
1776 device.destroy_descriptor_set_layout(self.descriptor_set_layout, None);
1777 device.destroy_pipeline(self.pipeline, None);
1778 device.destroy_pipeline_layout(self.pipeline_layout, None);
1779 device.destroy_shader_module(self.shader_module, None);
1780 }
1781 }
1782 }
1783
1784 // --- Headless offscreen rendering (thumbnails, previews) ---
1785
1786 /// One-shot path-traced rendering with no window anywhere: a headless
1787 /// [`super::VkCore`] + an [`RtStage`] whose "backdrop" is a private sRGB
1788 /// target image, read back to CPU pixels. This is the seam consumers like
1789 /// the cce-files thumbnailer sit on.
1790 ///
1791 /// Not `Send`-safe by design intent (owns a device); create it on the worker
1792 /// thread that renders.
1793 pub struct RtOffscreen {
1794 stage: RtStage,
1795 target_image: vk::Image,
1796 target_allocation: Option<Allocation>,
1797 readback: AllocatedBuffer,
1798 size: (u32, u32),
1799 cmd: vk::CommandBuffer,
1800 fence: vk::Fence,
1801 // Declared last: dropped after everything above is destroyed in Drop.
1802 core: super::VkCore,
1803 }
1804
1805 impl RtOffscreen {
1806 /// Samples per submit: keeps each dispatch well under GPU watchdog
1807 /// timeouts even at large sizes; a render loops submits to reach the
1808 /// requested sample count.
1809 const CHUNK_SPP: u32 = 8;
1810
1811 pub fn new() -> Self {
1812 let mut core = super::VkCore::new_headless();
1813 let device = core.device.clone();
1814 let accel_loader = core.accel_loader.clone();
1815 let as_scratch_align = core.as_scratch_align;
1816 let min_uniform_align = core.min_uniform_align;
1817 let (queue, command_pool) = (core.queue, core.command_pool);
1818 let allocator = core.allocator.as_mut().unwrap();
1819 let stage = RtStage::new(
1820 &device,
1821 allocator,
1822 1,
1823 accel_loader.as_ref(),
1824 as_scratch_align,
1825 min_uniform_align,
1826 queue,
1827 command_pool,
1828 );
1829 unsafe {
1830 let cmd = device
1831 .allocate_command_buffers(
1832 &vk::CommandBufferAllocateInfo::default()
1833 .command_pool(core.command_pool)
1834 .level(vk::CommandBufferLevel::PRIMARY)
1835 .command_buffer_count(1),
1836 )
1837 .expect("Failed to allocate RT offscreen command buffer")[0];
1838 let fence = device
1839 .create_fence(&vk::FenceCreateInfo::default(), None)
1840 .expect("Failed to create RT offscreen fence");
1841 RtOffscreen {
1842 stage,
1843 target_image: vk::Image::null(),
1844 target_allocation: None,
1845 readback: AllocatedBuffer::null(),
1846 size: (0, 0),
1847 cmd,
1848 fence,
1849 core,
1850 }
1851 }
1852 }
1853
1854 /// What a camera ray that meets nothing shows — see
1855 /// `VkRenderer::set_rt_background`.
1856 pub fn set_background(&mut self, background: Option<[f32; 3]>) {
1857 self.stage.set_background(background);
1858 }
1859
1860 /// The sky and sun — see [`RtEnvironment`].
1861 pub fn set_environment(&mut self, environment: RtEnvironment) {
1862 self.stage.set_environment(environment);
1863 }
1864
1865 /// Replace the scene (same schema as `VkRenderer::set_rt_scene`).
1866 pub fn set_scene(&mut self, triangles: &[RtTriangle], materials: &[RtMaterial]) {
1867 self.set_scene_with_image(triangles, materials, None);
1868 }
1869
1870 /// [`set_scene`](Self::set_scene), with an image standing in the scene.
1871 pub fn set_scene_with_image(
1872 &mut self,
1873 triangles: &[RtTriangle],
1874 materials: &[RtMaterial],
1875 image: Option<RtImagePixels>,
1876 ) {
1877 let packed = pack_scene(
1878 triangles.to_vec(),
1879 materials,
1880 image.as_ref().map(|i| i.corners),
1881 self.stage.needs_bvh(),
1882 );
1883 unsafe {
1884 let _ = self.core.device.device_wait_idle();
1885 }
1886 let device = self.core.device.clone();
1887 let queue = self.core.queue;
1888 let command_pool = self.core.command_pool;
1889 self.stage.set_scene(
1890 &device,
1891 self.core.allocator.as_mut().unwrap(),
1892 queue,
1893 command_pool,
1894 &packed,
1895 image.map(|i| {
1896 (
1897 RtImageSource::Pixels { pixels: i.pixels, width: i.width, height: i.height },
1898 i.corners,
1899 i.opacity,
1900 )
1901 }),
1902 );
1903 }
1904
1905 /// Render `samples` paths per pixel and return tightly packed
1906 /// sRGB-encoded RGBA8 pixels (`width * height * 4` bytes). Blocks until
1907 /// the GPU finishes; meant for worker threads, not frame loops.
1908 pub fn render(
1909 &mut self,
1910 camera: RtCamera,
1911 width: u32,
1912 height: u32,
1913 samples: u32,
1914 ) -> Vec<u8> {
1915 let width = width.max(1);
1916 let height = height.max(1);
1917 let samples = samples.clamp(1, MAX_SAMPLES);
1918 let device = self.core.device.clone();
1919 self.ensure_target(width, height);
1920
1921 // Fresh accumulation every render: thumbnails are one-shot.
1922 self.stage.sample_index = 0;
1923 let extent = vk::Extent2D { width, height };
1924 let mut done = 0u32;
1925 while done < samples {
1926 self.stage.spp = Self::CHUNK_SPP.min(samples - done);
1927 self.stage.stage(
1928 &device,
1929 self.core.allocator.as_mut().unwrap(),
1930 (0, 0, width, height),
1931 camera,
1932 );
1933 // stage() resets sample_index when the camera or size changed —
1934 // keep our resume point, not the reset, after the first chunk.
1935 self.stage.sample_index = done;
1936 self.stage.write_frame_uniforms(&device, 0, &|_| None);
1937 unsafe {
1938 device
1939 .begin_command_buffer(self.cmd, &vk::CommandBufferBeginInfo::default())
1940 .unwrap();
1941 let recorded =
1942 self.stage.record(&device, self.cmd, 0, self.target_image, extent, true);
1943 device.end_command_buffer(self.cmd).unwrap();
1944 assert!(recorded, "RT offscreen: nothing recorded (empty scene?)");
1945 self.submit_and_wait();
1946 }
1947 done += Self::CHUNK_SPP.min(samples - done);
1948 }
1949
1950 // Copy the sRGB target (left in TRANSFER_SRC by record) to the
1951 // readback buffer and map it.
1952 unsafe {
1953 device
1954 .begin_command_buffer(self.cmd, &vk::CommandBufferBeginInfo::default())
1955 .unwrap();
1956 device.cmd_copy_image_to_buffer(
1957 self.cmd,
1958 self.target_image,
1959 vk::ImageLayout::TRANSFER_SRC_OPTIMAL,
1960 self.readback.buffer,
1961 &[vk::BufferImageCopy::default()
1962 .image_subresource(
1963 vk::ImageSubresourceLayers::default()
1964 .aspect_mask(vk::ImageAspectFlags::COLOR)
1965 .layer_count(1),
1966 )
1967 .image_extent(vk::Extent3D { width, height, depth: 1 })],
1968 );
1969 device.cmd_pipeline_barrier(
1970 self.cmd,
1971 vk::PipelineStageFlags::TRANSFER,
1972 vk::PipelineStageFlags::HOST,
1973 vk::DependencyFlags::empty(),
1974 &[],
1975 &[vk::BufferMemoryBarrier::default()
1976 .src_access_mask(vk::AccessFlags::TRANSFER_WRITE)
1977 .dst_access_mask(vk::AccessFlags::HOST_READ)
1978 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1979 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
1980 .buffer(self.readback.buffer)
1981 .size(vk::WHOLE_SIZE)],
1982 &[],
1983 );
1984 device.end_command_buffer(self.cmd).unwrap();
1985 self.submit_and_wait();
1986 }
1987 let len = (width * height * 4) as usize;
1988 self.readback.allocation.as_ref().unwrap().mapped_slice().unwrap()[..len].to_vec()
1989 }
1990
1991 unsafe fn submit_and_wait(&mut self) {
1992 let device = &self.core.device;
1993 let cmds = [self.cmd];
1994 device
1995 .queue_submit(
1996 self.core.queue,
1997 &[vk::SubmitInfo::default().command_buffers(&cmds)],
1998 self.fence,
1999 )
2000 .expect("RT offscreen submit failed");
2001 device
2002 .wait_for_fences(&[self.fence], true, u64::MAX)
2003 .expect("RT offscreen fence wait failed");
2004 device.reset_fences(&[self.fence]).unwrap();
2005 }
2006
2007 fn ensure_target(&mut self, width: u32, height: u32) {
2008 if (width, height) == self.size {
2009 return;
2010 }
2011 let device = self.core.device.clone();
2012 unsafe {
2013 let _ = device.device_wait_idle();
2014 }
2015 self.destroy_target();
2016 let allocator = self.core.allocator.as_mut().unwrap();
2017 unsafe {
2018 let image = device
2019 .create_image(
2020 &vk::ImageCreateInfo::default()
2021 .image_type(vk::ImageType::TYPE_2D)
2022 .format(vk::Format::R8G8B8A8_SRGB)
2023 .extent(vk::Extent3D { width, height, depth: 1 })
2024 .mip_levels(1)
2025 .array_layers(1)
2026 .samples(vk::SampleCountFlags::TYPE_1)
2027 .tiling(vk::ImageTiling::OPTIMAL)
2028 .usage(
2029 vk::ImageUsageFlags::TRANSFER_DST | vk::ImageUsageFlags::TRANSFER_SRC,
2030 )
2031 .initial_layout(vk::ImageLayout::UNDEFINED),
2032 None,
2033 )
2034 .expect("Failed to create RT offscreen target");
2035 let requirements = device.get_image_memory_requirements(image);
2036 let allocation = allocator
2037 .allocate(&AllocationCreateDesc {
2038 name: "rt-offscreen-target",
2039 requirements,
2040 location: MemoryLocation::GpuOnly,
2041 linear: false,
2042 allocation_scheme: AllocationScheme::GpuAllocatorManaged,
2043 })
2044 .expect("Failed to allocate RT offscreen target memory");
2045 device
2046 .bind_image_memory(image, allocation.memory(), allocation.offset())
2047 .expect("Failed to bind RT offscreen target memory");
2048 self.target_image = image;
2049 self.target_allocation = Some(allocation);
2050
2051 self.readback = create_cpu_buffer(
2052 &device,
2053 allocator,
2054 (width as vk::DeviceSize) * (height as vk::DeviceSize) * 4,
2055 vk::BufferUsageFlags::TRANSFER_DST,
2056 "rt-readback",
2057 );
2058
2059 // RtStage::record expects the blit destination in TRANSFER_SRC
2060 // (the steady state SceneStage leaves the backdrop in).
2061 device
2062 .begin_command_buffer(self.cmd, &vk::CommandBufferBeginInfo::default())
2063 .unwrap();
2064 device.cmd_pipeline_barrier(
2065 self.cmd,
2066 vk::PipelineStageFlags::TOP_OF_PIPE,
2067 vk::PipelineStageFlags::TRANSFER,
2068 vk::DependencyFlags::empty(),
2069 &[],
2070 &[],
2071 &[vk::ImageMemoryBarrier::default()
2072 .src_access_mask(vk::AccessFlags::empty())
2073 .dst_access_mask(vk::AccessFlags::TRANSFER_READ)
2074 .old_layout(vk::ImageLayout::UNDEFINED)
2075 .new_layout(vk::ImageLayout::TRANSFER_SRC_OPTIMAL)
2076 .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2077 .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED)
2078 .image(image)
2079 .subresource_range(
2080 vk::ImageSubresourceRange::default()
2081 .aspect_mask(vk::ImageAspectFlags::COLOR)
2082 .level_count(1)
2083 .layer_count(1),
2084 )],
2085 );
2086 device.end_command_buffer(self.cmd).unwrap();
2087 self.submit_and_wait();
2088 }
2089 self.size = (width, height);
2090 }
2091
2092 fn destroy_target(&mut self) {
2093 unsafe {
2094 if self.target_image != vk::Image::null() {
2095 self.core.device.destroy_image(self.target_image, None);
2096 self.target_image = vk::Image::null();
2097 }
2098 }
2099 if let Some(a) = self.target_allocation.take() {
2100 let _ = self.core.allocator.as_mut().unwrap().free(a);
2101 }
2102 let device = self.core.device.clone();
2103 destroy_cpu_buffer(&device, self.core.allocator.as_mut().unwrap(), &mut self.readback);
2104 self.size = (0, 0);
2105 }
2106 }
2107
2108 impl Default for RtOffscreen {
2109 fn default() -> Self {
2110 Self::new()
2111 }
2112 }
2113
2114 impl Drop for RtOffscreen {
2115 fn drop(&mut self) {
2116 unsafe {
2117 let _ = self.core.device.device_wait_idle();
2118 }
2119 self.destroy_target();
2120 let device = self.core.device.clone();
2121 self.stage.destroy(&device, self.core.allocator.as_mut().unwrap());
2122 unsafe {
2123 self.core.device.destroy_fence(self.fence, None);
2124 // The command buffer dies with the pool in VkCore's Drop.
2125 }
2126 }
2127 }
2128
2129 #[cfg(test)]
2130 mod tests {
2131 use super::*;
2132
2133 #[test]
2134 fn test_rt_shaders_compile() {
2135 // naga parse + validate + SPIR-V write for both tiers; panics on failure.
2136 let tier1 = compile_wgsl(&crate::draw::shaders::rt_bvh_source());
2137 assert!(!tier1.is_empty());
2138 let tier2 = compile_wgsl_ray_query(&format!("{}\n{}", crate::draw::shaders::RT_COMMON, crate::draw::shaders::RT_QUERY));
2139 assert!(!tier2.is_empty());
2140 let denoise = compile_wgsl(crate::draw::shaders::RT_DENOISE);
2141 assert!(!denoise.is_empty());
2142 }
2143
2144 /// An image in the traced scene, end to end: a quad half red and half
2145 /// clear, in front of a green wall. The red half shows red, the clear
2146 /// half shows the wall behind it, and beside the quad is the wall too.
2147 /// Run with: cargo test --lib vk::rt -- --ignored
2148 #[test]
2149 #[ignore = "requires a Vulkan device"]
2150 fn test_offscreen_renders_an_image() {
2151 let mut off = RtOffscreen::new();
2152 // 2x1: a red texel, a clear one.
2153 let pixels = [255u8, 0, 0, 255, 0, 0, 0, 0];
2154 let wall = |p0, p1, p2| RtTriangle { p0, p1, p2, material: 0 };
2155 off.set_scene_with_image(
2156 &[
2157 wall([-9.0, -9.0, -1.0], [9.0, -9.0, -1.0], [9.0, 9.0, -1.0]),
2158 wall([-9.0, -9.0, -1.0], [9.0, 9.0, -1.0], [-9.0, 9.0, -1.0]),
2159 ],
2160 &[RtMaterial { albedo: [0.1, 0.9, 0.1], emission: [0.0; 3] }],
2161 Some(RtImagePixels {
2162 pixels: &pixels,
2163 width: 2,
2164 height: 1,
2165 corners: [[-1.0, 0.5, 0.0], [1.0, 0.5, 0.0], [1.0, -0.5, 0.0], [-1.0, -0.5, 0.0]],
2166 opacity: 1.0,
2167 }),
2168 );
2169 let proj = glam::Mat4::perspective_rh(0.9, 1.0, 0.1, 100.0);
2170 let view = glam::Mat4::look_at_rh(
2171 glam::Vec3::new(0.0, 0.0, 3.0),
2172 glam::Vec3::ZERO,
2173 glam::Vec3::Y,
2174 );
2175 let camera = RtCamera { inv_mvp: (proj * view).inverse().to_cols_array_2d() };
2176 let (w, h) = (64u32, 64u32);
2177 let px = off.render(camera, w, h, 64);
2178 let at = |x: u32, y: u32| {
2179 let i = ((y * w + x) * 4) as usize;
2180 (px[i] as i32, px[i + 1] as i32, px[i + 2] as i32)
2181 };
2182 // The quad spans x in -1..1 of a view about 2.9 wide at z = 0: the
2183 // pane's columns 10 to 54, and rows 21 to 43.
2184 // Unlit: the texel's own colour, whatever the sky is doing.
2185 let (r, g, b) = at(16, 32);
2186 assert!(r >= 250 && g <= 5 && b <= 5, "the image's red half is not its red: {r} {g} {b}");
2187 let (r, g, _) = at(48, 32);
2188 assert!(g > r + 40, "the image's clear half hides the wall: r={r} g={g}");
2189 let (r, g, _) = at(32, 6);
2190 assert!(g > r + 40, "beside the image is not the wall: r={r} g={g}");
2191
2192 // Without its image the scene is the wall alone.
2193 off.set_scene(
2194 &[wall([-9.0, -9.0, -1.0], [9.0, -9.0, -1.0], [9.0, 9.0, -1.0])],
2195 &[RtMaterial { albedo: [0.1, 0.9, 0.1], emission: [0.0; 3] }],
2196 );
2197 let px = off.render(camera, w, h, 16);
2198 let i = ((32 * w + 40) * 4) as usize;
2199 assert!(px[i + 1] > px[i], "the image outlived its scene");
2200 }
2201
2202 /// End-to-end GPU test — needs a Vulkan device, so ignored by default.
2203 /// Run with: cargo test --lib vk::rt -- --ignored
2204 #[test]
2205 #[ignore = "requires a Vulkan device"]
2206 fn test_offscreen_render_smoke() {
2207 let mut off = RtOffscreen::new();
2208 // A red triangle filling the view center, camera looking down -Z.
2209 off.set_scene(
2210 &[RtTriangle {
2211 p0: [-1.0, -1.0, 0.0],
2212 p1: [1.0, -1.0, 0.0],
2213 p2: [0.0, 1.5, 0.0],
2214 material: 0,
2215 }],
2216 &[RtMaterial { albedo: [0.9, 0.1, 0.1], emission: [0.0; 3] }],
2217 );
2218 let proj = glam::Mat4::perspective_rh(0.9, 1.0, 0.1, 100.0);
2219 let view = glam::Mat4::look_at_rh(
2220 glam::Vec3::new(0.0, 0.0, 3.0),
2221 glam::Vec3::ZERO,
2222 glam::Vec3::Y,
2223 );
2224 let camera = RtCamera { inv_mvp: (proj * view).inverse().to_cols_array_2d() };
2225 let (w, h) = (64u32, 64u32);
2226 let px = off.render(camera, w, h, 16);
2227 assert_eq!(px.len(), (w * h * 4) as usize);
2228 // Center pixel hits the triangle: red-dominant. Corner pixel is sky:
2229 // blue >= red. Alpha opaque everywhere.
2230 let at = |x: u32, y: u32| {
2231 let i = ((y * w + x) * 4) as usize;
2232 (px[i], px[i + 1], px[i + 2], px[i + 3])
2233 };
2234 let (cr, _cg, cb, ca) = at(w / 2, h / 2);
2235 assert!(ca == 255, "alpha not opaque: {ca}");
2236 assert!(cr > cb, "center not red-dominant: r={cr} b={cb}");
2237 let (sr, _sg, sb, _sa) = at(1, 1);
2238 assert!(sb >= sr, "corner sky not blue-ish: r={sr} b={sb}");
2239 }
2240
2241 /// A background colour is what a camera ray that meets nothing shows —
2242 /// every sample's, not only the one that writes the denoiser's features
2243 /// (a render takes several per dispatch) — while the sky still lights
2244 /// what is hit; and None is the sky again.
2245 #[test]
2246 #[ignore = "requires a Vulkan device"]
2247 fn test_offscreen_background_is_what_a_miss_shows() {
2248 let mut off = RtOffscreen::new();
2249 off.set_scene(
2250 &[RtTriangle { p0: [-1.0, -1.0, 0.0], p1: [1.0, -1.0, 0.0], p2: [0.0, 1.5, 0.0], material: 0 }],
2251 &[RtMaterial { albedo: [0.8, 0.8, 0.8], emission: [0.0; 3] }],
2252 );
2253 let proj = glam::Mat4::perspective_rh(0.9, 1.0, 0.1, 100.0);
2254 let view = glam::Mat4::look_at_rh(glam::Vec3::new(0.0, 0.0, 3.0), glam::Vec3::ZERO, glam::Vec3::Y);
2255 let camera = RtCamera { inv_mvp: (proj * view).inverse().to_cols_array_2d() };
2256 let (w, h) = (32u32, 32u32);
2257 let render = |off: &mut RtOffscreen| {
2258 let px = off.render(camera, w, h, 16);
2259 let at = |x: u32, y: u32| {
2260 let i = ((y * w + x) * 4) as usize;
2261 [px[i], px[i + 1], px[i + 2]]
2262 };
2263 (at(1, 1), at(w / 2, h / 2))
2264 };
2265 let (sky, lit) = render(&mut off);
2266
2267 off.set_background(Some([0.0, 0.0, 0.0]));
2268 let (corner, centre) = render(&mut off);
2269 assert_eq!(corner, [0, 0, 0], "the corner is the backdrop, in every sample");
2270 assert!(centre.iter().all(|&c| c > 60), "the sky still lights the triangle: {centre:?}");
2271 assert!(centre.iter().zip(lit).all(|(&a, b)| a.abs_diff(b) < 24), "and lights it as before: {centre:?} against {lit:?}");
2272
2273 off.set_background(None);
2274 assert_eq!(render(&mut off).0, sky, "None is the sky");
2275 }
2276
2277 /// The environment is the scene's light: a black one leaves a grey
2278 /// triangle black, a sun in front of it lights it and the same sun
2279 /// behind it does not, and the sky's colours are what a miss shows.
2280 #[test]
2281 #[ignore = "requires a Vulkan device"]
2282 fn test_offscreen_environment_lights_the_scene() {
2283 let mut off = RtOffscreen::new();
2284 off.set_scene(
2285 &[RtTriangle { p0: [-1.0, -1.0, 0.0], p1: [1.0, -1.0, 0.0], p2: [0.0, 1.5, 0.0], material: 0 }],
2286 &[RtMaterial { albedo: [0.8, 0.8, 0.8], emission: [0.0; 3] }],
2287 );
2288 let proj = glam::Mat4::perspective_rh(0.9, 1.0, 0.1, 100.0);
2289 let view = glam::Mat4::look_at_rh(glam::Vec3::new(0.0, 0.0, 3.0), glam::Vec3::ZERO, glam::Vec3::Y);
2290 let camera = RtCamera { inv_mvp: (proj * view).inverse().to_cols_array_2d() };
2291 let (w, h) = (32u32, 32u32);
2292 let mut render = |env: RtEnvironment| {
2293 off.set_environment(env);
2294 let px = off.render(camera, w, h, 32);
2295 let at = |x: u32, y: u32| {
2296 let i = ((y * w + x) * 4) as usize;
2297 [px[i], px[i + 1], px[i + 2]]
2298 };
2299 (at(1, 1), at(w / 2, h / 2))
2300 };
2301 let dark = RtEnvironment { sun_direction: [0.0, 0.0, 1.0], sun_color: [0.0; 3], sky_zenith: [0.0; 3], sky_nadir: [0.0; 3] };
2302 assert_eq!(render(dark), ([0, 0, 0], [0, 0, 0]), "no light, nothing seen");
2303 let front = render(RtEnvironment { sun_color: [40.0; 3], ..dark }).1;
2304 let behind = render(RtEnvironment { sun_direction: [0.0, 0.0, -1.0], sun_color: [40.0; 3], ..dark }).1;
2305 assert!(front[0] > 100, "a sun in front lights the face: {front:?}");
2306 assert!(behind[0] < front[0] / 4, "a sun behind it does not: {behind:?} against {front:?}");
2307 let (corner, _) = render(RtEnvironment { sky_zenith: [0.0, 1.0, 0.0], sky_nadir: [0.0, 1.0, 0.0], ..dark });
2308 assert!(corner[1] > 200 && corner[0] < 10, "a miss shows the sky's colour: {corner:?}");
2309 }
2310 }