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")