Skip to content

Commit 7b2115d

Browse files
committed
Harden Metal ICB prepass edge cases
Fixes from an adversarial pre-push review of the prepass rework: - Gate setSupportIndirectCommandBuffers on MTLMeshRenderPipelineDescriptor behind macOS 14 / iOS 17 / tvOS 18.1 / visionOS 2.1: the property is one OS generation newer there than mesh pipelines themselves, so macOS 13 / iOS 16 would hit an unrecognized selector. Mesh pipelines created without the flag fall back to the per-draw loop. - Raise the ICB availability floor to macOS 10.15 / iOS 13 / tvOS 13: inheritPipelineState needs iOS/tvOS 13, and useResource:usage: only participates in hazard tracking from macOS 10.15 / iOS 13, which the deferred generation relies on. - Fix the tvOS entry in the ICB feature-set table (tvOS_GPUFamily1_v2 admitted the Apple2-class A8; the floor is tvOS_GPUFamily2_v2 / A10X). - Drop the begin_render_pass debug_assert on the deferred queue: wgpu-core legally records discarded-surface fixup clears (render passes) in the internal pre-pass before draining the queue. - Cache generation-kernel compile failures so a driver that rejects the library doesn't recompile it on every multi-draw call. - Report the retry error (the pipeline's real problem) when creation fails both with and without the ICB flag, and log a breadcrumb when a pipeline is silently downgraded to non-ICB. - Use the stage-scoped useResource for the ICB-referenced index buffer. The ICB itself keeps the stage-less variant: it is consumed by command ingestion, not a shader stage, and the stage-scoped call was observed on M4 to make the executed draws render nothing. - Rename the stale MULTI_DRAW_INDIRECT_BIND_GROUP_FALLBACK test (bind groups no longer force a fallback; the test now verifies bind-group inheritance through the ICB path), document the test-count constant's coupling to ICB_MIN_DRAW_COUNT, document the hal contract in the changelog, and correct the ICB bind-count constant's rationale.
1 parent a95a810 commit 7b2115d

7 files changed

Lines changed: 87 additions & 50 deletions

File tree

CHANGELOG.md

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

4545
### Added/New Features
4646

47-
#### Hal
48-
49-
- Add `BufferBinding::buffer`, a public read accessor for the bound buffer, which was previously inaccessible to out-of-tree `wgpu_hal::Api` implementations. By @danlehmann in [#9820](https://github.com/gfx-rs/wgpu/pull/9820).
50-
5147
#### Metal
5248

5349
- 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).
5450

51+
#### Hal
52+
53+
- Add `BufferBinding::buffer`, a public read accessor for the bound buffer, which was previously inaccessible to out-of-tree `wgpu_hal::Api` implementations. By @danlehmann in [#9820](https://github.com/gfx-rs/wgpu/pull/9820).
54+
- Added `CommandEncoder::encode_deferred_multi_draws` (default no-op). Backends may defer part of the work for indirect multi-draws recorded in a render pass; after ending such a pass, callers must invoke this method while recording a command buffer that the queue executes before the pass's. wgpu-core does this in its internal pre-pass. By @matthargett in [#9640](https://github.com/gfx-rs/wgpu/pull/9640).
55+
5556
### Changes
5657

5758
#### naga

tests/tests/wgpu-gpu/draw_indirect.rs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub fn all_tests(vec: &mut Vec<GpuTestInitializer>) {
3838
MULTI_DRAW_INDIRECT_OVER_ICB_WORKGROUP,
3939
MULTI_DRAW_INDIRECT_FIRST_VERTEX_AND_INSTANCE,
4040
MULTI_DRAW_INDIRECT_MIXED_SEQUENCE,
41-
MULTI_DRAW_INDIRECT_BIND_GROUP_FALLBACK,
41+
MULTI_DRAW_INDIRECT_WITH_BIND_GROUPS,
4242
MULTI_DRAW_INDEXED_INDIRECT_U16,
4343
MULTI_DRAW_INDEXED_INDIRECT_POSITIVE_BASE_VERTEX,
4444
MULTI_DRAW_INDEXED_INDIRECT_NEGATIVE_BASE_VERTEX,
@@ -641,6 +641,9 @@ fn create_draw_indexed_indirect_buffer(
641641
})
642642
}
643643

644+
/// Kept in sync with `ICB_MIN_DRAW_COUNT` in `wgpu-hal/src/metal/command.rs`
645+
/// so these tests exercise Metal's indirect-command-buffer lowering rather
646+
/// than the small-count per-draw loop.
644647
const ICB_MULTI_DRAW_TEST_COUNT: usize = 8;
645648

646649
async fn run_multi_draw_indirect_over_icb_workgroup(ctx: TestingContext) {
@@ -876,7 +879,9 @@ async fn run_multi_draw_indirect_mixed_sequence(ctx: TestingContext) {
876879
assert_all_pixels_rgba8(&data, [u8::MAX; 4]);
877880
}
878881

879-
async fn run_multi_draw_indirect_bind_group_fallback(ctx: TestingContext) {
882+
/// Multi-draw with an active bind group; on Metal's ICB path the bind-group
883+
/// bindings must be inherited correctly by the generated commands.
884+
async fn run_multi_draw_indirect_with_bind_groups(ctx: TestingContext) {
880885
let shader = ctx
881886
.device
882887
.create_shader_module(wgpu::ShaderModuleDescriptor {
@@ -1287,13 +1292,13 @@ static MULTI_DRAW_INDIRECT_MIXED_SEQUENCE: GpuTestConfiguration = GpuTestConfigu
12871292
.run_async(run_multi_draw_indirect_mixed_sequence);
12881293

12891294
#[gpu_test]
1290-
static MULTI_DRAW_INDIRECT_BIND_GROUP_FALLBACK: GpuTestConfiguration = GpuTestConfiguration::new()
1295+
static MULTI_DRAW_INDIRECT_WITH_BIND_GROUPS: GpuTestConfiguration = GpuTestConfiguration::new()
12911296
.parameters(
12921297
TestParameters::default()
12931298
.downlevel_flags(wgpu::DownlevelFlags::INDIRECT_EXECUTION)
12941299
.limits(wgpu::Limits::downlevel_defaults()),
12951300
)
1296-
.run_async(run_multi_draw_indirect_bind_group_fallback);
1301+
.run_async(run_multi_draw_indirect_with_bind_groups);
12971302

12981303
#[gpu_test]
12991304
static MULTI_DRAW_INDEXED_INDIRECT_U16: GpuTestConfiguration = GpuTestConfiguration::new()

wgpu-hal/src/lib.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1684,12 +1684,20 @@ pub trait CommandEncoder: WasmNotSendSync + fmt::Debug {
16841684
first_instance: u32,
16851685
instance_count: u32,
16861686
);
1687+
/// # Safety
1688+
///
1689+
/// - If `draw_count > 1`, see the deferred-work obligation on
1690+
/// [`encode_deferred_multi_draws`](CommandEncoder::encode_deferred_multi_draws).
16871691
unsafe fn draw_indirect(
16881692
&mut self,
16891693
buffer: &<Self::A as Api>::Buffer,
16901694
offset: wgt::BufferAddress,
16911695
draw_count: u32,
16921696
);
1697+
/// # Safety
1698+
///
1699+
/// - If `draw_count > 1`, see the deferred-work obligation on
1700+
/// [`encode_deferred_multi_draws`](CommandEncoder::encode_deferred_multi_draws).
16931701
unsafe fn draw_indexed_indirect(
16941702
&mut self,
16951703
buffer: &<Self::A as Api>::Buffer,

wgpu-hal/src/metal/adapter.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ const INDIRECT_DRAW_DISPATCH_SUPPORT: &[MTLFeatureSet] = &[
583583
/// kernels; widen this table as device validation lands.
584584
const INDIRECT_COMMAND_BUFFERS_RENDERING_SUPPORT: &[MTLFeatureSet] = &[
585585
MTLFeatureSet::iOS_GPUFamily5_v1,
586-
MTLFeatureSet::tvOS_GPUFamily1_v2,
586+
MTLFeatureSet::tvOS_GPUFamily2_v2,
587587
MTLFeatureSet::macOS_GPUFamily2_v1,
588588
];
589589

@@ -722,8 +722,12 @@ impl super::CapabilitiesQuery {
722722
// - First-generation Apple TV 4K (A10X, Apple3) is allowed because it
723723
// was validated directly on hardware; other Apple3/Apple4 devices
724724
// stay excluded until they're validated with this backend.
725-
// `MTLIndirectCommandBuffer` and friends need macOS 10.14 / iOS 12.
726-
let icb_api_check = available!(macos = 10.14, ios = 12.0, tvos = 12.0, visionos = 1.0);
725+
// `MTLIndirectCommandBuffer` needs macOS 10.14 / iOS 12, but the
726+
// pieces this backend depends on are newer: `inheritPipelineState`
727+
// needs iOS/tvOS 13, and `useResource:usage:` only participates in
728+
// hazard tracking from macOS 10.15 / iOS 13 — which the deferred
729+
// generation relies on to order the ICB write against its execution.
730+
let icb_api_check = available!(macos = 10.15, ios = 13.0, tvos = 13.0, visionos = 1.0);
727731
let icb_family_support = family_check
728732
&& if os_type == super::OsType::Macos {
729733
device.supportsFamily(MTLGPUFamily::Mac2)

wgpu-hal/src/metal/command.rs

Lines changed: 34 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ use objc2_metal::{
1111
MTLComputeCommandEncoder, MTLComputePassDescriptor, MTLComputePipelineState,
1212
MTLCounterDontSample, MTLDevice, MTLFunction, MTLIndexType, MTLIndirectCommandBuffer,
1313
MTLIndirectCommandBufferDescriptor, MTLIndirectCommandType, MTLLibrary, MTLLoadAction,
14-
MTLPrimitiveType, MTLRenderCommandEncoder, MTLRenderPassDescriptor, MTLResidencySet,
15-
MTLResidencySetDescriptor, MTLResource, MTLResourceOptions, MTLResourceUsage, MTLSamplerState,
16-
MTLScissorRect, MTLSize, MTLStoreAction, MTLTexture, MTLVertexAmplificationViewMapping,
17-
MTLViewport, MTLVisibilityResultMode,
14+
MTLPrimitiveType, MTLRenderCommandEncoder, MTLRenderPassDescriptor, MTLRenderStages,
15+
MTLResidencySet, MTLResidencySetDescriptor, MTLResource, MTLResourceOptions, MTLResourceUsage,
16+
MTLSamplerState, MTLScissorRect, MTLSize, MTLStoreAction, MTLTexture,
17+
MTLVertexAmplificationViewMapping, MTLViewport, MTLVisibilityResultMode,
1818
};
1919

2020
use super::{
@@ -40,9 +40,12 @@ const WORD_SIZE: usize = 4;
4040
/// on A12 through M4 hardware.
4141
const ICB_MIN_DRAW_COUNT: u32 = 8;
4242

43-
/// Upper bound declared for `maxVertexBufferBindCount` when the ICB inherits
44-
/// buffer bindings: Metal's per-stage buffer argument table has 31 slots, so
45-
/// 31 covers every possible inherited binding.
43+
/// Value declared for `maxVertexBufferBindCount` on ICB descriptors. The
44+
/// generated commands never set buffers themselves (all bindings are
45+
/// inherited from the encoder), so per Metal's documentation these counts
46+
/// only size command-side binding storage; the full 31-slot argument-table
47+
/// size is declared anyway because driver validation of the interaction with
48+
/// `inheritBuffers` has proven underdocumented across OS generations.
4649
const ICB_MAX_INHERITED_BUFFER_BIND_COUNT: usize = 31;
4750

4851
// Primitive-topology tags passed to the ICB generation kernels.
@@ -68,8 +71,8 @@ pub(super) struct IcbCommandPipelines {
6871
indexed_u16: IcbCommandPipeline,
6972
indexed_u32: IcbCommandPipeline,
7073
/// Compiled on first use: mesh commands in ICBs need a newer OS baseline
71-
/// than plain draws.
72-
mesh: Option<IcbCommandPipeline>,
74+
/// than plain draws. Failures are cached.
75+
mesh: Option<Result<IcbCommandPipeline, crate::DeviceError>>,
7376
}
7477

7578
#[derive(Clone, Debug)]
@@ -189,15 +192,14 @@ impl IcbCommandPipelines {
189192
&mut self,
190193
shared: &super::AdapterShared,
191194
) -> Result<IcbCommandPipeline, crate::DeviceError> {
192-
if self.mesh.is_none() {
193-
let library = Self::make_library(shared, ICB_MESH_GENERATION_SHADER)?;
194-
self.mesh = Some(Self::make_pipeline_from_library(
195-
shared,
196-
&library,
197-
"wgpu_generate_mesh_mdi_icb",
198-
)?);
199-
}
200-
Ok(self.mesh.as_ref().unwrap().clone())
195+
// Failures are cached too, so a driver that rejects the mesh kernel
196+
// doesn't recompile it on every mesh multi-draw.
197+
self.mesh
198+
.get_or_insert_with(|| {
199+
let library = Self::make_library(shared, ICB_MESH_GENERATION_SHADER)?;
200+
Self::make_pipeline_from_library(shared, &library, "wgpu_generate_mesh_mdi_icb")
201+
})
202+
.clone()
201203
}
202204
}
203205

@@ -378,18 +380,20 @@ impl super::CommandEncoder {
378380

379381
fn get_icb_command_pipelines(&self) -> Result<IcbCommandPipelines, crate::DeviceError> {
380382
let mut pipelines = self.shared.icb_command_pipelines.lock();
381-
if pipelines.is_none() {
382-
*pipelines = Some(IcbCommandPipelines::new(&self.shared)?);
383-
}
384-
Ok(pipelines.as_ref().unwrap().clone())
383+
// A compile failure is cached so a broken driver doesn't recompile
384+
// the generation library on every multi-draw call.
385+
pipelines
386+
.get_or_insert_with(|| IcbCommandPipelines::new(&self.shared))
387+
.clone()
385388
}
386389

387390
fn get_icb_mesh_command_pipeline(&self) -> Result<IcbCommandPipeline, crate::DeviceError> {
388391
let mut pipelines = self.shared.icb_command_pipelines.lock();
389-
if pipelines.is_none() {
390-
*pipelines = Some(IcbCommandPipelines::new(&self.shared)?);
391-
}
392-
pipelines.as_mut().unwrap().mesh(&self.shared)
392+
pipelines
393+
.get_or_insert_with(|| IcbCommandPipelines::new(&self.shared))
394+
.as_mut()
395+
.map_err(|err| err.clone())?
396+
.mesh(&self.shared)
393397
}
394398

395399
fn icb_primitive_type_value(
@@ -568,10 +572,12 @@ impl super::CommandEncoder {
568572
{
569573
// The generated commands reference the index buffer via a
570574
// device pointer baked in at generation time, which residency
571-
// tracking can't see.
572-
encoder.useResource_usage(
575+
// tracking can't see. Unlike the ICB, this is an ordinary
576+
// vertex-stage read, so the stage-scoped variant is correct.
577+
encoder.useResource_usage_stages(
573578
ProtocolObject::from_ref(&**index_buffer),
574579
MTLResourceUsage::Read,
580+
MTLRenderStages::Vertex,
575581
);
576582
}
577583
encoder.executeCommandsInBuffer_withRange(
@@ -1342,9 +1348,6 @@ impl crate::CommandEncoder for super::CommandEncoder {
13421348
assert!(self.state.blit.is_none());
13431349
assert!(self.state.compute.is_none());
13441350
assert!(self.state.render.is_none());
1345-
// Multi-draws deferred by the previous pass must have been encoded via
1346-
// `encode_deferred_multi_draws` before another pass begins.
1347-
debug_assert!(self.deferred_multi_draws.is_empty());
13481351

13491352
autoreleasepool(|_| {
13501353
let descriptor = MTLRenderPassDescriptor::new();

wgpu-hal/src/metal/device.rs

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1754,7 +1754,17 @@ impl crate::Device for super::Device {
17541754
// used with indirect command buffers"), so a failed creation is
17551755
// retried without it and the multi-draw ICB lowering then skips
17561756
// draws issued with that pipeline.
1757-
let request_icb_support = self.shared.private_caps.indirect_command_buffers_rendering;
1757+
//
1758+
// On `MTLMeshRenderPipelineDescriptor` the property needs a newer
1759+
// OS than mesh pipelines themselves (and than the property on the
1760+
// standard descriptor).
1761+
let request_icb_support = self.shared.private_caps.indirect_command_buffers_rendering
1762+
&& match descriptor {
1763+
MetalGenericRenderPipelineDescriptor::Standard(_) => true,
1764+
MetalGenericRenderPipelineDescriptor::Mesh(_) => {
1765+
available!(macos = 14.0, ios = 17.0, tvos = 18.1, visionos = 2.1)
1766+
}
1767+
};
17581768
if request_icb_support {
17591769
descriptor.setSupportIndirectCommandBuffers(true);
17601770
}
@@ -1780,15 +1790,21 @@ impl crate::Device for super::Device {
17801790
Err(first_err) if request_icb_support => {
17811791
descriptor.setSupportIndirectCommandBuffers(false);
17821792
supports_indirect_command_buffers = false;
1783-
create(&descriptor).map_err(|_| {
1784-
// Report the original error: if the pipeline is
1785-
// invalid regardless, the first message is the
1786-
// relevant one.
1793+
let raw = create(&descriptor).map_err(|retry_err| {
1794+
// The retry error is the pipeline's real problem; the
1795+
// first attempt may only have failed because of the
1796+
// ICB flag.
17871797
crate::PipelineError::Linkage(
17881798
wgt::ShaderStages::VERTEX | wgt::ShaderStages::FRAGMENT,
1789-
format!("new_render_pipeline_state: {first_err:?}"),
1799+
format!("new_render_pipeline_state: {retry_err:?}"),
17901800
)
1791-
})?
1801+
})?;
1802+
log::debug!(
1803+
"created render pipeline {:?} without indirect command buffer support, \
1804+
multi-draws recorded with it won't use ICBs: {first_err:?}",
1805+
desc.label,
1806+
);
1807+
raw
17921808
}
17931809
Err(e) => {
17941810
return Err(crate::PipelineError::Linkage(

wgpu-hal/src/metal/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,7 +407,7 @@ struct AdapterShared {
407407
private_texture_format_caps: PrivateTextureFormatCapabilities,
408408
settings: Settings,
409409
presentation_timer: time::PresentationTimer,
410-
icb_command_pipelines: Mutex<Option<command::IcbCommandPipelines>>,
410+
icb_command_pipelines: Mutex<Option<Result<command::IcbCommandPipelines, crate::DeviceError>>>,
411411
}
412412

413413
#[cfg(send_sync)]

0 commit comments

Comments
 (0)