Repository navigation
Use Metal ICBs for multi-draw indirect - #3
Closed
matthargett wants to merge 209 commits into
Closed
matthargett wants to merge 209 commits into
matthargett wants to merge 209 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 553458126a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Add Metal ICB generation for mesh multi-draw indirect and opt-in count-buffer variants. Wire the Chromium experimental multi-draw indirect API surface for CTS, add wgpu-owned readback tests for normal/indexed count-buffer draws and mesh MDI, and validate draw-count-buffer offset alignment.
…aw-indirect # Conflicts: # CHANGELOG.md # deno_webgpu/render_pass.rs
Extract the inline MSL generation kernels into wgpu-hal/src/metal/shaders/ and include_str! them, following the gles/shaders precedent, and document the ICB tuning and primitive-topology constants. Requested in review of gfx-rs#9640.
Document why MULTI_DRAW_INDIRECT_COUNT is exposed past all_webgpu_mask(): it surfaces as Chromium's experimental chromium-experimental-multi-draw-indirect WebGPU feature. Requested in review of gfx-rs#9640.
Move the MULTI_DRAW_INDIRECT_COUNT surface (deno bindings and the chromium-experimental-multi-draw-indirect mapping) out to gfx-rs#9679, which owns the count feature. Requested in review: the env-gated feature exposure didn't belong in this PR.
Rework requested in review of gfx-rs#9640: instead of suspending and resuming the render encoder around each multi-draw, the ICB is allocated and its execution recorded at draw time, and the reset/generate/optimize work is deferred into the same internal pre-pass command buffer that carries indirect-draw validation, via a new default-no-op hal method CommandEncoder::encode_deferred_multi_draws. The queue executes that buffer before the pass, so the pass is never interrupted and no render state needs to be captured or restored. This deletes the suspend/resume machinery, the render-state shadow tracking, the store-action rewriting, the pass-resumability gating, and both environment variables (WGPU_METAL_FORCE_ICB_MDI, WGPU_METAL_REQUIRE_ICB_MDI). Adapter gating is restated in the CapabilitiesQuery idiom: feature-set tables plus family checks, with the paravirtual exclusion kept and the splice-motivated OS-version gates dropped. The count-buffer offset validation moves to gfx-rs#9679 with the rest of the count feature. Running the full suite on device also surfaced a latent bug in the previous revision: setSupportIndirectCommandBuffers(true) makes pipeline creation fail for shaders Metal rejects in ICBs (e.g. wgpu's own TextureBlitter fragment shader on Apple GPUs). Pipeline creation now retries without the flag and multi-draws recorded under such pipelines fall back to the per-draw loop. Generation encoders, ICBs, and argument buffers are labeled and the per-pass generation runs as one labeled compute encoder, so GPU captures in Xcode show one legible generation node per pass. All 471 wgpu-gpu tests pass on Apple M4 Max (modulo failures also present on trunk on the same machine).
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.
The prepass rework dropped watchOS from the ICB gating along with the splice-era OS-version gates, but watchOS support was genuine hardware capability, not a splice workaround. Restore it. Validated end-to-end on Apple Watch SE 2 (S8 SiP, watchOS 11.6): the GPU reports "Apple S4 GPU", supports the Apple5 family, and an indirect command buffer GPU-generated by the multi-draw generation kernel renders correctly via executeCommandsInBuffer: (readback + visual). objc2-metal exposes no MTLFeatureSet::watchOS_* values, so the feature-set table can't cover watchOS, and watchOS classifies as OsType::Ios here with no watchos key in family_check; the watch is therefore detected purely by GPU family, folded into icb_family_support behind available!(watchos = 11.0) (false on every other platform). watchOS 11 is the floor for the same reason iOS/iPadOS/tvOS 17.x are excluded: earlier drivers mishandle the render-ICB path.
matthargett
force-pushed
the
metal-icb-multi-draw-indirect
branch
from
July 9, 2026 00:12
68eb50d to
4ada51d
Compare
Signed-off-by: fengxueweilai <fengxueweilai@outlook.com>
…10573) Co-authored-by: Andy Leiserson <aleiserson@mozilla.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
al types are in wgpu-core-remote-types which uses serde unconditionally Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
…-rs#10763) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
…er (gfx-rs#10748) Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Michaël DM <micode@micorp.fr>
…y non-MSAA framebuffers) (gfx-rs#10624)
…fx-rs#10581) Co-authored-by: Teodor Tanasoaia <28601907+teoxoy@users.noreply.github.com>
…pter that matches `RequestAdapterOptions` Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
Signed-off-by: sagudev <16504129+sagudev@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Member
Author
|
merged upstream 🎉 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Connections
Reference workload: this was motivated by BabylonNative WebGPU rendering work where CPU-side multi-draw looping became visible in large batched scenes and AR rendering. The relevant application cases were AR Portal camera + rendering and (Back to the Future) Hill Valley-style GLTF's baked/static geometry batches that included PBR materials and a WGSL SSAO workload.
AR portal demo reference:
https://doc.babylonjs.com/features/featuresDeepDive/webXR/webXRDemos#ar-demo
Description
This ports Metal fixed-count multi-draw indirect to a real indirect-command-buffer path when the render pass can be safely suspended and resumed, instead of always looping over draws on the CPU.
The ICB fast path is intentionally conservative. It currently applies to larger static/material-baked multi-draw batches whose render state is pipeline plus vertex/index buffers only. Passes that bind textures, uniforms, or storage through render bind groups, or use immediates, stay on the CPU-loop fallback. This keeps common material-heavy passes safe while still reducing bridge/encoder overhead for large baked-geometry batches.
Changes:
iOS/iPadOS 18,tvOS 18,visionOS 2,watchOS 11). Apple3/A10X remains on the CPU fallback for now.MTLPrimitiveTypeinteger values matching MSLprimitive_typevalues.dispatchThreadgroups, using the compute pipeline'sthreadExecutionWidth()and an in-shadercmdIndex >= drawCountguard instead of relying on non-uniform threadgroups fromdispatchThreads.wgpu-gpuconformance coverage for GPU-generated non-indexed and indexed multi-draw indirect arguments.base_vertex, non-zerofirst_vertex/first_instance, draw counts crossing the ICB generation workgroup width, mixed single/multi indirect sequencing in one render pass, and bind-group forced fallback.Hardware and driver quirks found:
iPad7,3) on iPadOS 17.7 advertises the legacy iOS GPUFamily3 ICB feature set, but throws a foreignNSExceptionfrom thedraw_indirectpath before ICB generation while ending/suspending the active render encoder for the ICB splice. This PR therefore keeps Apple3/A10X on the existing CPU-loop fallback even if a later runtime reports related feature sets.dispatchThreadgroupssized bythreadExecutionWidth(), with rounded-up extra threads neutralized by an in-shader draw-count guard.MetalSpeedprobe. The probe used a Metal shared-buffer readback for CPU-side pixel verification, while the ICB path itself remains GPU-side command generation plus indirect draw execution.MTLIndirectCommandBufferDescriptorinheritance defaults are usable, but calling optional generated objc2 setters such assetInheritDepthStencilState:can crash because the selector is not present on that runtime. The implementation relies on default inheritance instead of calling those optional setters.maxVertexBufferBindCountwas set from the currently bound vertex-buffer count. The fragment bind count is currently set to0because this fast path rejects active render bind groups and immediates and does not restore fragment buffer bindings into ICB state.Apple Paravirtual device, Metal reports ICB-related feature sets/families and the debug dump showedindirect_command_buffers_rendering: trueandindirect_command_buffers_compute: true, but all MDI cases SIGABRTed when the ICB path executed. This PR treats virtual adapters like the existing mesh-shader virtual-device quirk and keeps them on the CPU fallback until that driver path is independently validated.Gaps:
Testing
Local validation:
cargo check -p wgpu-hal --features metal --no-default-features.cargo check -p wgpu-test --test wgpu-gpu.WGPU_BACKEND=metal cargo test -p wgpu-test --test wgpu-gpu multi_draw -- --test-threads=1 --nocapture, 13 passed.WGPU_BACKEND=metal cargo test -p wgpu-test --test wgpu-gpu draw_indirect -- --test-threads=1 --nocapture, 33 passed.cargo xtask changelog rebecker/trunk.Device validation:
aarch64-apple-iosprobe app passed GPU-generated non-indexed and indexed MDI after the OS-version-aware mobile ICB gate.MetalSpeedprobe launched withPROBE_AUTORUN_CAPABILITY=multiDrawIndirectSubset, verified build stampmdi-watch-se2-shared-readback-20260603-175517, created a Metal shared readback buffer, and completed Phase 2 withsuccess=trueand 26,448 non-background pixels (40.4% coverage).UIDeviceFamily = 2,MinimumOSVersion = 17.0) passed GPU-generated non-indexed and indexed MDI through the CPU fallback after the OS-version-aware mobile ICB gate.draw_count > 1was enabled during one M4 run and one iPhone XS run; both passed, proving these generated-argument cases did not use CPU-side per-draw fallback. The trap was removed before this PR.CI notes:
Apple Paravirtual deviceSIGABRTs inmulti_draw_indirect,multi_draw_indexed_indirect, and both GPU-generated-argument variants. I gated ICB capability exposure on virtual Metal adapters so CI exercises (and simulators) the existing CPU fallback there while unfettered Apple GPUs continue to use ICBs.Squash or Rebase?
Squash before merge. The branch currently keeps the development and hardening iterations visible for review, but the final upstream landing would be cleaner as one feature commit or a maintainer-curated split.
Checklist
wgpumay be affected behaviorally (if they enable MDI feature).CHANGELOG.mdentries for the user-facing effects of this change are present.