Skip to content

Commit 12e58bd

Browse files
authored
feat: complete three-layer WebGPU public API (v0.21.0)
* feat(core): complete all 12 stub resource types (CORE-001) Replace empty struct{} stubs in core/resource.go with full implementations following the existing Buffer pattern: - Texture: format, dimension, usage, size, mip levels, sample count - Sampler, ShaderModule, QuerySet: HAL handle + properties - BindGroupLayout, PipelineLayout, BindGroup: binding infrastructure - RenderPipeline, ComputePipeline: pipeline types - CommandEncoder, CommandBuffer: command recording types - Surface: HAL surface (not Snatchable, owned by Instance) Each type has: Snatchable HAL handle (where applicable), device reference, WebGPU properties, constructor function, and Raw() accessor. Foundation for CORE-002 (Surface lifecycle), CORE-003 (CommandEncoder state machine), and CORE-004 (pipeline validation). * feat(core): Surface lifecycle state machine with PrepareFrame hook (CORE-002) Implement full surface lifecycle management in core.Surface: - State machine: Unconfigured → Configured → Acquired → Configured - Mutex-protected thread-safe transitions - Configure/Unconfigure with device validation - AcquireTexture with state check + PrepareFrame hook - Present with acquired texture validation - DiscardTexture for error recovery - PrepareFrameFunc callback for platform DPI/scale integration (Metal contentsScale, Windows WM_DPICHANGED, Wayland wl_output.scale) - Auto-reconfigure on dimension change from PrepareFrame Global registry changed from Registry[Surface] to Registry[*Surface] to support sync.Mutex (non-copyable). 16 new tests covering all state transitions and error conditions. * feat: migrate public API Surface to core.Surface delegation (CORE-005) wgpu.Surface now delegates to core.Surface instead of using hal.Surface directly. All lifecycle methods go through core validation and state tracking: - Configure/Unconfigure → core state machine validation - GetCurrentTexture → PrepareFrame hook + state check + HAL acquire - Present → acquired texture validation + HAL present New public methods: - SetPrepareFrame(fn) — platform hook for HiDPI/DPI changes (Metal contentsScale, Windows WM_DPICHANGED, Wayland wl_output.scale) - HAL() — escape hatch for backward-compatible direct HAL access This enables gogpu PLAT-001 (PrepareFrame architecture) to register platform callbacks on the wgpu Surface. * feat(core): CommandEncoder/CommandBuffer state machine (CORE-003) CommandEncoder tracks pass state with validated transitions: - Recording → BeginRenderPass → InRenderPass → EndRenderPass → Recording - Recording → BeginComputePass → InComputePass → EndComputePass → Recording - Recording → Finish → Finished (validates no open passes) - Any state → RecordError → Error (captures first error message) CommandBuffer tracks submission state: - Available → MarkSubmitted → Submitted (prevents double-submit) 13 new tests covering all state transitions and error conditions. * feat(core): resource accessor methods and Destroy for all types (CORE-004) Add read-only accessors and idempotent Destroy for 9 resource types: - Texture: Format, Dimension, Usage, Size, MipLevelCount, SampleCount - BindGroupLayout: Entries, EntryCount - QuerySet: QueryType, Count - All types: Label, Destroy, IsDestroyed Destroy follows Buffer snatch-and-release pattern: read-lock device, release lock, write-lock to snatch HAL handle, destroy via device. Safe to call multiple times (idempotent). Methods in separate resource_accessors.go with full test coverage. * feat: complete public API for gogpu renderer migration Add missing public API methods needed for full gogpu migration from HAL direct to wgpu public API: - Fence type wrapper with Release() (fence.go) - Device: CreateFence, ResetFence, GetFenceStatus, WaitForFence, FreeCommandBuffer (for FencePool pattern) - Queue: SubmitWithFence for non-blocking async submission - Surface: DiscardTexture for error recovery All existing resource types already had Release() methods. CommandEncoder/RenderPassEncoder/CommandBuffer wrappers already existed. This completes the wgpu public API surface needed for gogpu to stop using HAL directly and go through the proper wgpu → core → HAL chain. * feat: HAL accessor methods + wgpu API triangle examples - texture.go: HalTextureView() on *TextureView for gg integration - wrap.go: HalDevice(), HalQueue() on *Device for legacy HAL access - surface.go: fix nil TextureViewDescriptor (pass nil to HAL, not empty), remove unnecessary fence in GetCurrentTexture, remove debug logging - cmd/wgpu-triangle/: single-threaded wgpu public API triangle example - cmd/wgpu-triangle-mt/: multi-threaded wgpu public API triangle example (same architecture as gogpu renderer — validates wgpu API thread safety) * feat: add CopyTextureToBuffer, TransitionTextures, HalTexture for gg migration CommandEncoder: CopyTextureToBuffer, TransitionTextures, DiscardEncoding. Texture: HalTexture() accessor for texture barrier interop. Required by gg GPU-API-001 migration (readback + Vulkan layout transitions). * feat: promote HAL types to wgpu public API (WGPU-API-002) Replace type aliases to hal with proper struct definitions + toHAL() converters: - Extent3D, Origin3D, ImageDataLayout, DepthStencilState, StencilFaceState - New: TextureBarrier, TextureRange, TextureUsageTransition, BufferTextureCopy - StencilOperation kept as alias (uint8 enum, same as CompareFunction) - CommandEncoder signatures use wgpu types (no hal in public API) - New wgpu.SetLogger()/Logger() for stack-wide logging propagation - Device.CreateTexture uses desc.toHAL() (removed duplication) * docs: add HAL escape hatch note to timestamp queries guide * chore: update naga v0.14.6 → v0.14.7 Fixes MSL sequential per-type binding indices across bind groups. * docs: update CHANGELOG and ROADMAP for v0.21.0 release * fix: lint issues in triangle examples
1 parent c80d13c commit 12e58bd

29 files changed

Lines changed: 4496 additions & 107 deletions

CHANGELOG.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.21.0] - 2026-03-15
11+
12+
### Added
13+
14+
- **public API: complete three-layer WebGPU stack** — The root `wgpu` package now
15+
provides a full typed API for GPU programming. All operations go through
16+
wgpu (public) → wgpu/core (validation) → wgpu/hal (backend). Consumers never
17+
need to import `wgpu/hal` for standard use.
18+
19+
- **public API: SetLogger / Logger**`wgpu.SetLogger()` and `wgpu.Logger()`
20+
propagate the logger to the entire stack (API, core, HAL backends).
21+
22+
- **public API: Fence and async submission**`Fence` type, `Device.CreateFence()`,
23+
`WaitForFence()`, `ResetFence()`, `GetFenceStatus()`, `FreeCommandBuffer()`.
24+
`Queue.SubmitWithFence()` for non-blocking GPU submission with fence signaling.
25+
26+
- **public API: Surface lifecycle**`Surface.SetPrepareFrame()` for platform
27+
HiDPI/DPI hooks. `Surface.DiscardTexture()` for canceled frames. `Surface.HAL()`
28+
escape hatch. Delegates to `core.Surface` state machine.
29+
30+
- **public API: CommandEncoder extensions**`CopyTextureToBuffer()`,
31+
`TransitionTextures()`, `DiscardEncoding()`. All use wgpu types (no hal in signatures).
32+
33+
- **public API: HAL accessors**`Device.HalDevice()`, `Device.HalQueue()`,
34+
`Texture.HalTexture()`, `TextureView.HalTextureView()` for advanced interop.
35+
36+
- **public API: proper type definitions** — Replaced hal type aliases with proper
37+
structs: `Extent3D`, `Origin3D`, `ImageDataLayout`, `DepthStencilState`,
38+
`StencilFaceState`, `TextureBarrier`, `TextureRange`, `TextureUsageTransition`,
39+
`BufferTextureCopy`. Unexported `toHAL()` converters. No hal leakage in godoc.
40+
41+
- **core: complete resource types (CORE-001)** — All 12 stub resource types
42+
(Texture, Sampler, BindGroupLayout, PipelineLayout, BindGroup, ShaderModule,
43+
RenderPipeline, ComputePipeline, CommandEncoder, CommandBuffer, QuerySet, Surface)
44+
now have full struct definitions with HAL handle wrapping.
45+
46+
- **core: Surface state machine (CORE-002)** — Unconfigured → Configured → Acquired
47+
lifecycle with PrepareFrameFunc hook and auto-reconfigure on dimension changes.
48+
49+
- **core: CommandEncoder state machine (CORE-003)** — Recording/InRenderPass/
50+
InComputePass/Finished/Error states with validated transitions.
51+
52+
- **core: resource accessors (CORE-004)** — Read-only accessors and idempotent
53+
Destroy() for all resource types.
54+
55+
- **cmd/wgpu-triangle** — Single-threaded wgpu API triangle example.
56+
57+
- **cmd/wgpu-triangle-mt** — Multi-threaded wgpu API triangle example.
58+
59+
### Changed
60+
61+
- **Updated naga v0.14.6 → v0.14.7** — Fixes MSL sequential per-type binding
62+
indices across bind groups.
63+
1064
## [0.20.2] - 2026-03-12
1165

1266
### Fixed

ROADMAP.md

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,31 @@
1919

2020
---
2121

22-
## Current State: v0.18.1
22+
## Current State: v0.21.0
2323

2424
**All 5 HAL backends complete** (~80K LOC, ~100K total)
25-
**Public API root package**`import "github.com/gogpu/wgpu"`
26-
27-
**New in v0.18.1:**
28-
- Vulkan: fix buffer-to-image copy row stride corruption — use format's block copy size instead of inferring from padded `BytesPerRow / Width` (gogpu#96)
29-
30-
**New in v0.18.0:**
31-
- Public API root package with 20 user-facing types wrapping core/ and hal/
25+
**Three-layer WebGPU stack** — wgpu API → wgpu/core → wgpu/hal
26+
**Complete public API** — consumers never import `wgpu/hal`
27+
28+
**New in v0.21.0:**
29+
- Complete three-layer architecture: public API → core validation → HAL backends
30+
- core: Surface lifecycle state machine, CommandEncoder state machine, 12 resource types
31+
- Proper type definitions (no hal aliases in godoc): Extent3D, DepthStencilState, TextureBarrier, etc.
32+
- Fence + async submission (SubmitWithFence), Surface PrepareFrame hook
33+
- SetLogger/Logger for stack-wide logging propagation
34+
- naga v0.14.7 (MSL binding index fix)
35+
36+
**New in v0.20.2:**
37+
- Vulkan: validate WSI query functions in LoadInstance (prevents nil pointer SIGSEGV)
38+
39+
**New in v0.20.1:**
40+
- Metal: missing stencil attachment in render pass (macOS rendering fix)
41+
- Metal: missing setClearDepth: call
42+
43+
**New in v0.20.0:**
44+
- Public API root package with typed wrappers for core/ and hal/
3245
- WebGPU-spec-aligned flow: `CreateInstance()``RequestAdapter()``RequestDevice()`
3346
- Synchronous `Queue.Submit()` with internal fence management
34-
- Type aliases from `gputypes` — no extra imports needed
3547
- Deterministic `Release()` cleanup on all resource types
3648

3749
**New in v0.16.17:**

cmd/wgpu-triangle-mt/main.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
//go:build windows
2+
3+
// Command wgpu-triangle tests the wgpu public API rendering pipeline.
4+
// Multi-threaded: main thread = window events, render thread = GPU ops.
5+
// Same architecture as gogpu renderer.
6+
package main
7+
8+
import (
9+
"fmt"
10+
"log"
11+
"os"
12+
"runtime"
13+
"time"
14+
15+
"github.com/gogpu/gputypes"
16+
"github.com/gogpu/wgpu"
17+
_ "github.com/gogpu/wgpu/hal/vulkan"
18+
"github.com/gogpu/wgpu/internal/thread"
19+
)
20+
21+
const (
22+
windowWidth = 800
23+
windowHeight = 600
24+
windowTitle = "wgpu API Triangle Test (Multi-Thread)"
25+
)
26+
27+
func init() {
28+
runtime.LockOSThread()
29+
}
30+
31+
func main() {
32+
if err := run(); err != nil {
33+
fmt.Fprintf(os.Stderr, "FATAL: %v\n", err)
34+
os.Exit(1)
35+
}
36+
}
37+
38+
//nolint:gocognit,gocyclo,cyclop,funlen // example code — intentionally sequential
39+
func run() error {
40+
log.Println("=== wgpu Multi-Thread Triangle Test ===")
41+
42+
// 1. Window (main thread)
43+
window, err := NewWindow(windowTitle, windowWidth, windowHeight)
44+
if err != nil {
45+
return fmt.Errorf("window: %w", err)
46+
}
47+
defer window.Destroy()
48+
log.Println("1. Window created")
49+
50+
// 2. Render thread
51+
renderLoop := thread.NewRenderLoop()
52+
defer renderLoop.Stop()
53+
log.Println("2. Render thread created")
54+
55+
// 3-9. Init GPU on render thread
56+
var instance *wgpu.Instance
57+
var surface *wgpu.Surface
58+
var device *wgpu.Device
59+
var pipeline *wgpu.RenderPipeline
60+
var pipelineLayout *wgpu.PipelineLayout
61+
var shader *wgpu.ShaderModule
62+
var initErr error
63+
64+
renderLoop.RunOnRenderThreadVoid(func() {
65+
instance, err = wgpu.CreateInstance(&wgpu.InstanceDescriptor{
66+
Backends: gputypes.BackendsVulkan,
67+
})
68+
if err != nil {
69+
initErr = fmt.Errorf("instance: %w", err)
70+
return
71+
}
72+
73+
surface, err = instance.CreateSurface(0, window.Handle())
74+
if err != nil {
75+
initErr = fmt.Errorf("surface: %w", err)
76+
return
77+
}
78+
79+
adapter, err := instance.RequestAdapter(nil)
80+
if err != nil {
81+
initErr = fmt.Errorf("adapter: %w", err)
82+
return
83+
}
84+
log.Printf(" Adapter: %s", adapter.Info().Name)
85+
86+
device, err = adapter.RequestDevice(nil)
87+
if err != nil {
88+
initErr = fmt.Errorf("device: %w", err)
89+
return
90+
}
91+
92+
w, h := window.Size()
93+
err = surface.Configure(device, &wgpu.SurfaceConfiguration{
94+
Format: gputypes.TextureFormatBGRA8Unorm,
95+
Usage: gputypes.TextureUsageRenderAttachment,
96+
Width: safeUint32(w),
97+
Height: safeUint32(h),
98+
PresentMode: gputypes.PresentModeFifo,
99+
AlphaMode: gputypes.CompositeAlphaModeOpaque,
100+
})
101+
if err != nil {
102+
initErr = fmt.Errorf("configure: %w", err)
103+
return
104+
}
105+
106+
shader, err = device.CreateShaderModule(&wgpu.ShaderModuleDescriptor{
107+
Label: "Triangle",
108+
WGSL: triangleShaderWGSL,
109+
})
110+
if err != nil {
111+
initErr = fmt.Errorf("shader: %w", err)
112+
return
113+
}
114+
115+
pipelineLayout, err = device.CreatePipelineLayout(&wgpu.PipelineLayoutDescriptor{
116+
Label: "Triangle Layout",
117+
})
118+
if err != nil {
119+
initErr = fmt.Errorf("layout: %w", err)
120+
return
121+
}
122+
123+
pipeline, err = device.CreateRenderPipeline(&wgpu.RenderPipelineDescriptor{
124+
Label: "Triangle Pipeline",
125+
Layout: pipelineLayout,
126+
Vertex: wgpu.VertexState{
127+
Module: shader,
128+
EntryPoint: "vs_main",
129+
},
130+
Fragment: &wgpu.FragmentState{
131+
Module: shader,
132+
EntryPoint: "fs_main",
133+
Targets: []gputypes.ColorTargetState{{
134+
Format: gputypes.TextureFormatBGRA8Unorm,
135+
WriteMask: gputypes.ColorWriteMaskAll,
136+
}},
137+
},
138+
})
139+
if err != nil {
140+
initErr = fmt.Errorf("pipeline: %w", err)
141+
return
142+
}
143+
144+
log.Println("3-9. GPU initialized on render thread")
145+
})
146+
147+
if initErr != nil {
148+
return initErr
149+
}
150+
151+
// 10. Render loop
152+
log.Println("=== Render loop started ===")
153+
frameCount := 0
154+
startTime := time.Now()
155+
156+
for window.PollEvents() {
157+
var frameErr error
158+
159+
renderLoop.RunOnRenderThreadVoid(func() {
160+
// Acquire
161+
surfaceTex, _, err := surface.GetCurrentTexture()
162+
if err != nil {
163+
frameErr = fmt.Errorf("GetCurrentTexture: %w", err)
164+
return
165+
}
166+
167+
view, err := surfaceTex.CreateView(nil)
168+
if err != nil {
169+
frameErr = fmt.Errorf("CreateView: %w", err)
170+
surface.DiscardTexture()
171+
return
172+
}
173+
174+
// Encode
175+
encoder, err := device.CreateCommandEncoder(&wgpu.CommandEncoderDescriptor{Label: "Frame"})
176+
if err != nil {
177+
frameErr = fmt.Errorf("CreateCommandEncoder: %w", err)
178+
view.Release()
179+
return
180+
}
181+
182+
renderPass, err := encoder.BeginRenderPass(&wgpu.RenderPassDescriptor{
183+
ColorAttachments: []wgpu.RenderPassColorAttachment{{
184+
View: view,
185+
LoadOp: gputypes.LoadOpClear,
186+
StoreOp: gputypes.StoreOpStore,
187+
ClearValue: gputypes.Color{R: 0, G: 0, B: 0.5, A: 1},
188+
}},
189+
})
190+
if err != nil {
191+
frameErr = fmt.Errorf("BeginRenderPass: %w", err)
192+
view.Release()
193+
return
194+
}
195+
196+
renderPass.SetPipeline(pipeline)
197+
renderPass.Draw(3, 1, 0, 0)
198+
if err := renderPass.End(); err != nil {
199+
frameErr = fmt.Errorf("end: %w", err)
200+
view.Release()
201+
return
202+
}
203+
204+
commands, err := encoder.Finish()
205+
if err != nil {
206+
frameErr = fmt.Errorf("finish: %w", err)
207+
view.Release()
208+
return
209+
}
210+
211+
if err := device.Queue().Submit(commands); err != nil {
212+
frameErr = fmt.Errorf("submit: %w", err)
213+
}
214+
215+
if err := surface.Present(surfaceTex); err != nil {
216+
frameErr = fmt.Errorf("present: %w", err)
217+
}
218+
219+
view.Release()
220+
})
221+
222+
if frameErr != nil {
223+
log.Printf("Frame error: %v", frameErr)
224+
continue
225+
}
226+
227+
frameCount++
228+
if frameCount%60 == 0 {
229+
fps := float64(frameCount) / time.Since(startTime).Seconds()
230+
log.Printf("Frame %d (%.1f FPS)", frameCount, fps)
231+
}
232+
}
233+
234+
// Cleanup on render thread
235+
renderLoop.RunOnRenderThreadVoid(func() {
236+
pipeline.Release()
237+
pipelineLayout.Release()
238+
shader.Release()
239+
surface.Unconfigure()
240+
surface.Release()
241+
})
242+
243+
log.Printf("Done. %d frames", frameCount)
244+
return nil
245+
}
246+
247+
const triangleShaderWGSL = `
248+
@vertex
249+
fn vs_main(@builtin(vertex_index) idx: u32) -> @builtin(position) vec4<f32> {
250+
var positions = array<vec2<f32>, 3>(
251+
vec2<f32>(0.0, 0.5),
252+
vec2<f32>(-0.5, -0.5),
253+
vec2<f32>(0.5, -0.5)
254+
);
255+
return vec4<f32>(positions[idx], 0.0, 1.0);
256+
}
257+
258+
@fragment
259+
fn fs_main() -> @location(0) vec4<f32> {
260+
return vec4<f32>(1.0, 0.0, 0.0, 1.0);
261+
}
262+
`
263+
264+
func safeUint32(v int32) uint32 {
265+
if v < 0 {
266+
return 0
267+
}
268+
return uint32(v)
269+
}

0 commit comments

Comments
 (0)