wgpu_core/indirect_validation/
draw.rs

1use super::{
2    utils::{BufferBarrierScratch, BufferBarriers, UniqueIndexExt as _, UniqueIndexScratch},
3    CreateIndirectValidationPipelineError,
4};
5use crate::{
6    command::{get_src_stride_of_indirect_args, RenderPassErrorInner},
7    device::{queue::TempResource, Device, DeviceError},
8    hal_label,
9    lock::{rank, Mutex},
10    pipeline::{CreateComputePipelineError, CreateShaderModuleError},
11    resource::{RawResourceAccess as _, StagingBuffer, Trackable},
12    snatch::SnatchGuard,
13    track::TrackerIndex,
14    FastHashMap,
15};
16use alloc::{boxed::Box, string::ToString, sync::Arc, vec, vec::Vec};
17use core::{mem::size_of, num::NonZeroU64};
18use scopeguard::{guard, ScopeGuard};
19use wgt::Limits;
20
21/// Note: This needs to be under:
22///
23/// default max_compute_workgroups_per_dimension * size_of::<wgt::DrawIndirectArgs>() * `workgroup_size` used by the shader
24///
25/// = (2^16 - 1) * 2^4 * 2^6
26///
27/// It is currently set to:
28///
29/// = (2^16 - 1) * 2^4
30///
31/// This is enough space for:
32///
33/// - 65535 [`wgt::DrawIndirectArgs`] / [`MetadataEntry`]
34/// - 52428 [`wgt::DrawIndexedIndirectArgs`]
35const BUFFER_SIZE: wgt::BufferSize = wgt::BufferSize::new(1_048_560).unwrap();
36
37/// Holds all device-level resources that are needed to validate indirect draws.
38///
39/// This machinery requires the following limits:
40///
41/// - max_bind_groups: 3,
42/// - max_dynamic_storage_buffers_per_pipeline_layout: 1,
43/// - max_storage_buffers_per_shader_stage: 3,
44/// - max_immediate_size: 8,
45///
46/// These are all indirectly satisfied by `DownlevelFlags::INDIRECT_EXECUTION`, which is also
47/// required for this module's functionality to work.
48#[derive(Debug)]
49pub(crate) struct Draw {
50    module: Box<dyn hal::DynShaderModule>,
51    metadata_bind_group_layout: Box<dyn hal::DynBindGroupLayout>,
52    src_bind_group_layout: Box<dyn hal::DynBindGroupLayout>,
53    dst_bind_group_layout: Box<dyn hal::DynBindGroupLayout>,
54    pipeline_layout: Box<dyn hal::DynPipelineLayout>,
55    pipeline: Box<dyn hal::DynComputePipeline>,
56
57    free_indirect_entries: Mutex<Vec<BufferPoolEntry>>,
58    free_metadata_entries: Mutex<Vec<BufferPoolEntry>>,
59}
60
61impl Draw {
62    pub(super) fn new(
63        device: &dyn hal::DynDevice,
64        required_features: &wgt::Features,
65        instance_flags: wgt::InstanceFlags,
66        backend: wgt::Backend,
67        limits: &Limits,
68    ) -> Result<Self, CreateIndirectValidationPipelineError> {
69        // Indirect draw validation doesn't support buffer sizes higher than u32
70        // since its offsets in the shader and dynamic offsets are u32.
71        //
72        // See also: `u64_offset_to_u32_offset`.
73        assert!(limits.max_buffer_size <= u32::MAX as u64);
74
75        let module = create_validation_module(device, instance_flags)?;
76        let module = guard(module, |module| unsafe {
77            device.destroy_shader_module(module)
78        });
79
80        let metadata_bind_group_layout = create_bind_group_layout(
81            device,
82            true,
83            false,
84            BUFFER_SIZE,
85            hal_label(
86                Some("(wgpu internal) Indirect draw validation metadata bind group layout"),
87                instance_flags,
88            ),
89        )?;
90        let metadata_bind_group_layout = guard(metadata_bind_group_layout, |bgl| unsafe {
91            device.destroy_bind_group_layout(bgl)
92        });
93
94        let src_bind_group_layout = create_bind_group_layout(
95            device,
96            true,
97            true,
98            wgt::BufferSize::new(4 * 4).unwrap(),
99            hal_label(
100                Some("(wgpu internal) Indirect draw validation source bind group layout"),
101                instance_flags,
102            ),
103        )?;
104        let src_bind_group_layout = guard(src_bind_group_layout, |bgl| unsafe {
105            device.destroy_bind_group_layout(bgl)
106        });
107
108        let dst_bind_group_layout = create_bind_group_layout(
109            device,
110            false,
111            false,
112            BUFFER_SIZE,
113            hal_label(
114                Some("(wgpu internal) Indirect draw validation destination bind group layout"),
115                instance_flags,
116            ),
117        )?;
118        let dst_bind_group_layout = guard(dst_bind_group_layout, |bgl| unsafe {
119            device.destroy_bind_group_layout(bgl)
120        });
121
122        let pipeline_layout_desc = hal::PipelineLayoutDescriptor {
123            label: hal_label(
124                Some("(wgpu internal) Indirect draw validation pipeline layout"),
125                instance_flags,
126            ),
127            flags: hal::PipelineLayoutFlags::empty(),
128            bind_group_layouts: &[
129                Some(metadata_bind_group_layout.as_ref()),
130                Some(src_bind_group_layout.as_ref()),
131                Some(dst_bind_group_layout.as_ref()),
132            ],
133            immediate_size: 8,
134        };
135        let pipeline_layout = unsafe {
136            device
137                .create_pipeline_layout(&pipeline_layout_desc)
138                .map_err(DeviceError::from_hal)?
139        };
140        let pipeline_layout = guard(pipeline_layout, |pipeline_layout| unsafe {
141            device.destroy_pipeline_layout(pipeline_layout)
142        });
143
144        let supports_indirect_first_instance =
145            required_features.contains(wgt::Features::INDIRECT_FIRST_INSTANCE);
146        let write_d3d12_special_constants = backend == wgt::Backend::Dx12;
147        let pipeline = create_validation_pipeline(
148            device,
149            module.as_ref(),
150            pipeline_layout.as_ref(),
151            supports_indirect_first_instance,
152            write_d3d12_special_constants,
153            instance_flags,
154        )?;
155        let pipeline = guard(pipeline, |pipeline| unsafe {
156            device.destroy_compute_pipeline(pipeline)
157        });
158
159        // Error returns after we start consuming guards could bypass resource cleanup.
160        #[deny(clippy::question_mark_used)]
161        Ok(Self {
162            module: ScopeGuard::into_inner(module),
163            metadata_bind_group_layout: ScopeGuard::into_inner(metadata_bind_group_layout),
164            src_bind_group_layout: ScopeGuard::into_inner(src_bind_group_layout),
165            dst_bind_group_layout: ScopeGuard::into_inner(dst_bind_group_layout),
166            pipeline_layout: ScopeGuard::into_inner(pipeline_layout),
167            pipeline: ScopeGuard::into_inner(pipeline),
168
169            free_indirect_entries: Mutex::new(rank::BUFFER_POOL, Vec::new()),
170            free_metadata_entries: Mutex::new(rank::BUFFER_POOL, Vec::new()),
171        })
172    }
173
174    /// `Ok(None)` will only be returned if `buffer_size` is `0`.
175    pub(super) fn create_src_bind_group(
176        &self,
177        device: &dyn hal::DynDevice,
178        limits: &Limits,
179        buffer_size: u64,
180        buffer: &dyn hal::DynBuffer,
181        instance_flags: wgt::InstanceFlags,
182    ) -> Result<Option<Box<dyn hal::DynBindGroup>>, DeviceError> {
183        let binding_size = calculate_src_buffer_binding_size(buffer_size, limits);
184        let Some(binding_size) = NonZeroU64::new(binding_size) else {
185            return Ok(None);
186        };
187        let hal_desc = hal::BindGroupDescriptor {
188            label: hal_label(
189                Some("(wgpu internal) Indirect draw validation source bind group"),
190                instance_flags,
191            ),
192            layout: self.src_bind_group_layout.as_ref(),
193            entries: &[hal::BindGroupEntry {
194                binding: 0,
195                resource_index: 0,
196                count: 1,
197            }],
198            // SAFETY: We calculated the binding size to fit within the buffer.
199            buffers: &[hal::BufferBinding::new_unchecked(buffer, 0, binding_size)],
200            samplers: &[],
201            textures: &[],
202            acceleration_structures: &[],
203            external_textures: &[],
204        };
205        unsafe {
206            device
207                .create_bind_group(&hal_desc)
208                .map(Some)
209                .map_err(DeviceError::from_hal)
210        }
211    }
212
213    fn acquire_dst_entry(
214        &self,
215        device: &dyn hal::DynDevice,
216        instance_flags: wgt::InstanceFlags,
217    ) -> Result<BufferPoolEntry, hal::DeviceError> {
218        let mut free_buffers = self.free_indirect_entries.lock();
219        match free_buffers.pop() {
220            Some(buffer) => Ok(buffer),
221            None => {
222                let usage = wgt::BufferUses::INDIRECT | wgt::BufferUses::STORAGE_READ_WRITE;
223                create_buffer_and_bind_group(
224                    device,
225                    usage,
226                    self.dst_bind_group_layout.as_ref(),
227                    hal_label(Some("(wgpu internal) Indirect draw validation destination buffer"), instance_flags),
228                    hal_label(Some("(wgpu internal) Indirect draw validation destination bind group layout"), instance_flags),
229                )
230            }
231        }
232    }
233
234    fn release_dst_entries(&self, entries: impl Iterator<Item = BufferPoolEntry>) {
235        self.free_indirect_entries.lock().extend(entries);
236    }
237
238    fn acquire_metadata_entry(
239        &self,
240        device: &dyn hal::DynDevice,
241        instance_flags: wgt::InstanceFlags,
242    ) -> Result<BufferPoolEntry, hal::DeviceError> {
243        let mut free_buffers = self.free_metadata_entries.lock();
244        match free_buffers.pop() {
245            Some(buffer) => Ok(buffer),
246            None => {
247                let usage = wgt::BufferUses::COPY_DST | wgt::BufferUses::STORAGE_READ_ONLY;
248                create_buffer_and_bind_group(
249                    device,
250                    usage,
251                    self.metadata_bind_group_layout.as_ref(),
252                    hal_label(
253                        Some("(wgpu internal) Indirect draw validation metadata buffer"),
254                        instance_flags,
255                    ),
256                    hal_label(
257                        Some("(wgpu internal) Indirect draw validation metadata bind group layout"),
258                        instance_flags,
259                    ),
260                )
261            }
262        }
263    }
264
265    fn release_metadata_entries(&self, entries: impl Iterator<Item = BufferPoolEntry>) {
266        self.free_metadata_entries.lock().extend(entries);
267    }
268
269    /// Injects a compute pass that will validate all indirect draws in the current render pass.
270    pub(crate) fn inject_validation_pass(
271        &self,
272        device: &Arc<Device>,
273        snatch_guard: &SnatchGuard,
274        resources: &mut DrawResources,
275        temp_resources: &mut Vec<TempResource>,
276        encoder: &mut dyn hal::DynCommandEncoder,
277        batcher: DrawBatcher,
278    ) -> Result<(), RenderPassErrorInner> {
279        let mut batches = batcher.batches;
280
281        if batches.is_empty() {
282            return Ok(());
283        }
284
285        let max_staging_buffer_size = 1 << 26; // ~67MiB
286
287        let mut staging_buffers = Vec::new();
288
289        let mut current_size = 0;
290        for batch in batches.values_mut() {
291            let data = batch.metadata();
292            let offset = if current_size + data.len() > max_staging_buffer_size {
293                let staging_buffer =
294                    StagingBuffer::new(device, NonZeroU64::new(current_size as u64).unwrap())?;
295                staging_buffers.push(staging_buffer);
296                current_size = data.len();
297                0
298            } else {
299                let offset = current_size;
300                current_size += data.len();
301                offset as u64
302            };
303            batch.staging_buffer_index = staging_buffers.len();
304            batch.staging_buffer_offset = offset;
305        }
306        if current_size != 0 {
307            let staging_buffer =
308                StagingBuffer::new(device, NonZeroU64::new(current_size as u64).unwrap())?;
309            staging_buffers.push(staging_buffer);
310        }
311
312        for batch in batches.values() {
313            let data = batch.metadata();
314            let staging_buffer = &mut staging_buffers[batch.staging_buffer_index];
315            unsafe {
316                staging_buffer.write_with_offset(
317                    data,
318                    0,
319                    batch.staging_buffer_offset as isize,
320                    data.len(),
321                )
322            };
323        }
324
325        let staging_buffers: Vec<_> = staging_buffers
326            .into_iter()
327            .map(|buffer| buffer.flush())
328            .collect();
329
330        let mut current_metadata_entry = None;
331        for batch in batches.values_mut() {
332            let data = batch.metadata();
333            let (metadata_resource_index, metadata_buffer_offset) =
334                resources.get_metadata_subrange(data.len() as u64, &mut current_metadata_entry)?;
335            batch.metadata_resource_index = metadata_resource_index;
336            batch.metadata_buffer_offset = metadata_buffer_offset;
337        }
338
339        let buffer_barrier_scratch = &mut BufferBarrierScratch::new();
340        let unique_index_scratch = &mut UniqueIndexScratch::new();
341
342        BufferBarriers::new(buffer_barrier_scratch)
343            .extend(
344                batches
345                    .values()
346                    .map(|batch| batch.staging_buffer_index)
347                    .unique(unique_index_scratch)
348                    .map(|index| hal::BufferBarrier {
349                        buffer: staging_buffers[index].raw(),
350                        usage: hal::StateTransition {
351                            from: wgt::BufferUses::MAP_WRITE,
352                            to: wgt::BufferUses::COPY_SRC,
353                        },
354                    }),
355            )
356            .extend(
357                batches
358                    .values()
359                    .map(|batch| batch.metadata_resource_index)
360                    .unique(unique_index_scratch)
361                    .map(|index| hal::BufferBarrier {
362                        buffer: resources.get_metadata_buffer(index),
363                        usage: hal::StateTransition {
364                            from: wgt::BufferUses::STORAGE_READ_ONLY,
365                            to: wgt::BufferUses::COPY_DST,
366                        },
367                    }),
368            )
369            .encode(encoder);
370
371        for batch in batches.values() {
372            let data = batch.metadata();
373            let data_size = NonZeroU64::new(data.len() as u64).unwrap();
374
375            let staging_buffer = &staging_buffers[batch.staging_buffer_index];
376
377            let metadata_buffer = resources.get_metadata_buffer(batch.metadata_resource_index);
378
379            unsafe {
380                encoder.copy_buffer_to_buffer(
381                    staging_buffer.raw(),
382                    metadata_buffer,
383                    &[hal::BufferCopy {
384                        src_offset: batch.staging_buffer_offset,
385                        dst_offset: batch.metadata_buffer_offset,
386                        size: data_size,
387                    }],
388                );
389            }
390        }
391
392        for staging_buffer in staging_buffers {
393            temp_resources.push(TempResource::StagingBuffer(staging_buffer));
394        }
395
396        BufferBarriers::new(buffer_barrier_scratch)
397            .extend(
398                batches
399                    .values()
400                    .map(|batch| batch.metadata_resource_index)
401                    .unique(unique_index_scratch)
402                    .map(|index| hal::BufferBarrier {
403                        buffer: resources.get_metadata_buffer(index),
404                        usage: hal::StateTransition {
405                            from: wgt::BufferUses::COPY_DST,
406                            to: wgt::BufferUses::STORAGE_READ_ONLY,
407                        },
408                    }),
409            )
410            .extend(
411                batches
412                    .values()
413                    .map(|batch| batch.dst_resource_index)
414                    .unique(unique_index_scratch)
415                    .map(|index| hal::BufferBarrier {
416                        buffer: resources.get_dst_buffer(index),
417                        usage: hal::StateTransition {
418                            from: wgt::BufferUses::INDIRECT,
419                            to: wgt::BufferUses::STORAGE_READ_WRITE,
420                        },
421                    }),
422            )
423            .encode(encoder);
424
425        let desc = hal::ComputePassDescriptor {
426            label: hal_label(
427                Some("(wgpu internal) Indirect draw validation pass"),
428                device.instance_flags,
429            ),
430            timestamp_writes: None,
431        };
432        unsafe {
433            encoder.begin_compute_pass(&desc);
434        }
435        unsafe {
436            encoder.set_compute_pipeline(self.pipeline.as_ref());
437        }
438
439        for batch in batches.values() {
440            let pipeline_layout = self.pipeline_layout.as_ref();
441
442            let metadata_start =
443                (batch.metadata_buffer_offset / size_of::<MetadataEntry>() as u64) as u32;
444            let metadata_count = batch.entries.len() as u32;
445            unsafe {
446                encoder.set_immediates(pipeline_layout, 0, &[metadata_start, metadata_count]);
447            }
448
449            let metadata_bind_group =
450                resources.get_metadata_bind_group(batch.metadata_resource_index);
451            unsafe {
452                encoder.set_bind_group(pipeline_layout, 0, metadata_bind_group, &[]);
453            }
454
455            // Make sure the indirect buffer is still valid.
456            batch.src_buffer.try_raw(snatch_guard)?;
457
458            let src_bind_group = batch
459                .src_buffer
460                .indirect_validation_bind_groups
461                .get(snatch_guard)
462                .unwrap()
463                .draw
464                .as_ref();
465            unsafe {
466                encoder.set_bind_group(
467                    pipeline_layout,
468                    1,
469                    src_bind_group,
470                    &[u64_offset_to_u32_offset(batch.src_dynamic_offset)],
471                );
472            }
473
474            let dst_bind_group = resources.get_dst_bind_group(batch.dst_resource_index);
475            unsafe {
476                encoder.set_bind_group(pipeline_layout, 2, dst_bind_group, &[]);
477            }
478
479            unsafe {
480                encoder.dispatch_workgroups([(batch.entries.len() as u32).div_ceil(64), 1, 1]);
481            }
482        }
483
484        unsafe {
485            encoder.end_compute_pass();
486        }
487
488        BufferBarriers::new(buffer_barrier_scratch)
489            .extend(
490                batches
491                    .values()
492                    .map(|batch| batch.dst_resource_index)
493                    .unique(unique_index_scratch)
494                    .map(|index| hal::BufferBarrier {
495                        buffer: resources.get_dst_buffer(index),
496                        usage: hal::StateTransition {
497                            from: wgt::BufferUses::STORAGE_READ_WRITE,
498                            to: wgt::BufferUses::INDIRECT,
499                        },
500                    }),
501            )
502            .encode(encoder);
503
504        Ok(())
505    }
506
507    pub(super) fn dispose(self, device: &dyn hal::DynDevice) {
508        let Draw {
509            module,
510            metadata_bind_group_layout,
511            src_bind_group_layout,
512            dst_bind_group_layout,
513            pipeline_layout,
514            pipeline,
515
516            free_indirect_entries,
517            free_metadata_entries,
518        } = self;
519
520        for entry in free_indirect_entries.into_inner().drain(..) {
521            unsafe {
522                device.destroy_bind_group(entry.bind_group);
523                device.destroy_buffer(entry.buffer);
524            }
525        }
526
527        for entry in free_metadata_entries.into_inner().drain(..) {
528            unsafe {
529                device.destroy_bind_group(entry.bind_group);
530                device.destroy_buffer(entry.buffer);
531            }
532        }
533
534        unsafe {
535            device.destroy_compute_pipeline(pipeline);
536            device.destroy_pipeline_layout(pipeline_layout);
537            device.destroy_bind_group_layout(metadata_bind_group_layout);
538            device.destroy_bind_group_layout(src_bind_group_layout);
539            device.destroy_bind_group_layout(dst_bind_group_layout);
540            device.destroy_shader_module(module);
541        }
542    }
543}
544
545fn create_validation_module(
546    device: &dyn hal::DynDevice,
547    instance_flags: wgt::InstanceFlags,
548) -> Result<Box<dyn hal::DynShaderModule>, CreateIndirectValidationPipelineError> {
549    let src = include_str!("./validate_draw.wgsl");
550
551    #[cfg(feature = "wgsl")]
552    let module = naga::front::wgsl::parse_str(src).map_err(|inner| {
553        CreateShaderModuleError::Parsing(naga::error::ShaderError {
554            source: src.to_string(),
555            label: None,
556            inner: Box::new(inner),
557        })
558    })?;
559    #[cfg(not(feature = "wgsl"))]
560    #[allow(clippy::diverging_sub_expression)]
561    let module = panic!("Indirect validation requires the wgsl feature flag to be enabled!");
562
563    let info = crate::device::create_validator(
564        wgt::Features::IMMEDIATES,
565        wgt::DownlevelFlags::empty(),
566        naga::valid::ValidationFlags::all(),
567    )
568    .validate(&module)
569    .map_err(|inner| {
570        CreateShaderModuleError::Validation(naga::error::ShaderError {
571            source: src.to_string(),
572            label: None,
573            inner,
574        })
575    })?;
576    let hal_shader = hal::ShaderInput::Naga(hal::NagaShader {
577        module: alloc::borrow::Cow::Owned(module),
578        info,
579        debug_source: None,
580    });
581    let hal_desc = hal::ShaderModuleDescriptor {
582        label: hal_label(
583            Some("(wgpu internal) Indirect draw validation shader module"),
584            instance_flags,
585        ),
586        runtime_checks: wgt::ShaderRuntimeChecks::unchecked(),
587    };
588    let module = unsafe { device.create_shader_module(&hal_desc, hal_shader) }.map_err(
589        |error| match error {
590            hal::ShaderError::Device(error) => {
591                CreateShaderModuleError::Device(DeviceError::from_hal(error))
592            }
593            hal::ShaderError::Compilation(ref msg) => {
594                log::error!("Shader error: {msg}");
595                CreateShaderModuleError::Generation
596            }
597        },
598    )?;
599
600    Ok(module)
601}
602
603fn create_validation_pipeline(
604    device: &dyn hal::DynDevice,
605    module: &dyn hal::DynShaderModule,
606    pipeline_layout: &dyn hal::DynPipelineLayout,
607    supports_indirect_first_instance: bool,
608    write_d3d12_special_constants: bool,
609    instance_flags: wgt::InstanceFlags,
610) -> Result<Box<dyn hal::DynComputePipeline>, CreateIndirectValidationPipelineError> {
611    let pipeline_desc = hal::ComputePipelineDescriptor {
612        label: hal_label(
613            Some("(wgpu internal) Indirect draw validation pipeline"),
614            instance_flags,
615        ),
616        layout: pipeline_layout,
617        stage: hal::ProgrammableStage {
618            module,
619            entry_point: "main",
620            constants: &hashbrown::HashMap::from([
621                (
622                    "supports_indirect_first_instance".to_string(),
623                    f64::from(supports_indirect_first_instance),
624                ),
625                (
626                    "write_d3d12_special_constants".to_string(),
627                    f64::from(write_d3d12_special_constants),
628                ),
629            ]),
630            zero_initialize_workgroup_memory: false,
631        },
632        cache: None,
633    };
634    let pipeline =
635        unsafe { device.create_compute_pipeline(&pipeline_desc) }.map_err(|err| match err {
636            hal::PipelineError::Device(error) => {
637                CreateComputePipelineError::Device(DeviceError::from_hal(error))
638            }
639            hal::PipelineError::Linkage(_stages, msg) => CreateComputePipelineError::Internal(msg),
640            hal::PipelineError::EntryPoint(_stage) => CreateComputePipelineError::Internal(
641                crate::device::ENTRYPOINT_FAILURE_ERROR.to_string(),
642            ),
643            hal::PipelineError::PipelineConstants(_, error) => {
644                CreateComputePipelineError::PipelineConstants(error)
645            }
646        })?;
647
648    Ok(pipeline)
649}
650
651fn create_bind_group_layout(
652    device: &dyn hal::DynDevice,
653    read_only: bool,
654    has_dynamic_offset: bool,
655    min_binding_size: wgt::BufferSize,
656    label: Option<&'static str>,
657) -> Result<Box<dyn hal::DynBindGroupLayout>, CreateIndirectValidationPipelineError> {
658    let bind_group_layout_desc = hal::BindGroupLayoutDescriptor {
659        label,
660        flags: hal::BindGroupLayoutFlags::empty(),
661        entries: &[wgt::BindGroupLayoutEntry {
662            binding: 0,
663            visibility: wgt::ShaderStages::COMPUTE,
664            ty: wgt::BindingType::Buffer {
665                ty: wgt::BufferBindingType::Storage { read_only },
666                has_dynamic_offset,
667                min_binding_size: Some(min_binding_size),
668            },
669            count: None,
670        }],
671    };
672    let bind_group_layout = unsafe {
673        device
674            .create_bind_group_layout(&bind_group_layout_desc)
675            .map_err(DeviceError::from_hal)?
676    };
677
678    Ok(bind_group_layout)
679}
680
681/// Returns the largest binding size that when combined with dynamic offsets can address the whole buffer.
682fn calculate_src_buffer_binding_size(buffer_size: u64, limits: &Limits) -> u64 {
683    let max_storage_buffer_binding_size = limits.max_storage_buffer_binding_size;
684    let min_storage_buffer_offset_alignment = limits.min_storage_buffer_offset_alignment as u64;
685
686    if buffer_size <= max_storage_buffer_binding_size {
687        buffer_size
688    } else {
689        let buffer_rem = buffer_size % min_storage_buffer_offset_alignment;
690        let binding_rem = max_storage_buffer_binding_size % min_storage_buffer_offset_alignment;
691
692        // Can the buffer remainder fit in the binding remainder?
693        // If so, align max binding size and add buffer remainder
694        if buffer_rem <= binding_rem {
695            max_storage_buffer_binding_size - binding_rem + buffer_rem
696        }
697        // If not, align max binding size, shorten it by a chunk and add buffer remainder
698        else {
699            max_storage_buffer_binding_size - binding_rem - min_storage_buffer_offset_alignment
700                + buffer_rem
701        }
702    }
703}
704
705/// Splits the given `offset` into a dynamic offset & offset.
706fn calculate_src_offsets(
707    buffer_size: u64,
708    limits: &Limits,
709    offset: u64,
710    data_size: u64,
711) -> (u64, u64) {
712    const MAX_DATA_SIZE: u64 = 20; // indexed indirect draw params are 20B
713    let binding_size = calculate_src_buffer_binding_size(buffer_size, limits);
714    let min_storage_buffer_offset_alignment = limits.min_storage_buffer_offset_alignment as u64;
715
716    assert!([16, MAX_DATA_SIZE].contains(&data_size));
717    assert!([32, 64, 128, 256].contains(&min_storage_buffer_offset_alignment));
718    assert!(buffer_size >= data_size);
719    assert!(offset <= buffer_size - data_size);
720    assert!(binding_size <= buffer_size);
721
722    // Invariants that the outputs of this function must satisfy:
723    // - out_dynamic_offset + out_offset = offset
724    // - out_dynamic_offset % min_storage_buffer_offset_alignment = 0
725    // - out_dynamic_offset + binding_size <= buffer_size
726    // - out_offset + data_size <= binding_size
727
728    // Align the max offset in the binding and treat it as the stride between
729    // dynamic offsets.
730    //
731    // `dynamic_offset_stride` could just be `min_storage_buffer_offset_alignment`
732    // but we want to make it as large as possible since setting dynamic
733    // offsets requires extra calls to setBindGroup and then to dispatch,
734    // calls which we want to minimize.
735    //
736    // Use `MAX_DATA_SIZE` instead of the actual `data_size` so that the
737    // resulting stride is the same for both indexed and non-indexed draw calls,
738    // reducing the likelihood of `out_dynamic_offset` being different.
739    let dynamic_offset_stride = binding_size.saturating_sub(MAX_DATA_SIZE)
740        / min_storage_buffer_offset_alignment
741        * min_storage_buffer_offset_alignment;
742    if dynamic_offset_stride == 0 {
743        return (0, offset);
744    }
745
746    let max_dynamic_offset = buffer_size - binding_size;
747    let out_dynamic_offset =
748        max_dynamic_offset.min(offset / dynamic_offset_stride * dynamic_offset_stride);
749    let out_offset = offset - out_dynamic_offset;
750
751    (out_dynamic_offset, out_offset)
752}
753
754#[derive(Debug)]
755struct BufferPoolEntry {
756    buffer: Box<dyn hal::DynBuffer>,
757    bind_group: Box<dyn hal::DynBindGroup>,
758}
759
760fn create_buffer_and_bind_group(
761    device: &dyn hal::DynDevice,
762    usage: wgt::BufferUses,
763    bind_group_layout: &dyn hal::DynBindGroupLayout,
764    buffer_label: Option<&'static str>,
765    bind_group_label: Option<&'static str>,
766) -> Result<BufferPoolEntry, hal::DeviceError> {
767    let buffer_desc = hal::BufferDescriptor {
768        label: buffer_label,
769        size: BUFFER_SIZE.get(),
770        usage,
771        memory_flags: hal::MemoryFlags::empty(),
772    };
773    let buffer = unsafe { device.create_buffer(&buffer_desc) }?;
774    let bind_group_desc = hal::BindGroupDescriptor {
775        label: bind_group_label,
776        layout: bind_group_layout,
777        entries: &[hal::BindGroupEntry {
778            binding: 0,
779            resource_index: 0,
780            count: 1,
781        }],
782        // SAFETY: We just created the buffer with this size.
783        buffers: &[hal::BufferBinding::new_unchecked(
784            buffer.as_ref(),
785            0,
786            BUFFER_SIZE,
787        )],
788        samplers: &[],
789        textures: &[],
790        acceleration_structures: &[],
791        external_textures: &[],
792    };
793    let bind_group = unsafe { device.create_bind_group(&bind_group_desc) }?;
794    Ok(BufferPoolEntry { buffer, bind_group })
795}
796
797#[derive(Clone)]
798struct CurrentEntry {
799    index: usize,
800    offset: u64,
801}
802
803/// Holds all command buffer-level resources that are needed to validate indirect draws.
804pub(crate) struct DrawResources {
805    device: Arc<Device>,
806    dst_entries: Vec<BufferPoolEntry>,
807    metadata_entries: Vec<BufferPoolEntry>,
808}
809
810impl Drop for DrawResources {
811    fn drop(&mut self) {
812        if let Some(ref indirect_validation) = self.device.indirect_validation {
813            let indirect_draw_validation = &indirect_validation.draw;
814            indirect_draw_validation.release_dst_entries(self.dst_entries.drain(..));
815            indirect_draw_validation.release_metadata_entries(self.metadata_entries.drain(..));
816        }
817    }
818}
819
820impl DrawResources {
821    pub(crate) fn new(device: Arc<Device>) -> Self {
822        DrawResources {
823            device,
824            dst_entries: Vec::new(),
825            metadata_entries: Vec::new(),
826        }
827    }
828
829    pub(crate) fn get_dst_buffer(&self, index: usize) -> &dyn hal::DynBuffer {
830        self.dst_entries.get(index).unwrap().buffer.as_ref()
831    }
832
833    fn get_dst_bind_group(&self, index: usize) -> &dyn hal::DynBindGroup {
834        self.dst_entries.get(index).unwrap().bind_group.as_ref()
835    }
836
837    fn get_metadata_buffer(&self, index: usize) -> &dyn hal::DynBuffer {
838        self.metadata_entries.get(index).unwrap().buffer.as_ref()
839    }
840
841    fn get_metadata_bind_group(&self, index: usize) -> &dyn hal::DynBindGroup {
842        self.metadata_entries
843            .get(index)
844            .unwrap()
845            .bind_group
846            .as_ref()
847    }
848
849    fn get_dst_subrange(
850        &mut self,
851        size: u64,
852        current_entry: &mut Option<CurrentEntry>,
853    ) -> Result<(usize, u64), DeviceError> {
854        let indirect_draw_validation = &self.device.indirect_validation.as_ref().unwrap().draw;
855        let ensure_entry = |index: usize| {
856            if self.dst_entries.len() <= index {
857                let entry = indirect_draw_validation
858                    .acquire_dst_entry(self.device.raw(), self.device.instance_flags)?;
859                self.dst_entries.push(entry);
860            }
861            Ok(())
862        };
863        let entry_data = Self::get_subrange_impl(ensure_entry, current_entry, size)?;
864        Ok((entry_data.index, entry_data.offset))
865    }
866
867    fn get_metadata_subrange(
868        &mut self,
869        size: u64,
870        current_entry: &mut Option<CurrentEntry>,
871    ) -> Result<(usize, u64), DeviceError> {
872        let indirect_draw_validation = &self.device.indirect_validation.as_ref().unwrap().draw;
873        let ensure_entry = |index: usize| {
874            if self.metadata_entries.len() <= index {
875                let entry = indirect_draw_validation
876                    .acquire_metadata_entry(self.device.raw(), self.device.instance_flags)?;
877                self.metadata_entries.push(entry);
878            }
879            Ok(())
880        };
881        let entry_data = Self::get_subrange_impl(ensure_entry, current_entry, size)?;
882        Ok((entry_data.index, entry_data.offset))
883    }
884
885    fn get_subrange_impl(
886        ensure_entry: impl FnOnce(usize) -> Result<(), hal::DeviceError>,
887        current_entry: &mut Option<CurrentEntry>,
888        size: u64,
889    ) -> Result<CurrentEntry, DeviceError> {
890        let index = if let Some(current_entry) = current_entry.as_mut() {
891            if current_entry.offset + size <= BUFFER_SIZE.get() {
892                let entry_data = current_entry.clone();
893                current_entry.offset += size;
894                return Ok(entry_data);
895            } else {
896                current_entry.index + 1
897            }
898        } else {
899            0
900        };
901
902        ensure_entry(index).map_err(DeviceError::from_hal)?;
903
904        let entry_data = CurrentEntry { index, offset: 0 };
905
906        *current_entry = Some(CurrentEntry {
907            index,
908            offset: size,
909        });
910
911        Ok(entry_data)
912    }
913}
914
915/// This must match the `MetadataEntry` struct used by the shader.
916#[repr(C)]
917struct MetadataEntry {
918    src_offset: u32,
919    dst_offset: u32,
920    vertex_or_index_limit: u32,
921    instance_limit: u32,
922}
923
924impl MetadataEntry {
925    fn new(
926        indexed: bool,
927        src_offset: u64,
928        dst_offset: u64,
929        vertex_or_index_limit: u64,
930        instance_limit: u64,
931    ) -> Self {
932        const U32_MAX_AS_U64: u64 = u32::MAX as u64;
933
934        let src_offset = u64_offset_to_u32_offset(src_offset);
935        let src_offset = src_offset / 4; // translate byte offset to offset in u32's
936
937        // `src_offset` needs at most 30 bits,
938        // pack `indexed` in bit 31 of `src_offset`
939        let src_offset = src_offset | ((indexed as u32) << 31);
940
941        // max value for limits since first_X and X_count indirect draw arguments are u32
942        let max_limit = U32_MAX_AS_U64 + U32_MAX_AS_U64; // 1 11111111 11111111 11111111 11111110
943
944        let vertex_or_index_limit = vertex_or_index_limit.min(max_limit);
945        let vertex_or_index_limit_bit_32 = (vertex_or_index_limit >> 32) as u32; // extract bit 32
946        let vertex_or_index_limit = vertex_or_index_limit as u32; // truncate the limit to a u32
947
948        let instance_limit = instance_limit.min(max_limit);
949        let instance_limit_bit_32 = (instance_limit >> 32) as u32; // extract bit 32
950        let instance_limit = instance_limit as u32; // truncate the limit to a u32
951
952        let dst_offset = u64_offset_to_u32_offset(dst_offset);
953        let dst_offset = dst_offset / 4; // translate byte offset to offset in u32's
954
955        // `dst_offset` needs at most 30 bits,
956        // pack `vertex_or_index_limit_bit_32` in bit 30 of `dst_offset` and
957        // pack `instance_limit_bit_32` in bit 31 of `dst_offset`
958        let dst_offset =
959            dst_offset | (vertex_or_index_limit_bit_32 << 30) | (instance_limit_bit_32 << 31);
960
961        Self {
962            src_offset,
963            dst_offset,
964            vertex_or_index_limit,
965            instance_limit,
966        }
967    }
968}
969
970struct DrawIndirectValidationBatch {
971    src_buffer: Arc<crate::resource::Buffer>,
972    src_dynamic_offset: u64,
973    dst_resource_index: usize,
974    entries: Vec<MetadataEntry>,
975
976    staging_buffer_index: usize,
977    staging_buffer_offset: u64,
978    metadata_resource_index: usize,
979    metadata_buffer_offset: u64,
980}
981
982impl DrawIndirectValidationBatch {
983    /// Data to be written to the metadata buffer.
984    fn metadata(&self) -> &[u8] {
985        unsafe {
986            core::slice::from_raw_parts(
987                self.entries.as_ptr().cast::<u8>(),
988                self.entries.len() * size_of::<MetadataEntry>(),
989            )
990        }
991    }
992}
993
994/// Accumulates all needed data needed to validate indirect draws.
995pub(crate) struct DrawBatcher {
996    batches: FastHashMap<(TrackerIndex, u64, usize), DrawIndirectValidationBatch>,
997    current_dst_entry: Option<CurrentEntry>,
998}
999
1000impl DrawBatcher {
1001    pub(crate) fn new() -> Self {
1002        Self {
1003            batches: FastHashMap::default(),
1004            current_dst_entry: None,
1005        }
1006    }
1007
1008    /// Add an indirect draw to be validated.
1009    ///
1010    /// Returns the index of the indirect buffer in `indirect_draw_validation_resources`
1011    /// and the offset to be used for the draw.
1012    pub(crate) fn add<'a>(
1013        &mut self,
1014        indirect_draw_validation_resources: &'a mut DrawResources,
1015        device: &Device,
1016        src_buffer: &Arc<crate::resource::Buffer>,
1017        offset: u64,
1018        family: crate::command::DrawCommandFamily,
1019        vertex_or_index_limit: u64,
1020        instance_limit: u64,
1021    ) -> Result<(usize, u64), DeviceError> {
1022        let stride = crate::command::get_dst_stride_of_indirect_args(device.backend(), family);
1023
1024        let (dst_resource_index, dst_offset) = indirect_draw_validation_resources
1025            .get_dst_subrange(stride, &mut self.current_dst_entry)?;
1026
1027        let buffer_size = src_buffer.size;
1028        let limits = device.adapter.limits();
1029        let data_size = get_src_stride_of_indirect_args(family);
1030        let (src_dynamic_offset, src_offset) =
1031            calculate_src_offsets(buffer_size, &limits, offset, data_size);
1032
1033        let src_buffer_tracker_index = src_buffer.tracker_index();
1034
1035        let entry = MetadataEntry::new(
1036            family == crate::command::DrawCommandFamily::DrawIndexed,
1037            src_offset,
1038            dst_offset,
1039            vertex_or_index_limit,
1040            instance_limit,
1041        );
1042
1043        match self.batches.entry((
1044            src_buffer_tracker_index,
1045            src_dynamic_offset,
1046            dst_resource_index,
1047        )) {
1048            hashbrown::hash_map::Entry::Occupied(mut occupied_entry) => {
1049                occupied_entry.get_mut().entries.push(entry)
1050            }
1051            hashbrown::hash_map::Entry::Vacant(vacant_entry) => {
1052                vacant_entry.insert(DrawIndirectValidationBatch {
1053                    src_buffer: src_buffer.clone(),
1054                    src_dynamic_offset,
1055                    dst_resource_index,
1056                    entries: vec![entry],
1057
1058                    // these will be initialized once we accumulated all entries for the batch
1059                    staging_buffer_index: 0,
1060                    staging_buffer_offset: 0,
1061                    metadata_resource_index: 0,
1062                    metadata_buffer_offset: 0,
1063                });
1064            }
1065        }
1066
1067        Ok((dst_resource_index, dst_offset))
1068    }
1069}
1070
1071/// Indirect draw validation doesn't support u64 offsets.
1072///
1073/// This fn should never panic due to the assert in [`Draw::new`].
1074fn u64_offset_to_u32_offset(offset: u64) -> u32 {
1075    offset.try_into().unwrap()
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081
1082    #[test]
1083    fn calculate_src_offsets_test() {
1084        const MBUS: u64 = 256 << 20; // default max_buffer_size
1085        const MBIS: u64 = 128 << 20; // default max_storage_buffer_binding_size
1086
1087        #[rustfmt::skip]
1088        let cases: &[(u64, u64, u32, u64, u64, u64, u64)] = &[
1089            // (buffer_size, max_binding_size, offset_alignment, data_size, offset, out_dynamic_offset, out_offset)
1090
1091            // data at start of buffer
1092            (MBUS, MBIS, 32,  16, 0, 0, 0),
1093            // data at end of buffer
1094            (MBUS, MBIS, 32,  16, MBUS - 16, MBIS, MBIS - 16),
1095            // data at end of buffer, where buffer_size % alignment != 0
1096            (MBUS + 4, MBUS, 32, 16, MBUS + 4 - 16, 32, MBUS - 32 + 4 - 16),
1097            // data before/straddling/after middle of the buffer with
1098            // max binding size limit being half of the buffer size
1099            // alignment = 32
1100            (512, 256, 32,  16, 240, 224, 16), // before middle
1101            (512, 256, 32,  16, 248, 224, 24), // straddling middle
1102            (512, 256, 32,  16, 256, 224, 32), // after middle
1103            // alignment = 64
1104            (512, 256, 64,  16, 240, 192, 48), // before middle
1105            (512, 256, 64,  16, 248, 192, 56), // straddling middle
1106            (512, 256, 64,  16, 256, 192, 64), // after middle
1107            // alignment = 128
1108            (512, 256, 128, 16, 240, 128, 112), // before middle
1109            (512, 256, 128, 16, 248, 128, 120), // straddling middle
1110            (512, 256, 128, 16, 256, 256, 0), // after middle
1111            // as above but with data_size = 20
1112            // alignment = 32
1113            (512, 256, 32,  20, 236, 224, 12), // before middle
1114            (512, 256, 32,  20, 244, 224, 20), // straddling middle
1115            (512, 256, 32,  20, 252, 224, 28), // after middle
1116            // alignment = 64
1117            (512, 256, 64,  20, 236, 192, 44), // before middle
1118            (512, 256, 64,  20, 244, 192, 52), // straddling middle
1119            (512, 256, 64,  20, 252, 192, 60), // after middle
1120            // alignment = 128
1121            (512, 256, 128, 20, 236, 128, 108), // before middle
1122            (512, 256, 128, 20, 244, 128, 116), // straddling middle
1123            (512, 256, 128, 20, 252, 128, 124), // after middle
1124        ];
1125
1126        for &(
1127            buffer_size,
1128            max_storage_buffer_binding_size,
1129            min_storage_buffer_offset_alignment,
1130            data_size,
1131            offset,
1132            expected_out_dynamic_offset,
1133            expected_out_offset,
1134        ) in cases
1135        {
1136            let limits = Limits {
1137                max_storage_buffer_binding_size,
1138                min_storage_buffer_offset_alignment,
1139                ..Limits::default()
1140            };
1141            let (out_dynamic_offset, out_offset) =
1142                calculate_src_offsets(buffer_size, &limits, offset, data_size);
1143            let binding_size = calculate_src_buffer_binding_size(buffer_size, &limits);
1144            // check invariants
1145            assert_eq!(out_dynamic_offset + out_offset, offset);
1146            assert_eq!(
1147                out_dynamic_offset % min_storage_buffer_offset_alignment as u64,
1148                0
1149            );
1150            assert!(out_dynamic_offset + binding_size <= buffer_size);
1151            assert!(out_offset + data_size <= binding_size);
1152            // check output matches
1153            assert_eq!(
1154                (out_dynamic_offset, out_offset),
1155                (expected_out_dynamic_offset, expected_out_offset),
1156                "buffer_size={buffer_size} \
1157                 max_binding_size={max_storage_buffer_binding_size} \
1158                 offset_alignment={min_storage_buffer_offset_alignment} \
1159                 data_size={data_size} \
1160                 offset={offset}"
1161            );
1162        }
1163    }
1164}