git.lucas.co / cce-ui
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(&params));
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 }