diff --git a/copy_usage_coverage_test.go b/copy_usage_coverage_test.go new file mode 100644 index 0000000..d78b46c --- /dev/null +++ b/copy_usage_coverage_test.go @@ -0,0 +1,225 @@ +//go:build !rust && !(js && wasm) + +package wgpu + +import ( + "testing" + + "github.com/gogpu/gputypes" + "github.com/gogpu/wgpu/core" + "github.com/gogpu/wgpu/core/track" +) + +func TestCopyUsageHelpersSkipUntrackedResources(t *testing.T) { + t.Parallel() + + encoder := &CommandEncoder{} + if !encoder.recordCopyBufferUsages([]copyBufferUsage{{usage: track.BufferUsesCopySrc}}) { + t.Fatal("nil buffer should be ignored") + } + if !encoder.recordCopyUsages(nil, nil, track.BufferUsesNone) { + t.Fatal("encoder without core state should ignore copy usage") + } + + invalidCoreTexture := core.NewTexture( + nil, nil, + gputypes.TextureFormatRGBA8Unorm, + gputypes.TextureDimension2D, + gputypes.TextureUsageCopySrc, + gputypes.Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + 1, 1, "invalid-tracking", + ) + t.Cleanup(invalidCoreTexture.Destroy) + + requests := []copyTextureUsage{ + {texture: nil, usage: track.TextureUsesCopySrc}, + {texture: &Texture{}, usage: track.TextureUsesCopySrc}, + {texture: &Texture{coreTexture: &core.Texture{}}, usage: track.TextureUsesCopySrc}, + {texture: &Texture{coreTexture: invalidCoreTexture}, usage: track.TextureUsesCopySrc}, + } + prepared, err := prepareCopyTextureUsages(requests) + if err != nil { + t.Fatalf("prepareCopyTextureUsages: %v", err) + } + if len(prepared) != 0 { + t.Fatalf("prepared %d untracked resources, want 0", len(prepared)) + } +} + +func TestCopyUsagePreflightGuardBranches(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + + texture := createCopyScopeTexture(t, device, "preflight-guards", TextureUsageCopySrc|TextureUsageCopyDst) + defer texture.Release() + encoder, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + defer encoder.DiscardEncoding() + if !encoder.recordCopyBufferUsages([]copyBufferUsage{{usage: track.BufferUsesCopySrc}}) { + t.Fatal("nil buffer with live core encoder should be ignored") + } + if !encoder.recordCopyBufferUsages([]copyBufferUsage{{buffer: &core.Buffer{}, usage: track.BufferUsesCopySrc}}) { + t.Fatal("buffer without tracking data should be ignored") + } + + textureIndex := texture.coreTexture.TrackingData().Index() + if err := encoder.core.Mutable().TextureScope().SetUsage(textureIndex, track.TextureUsesColorTarget); err != nil { + t.Fatalf("seed texture usage: %v", err) + } + + request := preparedCopyTextureUsage{ + texture: texture, + index: textureIndex, + usage: track.TextureUsesCopySrc, + } + prepared, err := prepareCopyTextureUsages([]copyTextureUsage{ + {texture: texture, usage: track.TextureUsesCopySrc}, + {texture: texture, usage: track.TextureUsesResource}, + }) + if err != nil { + t.Fatalf("prepare compatible duplicate texture usages: %v", err) + } + if len(prepared) != 1 || prepared[0].usage != track.TextureUsesCopySrc|track.TextureUsesResource { + t.Fatalf("prepared duplicate usages = %+v, want one CopySrc|Resource request", prepared) + } + + encoder.core.Mutable().TextureScope().ReplaceUsage(textureIndex, track.TextureUsesResource) + compatibleRequests := []preparedCopyTextureUsage{request} + if err := encoder.preflightCopyTextureUsages(compatibleRequests); err != nil { + t.Fatalf("compatible texture usage rejected: %v", err) + } + if got := compatibleRequests[0].usage; got != track.TextureUsesResource|track.TextureUsesCopySrc { + t.Fatalf("merged compatible texture usage = %v, want Resource|CopySrc", got) + } + encoder.core.Mutable().TextureScope().ReplaceUsage(textureIndex, track.TextureUsesColorTarget) + + encoder.explicitTextureTransitions = map[*Texture]TextureUsage{ + texture: TextureUsageCopyDst, + } + if err := encoder.preflightCopyTextureUsages([]preparedCopyTextureUsage{request}); err == nil { + t.Fatal("mismatched explicit transition accepted incompatible usage") + } + + encoder.explicitTextureTransitions[texture] = TextureUsageCopySrc + requests := []preparedCopyTextureUsage{request} + if err := encoder.preflightCopyTextureUsages(requests); err != nil { + t.Fatalf("matching explicit transition rejected: %v", err) + } + if got := requests[0].usage; got != track.TextureUsesCopySrc { + t.Fatalf("usage after explicit transition = %v, want CopySrc", got) + } + + if _, _, tracked, err := encoder.preflightCopyBufferUsage(&core.Buffer{}, track.BufferUsesCopySrc); err != nil || tracked { + t.Fatalf("buffer without tracking data = (tracked %v, err %v), want ignored", tracked, err) + } + invalidBuffer := core.NewBuffer(nil, nil, gputypes.BufferUsageCopySrc, 4, "invalid-tracking") + defer invalidBuffer.Destroy() + if !encoder.recordCopyBufferUsages([]copyBufferUsage{{buffer: invalidBuffer, usage: track.BufferUsesCopySrc}}) { + t.Fatal("buffer with invalid tracker index should be ignored") + } + if _, _, tracked, err := encoder.preflightCopyBufferUsage(invalidBuffer, track.BufferUsesCopySrc); err != nil || tracked { + t.Fatalf("buffer with invalid tracker index = (tracked %v, err %v), want ignored", tracked, err) + } + + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Size: 4, + Usage: BufferUsageCopySrc | BufferUsageStorage, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + defer buffer.Release() + bufferIndex := buffer.core.TrackingData().Index() + if !encoder.recordCopyBufferUsages([]copyBufferUsage{ + {buffer: buffer.core, usage: track.BufferUsesCopySrc}, + {buffer: buffer.core, usage: track.BufferUsesUniform}, + }) { + t.Fatal("duplicate buffer endpoint rejected compatible usages") + } + if got := encoder.core.Mutable().BufferScope().GetUsage(bufferIndex); got != track.BufferUsesCopySrc|track.BufferUsesUniform { + t.Fatalf("combined duplicate buffer usage = %v, want CopySrc|Uniform", got) + } + encoder.core.Mutable().BufferScope().ReplaceUsage(bufferIndex, track.BufferUsesNone) + + if encoder.recordCopyBufferUsages([]copyBufferUsage{ + {buffer: buffer.core, usage: track.BufferUsesCopySrc}, + {buffer: buffer.core, usage: track.BufferUsesCopyDst}, + }) { + t.Fatal("duplicate buffer endpoint accepted incompatible usages") + } + + encoder.core.Mutable().BufferScope().ReplaceUsage(bufferIndex, track.BufferUsesVertex) + if !encoder.recordCopyBufferUsages([]copyBufferUsage{{buffer: buffer.core, usage: track.BufferUsesUniform}}) { + t.Fatal("compatible existing buffer usage was rejected") + } + if got := encoder.core.Mutable().BufferScope().GetUsage(bufferIndex); got != track.BufferUsesVertex|track.BufferUsesUniform { + t.Fatalf("merged compatible buffer usage = %v, want Vertex|Uniform", got) + } + if _, usage, tracked, err := encoder.preflightCopyBufferUsage(buffer.core, track.BufferUsesUniform); err != nil || !tracked || usage != track.BufferUsesVertex|track.BufferUsesUniform { + t.Fatalf("compatible buffer preflight = (usage %v, tracked %v, err %v), want Vertex|Uniform", usage, tracked, err) + } + encoder.core.Mutable().BufferScope().ReplaceUsage(bufferIndex, track.BufferUsesStorageWrite) + if encoder.recordCopyBufferUsages([]copyBufferUsage{{buffer: buffer.core, usage: track.BufferUsesCopySrc}}) { + t.Fatal("recordCopyBufferUsages accepted incompatible usage") + } +} + +func TestCopyBufferToBufferRejectsReleasedEndpoint(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + src := createCopyScopeBuffer(t, device, "released-copy-src") + dst := createCopyScopeBuffer(t, device, "released-copy-dst") + defer dst.Release() + src.Release() + + encoder, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + encoder.CopyBufferToBuffer(src, 0, dst, 0, 4) + if _, err := encoder.Finish(); err == nil { + t.Fatal("Finish succeeded after copying from a released buffer") + } +} + +func TestTransitionTexturesReusesExplicitTransitionMap(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + texture := createCopyScopeTexture(t, device, "explicit-transition-map", TextureUsageCopySrc|TextureUsageCopyDst) + defer texture.Release() + encoder, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + + encoder.TransitionTextures([]TextureBarrier{{ + Texture: texture, + Usage: TextureUsageTransition{ + OldUsage: TextureUsageTextureBinding, + NewUsage: TextureUsageCopySrc, + }, + }}) + encoder.TransitionTextures([]TextureBarrier{{ + Texture: texture, + Usage: TextureUsageTransition{ + OldUsage: TextureUsageCopySrc, + NewUsage: TextureUsageCopyDst, + }, + }}) + if got := encoder.explicitTextureTransitions[texture]; got != TextureUsageCopyDst { + t.Fatalf("explicit transition state = %v, want CopyDst", got) + } + commandBuffer, err := encoder.Finish() + if err != nil { + t.Fatalf("Finish: %v", err) + } + commandBuffer.Release() +} diff --git a/core/command.go b/core/command.go index a151ee8..00c3439 100644 --- a/core/command.go +++ b/core/command.go @@ -1017,6 +1017,41 @@ func (e *CoreCommandEncoder) RecordBufferUsage(buffer *Buffer, usage track.Buffe return e.mutable.bufferScope.SetUsage(td.Index(), usage) } +// RecordTextureUsage records a texture usage in the command buffer's texture +// scope. This is called by commands that use a texture without going through a +// texture view, such as copy commands. +// +// Textures without valid tracking data are silently skipped. Returns an error +// if the texture already has an incompatible usage in this command buffer. +// +// Reference: wgpu-core command/transfer.rs copy texture usage validation +func (e *CoreCommandEncoder) RecordTextureUsage(texture *Texture, usage track.TextureUses) error { + if e.mutable == nil || e.mutable.textureScope == nil || texture == nil { + return nil + } + td := texture.TrackingData() + if td == nil || !td.Index().IsValid() { + return nil + } + return e.mutable.textureScope.SetUsage(td.Index(), usage) +} + +// ReplaceTextureUsage records the state after an explicit texture transition. +// Unlike RecordTextureUsage, it replaces an incompatible earlier state because +// the caller has already encoded the barrier between those states. +// +// Reference: wgpu-core command/transfer.rs explicit texture transitions +func (e *CoreCommandEncoder) ReplaceTextureUsage(texture *Texture, usage track.TextureUses) { + if e.mutable == nil || e.mutable.textureScope == nil || texture == nil { + return + } + td := texture.TrackingData() + if td == nil || !td.Index().IsValid() { + return + } + e.mutable.textureScope.ReplaceUsage(td.Index(), usage) +} + // ============================================================================= // Core Render Pass Encoder // ============================================================================= diff --git a/core/texture_copy_scope_test.go b/core/texture_copy_scope_test.go new file mode 100644 index 0000000..6291a5b --- /dev/null +++ b/core/texture_copy_scope_test.go @@ -0,0 +1,48 @@ +//go:build !(js && wasm) + +package core + +import ( + "testing" + + "github.com/gogpu/gputypes" + "github.com/gogpu/wgpu/core/track" +) + +func TestCoreCommandEncoderRecordTextureUsage(t *testing.T) { + t.Parallel() + + device := NewDevice(&mockHALDevice{}, &Adapter{}, 0, gputypes.DefaultLimits(), "copy-scope") + texture := NewTexture( + mockTexture{}, device, + gputypes.TextureFormatRGBA8Unorm, gputypes.TextureDimension2D, + gputypes.TextureUsageCopySrc|gputypes.TextureUsageCopyDst, + gputypes.Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + 1, 1, "copy-texture", + ) + encoder, err := device.CreateCommandEncoder("copy-scope") + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + + if err := encoder.RecordTextureUsage(texture, track.TextureUsesCopySrc); err != nil { + t.Fatalf("RecordTextureUsage: %v", err) + } + idx := texture.TrackingData().Index() + if got := encoder.Mutable().TextureScope().GetUsage(idx); got != track.TextureUsesCopySrc { + t.Fatalf("usage = %v, want CopySrc", got) + } + if err := encoder.RecordTextureUsage(texture, track.TextureUsesCopyDst); err == nil { + t.Fatal("incompatible CopyDst usage succeeded") + } + + encoder.ReplaceTextureUsage(texture, track.TextureUsesCopyDst) + if got := encoder.Mutable().TextureScope().GetUsage(idx); got != track.TextureUsesCopyDst { + t.Fatalf("replacement usage = %v, want CopyDst", got) + } + + if err := encoder.RecordTextureUsage(nil, track.TextureUsesCopySrc); err != nil { + t.Fatalf("nil texture should be ignored: %v", err) + } + encoder.ReplaceTextureUsage(nil, track.TextureUsesCopySrc) +} diff --git a/core/texture_usage_coverage_test.go b/core/texture_usage_coverage_test.go new file mode 100644 index 0000000..4806643 --- /dev/null +++ b/core/texture_usage_coverage_test.go @@ -0,0 +1,53 @@ +//go:build !(js && wasm) + +package core + +import ( + "testing" + + "github.com/gogpu/gputypes" + "github.com/gogpu/wgpu/core/track" +) + +func TestCoreCommandEncoderTextureUsageGuards(t *testing.T) { + t.Parallel() + + encoders := []*CoreCommandEncoder{ + {}, + {mutable: &CommandBufferMutable{}}, + {mutable: &CommandBufferMutable{textureScope: track.NewTextureUsageScope()}}, + } + for i, encoder := range encoders { + if err := encoder.RecordTextureUsage(nil, track.TextureUsesCopySrc); err != nil { + t.Fatalf("encoder %d RecordTextureUsage(nil): %v", i, err) + } + encoder.ReplaceTextureUsage(nil, track.TextureUsesCopySrc) + } + + encoder := &CoreCommandEncoder{ + mutable: &CommandBufferMutable{textureScope: track.NewTextureUsageScope()}, + } + withoutTrackingData := &Texture{} + if err := encoder.RecordTextureUsage(withoutTrackingData, track.TextureUsesCopySrc); err != nil { + t.Fatalf("RecordTextureUsage without tracking data: %v", err) + } + encoder.ReplaceTextureUsage(withoutTrackingData, track.TextureUsesCopySrc) + + invalidTrackingData := NewTexture( + nil, nil, + gputypes.TextureFormatRGBA8Unorm, + gputypes.TextureDimension2D, + gputypes.TextureUsageCopySrc, + gputypes.Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + 1, 1, "invalid-tracking", + ) + t.Cleanup(invalidTrackingData.Destroy) + if err := encoder.RecordTextureUsage(invalidTrackingData, track.TextureUsesCopySrc); err != nil { + t.Fatalf("RecordTextureUsage with invalid tracking data: %v", err) + } + encoder.ReplaceTextureUsage(invalidTrackingData, track.TextureUsesCopySrc) + + if !encoder.mutable.textureScope.IsEmpty() { + t.Fatal("guarded texture usage unexpectedly mutated the scope") + } +} diff --git a/core/track/buffer.go b/core/track/buffer.go index 41ef780..56e5fe9 100644 --- a/core/track/buffer.go +++ b/core/track/buffer.go @@ -250,6 +250,15 @@ func (s *BufferUsageScope) SetUsage(index TrackerIndex, usage BufferUses) error return nil } +// ReplaceUsage unconditionally replaces the usage recorded for a buffer. It is +// intended only after every resource in a multi-resource command has passed +// preflight validation, so committing the command cannot partially fail. +func (s *BufferUsageScope) ReplaceUsage(index TrackerIndex, usage BufferUses) { + s.ensureSize(int(index) + 1) + s.states[index] = BufferState{usage: usage} + s.metadata.SetOwned(index, true) +} + // GetUsage returns the current usage in this scope. func (s *BufferUsageScope) GetUsage(index TrackerIndex) BufferUses { if int(index) < len(s.states) && s.metadata.IsOwned(index) { diff --git a/core/track/buffer_test.go b/core/track/buffer_test.go index c4c2432..467566f 100644 --- a/core/track/buffer_test.go +++ b/core/track/buffer_test.go @@ -233,6 +233,24 @@ func TestBufferUsageScope_SetUsage(t *testing.T) { } } +func TestBufferUsageScope_ReplaceUsage(t *testing.T) { + t.Parallel() + + scope := NewBufferUsageScope() + idx := TrackerIndex(7) + if err := scope.SetUsage(idx, BufferUsesCopySrc); err != nil { + t.Fatalf("SetUsage: %v", err) + } + + scope.ReplaceUsage(idx, BufferUsesCopyDst) + if got := scope.GetUsage(idx); got != BufferUsesCopyDst { + t.Fatalf("usage = %v, want CopyDst", got) + } + if !scope.IsUsed(idx) { + t.Fatal("replaced buffer is not marked used") + } +} + func TestBufferUsageScope_Clear(t *testing.T) { scope := NewBufferUsageScope() diff --git a/core/track/texture.go b/core/track/texture.go index a51dd20..4c42cd8 100644 --- a/core/track/texture.go +++ b/core/track/texture.go @@ -311,6 +311,15 @@ func (s *TextureUsageScope) SetUsage(index TrackerIndex, usage TextureUses) erro return nil } +// ReplaceUsage unconditionally replaces the usage recorded for a texture. It +// is intended only after preflight validation, or after an explicit transition +// where the scope must describe the state after the encoded barrier. +func (s *TextureUsageScope) ReplaceUsage(index TrackerIndex, usage TextureUses) { + s.ensureSize(int(index) + 1) + s.states[index] = TextureState{usage: usage} + s.metadata.SetOwned(index, true) +} + // GetUsage returns the current usage in this scope. func (s *TextureUsageScope) GetUsage(index TrackerIndex) TextureUses { if int(index) < len(s.states) && s.metadata.IsOwned(index) { diff --git a/core/track/texture_test.go b/core/track/texture_test.go index 5f43e14..532700d 100644 --- a/core/track/texture_test.go +++ b/core/track/texture_test.go @@ -376,6 +376,24 @@ func TestTextureUsageScope_Clear(t *testing.T) { } } +func TestTextureUsageScope_ReplaceUsage(t *testing.T) { + t.Parallel() + + scope := NewTextureUsageScope() + idx := TrackerIndex(7) + if err := scope.SetUsage(idx, TextureUsesColorTarget); err != nil { + t.Fatalf("SetUsage: %v", err) + } + + scope.ReplaceUsage(idx, TextureUsesCopySrc) + if got := scope.GetUsage(idx); got != TextureUsesCopySrc { + t.Fatalf("usage = %v, want CopySrc", got) + } + if !scope.IsUsed(idx) { + t.Fatal("replaced texture is not marked used") + } +} + func TestTextureTracker_Merge_Transition(t *testing.T) { t.Parallel() diff --git a/encoder_native.go b/encoder_native.go index a022439..52f4a2b 100644 --- a/encoder_native.go +++ b/encoder_native.go @@ -41,6 +41,12 @@ type CommandEncoder struct { // destroyed state. usedTextures map[*Texture]struct{} + // explicitTextureTransitions records the most recent explicit transition + // for each texture. When a following command uses that exact state, its + // usage replaces (rather than conflicts with) the pre-transition scope + // state because the caller already encoded the intervening barrier. + explicitTextureTransitions map[*Texture]TextureUsage + // usedBindGroups tracks bind groups referenced during encoding for // submit-time validation (VAL-B5). At Submit, each bind group is checked // for destroyed state. Matches Rust wgpu-core's cmd_buf_data.trackers.bind_groups @@ -104,15 +110,197 @@ func (e *CommandEncoder) trackBindGroup(bg *BindGroup) { e.usedBindGroups[bg] = struct{}{} } -// recordBufferUsage records a buffer usage in the core command encoder's -// buffer scope for submit-time barrier generation. Errors (usage conflicts) -// are recorded as deferred errors on the encoder. -func (e *CommandEncoder) recordBufferUsage(buf *core.Buffer, usage track.BufferUses) { - if e.core == nil || buf == nil { - return +// copyTextureUsage describes one texture endpoint of a copy command. +type copyTextureUsage struct { + texture *Texture + usage track.TextureUses +} + +type preparedCopyTextureUsage struct { + texture *Texture + index track.TrackerIndex + usage track.TextureUses +} + +type copyBufferUsage struct { + buffer *core.Buffer + usage track.BufferUses +} + +type preparedCopyBufferUsage struct { + index track.TrackerIndex + usage track.BufferUses +} + +// recordCopyBufferUsages preflights every buffer endpoint before committing +// any usage. This keeps failed multi-buffer copies atomic, matching the mixed +// texture/buffer copy paths below. +func (e *CommandEncoder) recordCopyBufferUsages(requests []copyBufferUsage) bool { + if e.core == nil { + return true + } + + prepared := make([]preparedCopyBufferUsage, 0, len(requests)) + positions := make(map[track.TrackerIndex]int, len(requests)) + for _, request := range requests { + if request.buffer == nil { + continue + } + td := request.buffer.TrackingData() + if td == nil || !td.Index().IsValid() { + continue + } + + index := td.Index() + if position, exists := positions[index]; exists { + existing := prepared[position].usage + if !existing.IsCompatible(request.usage) { + e.setError(fmt.Errorf("wgpu: buffer usage conflict: %w", &track.UsageConflictError{ + Index: index, Existing: existing, New: request.usage, + })) + return false + } + prepared[position].usage = existing | request.usage + continue + } + + positions[index] = len(prepared) + prepared = append(prepared, preparedCopyBufferUsage{index: index, usage: request.usage}) + } + + scope := e.core.Mutable().BufferScope() + for i := range prepared { + request := &prepared[i] + if !scope.IsUsed(request.index) { + continue + } + existing := scope.GetUsage(request.index) + if !existing.IsCompatible(request.usage) { + e.setError(fmt.Errorf("wgpu: buffer usage conflict: %w", &track.UsageConflictError{ + Index: request.index, Existing: existing, New: request.usage, + })) + return false + } + request.usage |= existing + } + + for _, request := range prepared { + scope.ReplaceUsage(request.index, request.usage) + } + return true +} + +// recordCopyUsages preflights every resource scope update before committing any +// of them. Copy commands span multiple independently tracked resources, so a +// conflict on one endpoint must not leave another endpoint recorded. +func (e *CommandEncoder) recordCopyUsages(textures []copyTextureUsage, buffer *core.Buffer, bufferUsage track.BufferUses) bool { + if e.core == nil { + return true + } + prepared, err := prepareCopyTextureUsages(textures) + if err != nil { + e.setError(fmt.Errorf("wgpu: texture usage conflict: %w", err)) + return false + } + if err := e.preflightCopyTextureUsages(prepared); err != nil { + e.setError(fmt.Errorf("wgpu: texture usage conflict: %w", err)) + return false } - if err := e.core.RecordBufferUsage(buf, usage); err != nil { + bufferIndex, finalBufferUsage, trackedBuffer, err := e.preflightCopyBufferUsage(buffer, bufferUsage) + if err != nil { e.setError(fmt.Errorf("wgpu: buffer usage conflict: %w", err)) + return false + } + + // All validation is complete. ReplaceUsage cannot fail, so the commit has no + // partial-failure path after the first scope mutation. + textureScope := e.core.Mutable().TextureScope() + for _, request := range prepared { + textureScope.ReplaceUsage(request.index, request.usage) + } + if trackedBuffer { + e.core.Mutable().BufferScope().ReplaceUsage(bufferIndex, finalBufferUsage) + } + return true +} + +// prepareCopyTextureUsages groups multiple roles for the same texture and +// rejects incompatible roles before consulting or changing the command scope. +func prepareCopyTextureUsages(requests []copyTextureUsage) ([]preparedCopyTextureUsage, error) { + prepared := make([]preparedCopyTextureUsage, 0, len(requests)) + positions := make(map[track.TrackerIndex]int, len(requests)) + for _, request := range requests { + if request.texture == nil || request.texture.coreTexture == nil { + continue + } + td := request.texture.coreTexture.TrackingData() + if td == nil || !td.Index().IsValid() { + continue + } + index := td.Index() + position, exists := positions[index] + if !exists { + positions[index] = len(prepared) + prepared = append(prepared, preparedCopyTextureUsage{ + texture: request.texture, index: index, usage: request.usage, + }) + continue + } + existing := prepared[position].usage + combined := existing | request.usage + if !combined.IsCompatible(combined) { + return nil, &track.TextureUsageConflictError{ + Index: index, Existing: existing, New: request.usage, + } + } + prepared[position].usage = combined + } + return prepared, nil +} + +func (e *CommandEncoder) preflightCopyTextureUsages(requests []preparedCopyTextureUsage) error { + scope := e.core.Mutable().TextureScope() + for i := range requests { + request := &requests[i] + if !scope.IsUsed(request.index) { + continue + } + existing := scope.GetUsage(request.index) + combined := existing | request.usage + if combined.IsCompatible(combined) { + request.usage = combined + continue + } + if transitioned, ok := e.explicitTextureTransitions[request.texture]; ok && + transitioned == request.usage.ToTextureUsage() { + continue + } + return &track.TextureUsageConflictError{ + Index: request.index, Existing: existing, New: request.usage, + } + } + return nil +} + +func (e *CommandEncoder) preflightCopyBufferUsage(buffer *core.Buffer, usage track.BufferUses) (track.TrackerIndex, track.BufferUses, bool, error) { + if buffer == nil { + return 0, track.BufferUsesNone, false, nil + } + td := buffer.TrackingData() + if td == nil || !td.Index().IsValid() { + return 0, track.BufferUsesNone, false, nil + } + index := td.Index() + scope := e.core.Mutable().BufferScope() + if !scope.IsUsed(index) { + return index, usage, true, nil + } + existing := scope.GetUsage(index) + if existing.IsCompatible(usage) { + return index, existing | usage, true, nil + } + return 0, track.BufferUsesNone, false, &track.UsageConflictError{ + Index: index, Existing: existing, New: usage, } } @@ -172,14 +360,6 @@ func (e *CommandEncoder) CopyBufferToBuffer(src *Buffer, srcOffset uint64, dst * e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyBufferToBuffer: destination buffer is nil")) return } - e.trackRef(src.core.Ref) - e.trackRef(dst.core.Ref) - e.trackBuffer(src) - e.trackBuffer(dst) - // Record buffer usage in the command buffer's buffer scope for - // submit-time barrier generation. - e.recordBufferUsage(src.core, track.BufferUsesCopySrc) - e.recordBufferUsage(dst.core, track.BufferUsesCopyDst) raw := e.core.RawEncoder() if raw == nil { return @@ -187,8 +367,19 @@ func (e *CommandEncoder) CopyBufferToBuffer(src *Buffer, srcOffset uint64, dst * halSrc := src.halBuffer() halDst := dst.halBuffer() if halSrc == nil || halDst == nil { + e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyBufferToBuffer: source or destination buffer is released: %w", ErrReleased)) return } + if !e.recordCopyBufferUsages([]copyBufferUsage{ + {buffer: src.core, usage: track.BufferUsesCopySrc}, + {buffer: dst.core, usage: track.BufferUsesCopyDst}, + }) { + return + } + e.trackRef(src.core.Ref) + e.trackRef(dst.core.Ref) + e.trackBuffer(src) + e.trackBuffer(dst) raw.CopyBufferToBuffer(halSrc, halDst, []hal.BufferCopy{ {SrcOffset: srcOffset, DstOffset: dstOffset, Size: size}, }) @@ -218,19 +409,28 @@ func (e *CommandEncoder) CopyTextureToBuffer(src *Texture, dst *Buffer, regions e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyTextureToBuffer: region texture is released: %w", ErrReleased)) return } - e.trackTexture(region.TextureBase.Texture) } - e.trackTexture(src) - e.trackBuffer(dst) - e.recordBufferUsage(dst.core, track.BufferUsesCopyDst) + halDst := dst.halBuffer() + if halDst == nil { + e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyTextureToBuffer: destination buffer is released: %w", ErrReleased)) + return + } raw := e.core.RawEncoder() if raw == nil { return } - halDst := dst.halBuffer() - if halDst == nil { + if !e.recordCopyUsages( + []copyTextureUsage{{texture: src, usage: track.TextureUsesCopySrc}}, + dst.core, track.BufferUsesCopyDst, + ) { return } + for _, region := range regions { + e.trackTexture(region.TextureBase.Texture) + } + e.trackTexture(src) + e.trackBuffer(dst) + e.trackRef(dst.core.Ref) halRegions := make([]hal.BufferTextureCopy, len(regions)) for i, r := range regions { halRegions[i] = r.toHAL() @@ -264,15 +464,23 @@ func (e *CommandEncoder) CopyTextureToTexture(src, dst *Texture, regions []Textu e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyTextureToTexture: region texture is released: %w", ErrReleased)) return } - e.trackTexture(region.Source.Texture) - e.trackTexture(region.Destination.Texture) } - e.trackTexture(src) - e.trackTexture(dst) raw := e.core.RawEncoder() if raw == nil { return } + if !e.recordCopyUsages([]copyTextureUsage{ + {texture: src, usage: track.TextureUsesCopySrc}, + {texture: dst, usage: track.TextureUsesCopyDst}, + }, nil, track.BufferUsesNone) { + return + } + for _, region := range regions { + e.trackTexture(region.Source.Texture) + e.trackTexture(region.Destination.Texture) + } + e.trackTexture(src) + e.trackTexture(dst) halRegions := make([]hal.TextureCopy, len(regions)) for i, r := range regions { halRegions[i] = r.toHAL() @@ -293,6 +501,7 @@ func (e *CommandEncoder) TransitionTextures(barriers []TextureBarrier) { return } halBarriers := make([]hal.TextureBarrier, 0, len(barriers)) + validBarriers := make([]TextureBarrier, 0, len(barriers)) for _, b := range barriers { if b.Texture != nil && b.Texture.resolveHAL() == nil { e.setError(fmt.Errorf("wgpu: CommandEncoder.TransitionTextures: texture is released: %w", ErrReleased)) @@ -303,9 +512,16 @@ func (e *CommandEncoder) TransitionTextures(barriers []TextureBarrier) { } e.trackTexture(b.Texture) halBarriers = append(halBarriers, b.toHAL()) + validBarriers = append(validBarriers, b) } if len(halBarriers) > 0 { raw.TransitionTextures(halBarriers) + if e.explicitTextureTransitions == nil { + e.explicitTextureTransitions = make(map[*Texture]TextureUsage) + } + for _, b := range validBarriers { + e.explicitTextureTransitions[b.Texture] = b.Usage.NewUsage + } } } @@ -328,13 +544,24 @@ func (e *CommandEncoder) CopyBufferToTexture(src *Buffer, dst *Texture, regions e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyBufferToTexture: destination texture is released: %w", ErrReleased)) return } - e.trackTexture(dst) - e.trackBuffer(src) - e.recordBufferUsage(src.core, track.BufferUsesCopySrc) + halSrc := src.halBuffer() + if halSrc == nil { + e.setError(fmt.Errorf("wgpu: CommandEncoder.CopyBufferToTexture: source buffer is released: %w", ErrReleased)) + return + } raw := e.core.RawEncoder() if raw == nil { return } + if !e.recordCopyUsages( + []copyTextureUsage{{texture: dst, usage: track.TextureUsesCopyDst}}, + src.core, track.BufferUsesCopySrc, + ) { + return + } + e.trackTexture(dst) + e.trackBuffer(src) + e.trackRef(src.core.Ref) halRegions := make([]hal.BufferTextureCopy, len(regions)) for i, r := range regions { halRegions[i] = hal.BufferTextureCopy{ @@ -351,7 +578,7 @@ func (e *CommandEncoder) CopyBufferToTexture(src *Buffer, dst *Texture, regions Size: hal.Extent3D(r.Size), } } - raw.CopyBufferToTexture(src.halBuffer(), halDst, halRegions) + raw.CopyBufferToTexture(halSrc, halDst, halRegions) } func validateRenderPassTextureViews(desc *RenderPassDescriptor) error { diff --git a/texture_copy_lifetime_test.go b/texture_copy_lifetime_test.go new file mode 100644 index 0000000..563d710 --- /dev/null +++ b/texture_copy_lifetime_test.go @@ -0,0 +1,157 @@ +//go:build !rust && !(js && wasm) + +package wgpu + +import ( + "testing" + + "github.com/gogpu/wgpu/core/track" + "github.com/gogpu/wgpu/hal" +) + +type delayedCompletionQueue struct { + hal.Queue + completed uint64 +} + +func TestTextureCopyUsageConflictDoesNotCloneBufferRef(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + texture := createCopyScopeTexture(t, device, "conflicting-copy", TextureUsageCopySrc) + defer texture.Release() + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Size: 256, + Usage: BufferUsageCopySrc | BufferUsageCopyDst, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + defer buffer.Release() + ref := buffer.core.Ref + + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + if !enc.recordCopyBufferUsages([]copyBufferUsage{{buffer: buffer.core, usage: track.BufferUsesCopySrc}}) { + t.Fatal("failed to establish conflicting CopySrc usage") + } + enc.CopyTextureToBuffer(texture, buffer, nil) + + if got := ref.RefCount(); got != 1 { + t.Fatalf("failed copy changed buffer refcount to %d, want 1", got) + } + if got := len(enc.trackedRefs); got != 0 { + t.Fatalf("failed copy retained %d resource refs, want 0", got) + } + if _, err := enc.Finish(); err == nil { + t.Fatal("Finish succeeded after incompatible buffer usages") + } +} + +func (q *delayedCompletionQueue) PollCompleted() uint64 { return q.completed } + +func TestTextureCopyBufferRefHeldUntilQueueRetirement(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + bufferUsage BufferUsage + textureUse TextureUsage + record func(*CommandEncoder, *Buffer, *Texture) + }{ + { + name: "texture to buffer destination", + bufferUsage: BufferUsageCopyDst, + textureUse: TextureUsageCopySrc, + record: func(enc *CommandEncoder, buffer *Buffer, texture *Texture) { + enc.CopyTextureToBuffer(texture, buffer, []BufferTextureCopy{{ + TextureBase: ImageCopyTexture{Texture: texture}, + BufferLayout: ImageDataLayout{BytesPerRow: 256, RowsPerImage: 1}, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + }}) + }, + }, + { + name: "buffer source to texture", + bufferUsage: BufferUsageCopySrc, + textureUse: TextureUsageCopyDst, + record: func(enc *CommandEncoder, buffer *Buffer, texture *Texture) { + enc.CopyBufferToTexture(buffer, texture, []BufferTextureCopy{{ + TextureBase: ImageCopyTexture{Texture: texture}, + BufferLayout: ImageDataLayout{BytesPerRow: 256, RowsPerImage: 1}, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + }}) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + + queue := device.Queue() + delayed := &delayedCompletionQueue{Queue: queue.hal} + queue.hal = delayed + + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Label: "texture-copy-lifetime-buffer", + Size: 256, + Usage: tt.bufferUsage, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + texture := createCopyScopeTexture(t, device, "texture-copy-lifetime", tt.textureUse) + defer texture.Release() + + ref := buffer.core.Ref + if got := ref.RefCount(); got != 1 { + t.Fatalf("initial buffer refcount = %d, want 1", got) + } + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + tt.record(enc, buffer, texture) + if got := ref.RefCount(); got != 2 { + t.Fatalf("buffer refcount after encoding = %d, want 2", got) + } + if got := len(enc.trackedRefs); got != 1 || enc.trackedRefs[0] != ref { + t.Fatalf("encoder tracked refs = %v, want exactly the copy buffer ref", enc.trackedRefs) + } + + cb, err := enc.Finish() + if err != nil { + t.Fatalf("Finish: %v", err) + } + if got := len(cb.trackedRefs); got != 1 || cb.trackedRefs[0] != ref { + t.Fatalf("command buffer tracked refs = %v, want exactly the copy buffer ref", cb.trackedRefs) + } + submission, err := queue.Submit(cb) + if err != nil { + t.Fatalf("Submit: %v", err) + } + if got := ref.RefCount(); got != 2 { + t.Fatalf("buffer refcount after incomplete submit = %d, want 2", got) + } + + buffer.Release() + if got := ref.RefCount(); got != 1 { + t.Fatalf("buffer refcount after user Release = %d, want in-flight ref 1", got) + } + device.destroyQueue().Triage(submission - 1) + if got := ref.RefCount(); got != 1 { + t.Fatalf("buffer refcount before retirement = %d, want 1", got) + } + delayed.completed = submission + device.destroyQueue().Triage(delayed.PollCompleted()) + if got := ref.RefCount(); got != 0 { + t.Fatalf("buffer refcount after retirement = %d, want 0", got) + } + }) + } +} diff --git a/texture_copy_scope_test.go b/texture_copy_scope_test.go new file mode 100644 index 0000000..8e724eb --- /dev/null +++ b/texture_copy_scope_test.go @@ -0,0 +1,477 @@ +//go:build !rust && !(js && wasm) + +package wgpu + +import ( + "testing" + + "github.com/gogpu/wgpu/core" + "github.com/gogpu/wgpu/core/track" +) + +func TestCopyCommandsPopulateTextureScope(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + + src := createCopyScopeTexture(t, device, "copy-scope-src", TextureUsageCopySrc) + defer src.Release() + dst := createCopyScopeTexture(t, device, "copy-scope-dst", TextureUsageCopyDst) + defer dst.Release() + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Label: "copy-scope-buffer", + Size: 256, + Usage: BufferUsageCopySrc | BufferUsageCopyDst, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + defer buffer.Release() + + bufferTextureRegion := []BufferTextureCopy{{ + BufferLayout: ImageDataLayout{BytesPerRow: 256, RowsPerImage: 1}, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + }} + textureRegion := []TextureCopy{{ + Source: ImageCopyTexture{Texture: src}, + Destination: ImageCopyTexture{Texture: dst}, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + }} + + tests := []struct { + name string + record func(*CommandEncoder) + want map[*Texture]track.TextureUses + }{ + { + name: "texture to buffer", + record: func(enc *CommandEncoder) { + regions := append([]BufferTextureCopy(nil), bufferTextureRegion...) + regions[0].TextureBase.Texture = src + enc.CopyTextureToBuffer(src, buffer, regions) + }, + want: map[*Texture]track.TextureUses{src: track.TextureUsesCopySrc}, + }, + { + name: "buffer to texture", + record: func(enc *CommandEncoder) { + regions := append([]BufferTextureCopy(nil), bufferTextureRegion...) + regions[0].TextureBase.Texture = dst + enc.CopyBufferToTexture(buffer, dst, regions) + }, + want: map[*Texture]track.TextureUses{dst: track.TextureUsesCopyDst}, + }, + { + name: "texture to texture", + record: func(enc *CommandEncoder) { enc.CopyTextureToTexture(src, dst, textureRegion) }, + want: map[*Texture]track.TextureUses{ + src: track.TextureUsesCopySrc, + dst: track.TextureUsesCopyDst, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + tt.record(enc) + cb, err := enc.Finish() + if err != nil { + t.Fatalf("Finish: %v", err) + } + defer cb.Release() + + scope := cb.core.TextureScope() + for tex, want := range tt.want { + idx := tex.coreTexture.TrackingData().Index() + if got := scope.GetUsage(idx); got != want { + t.Errorf("texture %q usage = %v, want %v", tex.coreTexture.Label(), got, want) + } + } + }) + } +} + +func TestCopyCommandTextureScopeDrivesBarriers(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + before track.TextureUses + copyUsage track.TextureUses + record func(*CommandEncoder, *Texture, *Buffer) + }{ + { + name: "sampled to copy source", + before: track.TextureUsesResource, + copyUsage: track.TextureUsesCopySrc, + record: func(enc *CommandEncoder, tex *Texture, buffer *Buffer) { + enc.CopyTextureToBuffer(tex, buffer, []BufferTextureCopy{{ + TextureBase: ImageCopyTexture{Texture: tex}, + BufferLayout: ImageDataLayout{BytesPerRow: 256, RowsPerImage: 1}, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + }}) + }, + }, + { + name: "render target to copy destination", + before: track.TextureUsesColorTarget, + copyUsage: track.TextureUsesCopyDst, + record: func(enc *CommandEncoder, tex *Texture, buffer *Buffer) { + enc.CopyBufferToTexture(buffer, tex, []BufferTextureCopy{{ + TextureBase: ImageCopyTexture{Texture: tex}, + BufferLayout: ImageDataLayout{BytesPerRow: 256, RowsPerImage: 1}, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + }}) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + tex := createCopyScopeTexture(t, device, tt.name, TextureUsageCopySrc|TextureUsageCopyDst) + defer tex.Release() + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Size: 256, + Usage: BufferUsageCopySrc | BufferUsageCopyDst, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + defer buffer.Release() + + idx := tex.coreTexture.TrackingData().Index() + device.core.Tracker().InsertTexture(idx, tt.before) + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + tt.record(enc, tex, buffer) + cb, err := enc.Finish() + if err != nil { + t.Fatalf("Finish: %v", err) + } + defer cb.Release() + + probeTracker := core.NewDeviceTracker() + probeTracker.InsertTexture(idx, tt.before) + transitions := mergeTextureScopes(probeTracker, []*CommandBuffer{cb}) + if len(transitions) != 1 { + t.Fatalf("copy scope produced %d transitions, want 1", len(transitions)) + } + transition := transitions[0] + if transition.Index != idx || transition.Usage.From != tt.before || transition.Usage.To != tt.copyUsage { + t.Fatalf("transition = %+v, want index %d %v -> %v", transition, idx, tt.before, tt.copyUsage) + } + + barrierCB, err := device.Queue().injectBarriers([]*CommandBuffer{cb}) + if err != nil { + t.Fatalf("injectBarriers: %v", err) + } + if barrierCB == nil { + t.Fatal("expected copy usage transition to inject a barrier command buffer") + } + if got := device.core.Tracker().Textures().GetUsage(idx); got != tt.copyUsage { + t.Errorf("tracked usage after injection = %v, want %v", got, tt.copyUsage) + } + }) + } +} + +func TestFailedCopyDoesNotPopulateTextureScope(t *testing.T) { + t.Parallel() + + t.Run("released destination buffer", func(t *testing.T) { + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + tex := createCopyScopeTexture(t, device, "failed-copy-src", TextureUsageCopySrc) + defer tex.Release() + buffer, err := device.CreateBuffer(&BufferDescriptor{Size: 256, Usage: BufferUsageCopyDst}) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + buffer.Release() + + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + enc.CopyTextureToBuffer(tex, buffer, nil) + + idx := tex.coreTexture.TrackingData().Index() + if scope := enc.core.Mutable().TextureScope(); scope.IsUsed(idx) { + t.Fatalf("failed copy recorded texture usage %v", scope.GetUsage(idx)) + } + if _, err := enc.Finish(); err == nil { + t.Fatal("Finish succeeded after copy to released buffer") + } + }) + + t.Run("released source buffer", func(t *testing.T) { + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + tex := createCopyScopeTexture(t, device, "failed-copy-dst", TextureUsageCopyDst) + defer tex.Release() + buffer, err := device.CreateBuffer(&BufferDescriptor{Size: 256, Usage: BufferUsageCopySrc}) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + buffer.Release() + + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + enc.CopyBufferToTexture(buffer, tex, nil) + + idx := tex.coreTexture.TrackingData().Index() + if scope := enc.core.Mutable().TextureScope(); scope.IsUsed(idx) { + t.Fatalf("failed copy recorded texture usage %v", scope.GetUsage(idx)) + } + if _, err := enc.Finish(); err == nil { + t.Fatal("Finish succeeded after copy from released buffer") + } + }) +} + +func createCopyScopeTexture(t *testing.T, device *Device, label string, copyUsage TextureUsage) *Texture { + t.Helper() + tex, err := device.CreateTexture(&TextureDescriptor{ + Label: label, + Size: Extent3D{Width: 1, Height: 1, DepthOrArrayLayers: 1}, + MipLevelCount: 1, + SampleCount: 1, + Dimension: TextureDimension2D, + Format: TextureFormatRGBA8Unorm, + Usage: copyUsage | TextureUsageTextureBinding | TextureUsageRenderAttachment, + }) + if err != nil { + t.Fatalf("CreateTexture: %v", err) + } + return tex +} + +func TestCopyUsageConflictsAreAtomic(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + seed func(*testing.T, *CommandEncoder, *Texture, *Texture, *Buffer) + record func(*CommandEncoder, *Texture, *Texture, *Buffer) + wantSrc track.TextureUses + wantDst track.TextureUses + wantBuffer track.BufferUses + }{ + { + name: "texture to buffer texture conflict leaves buffer untouched", + seed: func(t *testing.T, enc *CommandEncoder, src, _ *Texture, _ *Buffer) { + mustSetTextureScopeUsage(t, enc, src, track.TextureUsesCopyDst) + }, + record: func(enc *CommandEncoder, src, _ *Texture, buffer *Buffer) { + enc.CopyTextureToBuffer(src, buffer, nil) + }, + wantSrc: track.TextureUsesCopyDst, + }, + { + name: "texture to buffer buffer conflict leaves texture untouched", + seed: func(t *testing.T, enc *CommandEncoder, _, _ *Texture, buffer *Buffer) { + mustSetBufferScopeUsage(t, enc, buffer, track.BufferUsesCopySrc) + }, + record: func(enc *CommandEncoder, src, _ *Texture, buffer *Buffer) { + enc.CopyTextureToBuffer(src, buffer, nil) + }, + wantBuffer: track.BufferUsesCopySrc, + }, + { + name: "buffer to texture texture conflict leaves buffer untouched", + seed: func(t *testing.T, enc *CommandEncoder, _, dst *Texture, _ *Buffer) { + mustSetTextureScopeUsage(t, enc, dst, track.TextureUsesCopySrc) + }, + record: func(enc *CommandEncoder, _, dst *Texture, buffer *Buffer) { + enc.CopyBufferToTexture(buffer, dst, nil) + }, + wantDst: track.TextureUsesCopySrc, + }, + { + name: "buffer to texture buffer conflict leaves texture untouched", + seed: func(t *testing.T, enc *CommandEncoder, _, _ *Texture, buffer *Buffer) { + mustSetBufferScopeUsage(t, enc, buffer, track.BufferUsesCopyDst) + }, + record: func(enc *CommandEncoder, _, dst *Texture, buffer *Buffer) { + enc.CopyBufferToTexture(buffer, dst, nil) + }, + wantBuffer: track.BufferUsesCopyDst, + }, + { + name: "texture to texture source conflict leaves destination untouched", + seed: func(t *testing.T, enc *CommandEncoder, src, _ *Texture, _ *Buffer) { + mustSetTextureScopeUsage(t, enc, src, track.TextureUsesCopyDst) + }, + record: func(enc *CommandEncoder, src, dst *Texture, _ *Buffer) { + enc.CopyTextureToTexture(src, dst, nil) + }, + wantSrc: track.TextureUsesCopyDst, + }, + { + name: "texture to texture destination conflict leaves source untouched", + seed: func(t *testing.T, enc *CommandEncoder, _, dst *Texture, _ *Buffer) { + mustSetTextureScopeUsage(t, enc, dst, track.TextureUsesCopySrc) + }, + record: func(enc *CommandEncoder, src, dst *Texture, _ *Buffer) { + enc.CopyTextureToTexture(src, dst, nil) + }, + wantDst: track.TextureUsesCopySrc, + }, + { + name: "texture to texture same endpoint conflict leaves scope untouched", + seed: func(_ *testing.T, _ *CommandEncoder, _, _ *Texture, _ *Buffer) { + }, + record: func(enc *CommandEncoder, src, _ *Texture, _ *Buffer) { + enc.CopyTextureToTexture(src, src, nil) + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + src := createCopyScopeTexture(t, device, "atomic-src", TextureUsageCopySrc|TextureUsageCopyDst) + defer src.Release() + dst := createCopyScopeTexture(t, device, "atomic-dst", TextureUsageCopySrc|TextureUsageCopyDst) + defer dst.Release() + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Size: 256, Usage: BufferUsageCopySrc | BufferUsageCopyDst, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + defer buffer.Release() + + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + tt.seed(t, enc, src, dst, buffer) + tt.record(enc, src, dst, buffer) + + textureScope := enc.core.Mutable().TextureScope() + if got := textureScope.GetUsage(src.coreTexture.TrackingData().Index()); got != tt.wantSrc { + t.Errorf("source texture usage = %v, want %v", got, tt.wantSrc) + } + if got := textureScope.GetUsage(dst.coreTexture.TrackingData().Index()); got != tt.wantDst { + t.Errorf("destination texture usage = %v, want %v", got, tt.wantDst) + } + if got := enc.core.Mutable().BufferScope().GetUsage(buffer.core.TrackingData().Index()); got != tt.wantBuffer { + t.Errorf("buffer usage = %v, want %v", got, tt.wantBuffer) + } + if got := len(enc.trackedRefs); got != 0 { + t.Errorf("failed copy retained %d refs, want 0", got) + } + if len(enc.usedTextures) != 0 || len(enc.usedBuffers) != 0 { + t.Errorf("failed copy changed submit validation sets: textures=%d buffers=%d", len(enc.usedTextures), len(enc.usedBuffers)) + } + if _, err := enc.Finish(); err == nil { + t.Fatal("Finish succeeded after copy usage conflict") + } + }) + } +} + +func TestCopyBufferToBufferUsageRecordingIsAtomic(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + src := createCopyScopeBuffer(t, device, "atomic-buffer-src") + defer src.Release() + dst := createCopyScopeBuffer(t, device, "atomic-buffer-dst") + defer dst.Release() + + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + mustSetBufferScopeUsage(t, enc, dst, track.BufferUsesCopySrc) + enc.CopyBufferToBuffer(src, 0, dst, 0, 64) + + scope := enc.core.Mutable().BufferScope() + if index := src.core.TrackingData().Index(); scope.IsUsed(index) { + t.Fatalf("failed copy recorded source usage %v", scope.GetUsage(index)) + } + dstIndex := dst.core.TrackingData().Index() + if got := scope.GetUsage(dstIndex); got != track.BufferUsesCopySrc { + t.Errorf("destination usage = %v, want preserved %v", got, track.BufferUsesCopySrc) + } + if got := len(enc.trackedRefs); got != 0 { + t.Errorf("failed copy retained %d refs, want 0", got) + } + if len(enc.usedBuffers) != 0 { + t.Errorf("failed copy changed submit validation set: buffers=%d", len(enc.usedBuffers)) + } + if _, err := enc.Finish(); err == nil { + t.Fatal("Finish succeeded after copy usage conflict") + } +} + +func TestCopyBufferToBufferRecordsBothUsages(t *testing.T) { + t.Parallel() + + _, _, device := newTestDeviceWithTracker(t) + defer device.Release() + src := createCopyScopeBuffer(t, device, "buffer-copy-src") + defer src.Release() + dst := createCopyScopeBuffer(t, device, "buffer-copy-dst") + defer dst.Release() + + enc, err := device.CreateCommandEncoder(nil) + if err != nil { + t.Fatalf("CreateCommandEncoder: %v", err) + } + enc.CopyBufferToBuffer(src, 0, dst, 0, 64) + cb, err := enc.Finish() + if err != nil { + t.Fatalf("Finish: %v", err) + } + defer cb.Release() + + if got := cb.core.BufferScope().GetUsage(src.core.TrackingData().Index()); got != track.BufferUsesCopySrc { + t.Errorf("source usage = %v, want %v", got, track.BufferUsesCopySrc) + } + if got := cb.core.BufferScope().GetUsage(dst.core.TrackingData().Index()); got != track.BufferUsesCopyDst { + t.Errorf("destination usage = %v, want %v", got, track.BufferUsesCopyDst) + } +} + +func createCopyScopeBuffer(t *testing.T, device *Device, label string) *Buffer { + t.Helper() + buffer, err := device.CreateBuffer(&BufferDescriptor{ + Label: label, + Size: 256, + Usage: BufferUsageCopySrc | BufferUsageCopyDst, + }) + if err != nil { + t.Fatalf("CreateBuffer: %v", err) + } + return buffer +} + +func mustSetTextureScopeUsage(t *testing.T, enc *CommandEncoder, texture *Texture, usage track.TextureUses) { + t.Helper() + if err := enc.core.Mutable().TextureScope().SetUsage(texture.coreTexture.TrackingData().Index(), usage); err != nil { + t.Fatalf("seed texture scope: %v", err) + } +} + +func mustSetBufferScopeUsage(t *testing.T, enc *CommandEncoder, buffer *Buffer, usage track.BufferUses) { + t.Helper() + if err := enc.core.Mutable().BufferScope().SetUsage(buffer.core.TrackingData().Index(), usage); err != nil { + t.Fatalf("seed buffer scope: %v", err) + } +}