Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.21.2] - 2026-03-16

### Added

- **core: Binder struct for render/compute pass validation** — Tracks assigned vs expected
bind group layouts per slot (matching Rust wgpu-core pattern). At draw/dispatch time,
`checkCompatibility()` verifies all expected slots have compatible bind groups assigned.
13 binder tests.

- **core: comprehensive render/compute pass state validation** — SetBindGroup validates
MAX_BIND_GROUPS hard cap (8), pipeline bind group count, and dynamic offset alignment
(256 bytes). Draw/DrawIndexed validate pipeline is set, vertex buffer count, and index
buffer presence. Dispatch validates pipeline set + bind group compatibility.
25+ new tests.

### Fixed

- **core: SetBindGroup index bounds validation** — Prevents `vkCmdBindDescriptorSets`
crash on AMD/NVIDIA GPUs when bind group index exceeds pipeline layout set count.
Intel silently tolerates this spec violation; AMD/NVIDIA crash with access violation.
Fixes [ui#52](https://github.com/gogpu/ui/issues/52).

## [0.21.1] - 2026-03-15

### Fixed
Expand Down
14 changes: 6 additions & 8 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,17 @@

---

## Current State: v0.21.0
## Current State: v0.21.2

✅ **All 5 HAL backends complete** (~80K LOC, ~100K total)
✅ **Three-layer WebGPU stack** — wgpu API → wgpu/core → wgpu/hal
✅ **Complete public API** — consumers never import `wgpu/hal`
✅ **Core validation layer** — 14/17 Rust wgpu-core checks (Binder, SetBindGroup bounds, draw-time compatibility, dynamic offsets, vertex/index buffer)

**New in v0.21.0:**
- Complete three-layer architecture: public API → core validation → HAL backends
- core: Surface lifecycle state machine, CommandEncoder state machine, 12 resource types
- Proper type definitions (no hal aliases in godoc): Extent3D, DepthStencilState, TextureBarrier, etc.
- Fence + async submission (SubmitWithFence), Surface PrepareFrame hook
- SetLogger/Logger for stack-wide logging propagation
- naga v0.14.7 (MSL binding index fix)
### Remaining validation (planned)
- Blend constant tracking (pipeline blend state → draw-time check)
- Late buffer binding size (SPIR-V reflection → min binding size)
- Resource usage conflict detection (read/write tracking across bind groups)

**New in v0.20.2:**
- Vulkan: validate WSI query functions in LoadInstance (prevents nil pointer SIGSEGV)
Expand Down
11 changes: 10 additions & 1 deletion bind.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ func (l *BindGroupLayout) Release() {
}
}

// PipelineLayout defines the resource layout for a pipeline.
// PipelineLayout defines the bind group layout arrangement for a pipeline.
type PipelineLayout struct {
hal hal.PipelineLayout
device *Device
released bool
// bindGroupCount is the number of bind group layouts in this layout.
// Used for validation in SetBindGroup.
bindGroupCount uint32
// bindGroupLayouts stores the layouts used to create this pipeline layout.
// Used by the binder for draw-time compatibility validation.
bindGroupLayouts []*BindGroupLayout
}

// Release destroys the pipeline layout.
Expand All @@ -45,6 +51,9 @@ type BindGroup struct {
hal hal.BindGroup
device *Device
released bool
// layout is the bind group layout used to create this bind group.
// Stored for draw-time compatibility validation via the binder.
layout *BindGroupLayout
}

// Release destroys the bind group.
Expand Down
110 changes: 110 additions & 0 deletions binder.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package wgpu

import "fmt"

// binder tracks bind group assignments and validates compatibility at draw/dispatch
// time, matching Rust wgpu-core's Binder pattern.
//
// When SetPipeline is called, the expected layouts are set from the pipeline layout.
// When SetBindGroup is called, the assigned layout is recorded at that slot.
// Before Draw/DrawIndexed/Dispatch, checkCompatibility verifies that every slot
// expected by the pipeline has a compatible bind group assigned.
type binder struct {
// assigned holds the layout of the bind group set at each slot via SetBindGroup.
// nil means no bind group has been assigned to that slot.
assigned [MaxBindGroups]*BindGroupLayout

// expected holds the layout expected at each slot by the current pipeline.
// nil means the pipeline does not use that slot.
expected [MaxBindGroups]*BindGroupLayout

// maxSlots is the number of bind group slots expected by the current pipeline.
// This equals len(pipelineLayout.BindGroupLayouts).
maxSlots uint32
}

// reset clears all binder state. Called when a new pipeline is set.
func (b *binder) reset() {
b.assigned = [MaxBindGroups]*BindGroupLayout{}
b.expected = [MaxBindGroups]*BindGroupLayout{}
b.maxSlots = 0
}

// updateExpectations sets the expected layouts from a pipeline's bind group layouts.
// Called from SetPipeline. Previously assigned bind groups are preserved so that
// bind groups set before the pipeline remain valid (matching WebGPU spec behavior).
func (b *binder) updateExpectations(layouts []*BindGroupLayout) {
// Clear old expectations.
b.expected = [MaxBindGroups]*BindGroupLayout{}

n := uint32(len(layouts)) //nolint:gosec // layout count fits uint32
if n > MaxBindGroups {
n = MaxBindGroups
}
b.maxSlots = n

for i := uint32(0); i < n; i++ {
b.expected[i] = layouts[i]
}
}

// assign records a bind group assignment at the given slot.
// Called from SetBindGroup. The layout pointer is stored for later compatibility checks.
func (b *binder) assign(index uint32, layout *BindGroupLayout) {
if index < MaxBindGroups {
b.assigned[index] = layout
}
}

// validateSetBindGroup performs common validation for SetBindGroup on both
// render and compute passes. Returns a non-nil error message if validation fails.
func validateSetBindGroup(passName string, index uint32, group *BindGroup, offsets []uint32, pipelineBGCount uint32) error {
if group == nil {
return fmt.Errorf("wgpu: %s.SetBindGroup: bind group is nil", passName)
}
if index >= MaxBindGroups {
return fmt.Errorf("wgpu: %s.SetBindGroup: index %d >= MaxBindGroups (%d)", passName, index, MaxBindGroups)
}
if pipelineBGCount > 0 && index >= pipelineBGCount {
return fmt.Errorf("wgpu: %s.SetBindGroup: group index %d exceeds pipeline layout bind group count %d",
passName, index, pipelineBGCount)
}
for i, offset := range offsets {
if offset%256 != 0 {
return fmt.Errorf("wgpu: %s.SetBindGroup: dynamic offset[%d]=%d not aligned to 256", passName, i, offset)
}
}
return nil
}

// checkCompatibility validates that all slots expected by the current pipeline
// have compatible bind groups assigned. Returns an error describing the first
// incompatible or missing slot, or nil if all slots are satisfied.
//
// Compatibility is checked via pointer equality: two layouts are compatible if
// they are the same *BindGroupLayout object. This is correct because our API
// does not support creating equivalent-but-distinct layouts that should be
// considered compatible.
func (b *binder) checkCompatibility() error {
for i := uint32(0); i < b.maxSlots; i++ {
exp := b.expected[i]
if exp == nil {
// Pipeline does not use this slot.
continue
}
asg := b.assigned[i]
if asg == nil {
return fmt.Errorf(
"wgpu: bind group at index %d is required by the pipeline but not set (call SetBindGroup)",
i,
)
}
if asg != exp {
return fmt.Errorf(
"wgpu: bind group at index %d has incompatible layout (assigned layout %p != expected layout %p)",
i, asg, exp,
)
}
}
return nil
}
Loading
Loading