Skip to content

Commit 4a5a3eb

Browse files
committed
Expose Metal MULTI_DRAW_INDIRECT_COUNT via prepass ICBs
Rebuilt on the reworked gfx-rs#9640 prepass architecture: multi_draw_*_indirect_count (draw, indexed, and mesh-task) executes through a GPU-generated indirect command buffer driven by an execution range that a deferred kernel clamps to min(count, max_count), so the draw count is never read by the CPU. The range clamp and ICB generation ride the same internal pre-pass command buffer as indirect validation. Because MULTI_DRAW_INDIRECT_COUNT is advertised per adapter but some pipelines cannot execute inside an ICB (on current Apple silicon drivers, any fragment shader that accesses a texture through direct bindings is rejected — verified empirically on M4; textures bound through argument buffers are accepted, which matches the constraint Dawn is migrating to argument buffers to lift), count draws under such pipelines fall back to a GPU-clamped copy of the argument buffer consumed by a fixed-length per-draw loop. The count still never round-trips through the CPU. A new sampled-texture count test covers the fallback; the existing count readback tests were also verified against the fallback path directly on M4 hardware. Also adds wgpu_types::INDIRECT_BUFFER_OFFSET_ALIGNMENT (review asked for a named alignment rather than a hardcoded 4), validates the count buffer offset against it, restores the deno chromium-experimental- multi-draw-indirect surface, and documents the Metal behavior on the feature flag.
1 parent 7b2115d commit 4a5a3eb

12 files changed

Lines changed: 932 additions & 176 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,14 @@ Bottom level categories:
4444

4545
### Added/New Features
4646

47+
#### General
48+
49+
- Added `wgpu_types::INDIRECT_BUFFER_OFFSET_ALIGNMENT` naming the required 4-byte alignment of indirect-argument buffer offsets, and validate that the count-buffer offset in `multi_draw_*_indirect_count` honors it. By @matthargett in [#9679](https://github.com/gfx-rs/wgpu/pull/9679).
50+
4751
#### Metal
4852

4953
- Metal now lowers fixed-count `multi_draw_indirect` / `multi_draw_indexed_indirect` (and their mesh-task counterpart) with 8 or more draws to GPU-generated indirect command buffers. The generation compute runs in the internal pre-pass command buffer that already carries indirect-draw validation, so the render pass is never interrupted. By @matthargett in [#9640](https://github.com/gfx-rs/wgpu/pull/9640).
54+
- Expose `MULTI_DRAW_INDIRECT_COUNT` on Metal when indirect command buffers are supported. `multi_draw_*_indirect_count` executes through a GPU-generated indirect command buffer with a GPU-clamped execution range; draws recorded with a pipeline that can't execute inside an ICB fall back to a series of indirect draws over GPU-clamped arguments. The draw count is never read by the CPU. By @matthargett in [#9679](https://github.com/gfx-rs/wgpu/pull/9679).
5055

5156
#### Hal
5257

deno_webgpu/adapter.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,17 @@ impl GPUAdapter {
9393
self.features.get(scope, |scope| {
9494
let features = self.instance.adapter_features(self.id);
9595
// Only expose WebGPU features, not wgpu native-only features
96-
let features = features & wgpu_types::Features::all_webgpu_mask();
97-
GPUSupportedFeatures::new(scope, features)
96+
let mut exposed_features =
97+
features & wgpu_types::Features::all_webgpu_mask();
98+
// Exception: `MULTI_DRAW_INDIRECT_COUNT` is native-only in wgpu, but
99+
// Chromium exposes the same capability to WebGPU as the experimental
100+
// feature "chromium-experimental-multi-draw-indirect", so we surface it
101+
// under that name (see `GPUSupportedFeatures` and `webidl.rs`).
102+
exposed_features.set(
103+
wgpu_types::Features::MULTI_DRAW_INDIRECT_COUNT,
104+
features.contains(wgpu_types::Features::MULTI_DRAW_INDIRECT_COUNT),
105+
);
106+
GPUSupportedFeatures::new(scope, exposed_features)
98107
})
99108
}
100109

@@ -480,7 +489,13 @@ impl GPUSupportedFeatures {
480489
let set = v8::Set::new(scope);
481490

482491
for feature in features.iter() {
483-
let key = v8::String::new(scope, feature.as_str().unwrap()).unwrap();
492+
let name = match feature {
493+
wgpu_types::Features::MULTI_DRAW_INDIRECT_COUNT => {
494+
"chromium-experimental-multi-draw-indirect"
495+
}
496+
_ => feature.as_str().unwrap(),
497+
};
498+
let key = v8::String::new(scope, name).unwrap();
484499
set.add(scope, key.into());
485500
}
486501

deno_webgpu/render_pass.rs

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,80 @@ impl GPURenderPassEncoder {
448448
self.error_handler.push_error(err);
449449
}
450450

451+
#[required(3)]
452+
#[undefined]
453+
fn multi_draw_indirect(
454+
&self,
455+
#[webidl] indirect_buffer: Ptr<GPUBuffer>,
456+
#[webidl(options(enforce_range = true))] indirect_offset: u64,
457+
#[webidl(options(enforce_range = true))] max_draw_count: u32,
458+
#[webidl] draw_count_buffer: Option<Ptr<GPUBuffer>>,
459+
#[webidl(default = 0, options(enforce_range = true))]
460+
draw_count_buffer_offset: u64,
461+
) {
462+
let err = if let Some(draw_count_buffer) = draw_count_buffer {
463+
self
464+
.instance
465+
.render_pass_multi_draw_indirect_count(
466+
&mut self.render_pass.borrow_mut(),
467+
indirect_buffer.id,
468+
indirect_offset,
469+
draw_count_buffer.id,
470+
draw_count_buffer_offset,
471+
max_draw_count,
472+
)
473+
.err()
474+
} else {
475+
self
476+
.instance
477+
.render_pass_multi_draw_indirect(
478+
&mut self.render_pass.borrow_mut(),
479+
indirect_buffer.id,
480+
indirect_offset,
481+
max_draw_count,
482+
)
483+
.err()
484+
};
485+
self.error_handler.push_error(err);
486+
}
487+
488+
#[required(3)]
489+
#[undefined]
490+
fn multi_draw_indexed_indirect(
491+
&self,
492+
#[webidl] indirect_buffer: Ptr<GPUBuffer>,
493+
#[webidl(options(enforce_range = true))] indirect_offset: u64,
494+
#[webidl(options(enforce_range = true))] max_draw_count: u32,
495+
#[webidl] draw_count_buffer: Option<Ptr<GPUBuffer>>,
496+
#[webidl(default = 0, options(enforce_range = true))]
497+
draw_count_buffer_offset: u64,
498+
) {
499+
let err = if let Some(draw_count_buffer) = draw_count_buffer {
500+
self
501+
.instance
502+
.render_pass_multi_draw_indexed_indirect_count(
503+
&mut self.render_pass.borrow_mut(),
504+
indirect_buffer.id,
505+
indirect_offset,
506+
draw_count_buffer.id,
507+
draw_count_buffer_offset,
508+
max_draw_count,
509+
)
510+
.err()
511+
} else {
512+
self
513+
.instance
514+
.render_pass_multi_draw_indexed_indirect(
515+
&mut self.render_pass.borrow_mut(),
516+
indirect_buffer.id,
517+
indirect_offset,
518+
max_draw_count,
519+
)
520+
.err()
521+
};
522+
self.error_handler.push_error(err);
523+
}
524+
451525
#[required(2)]
452526
#[undefined]
453527
fn set_immediates<'a>(

deno_webgpu/webidl.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,13 @@ impl<'a> WebIdlConverter<'a> for GPUFeatureName {
374374
_options: &Self::Options,
375375
) -> Result<Self, WebIdlError> {
376376
let s = value.to_rust_string_lossy(scope);
377-
s.parse().map(Self).map_err(|()| {
377+
let feature = match s.as_str() {
378+
"chromium-experimental-multi-draw-indirect" => {
379+
Ok(wgpu_types::Features::MULTI_DRAW_INDIRECT_COUNT)
380+
}
381+
_ => s.parse(),
382+
};
383+
feature.map(Self).map_err(|()| {
378384
WebIdlError::new(
379385
prefix,
380386
context,

tests/tests/wgpu-gpu/draw_indirect.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub fn all_tests(vec: &mut Vec<GpuTestInitializer>) {
3535
MULTI_DRAW_INDEXED_INDIRECT_GPU_GENERATED_ARGS,
3636
MULTI_DRAW_INDIRECT_COUNT_READBACK,
3737
MULTI_DRAW_INDEXED_INDIRECT_COUNT_READBACK,
38+
MULTI_DRAW_INDIRECT_COUNT_SAMPLED_TEXTURE,
3839
MULTI_DRAW_INDIRECT_OVER_ICB_WORKGROUP,
3940
MULTI_DRAW_INDIRECT_FIRST_VERTEX_AND_INSTANCE,
4041
MULTI_DRAW_INDIRECT_MIXED_SEQUENCE,
@@ -1350,6 +1351,171 @@ static MULTI_DRAW_INDEXED_INDIRECT_COUNT_READBACK: GpuTestConfiguration =
13501351
)
13511352
.run_async(|ctx| run_multi_draw_indirect_count_readback(ctx, true));
13521353

1354+
/// Like `run_multi_draw_indirect_count_readback`, but the fragment shader
1355+
/// samples a texture. On Metal GPUs where sampled textures exclude a pipeline
1356+
/// from indirect-command-buffer execution, this exercises the GPU-clamped
1357+
/// per-draw fallback instead of the ICB path.
1358+
async fn run_multi_draw_indirect_count_sampled_texture(ctx: TestingContext) {
1359+
let shader = ctx
1360+
.device
1361+
.create_shader_module(wgpu::ShaderModuleDescriptor {
1362+
label: Some("count sampled texture"),
1363+
source: wgpu::ShaderSource::Wgsl(
1364+
"
1365+
@group(0) @binding(0) var color_texture: texture_2d<f32>;
1366+
@group(0) @binding(1) var color_sampler: sampler;
1367+
1368+
@vertex
1369+
fn vs_main(@builtin(vertex_index) vertex_index: u32) -> @builtin(position) vec4<f32> {
1370+
// One triangle covering the whole viewport; only
1371+
// `first_vertex == 0` draws are issued, since
1372+
// `vertex_index`'s interaction with `first_vertex` differs
1373+
// per backend on the (unvalidated) count path and isn't
1374+
// what this test targets.
1375+
var positions = array<vec2<f32>, 3>(
1376+
vec2<f32>(-1.0, -3.0),
1377+
vec2<f32>(3.0, 1.0),
1378+
vec2<f32>(-1.0, 1.0),
1379+
);
1380+
return vec4<f32>(positions[vertex_index], 0.0, 1.0);
1381+
}
1382+
1383+
@fragment
1384+
fn fs_main() -> @location(0) vec4<f32> {
1385+
return textureSample(color_texture, color_sampler, vec2<f32>(0.5, 0.5));
1386+
}
1387+
"
1388+
.into(),
1389+
),
1390+
});
1391+
1392+
let white_texture = ctx.device.create_texture_with_data(
1393+
&ctx.queue,
1394+
&wgpu::TextureDescriptor {
1395+
label: Some("white 1x1"),
1396+
size: wgpu::Extent3d {
1397+
width: 1,
1398+
height: 1,
1399+
depth_or_array_layers: 1,
1400+
},
1401+
mip_level_count: 1,
1402+
sample_count: 1,
1403+
dimension: wgpu::TextureDimension::D2,
1404+
format: wgpu::TextureFormat::Rgba8Unorm,
1405+
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1406+
view_formats: &[],
1407+
},
1408+
wgpu::util::TextureDataOrder::LayerMajor,
1409+
&[u8::MAX; 4],
1410+
);
1411+
let sampler = ctx
1412+
.device
1413+
.create_sampler(&wgpu::SamplerDescriptor::default());
1414+
1415+
let pipeline = ctx
1416+
.device
1417+
.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1418+
label: Some("count sampled texture"),
1419+
layout: None,
1420+
vertex: wgpu::VertexState {
1421+
module: &shader,
1422+
entry_point: Some("vs_main"),
1423+
compilation_options: Default::default(),
1424+
buffers: &[],
1425+
},
1426+
fragment: Some(wgpu::FragmentState {
1427+
module: &shader,
1428+
entry_point: Some("fs_main"),
1429+
compilation_options: Default::default(),
1430+
targets: &[Some(wgpu::TextureFormat::Rgba8Unorm.into())],
1431+
}),
1432+
primitive: wgpu::PrimitiveState::default(),
1433+
depth_stencil: None,
1434+
multisample: wgpu::MultisampleState::default(),
1435+
multiview_mask: None,
1436+
cache: None,
1437+
});
1438+
let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
1439+
label: None,
1440+
layout: &pipeline.get_bind_group_layout(0),
1441+
entries: &[
1442+
wgpu::BindGroupEntry {
1443+
binding: 0,
1444+
resource: wgpu::BindingResource::TextureView(
1445+
&white_texture.create_view(&wgpu::TextureViewDescriptor::default()),
1446+
),
1447+
},
1448+
wgpu::BindGroupEntry {
1449+
binding: 1,
1450+
resource: wgpu::BindingResource::Sampler(&sampler),
1451+
},
1452+
],
1453+
});
1454+
1455+
let max_draw_count = ICB_MULTI_DRAW_TEST_COUNT as u32;
1456+
let mut args = vec![
1457+
wgpu::util::DrawIndirectArgs {
1458+
vertex_count: 0,
1459+
instance_count: 1,
1460+
first_vertex: 0,
1461+
first_instance: 0,
1462+
};
1463+
ICB_MULTI_DRAW_TEST_COUNT
1464+
];
1465+
args[0] = wgpu::util::DrawIndirectArgs {
1466+
vertex_count: 3,
1467+
instance_count: 1,
1468+
first_vertex: 0,
1469+
first_instance: 0,
1470+
};
1471+
let indirect_buffer = create_draw_indirect_buffer(&ctx, &args);
1472+
let count_buffer = ctx.device.create_buffer_init(&BufferInitDescriptor {
1473+
label: None,
1474+
contents: bytemuck::cast_slice(&[1u32]),
1475+
usage: wgpu::BufferUsages::INDIRECT,
1476+
});
1477+
1478+
let (out_texture, out_texture_view) = create_rgba8_render_target(&ctx, 256, 256);
1479+
let mut encoder = ctx
1480+
.device
1481+
.create_command_encoder(&wgpu::CommandEncoderDescriptor::default());
1482+
{
1483+
let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1484+
label: None,
1485+
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1486+
ops: wgpu::Operations {
1487+
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
1488+
store: wgpu::StoreOp::Store,
1489+
},
1490+
resolve_target: None,
1491+
view: &out_texture_view,
1492+
depth_slice: None,
1493+
})],
1494+
depth_stencil_attachment: None,
1495+
timestamp_writes: None,
1496+
occlusion_query_set: None,
1497+
multiview_mask: None,
1498+
});
1499+
rpass.set_pipeline(&pipeline);
1500+
rpass.set_bind_group(0, &bind_group, &[]);
1501+
rpass.multi_draw_indirect_count(&indirect_buffer, 0, &count_buffer, 0, max_draw_count);
1502+
}
1503+
1504+
let data = submit_and_read_rgba8_texture(&ctx, encoder, &out_texture, 256, 256).await;
1505+
assert_all_pixels_rgba8(&data, [u8::MAX; 4]);
1506+
}
1507+
1508+
#[gpu_test]
1509+
static MULTI_DRAW_INDIRECT_COUNT_SAMPLED_TEXTURE: GpuTestConfiguration =
1510+
GpuTestConfiguration::new()
1511+
.parameters(
1512+
TestParameters::default()
1513+
.downlevel_flags(wgpu::DownlevelFlags::INDIRECT_EXECUTION)
1514+
.features(wgpu::Features::MULTI_DRAW_INDIRECT_COUNT)
1515+
.limits(wgpu::Limits::downlevel_defaults()),
1516+
)
1517+
.run_async(run_multi_draw_indirect_count_sampled_texture);
1518+
13531519
async fn run_gpu_generated_multi_draw_test(ctx: TestingContext, indexed: bool) {
13541520
let draw_count = ICB_MULTI_DRAW_TEST_COUNT as u32;
13551521
let indirect_stride = if indexed {

wgpu-core/src/command/render.rs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -967,8 +967,18 @@ pub enum RenderPassErrorInner {
967967
MissingFeatures(#[from] MissingFeatures),
968968
#[error(transparent)]
969969
MissingDownlevelFlags(#[from] MissingDownlevelFlags),
970-
#[error("Indirect buffer offset {0:?} is not a multiple of 4")]
970+
#[error(
971+
"Indirect buffer offset {offset:?} is not a multiple of {alignment}",
972+
offset = .0,
973+
alignment = wgt::INDIRECT_BUFFER_OFFSET_ALIGNMENT
974+
)]
971975
UnalignedIndirectBufferOffset(BufferAddress),
976+
#[error(
977+
"Indirect draw count buffer offset {offset:?} is not a multiple of {alignment}",
978+
offset = .0,
979+
alignment = wgt::INDIRECT_BUFFER_OFFSET_ALIGNMENT
980+
)]
981+
UnalignedIndirectCountBufferOffset(BufferAddress),
972982
#[error("Indirect draw arguments of {args_size} bytes (count = {count}) starting at {offset} would overrun buffer size of {buffer_size}")]
973983
IndirectBufferOverrun {
974984
count: u32,
@@ -1115,6 +1125,7 @@ impl WebGpuError for RenderPassError {
11151125
| RenderPassErrorInner::InvalidDepthOps
11161126
| RenderPassErrorInner::InvalidStencilOps
11171127
| RenderPassErrorInner::UnalignedIndirectBufferOffset(..)
1128+
| RenderPassErrorInner::UnalignedIndirectCountBufferOffset(..)
11181129
| RenderPassErrorInner::IndirectBufferOverrun { .. }
11191130
| RenderPassErrorInner::IndirectCountBufferOverrun { .. }
11201131
| RenderPassErrorInner::ResourceUsageCompatibility(..)
@@ -3314,7 +3325,7 @@ fn multi_draw_indirect(
33143325
indirect_buffer.check_usage(BufferUsages::INDIRECT)?;
33153326
indirect_buffer.check_destroyed(state.pass.base.snatch_guard)?;
33163327

3317-
if !offset.is_multiple_of(4) {
3328+
if !offset.is_multiple_of(wgt::INDIRECT_BUFFER_OFFSET_ALIGNMENT) {
33183329
return Err(RenderPassErrorInner::UnalignedIndirectBufferOffset(offset));
33193330
}
33203331

@@ -3533,9 +3544,14 @@ fn multi_draw_indirect_count(
35333544
count_buffer.check_usage(BufferUsages::INDIRECT)?;
35343545
let count_raw = count_buffer.try_raw(state.pass.base.snatch_guard)?;
35353546

3536-
if !offset.is_multiple_of(4) {
3547+
if !offset.is_multiple_of(wgt::INDIRECT_BUFFER_OFFSET_ALIGNMENT) {
35373548
return Err(RenderPassErrorInner::UnalignedIndirectBufferOffset(offset));
35383549
}
3550+
if !count_buffer_offset.is_multiple_of(wgt::INDIRECT_BUFFER_OFFSET_ALIGNMENT) {
3551+
return Err(RenderPassErrorInner::UnalignedIndirectCountBufferOffset(
3552+
count_buffer_offset,
3553+
));
3554+
}
35393555

35403556
let args_size = match stride.checked_mul(u64::from(max_count)) {
35413557
Some(sz) if sz <= indirect_buffer.size && indirect_buffer.size - sz >= offset => sz,

wgpu-hal/src/metal/adapter.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,6 +1364,10 @@ impl super::CapabilitiesQuery {
13641364
F::EXPERIMENTAL_MESH_SHADER_MULTIVIEW,
13651365
self.supported_vertex_amplification_factor > 1 && self.mesh_shaders,
13661366
);
1367+
features.set(
1368+
F::MULTI_DRAW_INDIRECT_COUNT,
1369+
self.indirect_command_buffers_rendering && self.indirect_command_buffers_compute,
1370+
);
13671371

13681372
// Cooperative matrix (simdgroup matrix) requires MSL 2.3+
13691373
features.set(

0 commit comments

Comments
 (0)