Skip to content

Metal: lower large indexed multi-draws to ICBs#263

Open
besmpl wants to merge 1 commit into
gogpu:mainfrom
besmpl:besmpl/metal-icb-lowering
Open

Metal: lower large indexed multi-draws to ICBs#263
besmpl wants to merge 1 commit into
gogpu:mainfrom
besmpl:besmpl/metal-icb-lowering

Conversation

@besmpl

@besmpl besmpl commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Stack and review order

This is a stacked PR built on the current head of #253 (1953452) and is ready for review. Please approve the prerequisite PRs before this one.

Please review and land the stack in this order:

  1. fix(dx12): match indirect descriptor ABI size #258
  2. Add fixed-count indirect multi-draw #253
  3. this PR

Until #253 lands, GitHub shows its commits in this PR's diff because its head branch is in a fork and cannot be selected as an upstream base. After the prerequisites settle, this branch will be rebased onto main and the verification below rerun.

What changed

  • Delay Metal render-command-encoder creation until the first draw while retaining bounded, latest-value pre-draw state. Clear/store/resolve-only passes still instantiate at pass end.
  • Privately mark eligible buffer-only triangle pipelines as ICB-compatible. A rejected flagged pipeline transparently retries ordinary creation.
  • Translate a profitable first indexed multi-draw into one bounded Metal indirect command buffer with one compute dispatch and one ICB execution.
  • Retain every ICB-referenced argument, index, vertex, and bound buffer through command-buffer completion, with once-only cleanup across completion, discard, setup failure, and teardown.
  • Preserve the exact Add fixed-count indirect multi-draw #253 loop for count zero, small or oversized counts, later draws, non-indexed draws, incompatible pipelines, writable storage bindings, unsupported devices, and every preflight failure.

Scope

The optimization is entirely private to hal/metal. It adds no public feature, flag, token, prepared-command API, or caller-managed lifetime. Metal feature reporting remains unchanged.

The initial eligibility policy is intentionally narrow:

  • Apple7-family support or later, with runtime selector checks
  • first draw in the pass
  • triangle-list indexed draw
  • 1,024 through 52,428 commands
  • buffer-only pipeline with no writable storage binding

Verification

  • CGO_ENABLED=0 go test ./...
  • focused Metal unit tests and race tests
  • native Metal pixel/readback parity for uint16 and uint32 indices, non-zero argument/index offsets, baseVertex, firstInstance, GPU-written indirect arguments, bound uniform/read-only-storage buffers, first-vs-later execution, and clear-only passes
  • Linux amd64 and Windows amd64 cgo-free builds
  • Rust-tag build
  • browser/WASM root build and browser-package compile
  • git diff --check and formatting checks

Paired Apple M1 complete-frame benchmarks against the exact #253 base measured median improvements of about 19% at 1,024 commands and 60% at 10,000 commands. Direct, count-1, and count-20 controls stayed within the 5% time/allocation gate in the stable paired run.

@besmpl
besmpl marked this pull request as ready for review July 15, 2026 11:52
@besmpl
besmpl requested a review from kolkov as a code owner July 15, 2026 11:52
@besmpl
besmpl force-pushed the besmpl/metal-icb-lowering branch 3 times, most recently from 79b7521 to 05f98b7 Compare July 22, 2026 06:44
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@besmpl
besmpl force-pushed the besmpl/metal-icb-lowering branch from 05f98b7 to 4da2d64 Compare July 22, 2026 13:34

@kolkov kolkov left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First review. This is high-quality engineering — an original optimization beyond Rust wgpu parity (Rust uses a simple loop for draw_indexed_indirect, confirmed at command.rs:1277-1308).

The architecture is sound at every level:

  1. Deferred render encoder — delays native encoder creation until first draw, enabling ICB compute dispatch for eligible first indexed multi-draws.
  2. Bounded state journaling — pipeline, bind groups, vertex buffers, viewport, scissor, blend, stencil recorded in fixed-size arrays with zero allocations (benchmarked).
  3. ICB eligibility — correctly gated: buffer-only bindings, triangle-list topology, Apple7+ GPU family, 1024–52428 command range. Non-eligible calls fall back to loop — fail-safe at every decision point.
  4. Compute kernel — per-index-format (uint16/uint32), translates DrawIndexedArgs into ICB commands. Cached per device.
  5. Ownership — ICB + argument buffer retained through command buffer completion with sync.Once cleanup. LIFO release order correct.

Test coverage is exceptional: 36 test functions, 7 benchmarks, pixel-level ICB vs loop parity test with GPU-written indirect args. 96% patch coverage.

Minor notes (non-blocking):

  • Per-call ICB creation (no pooling) — acceptable for v1, future optimization opportunity
  • DX12 unconditionally reports FeatureMultiDrawIndirect — correct for D3D12 but worth a comment

Should merge after #253 (stacked dependency). LGTM.

@kolkov

kolkov commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

#253 is merged. Since we squash-merged it, #263 now shows conflicts from the replay commits. Could you rebase onto current main and keep just the ICB commit? Approval stands.

@besmpl
besmpl force-pushed the besmpl/metal-icb-lowering branch from 0310f07 to b600f29 Compare July 22, 2026 17:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants