Skip to content

fix(core): scope texture/buffer usage conflicts per pass, not per encoder - #367

Merged
kolkov merged 1 commit into
mainfrom
fix/encoder-usage-scopes
Oct 18, 2026
Merged

kolkov merged 1 commit into
mainfrom
fix/encoder-usage-scopes

Conversation

@kolkov

@kolkov kolkov commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Problem

A render pass followed by a copy of its target in the same encoder fails at Finish:

finish encoder: cannot finish: encoder in Error state: wgpu: texture usage conflict:
track: texture 0 usage conflict: existing 32 incompatible with 4

This is the readback pattern of examples/triangle-headless, which fails like this on current main on Vulkan, DX12 and GLES. Uploading into a texture or vertex buffer and then rendering with it in the same encoder fails the same way.

It is a regression from v0.31.6 (#312): copy usages were merged into one encoder-wide usage scope, and conflicts were checked across the whole encoder. WebGPU defines usage conflicts only inside one usage scope: a render pass, a dispatch, or a single copy command. Between scopes, a resource may change usage freely; the implementation inserts the barrier.

Fix

Usage tracking now follows the Rust wgpu-core tracker model (wgpu-core/src/track/mod.rs):

  • Per-scope conflict checks. Each render pass and each copy command gets its own TextureUsageScope / BufferUsageScope. Conflicts are validated only inside it: the same texture as color and depth/stencil attachment, a texture or buffer copied onto itself.
  • Per-encoder tracker. New EncoderTextureTracker / EncoderBufferTracker (core/track/encoder_tracker.go) keep the first (start) and last (end) usage of every resource in the command buffer and return the transitions between scopes. This matches TextureTracker::set_single / set_from_usage_scope in track/texture.rs and BufferTracker::set_single in track/buffer.rs. Copy commands record their transitions right before the copy, as command/transfer.rs does (set_single(src, COPY_SRC), set_single(dst, COPY_DST)). Render pass attachments record theirs before the HAL pass begins. Example: ColorTarget → CopySrc before the readback copy.
  • Submit-time merge. Barriers go from the device state into the command buffer's start usage, and the device then adopts its end usage. Before this change, one OR-ed usage was used for both.
  • CommandEncoder.TransitionTextures updates the tracker, so the next command does not repeat an explicit barrier. This replaces the explicitTextureTransitions special case.

Tests

  • New encoder_usage_scope_test.go: render then readback, upload then render, upload then sample, copy then vertex buffer, render → copy → render, all in one encoder. A behaviour-only copy of this file was run against main: 4 of 5 fail with the false conflicts. The fifth (sampled in a pass) passes on main only because bind-group textures are not tracked yet (see below).
  • New core/track/encoder_tracker_test.go for start/end/transition bookkeeping.
  • Tests that asserted cross-command conflicts now assert the transitions. Self-copies are still rejected atomically.
  • examples/triangle-headless: SUCCESS on Vulkan (Khronos validation layer, log file empty at error,warn), DX12 and GLES (Intel Iris Xe). Main fails on all three with the error above.
  • gofmt, go build, go test ./..., go mod tidy, golangci-lint on windows/linux/darwin.

Not in this PR (pre-existing gaps, not regressions)

  • Bind-group and compute-pass resources are not tracked yet.
  • Device tracker never forgets resources (related: core: Texture.Destroy doesn't clear the device-tracker slot before recycling the index #365).
  • A copy within one texture is still rejected as a self-copy. Rust allows it between different mip levels or non-overlapping array layers (validate_copy_within_same_texture in command/transfer.rs).
  • Vulkan usage → layout mapping for depth and offscreen targets needs a separate review.

The diff is about 800 non-test lines across core/command.go, core/track/ and encoder_native.go. It is one change (the tracker model), so it is not split.

…oder

A render pass followed by CopyTextureToBuffer of its target in the same
encoder failed Finish with 'texture usage conflict: existing 32
incompatible with 4' (triangle-headless readback). Since v0.31.6 copy
usages were merged into one encoder-wide usage scope with conflict
checks, but WebGPU only defines conflicts inside one usage scope (a
render pass, a dispatch, a single copy command).

Follow the Rust wgpu-core tracker model (track/mod.rs):
- Each render pass and each copy command is its own TextureUsageScope /
  BufferUsageScope; conflicts are validated only inside it.
- New EncoderTextureTracker / EncoderBufferTracker keep the first
  (start) and last (end) usage of every resource in the command buffer
  and return transitions between usage scopes (set_single /
  set_from_usage_scope). Copies record them right before the copy;
  render pass attachments before the HAL pass begins.
- Submit-time merge emits device.end -> cb.start barriers and adopts
  cb.end (MergeEncoder), instead of one OR-ed state for both.
- CommandEncoder.TransitionTextures updates the tracker, replacing the
  explicitTextureTransitions special case.

Regression tests: render-then-readback, upload-then-render,
upload-then-sample, copy-then-vertex-buffer, render-copy-render in one
encoder. Tests that asserted cross-command conflicts now assert
transitions; self-copies remain rejected atomically.
@codecov

codecov Bot commented Oct 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.51988% with 31 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
core/command.go 75.00% 9 Missing and 10 partials ⚠️
encoder_native.go 92.50% 4 Missing and 2 partials ⚠️
core/track/buffer.go 90.00% 1 Missing and 1 partial ⚠️
core/track/encoder_tracker.go 98.40% 1 Missing and 1 partial ⚠️
core/track/texture.go 90.00% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@kolkov
kolkov merged commit 3e9824c into main Oct 18, 2026
15 checks passed
@kolkov
kolkov deleted the fix/encoder-usage-scopes branch October 18, 2026 17:41
@kolkov kolkov mentioned this pull request Oct 18, 2026
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.

1 participant