Skip to content

Commit 90d10ac

Browse files
feat: improve & document javascript error handling
1 parent dd4fe20 commit 90d10ac

12 files changed

Lines changed: 160 additions & 66 deletions

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ genuinely fail, such as `RequestAdapter`.
3333
If maintaining panic-free code is essential for your needs, there exists a
3434
`Try` variant for most methods, such as `TryWriteBuffer(...) error`, which allows you to handle errors without panics.
3535

36+
### WASM
37+
38+
Error handling in the browser's WebGPU implementation is often asynchronous. For instance, an invalid WGSL shader file will not immediately return an error or throw an exception from `CreateShaderModule`. Some errors do cause synchronous exceptions, such as missing required properties. This library attempts to address these by catching synchronous exceptions in `Try` methods, but some validation errors, like an invalid `usage` attribute for buffer creation, might be missed.
39+
3640
## Prebuild libraries
3741

3842
This repository uses prebuild libraries provided by `wgpu-native`. All libraries combined are more than 512mb in size,

wgpu/buffer_js.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,14 @@ func (g *Buffer) GetMappedRange(offset, size uint) []byte {
2626
}
2727

2828
func (g *Buffer) TryMapAsync(mode MapMode, offset uint64, size uint64, callback BufferMapCallback) (err error) {
29-
_, ok := jsx.Await(g.jsValue.Call("mapAsync", uint32(mode), offset, size))
29+
defer handleJsException(&err)
30+
31+
// mapAsync will just return a promise
32+
promise := g.jsValue.Call("mapAsync", uint32(mode), offset, size)
33+
34+
// TODO probably better to use .Then(...) on the promise
35+
// to schedule the callback some time in the future and not to block here
36+
_, ok := jsx.Await(promise)
3037
if !ok {
3138
callback(MapAsyncStatusError)
3239
return
@@ -37,6 +44,8 @@ func (g *Buffer) TryMapAsync(mode MapMode, offset uint64, size uint64, callback
3744
}
3845

3946
func (g *Buffer) TryUnmap() (err error) {
47+
defer handleJsException(&err)
48+
4049
g.jsValue.Call("unmap")
4150
return
4251
}

wgpu/command_encoder_js.go

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,60 +25,71 @@ func (g *CommandEncoder) BeginComputePass(descriptor *ComputePassDescriptor) *Co
2525
// TryCopyBufferToBuffer as described:
2626
// https://gpuweb.github.io/gpuweb/#dom-gpucommandencoder-copybuffertobuffer
2727
func (g *CommandEncoder) TryCopyBufferToBuffer(source *Buffer, sourceOffset uint64, destination *Buffer, destinationOffset uint64, size uint64) (err error) {
28+
defer handleJsException(&err)
2829
g.jsValue.Call("copyBufferToBuffer", pointerToJS(source), sourceOffset, pointerToJS(destination), destinationOffset, size)
2930
return nil
3031
}
3132

3233
// TryCopyBufferToTexture as described:
3334
// https://gpuweb.github.io/gpuweb/#dom-gpucommandencoder-copybuffertotexture
3435
func (g *CommandEncoder) TryCopyBufferToTexture(source *TexelCopyBufferInfo, destination *TexelCopyTextureInfo, copySize *Extent3D) (err error) {
36+
defer handleJsException(&err)
3537
g.jsValue.Call("copyBufferToTexture", pointerToJS(source), pointerToJS(destination), pointerToJS(copySize))
3638
return nil
3739
}
3840

3941
// TryCopyTextureToBuffer as described:
4042
// https://gpuweb.github.io/gpuweb/#dom-gpucommandencoder-copytexturetobuffer
4143
func (g *CommandEncoder) TryCopyTextureToBuffer(source *TexelCopyTextureInfo, destination *TexelCopyBufferInfo, copySize *Extent3D) (err error) {
44+
defer handleJsException(&err)
4245
g.jsValue.Call("copyTextureToBuffer", pointerToJS(source), pointerToJS(destination), pointerToJS(copySize))
4346
return nil
4447
}
4548

4649
// TryCopyTextureToTexture as described:
4750
// https://gpuweb.github.io/gpuweb/#dom-gpucommandencoder-copytexturetotexture
4851
func (g *CommandEncoder) TryCopyTextureToTexture(source *TexelCopyTextureInfo, destination *TexelCopyTextureInfo, copySize *Extent3D) (err error) {
52+
defer handleJsException(&err)
4953
g.jsValue.Call("copyTextureToTexture", pointerToJS(source), pointerToJS(destination), pointerToJS(copySize))
5054
return nil
5155
}
5256

5357
// TryFinish as described:
5458
// https://gpuweb.github.io/gpuweb/#dom-gpucommandencoder-finish
55-
func (g *CommandEncoder) TryFinish(descriptor *CommandBufferDescriptor) (*CommandBuffer, error) {
59+
func (g *CommandEncoder) TryFinish(descriptor *CommandBufferDescriptor) (_ *CommandBuffer, err error) {
60+
defer handleJsException(&err)
5661
jsBuffer := g.jsValue.Call("finish", pointerToJS(descriptor))
5762
return &CommandBuffer{
5863
jsValue: jsBuffer,
5964
}, nil
6065
}
6166

62-
func (g *CommandEncoder) TryClearBuffer(buffer *Buffer, offset uint64, size uint64) error {
63-
panic("unimplemented")
67+
func (g *CommandEncoder) TryClearBuffer(buffer *Buffer, offset uint64, size uint64) (err error) {
68+
defer handleJsException(&err)
69+
g.jsValue.Call("clearBuffer", buffer.toJS(), offset, size)
70+
return nil
6471
}
6572

66-
func (g *CommandEncoder) TryInsertDebugMarker(label string) error {
73+
func (g *CommandEncoder) TryInsertDebugMarker(label string) (err error) {
74+
defer handleJsException(&err)
6775
g.jsValue.Call("insertDebugMarker", label)
6876
return nil
6977
}
7078

71-
func (g *CommandEncoder) TryPopDebugGroup() error {
79+
func (g *CommandEncoder) TryPopDebugGroup() (err error) {
80+
defer handleJsException(&err)
7281
g.jsValue.Call("popDebugGroup")
7382
return nil
7483
}
7584

76-
func (g *CommandEncoder) TryPushDebugGroup(label string) error {
85+
func (g *CommandEncoder) TryPushDebugGroup(label string) (err error) {
86+
defer handleJsException(&err)
7787
g.jsValue.Call("pushDebugGroup", label)
7888
return nil
7989
}
8090

81-
func (g *CommandEncoder) TryResolveQuerySet(querySet *QuerySet, query uint32, count uint32, destination *Buffer, offset uint64) error {
91+
func (g *CommandEncoder) TryResolveQuerySet(querySet *QuerySet, query uint32, count uint32, destination *Buffer, offset uint64) (err error) {
92+
defer handleJsException(&err)
8293
g.jsValue.Call("resolveQuerySet", querySet.toJS(), query, count, destination.toJS(), offset)
8394
return nil
8495
}

wgpu/compute_pass_encoder_js.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@ func (g *ComputePassEncoder) DispatchWorkgroups(workgroupCountX, workgroupCountY
5454

5555
// TryEnd as described:
5656
// https://gpuweb.github.io/gpuweb/#dom-gpucomputepassencoder-end
57-
func (g *ComputePassEncoder) TryEnd() error {
57+
func (g *ComputePassEncoder) TryEnd() (err error) {
58+
defer handleJsException(&err)
59+
5860
g.jsValue.Call("end")
5961
return nil
6062
}

wgpu/device_ext_js.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ import (
99
// TODO(kai): this only needs to be separate for js because
1010
//
1111
// [Buffer.GetMappedRange] does not work correctly without GopherJS.
12-
func (g *Device) TryCreateBufferInit(descriptor *BufferInitDescriptor) (*Buffer, error) {
12+
func (g *Device) TryCreateBufferInit(descriptor *BufferInitDescriptor) (_ *Buffer, err error) {
13+
defer handleJsException(&err)
14+
1315
if len(descriptor.Contents) == 0 {
1416
return g.TryCreateBuffer(&BufferDescriptor{
1517
Label: descriptor.Label,

wgpu/device_js.go

Lines changed: 51 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,104 +6,111 @@ package wgpu
66
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-queue
77
func (g *Device) GetQueue() *Queue {
88
jsQueue := g.jsValue.Get("queue")
9-
return &Queue{
10-
jsValue: jsQueue,
11-
}
9+
return &Queue{jsValue: jsQueue}
1210
}
1311

14-
func (g *Device) TryCreateQuerySet(descriptor *QuerySetDescriptor) (*QuerySet, error) {
12+
func (g *Device) TryCreateQuerySet(descriptor *QuerySetDescriptor) (_ *QuerySet, err error) {
13+
defer handleJsException(&err)
14+
1515
jsQuerySet := g.jsValue.Call("createQuerySet", pointerToJS(descriptor))
1616
return &QuerySet{jsQuerySet}, nil
1717
}
1818

1919
// TryCreateCommandEncoder as described:
2020
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createcommandencoder
21-
func (g *Device) TryCreateCommandEncoder(descriptor *CommandEncoderDescriptor) (*CommandEncoder, error) {
21+
func (g *Device) TryCreateCommandEncoder(descriptor *CommandEncoderDescriptor) (_ *CommandEncoder, err error) {
22+
defer handleJsException(&err)
23+
2224
jsEncoder := g.jsValue.Call("createCommandEncoder", pointerToJS(descriptor))
23-
return &CommandEncoder{
24-
jsValue: jsEncoder,
25-
}, nil
25+
return &CommandEncoder{jsValue: jsEncoder}, nil
2626
}
2727

2828
// TryCreateBuffer as described:
2929
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createbuffer
30-
func (g *Device) TryCreateBuffer(descriptor *BufferDescriptor) (*Buffer, error) {
30+
func (g *Device) TryCreateBuffer(descriptor *BufferDescriptor) (_ *Buffer, err error) {
31+
defer handleJsException(&err)
32+
3133
jsBuffer := g.jsValue.Call("createBuffer", pointerToJS(descriptor))
32-
return &Buffer{
33-
jsValue: jsBuffer,
34-
}, nil
34+
return &Buffer{jsValue: jsBuffer}, nil
3535
}
3636

3737
// TryCreateShaderModule as described:
3838
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createshadermodule
39-
func (g *Device) TryCreateShaderModule(desc *ShaderModuleDescriptor) (*ShaderModule, error) {
39+
func (g *Device) TryCreateShaderModule(desc *ShaderModuleDescriptor) (_ *ShaderModule, err error) {
40+
defer handleJsException(&err)
41+
4042
jsShader := g.jsValue.Call("createShaderModule", pointerToJS(desc))
41-
return &ShaderModule{
42-
jsValue: jsShader,
43-
}, nil
43+
return &ShaderModule{jsValue: jsShader}, nil
4444
}
4545

4646
// TryCreateRenderPipeline as described:
4747
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createrenderpipeline
48-
func (g *Device) TryCreateRenderPipeline(descriptor *RenderPipelineDescriptor) (*RenderPipeline, error) {
48+
func (g *Device) TryCreateRenderPipeline(descriptor *RenderPipelineDescriptor) (_ *RenderPipeline, err error) {
49+
defer handleJsException(&err)
50+
4951
jsPipeline := g.jsValue.Call("createRenderPipeline", pointerToJS(descriptor))
50-
return &RenderPipeline{
51-
jsValue: jsPipeline,
52-
}, nil
52+
return &RenderPipeline{jsValue: jsPipeline}, nil
5353
}
5454

5555
// TryCreateBindGroup as described:
5656
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createbindgroup
57-
func (g *Device) TryCreateBindGroup(descriptor *BindGroupDescriptor) (*BindGroup, error) {
57+
func (g *Device) TryCreateBindGroup(descriptor *BindGroupDescriptor) (_ *BindGroup, err error) {
58+
defer handleJsException(&err)
59+
5860
jsBindGroup := g.jsValue.Call("createBindGroup", pointerToJS(descriptor))
59-
return &BindGroup{
60-
jsValue: jsBindGroup,
61-
}, nil
61+
return &BindGroup{jsValue: jsBindGroup}, nil
6262
}
6363

6464
// TryCreateBindGroupLayout as described:
6565
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createbindgrouplayout
66-
func (g *Device) TryCreateBindGroupLayout(descriptor *BindGroupLayoutDescriptor) (*BindGroupLayout, error) {
66+
func (g *Device) TryCreateBindGroupLayout(descriptor *BindGroupLayoutDescriptor) (_ *BindGroupLayout, err error) {
67+
defer handleJsException(&err)
68+
6769
jsLayout := g.jsValue.Call("createBindGroupLayout", pointerToJS(descriptor))
68-
return &BindGroupLayout{
69-
jsValue: jsLayout,
70-
}, nil
70+
return &BindGroupLayout{jsValue: jsLayout}, nil
7171
}
7272

7373
// TryCreatePipelineLayout as described:
7474
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createpipelinelayout
75-
func (g *Device) TryCreatePipelineLayout(descriptor *PipelineLayoutDescriptor) (*PipelineLayout, error) {
75+
func (g *Device) TryCreatePipelineLayout(descriptor *PipelineLayoutDescriptor) (_ *PipelineLayout, err error) {
76+
defer handleJsException(&err)
77+
7678
jsLayout := g.jsValue.Call("createPipelineLayout", pointerToJS(descriptor))
77-
return &PipelineLayout{
78-
jsValue: jsLayout,
79-
}, nil
79+
return &PipelineLayout{jsValue: jsLayout}, nil
8080
}
8181

8282
// TryCreateComputePipeline as described:
8383
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createcomputepipeline
84-
func (g *Device) TryCreateComputePipeline(descriptor *ComputePipelineDescriptor) (*ComputePipeline, error) {
84+
func (g *Device) TryCreateComputePipeline(descriptor *ComputePipelineDescriptor) (_ *ComputePipeline, err error) {
85+
defer handleJsException(&err)
86+
8587
jsPipeline := g.jsValue.Call("createComputePipeline", pointerToJS(descriptor))
86-
return &ComputePipeline{
87-
jsValue: jsPipeline,
88-
}, nil
88+
return &ComputePipeline{jsValue: jsPipeline}, nil
8989
}
9090

9191
// TryCreateTexture as described:
9292
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createtexture
93-
func (g *Device) TryCreateTexture(descriptor *TextureDescriptor) (*Texture, error) {
93+
func (g *Device) TryCreateTexture(descriptor *TextureDescriptor) (_ *Texture, err error) {
94+
defer handleJsException(&err)
95+
9496
jsTexture := g.jsValue.Call("createTexture", pointerToJS(descriptor))
95-
return &Texture{
96-
jsValue: jsTexture,
97-
}, nil
97+
return &Texture{jsValue: jsTexture}, nil
9898
}
9999

100100
// TryCreateSampler as described:
101101
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-createsampler
102-
func (g *Device) TryCreateSampler(descriptor *SamplerDescriptor) (*Sampler, error) {
102+
func (g *Device) TryCreateSampler(descriptor *SamplerDescriptor) (_ *Sampler, err error) {
103+
defer handleJsException(&err)
104+
103105
jsSampler := g.jsValue.Call("createSampler", pointerToJS(descriptor))
104-
return &Sampler{
105-
jsValue: jsSampler,
106-
}, nil
106+
return &Sampler{jsValue: jsSampler}, nil
107+
}
108+
109+
func (g *Device) TryCreateRenderBundleEncoder(descriptor *RenderBundleEncoderDescriptor) (_ *RenderBundleEncoder, err error) {
110+
defer handleJsException(&err)
111+
112+
jsValue := g.jsValue.Call("createRenderBundleEncoder", descriptor.toJS())
113+
return &RenderBundleEncoder{jsValue: jsValue}, nil
107114
}
108115

109116
func (g *Device) GetLimits() Limits {
@@ -113,8 +120,3 @@ func (g *Device) GetLimits() Limits {
113120
func (g *Device) Poll(wait bool, wrappedSubmissionIndex *uint64) (queueEmpty bool) {
114121
return false // no-op
115122
}
116-
117-
func (g *Device) TryCreateRenderBundleEncoder(descriptor *RenderBundleEncoderDescriptor) (*RenderBundleEncoder, error) {
118-
// TODO implement this
119-
panic("unimplemented")
120-
}

wgpu/error_js.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package wgpu
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"runtime"
7+
"strings"
8+
)
9+
10+
func handleJsException(err *error) {
11+
r := recover()
12+
if r == nil {
13+
return
14+
}
15+
16+
// get the callers.
17+
// skip: this method, panic, js.Value.Call
18+
var callers [1]uintptr
19+
runtime.Callers(4, callers[:])
20+
21+
frames := runtime.CallersFrames(callers[:])
22+
frame, _ := frames.Next()
23+
context := frame.Func.Name()
24+
25+
// strip github.com/.../ from method name
26+
if idx := strings.LastIndexByte(context, '/'); idx >= 0 {
27+
context = context[idx+1:]
28+
}
29+
30+
switch r := r.(type) {
31+
case error:
32+
*err = wrap(r, context)
33+
34+
case fmt.Stringer:
35+
*err = Error{
36+
Context: context,
37+
Wrapped: errors.New(r.String()),
38+
}
39+
40+
default:
41+
*err = Error{
42+
Context: context,
43+
Wrapped: fmt.Errorf("%v", r),
44+
}
45+
}
46+
}

wgpu/queue_js.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,27 +23,29 @@ func (g *Queue) Submit(commandBuffers ...*CommandBuffer) SubmissionIndex {
2323
// TryWriteBuffer as described:
2424
// https://gpuweb.github.io/gpuweb/#dom-gpuqueue-writebuffer
2525
func (g *Queue) TryWriteBuffer(buffer *Buffer, offset uint64, data []byte) (err error) {
26-
defer runtime.KeepAlive(data)
27-
26+
defer handleJsException(&err)
2827
address := uintptr(unsafe.Pointer(&data[0]))
2928
queueWriteBuffer.Invoke(g.jsValue, pointerToJS(buffer), offset, address, uint64(0), len(data))
29+
runtime.KeepAlive(data)
3030
return
3131
}
3232

3333
// TryWriteTexture as described:
3434
// https://gpuweb.github.io/gpuweb/#dom-gpuqueue-writetexture
3535
func (g *Queue) TryWriteTexture(destination *TexelCopyTextureInfo, data []byte, dataLayout *TexelCopyBufferLayout, writeSize *Extent3D) (err error) {
36-
defer runtime.KeepAlive(data)
36+
defer handleJsException(&err)
3737

3838
address := uintptr(unsafe.Pointer(&data[0]))
3939
queueWriteTexture.Invoke(g.jsValue, pointerToJS(destination), address, len(data), pointerToJS(dataLayout), pointerToJS(writeSize))
40+
runtime.KeepAlive(data)
4041
return
4142
}
4243

4344
// OnSubmittedWorkDone as described:
4445
// https://gpuweb.github.io/gpuweb/#dom-gpuqueue-onsubmittedworkdone
4546
func (g *Queue) OnSubmittedWorkDone(callback QueueWorkDoneCallback) {
46-
jsx.Await(g.jsValue.Call("onSubmittedWorkDone")) // TODO(kai): is this correct?
47+
// TODO should probably just schedule the callback using .then
48+
jsx.Await(g.jsValue.Call("onSubmittedWorkDone"))
4749
callback(QueueWorkDoneStatusSuccess)
4850
}
4951

wgpu/render_pass_encoder_js.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ func (g *RenderPassEncoder) DrawIndexed(indexCount uint32, instanceCount uint32,
8484

8585
// TryEnd as described:
8686
// https://gpuweb.github.io/gpuweb/#dom-gpurenderpassencoder-end
87-
func (g *RenderPassEncoder) TryEnd() error {
87+
func (g *RenderPassEncoder) TryEnd() (err error) {
88+
defer handleJsException(&err)
8889
g.jsValue.Call("end")
8990
return nil
9091
}

0 commit comments

Comments
 (0)