From 7ef172c9c04355fd8f36897ccc9689f2cf23449d Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 15 Jul 2026 13:38:11 +0300 Subject: [PATCH 1/5] feat: expose queue timestamp period --- examples/timestamp_query/main.go | 17 +++--- wgpu/command.go | 25 +++++++++ wgpu/loader.go | 8 +++ wgpu/loader_unix.go | 37 ++++++++++++ wgpu/loader_windows.go | 35 ++++++++++++ wgpu/queue_timestamp_period_test.go | 87 +++++++++++++++++++++++++++++ wgpu/wgpu.go | 6 +- 7 files changed, 205 insertions(+), 10 deletions(-) create mode 100644 wgpu/queue_timestamp_period_test.go diff --git a/examples/timestamp_query/main.go b/examples/timestamp_query/main.go index ef89a8b..2938211 100644 --- a/examples/timestamp_query/main.go +++ b/examples/timestamp_query/main.go @@ -222,20 +222,21 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { log.Printf("unmap staging buffer: %v", err) } - // Calculate elapsed ticks. - // Note: To convert to nanoseconds, you need the timestamp period - // from the adapter (typically 1 ns/tick, but varies by GPU). + // Calculate elapsed ticks and convert them with the period reported by + // the queue. The period varies by GPU and backend. elapsedTicks := endTimestamp - startTimestamp - - // Assume 1 ns/tick (common on most GPUs). - const assumedPeriodNs = 1.0 - elapsedNs := float64(elapsedTicks) * assumedPeriodNs + period := queue.GetTimestampPeriod() + if period <= 0 { + return fmt.Errorf("timestamp period unavailable") + } + elapsedNs := float64(elapsedTicks) * float64(period) fmt.Printf("Timestamp Query Results:\n") fmt.Printf(" Start timestamp: %d ticks\n", startTimestamp) fmt.Printf(" End timestamp: %d ticks\n", endTimestamp) fmt.Printf(" Elapsed ticks: %d\n", elapsedTicks) - fmt.Printf(" GPU execution time: ~%.3f ms (assuming 1 ns/tick)\n", elapsedNs/1_000_000) + fmt.Printf(" Timestamp period: %.6f ns/tick\n", period) + fmt.Printf(" GPU execution time: ~%.3f ms\n", elapsedNs/1_000_000) fmt.Println() fmt.Println("GPU timestamp queries provide accurate profiling!") diff --git a/wgpu/command.go b/wgpu/command.go index 993d90e..7d0a6a9 100644 --- a/wgpu/command.go +++ b/wgpu/command.go @@ -478,6 +478,31 @@ func (q *Queue) Submit(commands ...*CommandBuffer) (uint64, error) { return uint64(submissionIndex), nil } +// GetTimestampPeriod returns the duration of one GPU timestamp tick in +// nanoseconds, as reported by wgpu-native. It returns zero for a nil or +// released queue, or when the native call is unavailable. +func (q *Queue) GetTimestampPeriod() float32 { + if q == nil || q.handle == 0 { + return 0 + } + if procQueueGetTimestampPeriod == nil { + mustInit() + } + if procQueueGetTimestampPeriod == nil { + return 0 + } + + proc, ok := procQueueGetTimestampPeriod.(float32Proc) + if !ok { + return 0 + } + period, err := proc.CallFloat32(q.handle) + if err != nil { + return 0 + } + return period +} + // Release releases the command buffer. func (cb *CommandBuffer) Release() { if cb.handle != 0 { diff --git a/wgpu/loader.go b/wgpu/loader.go index f2d2890..fe66a20 100644 --- a/wgpu/loader.go +++ b/wgpu/loader.go @@ -18,3 +18,11 @@ type Proc interface { // Arguments are passed as uintptr to match C ABI. Call(args ...uintptr) (uintptr, uintptr, error) } + +// float32Proc is implemented by platform loaders for procedures whose native +// return type is float32. Proc.Call intentionally keeps the existing integer +// return contract for the rest of the WebGPU API; this narrow interface lets +// those procedures use the platform's floating-point return ABI instead. +type float32Proc interface { + CallFloat32(args ...uintptr) (float32, error) +} diff --git a/wgpu/loader_unix.go b/wgpu/loader_unix.go index 3ddd56d..047efc6 100644 --- a/wgpu/loader_unix.go +++ b/wgpu/loader_unix.go @@ -125,3 +125,40 @@ func (u *unixProc) Call(args ...uintptr) (uintptr, uintptr, error) { // This matches Windows syscall.LazyProc.Call signature return result, 0, nil } + +// CallFloat32 invokes a procedure whose native return type is float32. +// +// Proc.Call uses a pointer-sized return descriptor for the rest of the API. +// A float32 is returned in the platform floating-point register instead, so +// it needs a call interface prepared with FloatTypeDescriptor. +func (u *unixProc) CallFloat32(args ...uintptr) (float32, error) { + if u.fnPtr == nil { + return 0, fmt.Errorf("wgpu: failed to get symbol %s from %s", u.name, u.lib.name) + } + + argTypes := make([]*types.TypeDescriptor, len(args)) + for i := range argTypes { + argTypes[i] = types.PointerTypeDescriptor + } + + var cif types.CallInterface + if err := ffi.PrepareCallInterface( + &cif, + types.UnixCallingConvention, + types.FloatTypeDescriptor, + argTypes, + ); err != nil { + return 0, fmt.Errorf("wgpu: failed to prepare CIF for %s: %w", u.name, err) + } + + argPtrs := make([]unsafe.Pointer, len(args)) + for i := range args { + argPtrs[i] = unsafe.Pointer(&args[i]) + } + + var result float32 + if _, err := ffi.CallFunction(&cif, u.fnPtr, unsafe.Pointer(&result), argPtrs); err != nil { + return 0, fmt.Errorf("wgpu: call to %s failed: %w", u.name, err) + } + return result, nil +} diff --git a/wgpu/loader_windows.go b/wgpu/loader_windows.go index d96582e..d378d27 100644 --- a/wgpu/loader_windows.go +++ b/wgpu/loader_windows.go @@ -4,6 +4,10 @@ package wgpu import ( "syscall" + "unsafe" + + "github.com/go-webgpu/goffi/ffi" + "github.com/go-webgpu/goffi/types" ) // windowsLibrary wraps syscall.LazyDLL to implement the Library interface. @@ -40,3 +44,34 @@ func (w *windowsLibrary) NewProc(name string) Proc { func (w *windowsProc) Call(args ...uintptr) (uintptr, uintptr, error) { return w.proc.Call(args...) } + +// CallFloat32 invokes a float32-returning procedure through goffi so the +// Windows x64 ABI reads XMM0. syscall.LazyProc.Call only exposes integer +// return registers and therefore cannot safely call this signature. +func (w *windowsProc) CallFloat32(args ...uintptr) (float32, error) { + if err := w.proc.Find(); err != nil { + return 0, err + } + + argTypes := make([]*types.TypeDescriptor, len(args)) + for i := range argTypes { + argTypes[i] = types.PointerTypeDescriptor + } + var cif types.CallInterface + if err := ffi.PrepareCallInterface( + &cif, + types.WindowsCallingConvention, + types.FloatTypeDescriptor, + argTypes, + ); err != nil { + return 0, err + } + + argPtrs := make([]unsafe.Pointer, len(args)) + for i := range args { + argPtrs[i] = unsafe.Pointer(&args[i]) + } + var result float32 + _, err := ffi.CallFunction(&cif, unsafe.Pointer(w.proc.Addr()), unsafe.Pointer(&result), argPtrs) + return result, err +} diff --git a/wgpu/queue_timestamp_period_test.go b/wgpu/queue_timestamp_period_test.go new file mode 100644 index 0000000..4cd032e --- /dev/null +++ b/wgpu/queue_timestamp_period_test.go @@ -0,0 +1,87 @@ +package wgpu + +import ( + "os" + "testing" +) + +type timestampPeriodProcStub struct { + handle uintptr + period float32 +} + +func (p *timestampPeriodProcStub) Call(args ...uintptr) (uintptr, uintptr, error) { + return 0, 0, nil +} + +func (p *timestampPeriodProcStub) CallFloat32(args ...uintptr) (float32, error) { + if len(args) == 1 { + p.handle = args[0] + } + return p.period, nil +} + +func TestQueueGetTimestampPeriodNullGuard(t *testing.T) { + var nilQueue *Queue + if got := nilQueue.GetTimestampPeriod(); got != 0 { + t.Fatalf("nil queue timestamp period = %v, want 0", got) + } + + releasedQueue := &Queue{} + if got := releasedQueue.GetTimestampPeriod(); got != 0 { + t.Fatalf("released queue timestamp period = %v, want 0", got) + } +} + +type integerOnlyTimestampPeriodProc struct{} + +func (*integerOnlyTimestampPeriodProc) Call(args ...uintptr) (uintptr, uintptr, error) { + return 0, 0, nil +} + +func TestQueueGetTimestampPeriodRequiresFloat32Proc(t *testing.T) { + original := procQueueGetTimestampPeriod + procQueueGetTimestampPeriod = &integerOnlyTimestampPeriodProc{} + defer func() { procQueueGetTimestampPeriod = original }() + + if got := (&Queue{handle: 0x1234}).GetTimestampPeriod(); got != 0 { + t.Fatalf("queue timestamp period = %v, want 0 for integer-only proc", got) + } +} + +func TestQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { + stub := ×tampPeriodProcStub{period: 0.125} + original := procQueueGetTimestampPeriod + procQueueGetTimestampPeriod = stub + defer func() { procQueueGetTimestampPeriod = original }() + + got := (&Queue{handle: 0x1234}).GetTimestampPeriod() + if got != stub.period { + t.Fatalf("queue timestamp period = %v, want %v", got, stub.period) + } + if stub.handle != 0x1234 { + t.Fatalf("queue handle = %#x, want %#x", stub.handle, uintptr(0x1234)) + } +} + +func TestQueueGetTimestampPeriodDynamicLibraryABI(t *testing.T) { + path := os.Getenv("WGPU_TIMESTAMP_PERIOD_ABI_STUB_LIBRARY") + if path == "" { + t.Skip("set WGPU_TIMESTAMP_PERIOD_ABI_STUB_LIBRARY to a shared library exporting the test symbol") + } + library, err := loadLibrary(path) + if err != nil { + t.Fatal(err) + } + proc, ok := library.NewProc("wgpuQueueGetTimestampPeriod").(float32Proc) + if !ok { + t.Fatal("platform loader does not implement float32 return calls") + } + got, err := proc.CallFloat32(0x1234) + if err != nil { + t.Fatal(err) + } + if got != 0.125 { + t.Fatalf("dynamic library timestamp period = %v, want 0.125", got) + } +} diff --git a/wgpu/wgpu.go b/wgpu/wgpu.go index d41a6bc..9963c2e 100644 --- a/wgpu/wgpu.go +++ b/wgpu/wgpu.go @@ -42,8 +42,9 @@ var ( procDeviceGetLimits Proc // Function pointers - Queue - procQueueRelease Proc - procQueueWriteBuffer Proc + procQueueRelease Proc + procQueueWriteBuffer Proc + procQueueGetTimestampPeriod Proc // Function pointers - Instance (global) procGetInstanceFeatures Proc // v29: global instance feature query @@ -277,6 +278,7 @@ func initSymbols() { // Queue procQueueRelease = wgpuLib.NewProc("wgpuQueueRelease") procQueueWriteBuffer = wgpuLib.NewProc("wgpuQueueWriteBuffer") + procQueueGetTimestampPeriod = wgpuLib.NewProc("wgpuQueueGetTimestampPeriod") // Instance global queries (v29) procGetInstanceFeatures = wgpuLib.NewProc("wgpuGetInstanceFeatures") From 078e63f47bb7f9a8e991770c8913e643fd82986e Mon Sep 17 00:00:00 2001 From: Mark Date: Wed, 22 Jul 2026 19:27:51 +0300 Subject: [PATCH 2/5] fix(wgpu): address timestamp period review --- examples/timestamp_query/main.go | 4 +++- wgpu/command.go | 3 --- wgpu/loader_unix.go | 1 + wgpu/loader_windows.go | 1 + wgpu/queue_timestamp_period_test.go | 10 ++++++++++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/timestamp_query/main.go b/examples/timestamp_query/main.go index 2938211..b705be9 100644 --- a/examples/timestamp_query/main.go +++ b/examples/timestamp_query/main.go @@ -227,7 +227,9 @@ fn main(@builtin(global_invocation_id) global_id: vec3) { elapsedTicks := endTimestamp - startTimestamp period := queue.GetTimestampPeriod() if period <= 0 { - return fmt.Errorf("timestamp period unavailable") + const fallbackPeriod = float32(1) + log.Printf("timestamp period unavailable; using %.1f ns/tick fallback", fallbackPeriod) + period = fallbackPeriod } elapsedNs := float64(elapsedTicks) * float64(period) diff --git a/wgpu/command.go b/wgpu/command.go index 7d0a6a9..1849f8e 100644 --- a/wgpu/command.go +++ b/wgpu/command.go @@ -485,9 +485,6 @@ func (q *Queue) GetTimestampPeriod() float32 { if q == nil || q.handle == 0 { return 0 } - if procQueueGetTimestampPeriod == nil { - mustInit() - } if procQueueGetTimestampPeriod == nil { return 0 } diff --git a/wgpu/loader_unix.go b/wgpu/loader_unix.go index 047efc6..21934e7 100644 --- a/wgpu/loader_unix.go +++ b/wgpu/loader_unix.go @@ -136,6 +136,7 @@ func (u *unixProc) CallFloat32(args ...uintptr) (float32, error) { return 0, fmt.Errorf("wgpu: failed to get symbol %s from %s", u.name, u.lib.name) } + // TODO: cache the prepared float-return CIF as Call does for integer returns. argTypes := make([]*types.TypeDescriptor, len(args)) for i := range argTypes { argTypes[i] = types.PointerTypeDescriptor diff --git a/wgpu/loader_windows.go b/wgpu/loader_windows.go index d378d27..5a4eafb 100644 --- a/wgpu/loader_windows.go +++ b/wgpu/loader_windows.go @@ -53,6 +53,7 @@ func (w *windowsProc) CallFloat32(args ...uintptr) (float32, error) { return 0, err } + // TODO: cache the prepared float-return CIF as unixProc.Call does for integer returns. argTypes := make([]*types.TypeDescriptor, len(args)) for i := range argTypes { argTypes[i] = types.PointerTypeDescriptor diff --git a/wgpu/queue_timestamp_period_test.go b/wgpu/queue_timestamp_period_test.go index 4cd032e..5a5382c 100644 --- a/wgpu/queue_timestamp_period_test.go +++ b/wgpu/queue_timestamp_period_test.go @@ -49,6 +49,16 @@ func TestQueueGetTimestampPeriodRequiresFloat32Proc(t *testing.T) { } } +func TestQueueGetTimestampPeriodUnavailable(t *testing.T) { + original := procQueueGetTimestampPeriod + procQueueGetTimestampPeriod = nil + defer func() { procQueueGetTimestampPeriod = original }() + + if got := (&Queue{handle: 0x1234}).GetTimestampPeriod(); got != 0 { + t.Fatalf("queue timestamp period = %v, want 0 for unavailable proc", got) + } +} + func TestQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { stub := ×tampPeriodProcStub{period: 0.125} original := procQueueGetTimestampPeriod From c6acf9eb319e03019612e3668891365dbec75b91 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 23 Jul 2026 21:37:31 +0300 Subject: [PATCH 3/5] deps: consume goffi Windows float return fix --- go.mod | 2 +- go.sum | 4 ++-- wgpu/queue_timestamp_period_test.go | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index 343a367..d78de1e 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/go-webgpu/webgpu go 1.25.0 -require github.com/go-webgpu/goffi v0.6.0 +require github.com/go-webgpu/goffi v0.6.2 require golang.org/x/sys v0.47.0 diff --git a/go.sum b/go.sum index 050135c..80075bf 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -github.com/go-webgpu/goffi v0.6.0 h1:dTBwfzj8CZUW0w0fgeMaYGBrIktK7nzfjMsnSpkSt4Y= -github.com/go-webgpu/goffi v0.6.0/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM= +github.com/go-webgpu/goffi v0.6.2 h1:xuMaUbqsNQ/xiyy5UwAKZb5vQZUDg9QRCrJIpHJaXSE= +github.com/go-webgpu/goffi v0.6.2/go.mod h1:wfoxNsJkU+5RFbV1kNN1kunhc1lFHuJKK3zpgx08/uM= github.com/gogpu/gputypes v0.5.1 h1:X38OPcP6umQqqubzzJYL6Nm1tXHSNQj6TRSAoxdAJmg= github.com/gogpu/gputypes v0.5.1/go.mod h1:cnXrDMwTpWTvJLW1Vreop3PcT6a2YP/i3s91rPaOavw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= diff --git a/wgpu/queue_timestamp_period_test.go b/wgpu/queue_timestamp_period_test.go index 5a5382c..683b33f 100644 --- a/wgpu/queue_timestamp_period_test.go +++ b/wgpu/queue_timestamp_period_test.go @@ -21,7 +21,7 @@ func (p *timestampPeriodProcStub) CallFloat32(args ...uintptr) (float32, error) return p.period, nil } -func TestQueueGetTimestampPeriodNullGuard(t *testing.T) { +func TestABIQueueGetTimestampPeriodNullGuard(t *testing.T) { var nilQueue *Queue if got := nilQueue.GetTimestampPeriod(); got != 0 { t.Fatalf("nil queue timestamp period = %v, want 0", got) @@ -39,7 +39,7 @@ func (*integerOnlyTimestampPeriodProc) Call(args ...uintptr) (uintptr, uintptr, return 0, 0, nil } -func TestQueueGetTimestampPeriodRequiresFloat32Proc(t *testing.T) { +func TestABIQueueGetTimestampPeriodRequiresFloat32Proc(t *testing.T) { original := procQueueGetTimestampPeriod procQueueGetTimestampPeriod = &integerOnlyTimestampPeriodProc{} defer func() { procQueueGetTimestampPeriod = original }() @@ -49,7 +49,7 @@ func TestQueueGetTimestampPeriodRequiresFloat32Proc(t *testing.T) { } } -func TestQueueGetTimestampPeriodUnavailable(t *testing.T) { +func TestABIQueueGetTimestampPeriodUnavailable(t *testing.T) { original := procQueueGetTimestampPeriod procQueueGetTimestampPeriod = nil defer func() { procQueueGetTimestampPeriod = original }() @@ -59,7 +59,7 @@ func TestQueueGetTimestampPeriodUnavailable(t *testing.T) { } } -func TestQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { +func TestABIQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { stub := ×tampPeriodProcStub{period: 0.125} original := procQueueGetTimestampPeriod procQueueGetTimestampPeriod = stub @@ -74,7 +74,7 @@ func TestQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { } } -func TestQueueGetTimestampPeriodDynamicLibraryABI(t *testing.T) { +func TestABIQueueGetTimestampPeriodDynamicLibrary(t *testing.T) { path := os.Getenv("WGPU_TIMESTAMP_PERIOD_ABI_STUB_LIBRARY") if path == "" { t.Skip("set WGPU_TIMESTAMP_PERIOD_ABI_STUB_LIBRARY to a shared library exporting the test symbol") From bee6abba005590889b3ad7b4362c8f802d7a5d0a Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 23 Jul 2026 21:40:56 +0300 Subject: [PATCH 4/5] test(wgpu): exercise timestamp period ABI in CI --- wgpu/queue_timestamp_period_test.go | 51 ++++++++++++++++++++- wgpu/queue_timestamp_period_unix_test.go | 20 ++++++++ wgpu/queue_timestamp_period_windows_test.go | 19 ++++++++ wgpu/testdata/timestamp_period.c | 9 ++++ 4 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 wgpu/queue_timestamp_period_unix_test.go create mode 100644 wgpu/queue_timestamp_period_windows_test.go create mode 100644 wgpu/testdata/timestamp_period.c diff --git a/wgpu/queue_timestamp_period_test.go b/wgpu/queue_timestamp_period_test.go index 683b33f..d8f6668 100644 --- a/wgpu/queue_timestamp_period_test.go +++ b/wgpu/queue_timestamp_period_test.go @@ -1,13 +1,18 @@ package wgpu import ( + "errors" "os" + "os/exec" + "path/filepath" + "runtime" "testing" ) type timestampPeriodProcStub struct { handle uintptr period float32 + err error } func (p *timestampPeriodProcStub) Call(args ...uintptr) (uintptr, uintptr, error) { @@ -18,7 +23,7 @@ func (p *timestampPeriodProcStub) CallFloat32(args ...uintptr) (float32, error) if len(args) == 1 { p.handle = args[0] } - return p.period, nil + return p.period, p.err } func TestABIQueueGetTimestampPeriodNullGuard(t *testing.T) { @@ -74,15 +79,27 @@ func TestABIQueueGetTimestampPeriodUsesNativeFloat32(t *testing.T) { } } +func TestABIQueueGetTimestampPeriodCallError(t *testing.T) { + stub := ×tampPeriodProcStub{period: 0.125, err: errors.New("call failed")} + original := procQueueGetTimestampPeriod + procQueueGetTimestampPeriod = stub + defer func() { procQueueGetTimestampPeriod = original }() + + if got := (&Queue{handle: 0x1234}).GetTimestampPeriod(); got != 0 { + t.Fatalf("queue timestamp period = %v, want 0 after call error", got) + } +} + func TestABIQueueGetTimestampPeriodDynamicLibrary(t *testing.T) { path := os.Getenv("WGPU_TIMESTAMP_PERIOD_ABI_STUB_LIBRARY") if path == "" { - t.Skip("set WGPU_TIMESTAMP_PERIOD_ABI_STUB_LIBRARY to a shared library exporting the test symbol") + path = buildTimestampPeriodABILibrary(t) } library, err := loadLibrary(path) if err != nil { t.Fatal(err) } + defer closeTimestampPeriodABILibrary(t, library) proc, ok := library.NewProc("wgpuQueueGetTimestampPeriod").(float32Proc) if !ok { t.Fatal("platform loader does not implement float32 return calls") @@ -95,3 +112,33 @@ func TestABIQueueGetTimestampPeriodDynamicLibrary(t *testing.T) { t.Fatalf("dynamic library timestamp period = %v, want 0.125", got) } } + +func buildTimestampPeriodABILibrary(t *testing.T) string { + t.Helper() + + name := "libtimestamp_period.so" + args := []string{"-shared", "-fPIC", "-O2"} + switch runtime.GOOS { + case "darwin": + name = "libtimestamp_period.dylib" + case "windows": + name = "timestamp_period.dll" + args = []string{"-shared", "-O2"} + } + + outputPath := filepath.Join(t.TempDir(), name) + args = append(args, "-o", outputPath, filepath.Join("testdata", "timestamp_period.c")) + compiler := os.Getenv("CC") + if compiler == "" { + compiler = "gcc" + } + output, err := exec.Command(compiler, args...).CombinedOutput() + if err == nil { + return outputPath + } + if os.Getenv("CI") != "" { + t.Fatalf("build timestamp-period ABI library: %v\n%s", err, output) + } + t.Skipf("timestamp-period ABI library requires a C compiler: %v", err) + return "" +} diff --git a/wgpu/queue_timestamp_period_unix_test.go b/wgpu/queue_timestamp_period_unix_test.go new file mode 100644 index 0000000..0fc2d38 --- /dev/null +++ b/wgpu/queue_timestamp_period_unix_test.go @@ -0,0 +1,20 @@ +//go:build linux || darwin + +package wgpu + +import ( + "testing" + + "github.com/go-webgpu/goffi/ffi" +) + +func closeTimestampPeriodABILibrary(t *testing.T, library Library) { + t.Helper() + unixLibrary, ok := library.(*unixLibrary) + if !ok { + t.Fatalf("timestamp-period ABI library has type %T, want *unixLibrary", library) + } + if err := ffi.FreeLibrary(unixLibrary.handle); err != nil { + t.Fatalf("close timestamp-period ABI library: %v", err) + } +} diff --git a/wgpu/queue_timestamp_period_windows_test.go b/wgpu/queue_timestamp_period_windows_test.go new file mode 100644 index 0000000..2f165c8 --- /dev/null +++ b/wgpu/queue_timestamp_period_windows_test.go @@ -0,0 +1,19 @@ +//go:build windows + +package wgpu + +import ( + "syscall" + "testing" +) + +func closeTimestampPeriodABILibrary(t *testing.T, library Library) { + t.Helper() + windowsLibrary, ok := library.(*windowsLibrary) + if !ok { + t.Fatalf("timestamp-period ABI library has type %T, want *windowsLibrary", library) + } + if err := syscall.FreeLibrary(syscall.Handle(windowsLibrary.dll.Handle())); err != nil { + t.Fatalf("close timestamp-period ABI library: %v", err) + } +} diff --git a/wgpu/testdata/timestamp_period.c b/wgpu/testdata/timestamp_period.c new file mode 100644 index 0000000..089fc34 --- /dev/null +++ b/wgpu/testdata/timestamp_period.c @@ -0,0 +1,9 @@ +#if defined(_WIN32) +#define EXPORT __declspec(dllexport) +#else +#define EXPORT __attribute__((visibility("default"))) +#endif + +EXPORT float wgpuQueueGetTimestampPeriod(const void *queue) { + return queue ? 0.125f : 0.0f; +} From e673b1258d8996f381fa3548a63bb2c784632113 Mon Sep 17 00:00:00 2001 From: Mark Date: Thu, 23 Jul 2026 21:45:22 +0300 Subject: [PATCH 5/5] refactor(wgpu): share float32 call path --- wgpu/loader_float.go | 62 +++++++++++++ wgpu/loader_float_test.go | 105 +++++++++++++++++++++++ wgpu/loader_unix.go | 28 +----- wgpu/loader_windows.go | 29 ++----- wgpu/queue_timestamp_period_unix_test.go | 10 +++ 5 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 wgpu/loader_float.go create mode 100644 wgpu/loader_float_test.go diff --git a/wgpu/loader_float.go b/wgpu/loader_float.go new file mode 100644 index 0000000..3b63336 --- /dev/null +++ b/wgpu/loader_float.go @@ -0,0 +1,62 @@ +package wgpu + +import ( + "fmt" + "syscall" + "unsafe" + + "github.com/go-webgpu/goffi/ffi" + "github.com/go-webgpu/goffi/types" +) + +type float32CallOps struct { + prepare func( + *types.CallInterface, + types.CallingConvention, + *types.TypeDescriptor, + []*types.TypeDescriptor, + ) error + call func( + *types.CallInterface, + unsafe.Pointer, + unsafe.Pointer, + []unsafe.Pointer, + ) (syscall.Errno, error) +} + +var nativeFloat32CallOps = float32CallOps{ + prepare: ffi.PrepareCallInterface, + call: ffi.CallFunction, +} + +// callFloat32 invokes a native function using the platform's scalar +// floating-point return convention. +func callFloat32( + ops float32CallOps, + name string, + convention types.CallingConvention, + fn unsafe.Pointer, + args ...uintptr, +) (float32, error) { + // TODO: cache float-return CIFs once each procedure's call shape is stable. + argTypes := make([]*types.TypeDescriptor, len(args)) + for i := range argTypes { + argTypes[i] = types.PointerTypeDescriptor + } + + var cif types.CallInterface + if err := ops.prepare(&cif, convention, types.FloatTypeDescriptor, argTypes); err != nil { + return 0, fmt.Errorf("wgpu: failed to prepare CIF for %s: %w", name, err) + } + + argPtrs := make([]unsafe.Pointer, len(args)) + for i := range args { + argPtrs[i] = unsafe.Pointer(&args[i]) + } + + var result float32 + if _, err := ops.call(&cif, fn, unsafe.Pointer(&result), argPtrs); err != nil { + return 0, fmt.Errorf("wgpu: call to %s failed: %w", name, err) + } + return result, nil +} diff --git a/wgpu/loader_float_test.go b/wgpu/loader_float_test.go new file mode 100644 index 0000000..c46b4ba --- /dev/null +++ b/wgpu/loader_float_test.go @@ -0,0 +1,105 @@ +package wgpu + +import ( + "errors" + "syscall" + "testing" + "unsafe" + + "github.com/go-webgpu/goffi/types" +) + +func TestABIFloat32Call(t *testing.T) { + const argument = uintptr(0x1234) + functionToken := byte(1) + function := unsafe.Pointer(&functionToken) + + t.Run("success", func(t *testing.T) { + ops := float32CallOps{ + prepare: func( + _ *types.CallInterface, + convention types.CallingConvention, + returnType *types.TypeDescriptor, + argTypes []*types.TypeDescriptor, + ) error { + if convention != types.UnixCallingConvention { + t.Fatalf("calling convention = %v, want Unix", convention) + } + if returnType != types.FloatTypeDescriptor { + t.Fatalf("return type = %v, want float32", returnType) + } + if len(argTypes) != 1 || argTypes[0] != types.PointerTypeDescriptor { + t.Fatalf("argument types = %v, want one pointer", argTypes) + } + return nil + }, + call: func( + _ *types.CallInterface, + gotFunction unsafe.Pointer, + result unsafe.Pointer, + args []unsafe.Pointer, + ) (syscall.Errno, error) { + if gotFunction != function { + t.Fatalf("function = %p, want %p", gotFunction, function) + } + if len(args) != 1 || *(*uintptr)(args[0]) != argument { + t.Fatalf("arguments do not preserve %#x", argument) + } + *(*float32)(result) = 0.125 + return 0, nil + }, + } + + got, err := callFloat32(ops, "testFloat32", types.UnixCallingConvention, function, argument) + if err != nil { + t.Fatal(err) + } + if got != 0.125 { + t.Fatalf("result = %v, want 0.125", got) + } + }) + + t.Run("prepare error", func(t *testing.T) { + wantErr := errors.New("prepare failed") + ops := float32CallOps{ + prepare: func( + *types.CallInterface, + types.CallingConvention, + *types.TypeDescriptor, + []*types.TypeDescriptor, + ) error { + return wantErr + }, + } + + if _, err := callFloat32(ops, "testFloat32", types.UnixCallingConvention, function, argument); !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want wrapped %v", err, wantErr) + } + }) + + t.Run("call error", func(t *testing.T) { + wantErr := errors.New("call failed") + ops := float32CallOps{ + prepare: func( + *types.CallInterface, + types.CallingConvention, + *types.TypeDescriptor, + []*types.TypeDescriptor, + ) error { + return nil + }, + call: func( + *types.CallInterface, + unsafe.Pointer, + unsafe.Pointer, + []unsafe.Pointer, + ) (syscall.Errno, error) { + return 0, wantErr + }, + } + + if _, err := callFloat32(ops, "testFloat32", types.UnixCallingConvention, function, argument); !errors.Is(err, wantErr) { + t.Fatalf("error = %v, want wrapped %v", err, wantErr) + } + }) +} diff --git a/wgpu/loader_unix.go b/wgpu/loader_unix.go index 21934e7..e5b3ce7 100644 --- a/wgpu/loader_unix.go +++ b/wgpu/loader_unix.go @@ -135,31 +135,5 @@ func (u *unixProc) CallFloat32(args ...uintptr) (float32, error) { if u.fnPtr == nil { return 0, fmt.Errorf("wgpu: failed to get symbol %s from %s", u.name, u.lib.name) } - - // TODO: cache the prepared float-return CIF as Call does for integer returns. - argTypes := make([]*types.TypeDescriptor, len(args)) - for i := range argTypes { - argTypes[i] = types.PointerTypeDescriptor - } - - var cif types.CallInterface - if err := ffi.PrepareCallInterface( - &cif, - types.UnixCallingConvention, - types.FloatTypeDescriptor, - argTypes, - ); err != nil { - return 0, fmt.Errorf("wgpu: failed to prepare CIF for %s: %w", u.name, err) - } - - argPtrs := make([]unsafe.Pointer, len(args)) - for i := range args { - argPtrs[i] = unsafe.Pointer(&args[i]) - } - - var result float32 - if _, err := ffi.CallFunction(&cif, u.fnPtr, unsafe.Pointer(&result), argPtrs); err != nil { - return 0, fmt.Errorf("wgpu: call to %s failed: %w", u.name, err) - } - return result, nil + return callFloat32(nativeFloat32CallOps, u.name, types.UnixCallingConvention, u.fnPtr, args...) } diff --git a/wgpu/loader_windows.go b/wgpu/loader_windows.go index 5a4eafb..c113cd3 100644 --- a/wgpu/loader_windows.go +++ b/wgpu/loader_windows.go @@ -6,7 +6,6 @@ import ( "syscall" "unsafe" - "github.com/go-webgpu/goffi/ffi" "github.com/go-webgpu/goffi/types" ) @@ -52,27 +51,11 @@ func (w *windowsProc) CallFloat32(args ...uintptr) (float32, error) { if err := w.proc.Find(); err != nil { return 0, err } - - // TODO: cache the prepared float-return CIF as unixProc.Call does for integer returns. - argTypes := make([]*types.TypeDescriptor, len(args)) - for i := range argTypes { - argTypes[i] = types.PointerTypeDescriptor - } - var cif types.CallInterface - if err := ffi.PrepareCallInterface( - &cif, + return callFloat32( + nativeFloat32CallOps, + w.proc.Name, types.WindowsCallingConvention, - types.FloatTypeDescriptor, - argTypes, - ); err != nil { - return 0, err - } - - argPtrs := make([]unsafe.Pointer, len(args)) - for i := range args { - argPtrs[i] = unsafe.Pointer(&args[i]) - } - var result float32 - _, err := ffi.CallFunction(&cif, unsafe.Pointer(w.proc.Addr()), unsafe.Pointer(&result), argPtrs) - return result, err + unsafe.Pointer(w.proc.Addr()), + args..., + ) } diff --git a/wgpu/queue_timestamp_period_unix_test.go b/wgpu/queue_timestamp_period_unix_test.go index 0fc2d38..1e341f5 100644 --- a/wgpu/queue_timestamp_period_unix_test.go +++ b/wgpu/queue_timestamp_period_unix_test.go @@ -18,3 +18,13 @@ func closeTimestampPeriodABILibrary(t *testing.T, library Library) { t.Fatalf("close timestamp-period ABI library: %v", err) } } + +func TestABIFloat32ProcMissingSymbol(t *testing.T) { + proc := &unixProc{ + lib: &unixLibrary{name: "missing-library"}, + name: "missing-symbol", + } + if _, err := proc.CallFloat32(0x1234); err == nil { + t.Fatal("missing symbol returned no error") + } +}