deduplicate vertex buffer size validation (#7124)

This commit is contained in:
Teodor Tanasoaia 2025-02-14 10:33:28 +01:00 committed by GitHub
parent bae0e70e8d
commit ef0e6f782a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 134 additions and 216 deletions

View File

@ -111,91 +111,6 @@ use super::{
DrawKind,
};
/// <https://gpuweb.github.io/gpuweb/#dom-gpurendercommandsmixin-draw>
fn validate_draw(
vertex: &[Option<VertexState>],
step: &[VertexStep],
first_vertex: u32,
vertex_count: u32,
first_instance: u32,
instance_count: u32,
) -> Result<(), DrawError> {
let vertices_end = first_vertex as u64 + vertex_count as u64;
let instances_end = first_instance as u64 + instance_count as u64;
for (idx, (vbs, step)) in vertex.iter().zip(step).enumerate() {
let Some(vbs) = vbs else {
continue;
};
let stride_count = match step.mode {
wgt::VertexStepMode::Vertex => vertices_end,
wgt::VertexStepMode::Instance => instances_end,
};
if stride_count == 0 {
continue;
}
let offset = (stride_count - 1) * step.stride + step.last_stride;
let limit = vbs.range.end - vbs.range.start;
if offset > limit {
return Err(DrawError::VertexOutOfBounds {
step_mode: step.mode,
offset,
limit,
slot: idx as u32,
});
}
}
Ok(())
}
// See https://gpuweb.github.io/gpuweb/#dom-gpurendercommandsmixin-drawindexed
fn validate_indexed_draw(
vertex: &[Option<VertexState>],
step: &[VertexStep],
index_state: &IndexState,
first_index: u32,
index_count: u32,
first_instance: u32,
instance_count: u32,
) -> Result<(), DrawError> {
let last_index = first_index as u64 + index_count as u64;
let index_limit = index_state.limit();
if last_index > index_limit {
return Err(DrawError::IndexBeyondLimit {
last_index,
index_limit,
});
}
let stride_count = first_instance as u64 + instance_count as u64;
for (idx, (vbs, step)) in vertex.iter().zip(step).enumerate() {
let Some(vbs) = vbs else {
continue;
};
if stride_count == 0 || step.mode != wgt::VertexStepMode::Instance {
continue;
}
let offset = (stride_count - 1) * step.stride + step.last_stride;
let limit = vbs.range.end - vbs.range.start;
if offset > limit {
return Err(DrawError::VertexOutOfBounds {
step_mode: step.mode,
offset,
limit,
slot: idx as u32,
});
}
}
Ok(())
}
/// Describes a [`RenderBundleEncoder`].
#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
@ -359,7 +274,7 @@ impl RenderBundleEncoder {
trackers: RenderBundleScope::new(),
pipeline: None,
bind: (0..hal::MAX_BIND_GROUPS).map(|_| None).collect(),
vertex: (0..hal::MAX_VERTEX_BUFFERS).map(|_| None).collect(),
vertex: Default::default(),
index: None,
flat_dynamic_offsets: Vec::new(),
device: device.clone(),
@ -781,14 +696,9 @@ fn draw(
let pipeline = state.pipeline()?;
let used_bind_groups = pipeline.used_bind_groups;
validate_draw(
&state.vertex[..],
&pipeline.steps,
first_vertex,
vertex_count,
first_instance,
instance_count,
)?;
let vertex_limits = super::VertexLimits::new(state.vertex_buffer_sizes(), &pipeline.steps);
vertex_limits.validate_vertex_limit(first_vertex, vertex_count)?;
vertex_limits.validate_instance_limit(first_instance, instance_count)?;
if instance_count > 0 && vertex_count > 0 {
state.flush_vertices();
@ -819,15 +729,18 @@ fn draw_indexed(
None => return Err(DrawError::MissingIndexBuffer.into()),
};
validate_indexed_draw(
&state.vertex[..],
&pipeline.steps,
index,
first_index,
index_count,
first_instance,
instance_count,
)?;
let vertex_limits = super::VertexLimits::new(state.vertex_buffer_sizes(), &pipeline.steps);
let last_index = first_index as u64 + index_count as u64;
let index_limit = index.limit();
if last_index > index_limit {
return Err(DrawError::IndexBeyondLimit {
last_index,
index_limit,
}
.into());
}
vertex_limits.validate_instance_limit(first_instance, instance_count)?;
if instance_count > 0 && index_count > 0 {
state.flush_index();
@ -1330,7 +1243,7 @@ struct State {
bind: ArrayVec<Option<BindState>, { hal::MAX_BIND_GROUPS }>,
/// The state of each vertex buffer slot.
vertex: ArrayVec<Option<VertexState>, { hal::MAX_VERTEX_BUFFERS }>,
vertex: [Option<VertexState>; hal::MAX_VERTEX_BUFFERS],
/// The current index buffer, if one has been set. We flush this state
/// before indexed draw commands.
@ -1513,6 +1426,12 @@ impl State {
self.commands.extend(commands);
}
fn vertex_buffer_sizes(&self) -> impl Iterator<Item = Option<wgt::BufferAddress>> + '_ {
self.vertex
.iter()
.map(|vbs| vbs.as_ref().map(|vbs| vbs.range.end - vbs.range.start))
}
}
/// Error encountered when finishing recording a render bundle.

View File

@ -6,7 +6,6 @@ use crate::{
},
track::ResourceUsageCompatibilityError,
};
use wgt::VertexStepMode;
use thiserror::Error;
@ -35,13 +34,6 @@ pub enum DrawError {
vertex_limit: u64,
slot: u32,
},
#[error("{step_mode:?} buffer out of bounds at slot {slot}. Offset {offset} beyond limit {limit}. Did you bind the correct `Vertex` step-rate vertex buffer?")]
VertexOutOfBounds {
step_mode: VertexStepMode,
offset: u64,
limit: u64,
slot: u32,
},
#[error("Instance {last_instance} extends beyond limit {instance_limit} imposed by the buffer in slot {slot}. Did you bind the correct `Instance` step-rate vertex buffer?")]
InstanceBeyondLimit {
last_instance: u64,

View File

@ -3,7 +3,7 @@ use crate::command::{
validate_and_begin_occlusion_query, validate_and_begin_pipeline_statistics_query,
};
use crate::init_tracker::BufferInitTrackerAction;
use crate::pipeline::RenderPipeline;
use crate::pipeline::{RenderPipeline, VertexStep};
use crate::resource::InvalidResourceError;
use crate::snatch::SnatchGuard;
use crate::{
@ -24,7 +24,7 @@ use crate::{
global::Global,
hal_label, id,
init_tracker::{MemoryInitKind, TextureInitRange, TextureInitTrackerAction},
pipeline::{self, PipelineFlags},
pipeline::PipelineFlags,
resource::{
DestroyedResourceError, Labeled, MissingBufferUsageError, MissingTextureUsageError,
ParentDevice, QuerySet, Texture, TextureView, TextureViewNotRenderableReason,
@ -45,7 +45,7 @@ use serde::Deserialize;
#[cfg(feature = "serde")]
use serde::Serialize;
use std::{borrow::Cow, fmt, iter, mem::size_of, num::NonZeroU32, ops::Range, str, sync::Arc};
use std::{borrow::Cow, fmt, mem::size_of, num::NonZeroU32, ops::Range, str, sync::Arc};
use super::render_command::ArcRenderCommand;
use super::{
@ -368,57 +368,45 @@ impl IndexState {
}
}
#[derive(Clone, Copy, Debug)]
struct VertexBufferState {
total_size: BufferAddress,
step: pipeline::VertexStep,
bound: bool,
}
impl VertexBufferState {
const EMPTY: Self = Self {
total_size: 0,
step: pipeline::VertexStep {
stride: 0,
last_stride: 0,
mode: VertexStepMode::Vertex,
},
bound: false,
};
}
#[derive(Debug, Default)]
struct VertexState {
inputs: ArrayVec<VertexBufferState, { hal::MAX_VERTEX_BUFFERS }>,
pub(crate) struct VertexLimits {
/// Length of the shortest vertex rate vertex buffer
vertex_limit: u64,
pub(crate) vertex_limit: u64,
/// Buffer slot which the shortest vertex rate vertex buffer is bound to
vertex_limit_slot: u32,
/// Length of the shortest instance rate vertex buffer
instance_limit: u64,
pub(crate) instance_limit: u64,
/// Buffer slot which the shortest instance rate vertex buffer is bound to
instance_limit_slot: u32,
}
impl VertexState {
fn update_limits(&mut self) {
impl VertexLimits {
pub(crate) fn new(
buffer_sizes: impl Iterator<Item = Option<BufferAddress>>,
pipeline_steps: &[VertexStep],
) -> Self {
// Implements the validation from https://gpuweb.github.io/gpuweb/#dom-gpurendercommandsmixin-draw
// Except that the formula is shuffled to extract the number of vertices in order
// to carry the bulk of the computation when changing states instead of when producing
// draws. Draw calls tend to happen at a higher frequency. Here we determine vertex
// limits that can be cheaply checked for each draw call.
self.vertex_limit = u32::MAX as u64;
self.instance_limit = u32::MAX as u64;
for (idx, vbs) in self.inputs.iter().enumerate() {
if !vbs.bound {
continue;
}
let limit = if vbs.total_size < vbs.step.last_stride {
let mut vertex_limit = u64::MAX;
let mut vertex_limit_slot = 0;
let mut instance_limit = u64::MAX;
let mut instance_limit_slot = 0;
for (idx, (buffer_size, step)) in buffer_sizes.zip(pipeline_steps).enumerate() {
let Some(buffer_size) = buffer_size else {
// Missing required vertex buffer
return Self::default();
};
let limit = if buffer_size < step.last_stride {
// The buffer cannot fit the last vertex.
0
} else {
if vbs.step.stride == 0 {
if step.stride == 0 {
// We already checked that the last stride fits, the same
// vertex will be repeated so this slot can accommodate any number of
// vertices.
@ -426,30 +414,79 @@ impl VertexState {
}
// The general case.
(vbs.total_size - vbs.step.last_stride) / vbs.step.stride + 1
(buffer_size - step.last_stride) / step.stride + 1
};
match vbs.step.mode {
match step.mode {
VertexStepMode::Vertex => {
if limit < self.vertex_limit {
self.vertex_limit = limit;
self.vertex_limit_slot = idx as _;
if limit < vertex_limit {
vertex_limit = limit;
vertex_limit_slot = idx as _;
}
}
VertexStepMode::Instance => {
if limit < self.instance_limit {
self.instance_limit = limit;
self.instance_limit_slot = idx as _;
if limit < instance_limit {
instance_limit = limit;
instance_limit_slot = idx as _;
}
}
}
}
Self {
vertex_limit,
vertex_limit_slot,
instance_limit,
instance_limit_slot,
}
}
fn reset(&mut self) {
self.inputs.clear();
self.vertex_limit = 0;
self.instance_limit = 0;
pub(crate) fn validate_vertex_limit(
&self,
first_vertex: u32,
vertex_count: u32,
) -> Result<(), DrawError> {
let last_vertex = first_vertex as u64 + vertex_count as u64;
let vertex_limit = self.vertex_limit;
if last_vertex > vertex_limit {
return Err(DrawError::VertexBeyondLimit {
last_vertex,
vertex_limit,
slot: self.vertex_limit_slot,
});
}
Ok(())
}
pub(crate) fn validate_instance_limit(
&self,
first_instance: u32,
instance_count: u32,
) -> Result<(), DrawError> {
let last_instance = first_instance as u64 + instance_count as u64;
let instance_limit = self.instance_limit;
if last_instance > instance_limit {
return Err(DrawError::InstanceBeyondLimit {
last_instance,
instance_limit,
slot: self.instance_limit_slot,
});
}
Ok(())
}
}
#[derive(Debug, Default)]
struct VertexState {
buffer_sizes: [Option<BufferAddress>; hal::MAX_VERTEX_BUFFERS],
limits: VertexLimits,
}
impl VertexState {
fn update_limits(&mut self, pipeline_steps: &[VertexStep]) {
self.limits = VertexLimits::new(self.buffer_sizes.iter().copied(), pipeline_steps);
}
}
@ -496,8 +533,12 @@ impl<'scope, 'snatch_guard, 'cmd_buf, 'raw_encoder>
}
// Determine how many vertex buffers have already been bound
let vertex_buffer_count =
self.vertex.inputs.iter().take_while(|v| v.bound).count() as u32;
let vertex_buffer_count = self
.vertex
.buffer_sizes
.iter()
.take_while(|v| v.is_some())
.count() as u32;
// Compare with the needed quantity
if vertex_buffer_count < pipeline.vertex_steps.len() as u32 {
return Err(DrawError::MissingVertexBuffer {
@ -536,7 +577,7 @@ impl<'scope, 'snatch_guard, 'cmd_buf, 'raw_encoder>
self.binder.reset();
self.pipeline = None;
self.index.reset();
self.vertex.reset();
self.vertex = Default::default();
}
}
@ -2119,23 +2160,8 @@ fn set_pipeline(
}
}
// Initialize each `vertex.inputs[i].step` from
// `pipeline.vertex_steps[i]`. Enlarge `vertex.inputs`
// as necessary to accommodate all slots in the
// pipeline. If `vertex.inputs` is longer, fill the
// extra entries with default `VertexStep`s.
while state.vertex.inputs.len() < pipeline.vertex_steps.len() {
state.vertex.inputs.push(VertexBufferState::EMPTY);
}
// This is worse as a `zip`, but it's close.
let mut steps = pipeline.vertex_steps.iter();
for input in state.vertex.inputs.iter_mut() {
input.step = steps.next().cloned().unwrap_or_default();
}
// Update vertex buffer limits.
state.vertex.update_limits();
state.vertex.update_limits(&pipeline.vertex_steps);
Ok(())
}
@ -2218,24 +2244,18 @@ fn set_vertex_buffer(
buffer.check_usage(BufferUsages::VERTEX)?;
let buf_raw = buffer.try_raw(state.snatch_guard)?;
let empty_slots = (1 + slot as usize).saturating_sub(state.vertex.inputs.len());
state
.vertex
.inputs
.extend(iter::repeat(VertexBufferState::EMPTY).take(empty_slots));
let vertex_state = &mut state.vertex.inputs[slot as usize];
//TODO: where are we checking that the offset is in bound?
vertex_state.total_size = match size {
let buffer_size = match size {
Some(s) => s.get(),
None => buffer.size - offset,
};
vertex_state.bound = true;
state.vertex.buffer_sizes[slot as usize] = Some(buffer_size);
state
.buffer_memory_init_actions
.extend(buffer.initialization_status.read().create_action(
&buffer,
offset..(offset + vertex_state.total_size),
offset..(offset + buffer_size),
MemoryInitKind::NeedsInitializedMemory,
));
@ -2247,7 +2267,9 @@ fn set_vertex_buffer(
unsafe {
hal::DynCommandEncoder::set_vertex_buffer(state.raw_encoder, slot, bb);
}
state.vertex.update_limits();
if let Some(pipeline) = state.pipeline.as_ref() {
state.vertex.update_limits(&pipeline.vertex_steps);
}
Ok(())
}
@ -2374,24 +2396,14 @@ fn draw(
state.is_ready(false)?;
let last_vertex = first_vertex as u64 + vertex_count as u64;
let vertex_limit = state.vertex.vertex_limit;
if last_vertex > vertex_limit {
return Err(DrawError::VertexBeyondLimit {
last_vertex,
vertex_limit,
slot: state.vertex.vertex_limit_slot,
});
}
let last_instance = first_instance as u64 + instance_count as u64;
let instance_limit = state.vertex.instance_limit;
if last_instance > instance_limit {
return Err(DrawError::InstanceBeyondLimit {
last_instance,
instance_limit,
slot: state.vertex.instance_limit_slot,
});
}
state
.vertex
.limits
.validate_vertex_limit(first_vertex, vertex_count)?;
state
.vertex
.limits
.validate_instance_limit(first_instance, instance_count)?;
unsafe {
if instance_count > 0 && vertex_count > 0 {
@ -2423,15 +2435,10 @@ fn draw_indexed(
index_limit,
});
}
let last_instance = first_instance as u64 + instance_count as u64;
let instance_limit = state.vertex.instance_limit;
if last_instance > instance_limit {
return Err(DrawError::InstanceBeyondLimit {
last_instance,
instance_limit,
slot: state.vertex.instance_limit_slot,
});
}
state
.vertex
.limits
.validate_instance_limit(first_instance, instance_count)?;
unsafe {
if instance_count > 0 && index_count > 0 {