From aab99b5474ed998d593ea6e725cc8a8cd4d8f9ea Mon Sep 17 00:00:00 2001 From: Stephen Gutekanst Date: Tue, 15 Mar 2022 10:42:14 -0700 Subject: [PATCH] gpu: make requestDevice callback-based, add waitForDevice helper Previously we were attempting to turn WebGPU async functions, which are exposed by `webgpu.h` as callbacks, into Zig async functions. This in practice turns out harder than expected. For example, `Buffer.mapAsync` / `wgpuBufferMapAsync` cannot easily be exposed as a Zig async function for a few reasons: 1. The callback is merely guaranteed to be called once the buffer's content is ready to be accessed via `wgpuBufferGetMappedRange` - but there is no strict guarantee about when that is. It could be 1-3 frames later, in theory, I believe. 2. The non-deterministic timing means that one would wish to poll "has the async function returned?" but this isn't trivial without our own scheduler. 3. Zig has a fair amount of async rework in the future that is coming, and I imagine it will be one of the later things that is fully supported by the WebAssembly backend (but I am speculating) - so it seems wise to punt on this until later. Instead, we are now retaining async functions as callback-based ones, with a helper in this case to wait for the callback to be invoked. For `wgpuBufferMapAsync` we will just have the callback approach. Signed-off-by: Stephen Gutekanst --- gpu/examples/sample_utils.zig | 2 +- gpu/src/Adapter.zig | 73 +++++++++++++++++++++++++---------- gpu/src/NativeInstance.zig | 33 +++++++--------- gpu/src/main.zig | 7 +++- 4 files changed, 74 insertions(+), 41 deletions(-) diff --git a/gpu/examples/sample_utils.zig b/gpu/examples/sample_utils.zig index abcfa907..574d4b4f 100644 --- a/gpu/examples/sample_utils.zig +++ b/gpu/examples/sample_utils.zig @@ -92,7 +92,7 @@ pub fn setup(allocator: std.mem.Allocator) !Setup { props.driver_description, }); - const device = switch (nosuspend backend_adapter.requestDevice(&.{})) { + const device = switch (backend_adapter.waitForDevice(&.{})) { .device => |v| v, .err => |err| { std.debug.print("failed to get device: error={} {s}\n", .{ err.code, err.message }); diff --git a/gpu/src/Adapter.zig b/gpu/src/Adapter.zig index 8c5ac474..3ea0d3f9 100644 --- a/gpu/src/Adapter.zig +++ b/gpu/src/Adapter.zig @@ -49,13 +49,14 @@ properties: Properties, ptr: *anyopaque, vtable: *const VTable, -// The @frameSize(func) of the implementations requestDevice async function -request_device_frame_size: usize, - pub const VTable = struct { reference: fn (ptr: *anyopaque) void, release: fn (ptr: *anyopaque) void, - requestDevice: fn requestDevice(ptr: *anyopaque, descriptor: *const Device.Descriptor) callconv(.Async) RequestDeviceResponse, + requestDevice: fn requestDevice( + ptr: *anyopaque, + descriptor: *const Device.Descriptor, + callback: *RequestDeviceCallback, + ) void, }; pub inline fn reference(adapter: Adapter) void { @@ -143,24 +144,53 @@ pub const RequestDeviceResponse = union(RequestDeviceResponseTag) { err: RequestDeviceError, }; -pub fn requestDevice(adapter: Adapter, descriptor: *const Device.Descriptor) callconv(.Async) RequestDeviceResponse { - var frame_buffer = std.heap.page_allocator.allocAdvanced( - u8, - 16, - adapter.request_device_frame_size, - std.mem.Allocator.Exact.at_least, - ) catch { - return .{ .err = .{ - .message = "Out of memory", - .code = RequestDeviceErrorCode.Error, - } }; - }; - defer std.heap.page_allocator.free(frame_buffer); +pub fn requestDevice( + adapter: Adapter, + descriptor: *const Device.Descriptor, + callback: *RequestDeviceCallback, +) void { + adapter.vtable.requestDevice(adapter.ptr, descriptor, callback); +} - var result: RequestDeviceResponse = undefined; - const f = @asyncCall(frame_buffer, &result, adapter.vtable.requestDevice, .{ adapter.ptr, descriptor }); - resume f; - return result; +pub const RequestDeviceCallback = struct { + type_erased_ctx: *anyopaque, + type_erased_callback: fn (ctx: *anyopaque, response: RequestDeviceResponse) callconv(.Inline) void, + + pub fn init( + comptime Context: type, + ctx: *Context, + comptime callback: fn (ctx: *Context, response: RequestDeviceResponse) void, + ) RequestDeviceCallback { + const erased = (struct { + pub inline fn erased(type_erased_ctx: *anyopaque, response: RequestDeviceResponse) void { + callback(@ptrCast(*Context, @alignCast(@alignOf(*Context), type_erased_ctx)), response); + } + }).erased; + + return .{ + .type_erased_ctx = ctx, + .type_erased_callback = erased, + }; + } +}; + +/// A helper which invokes requestDevice and blocks until the device is recieved. +pub fn waitForDevice(adapter: Adapter, descriptor: *const Device.Descriptor) RequestDeviceResponse { + const Context = RequestDeviceResponse; + var response: Context = undefined; + var callback = RequestDeviceCallback.init(Context, &response, (struct { + pub fn callback(ctx: *Context, callback_response: RequestDeviceResponse) void { + ctx.* = callback_response; + } + }).callback); + + adapter.requestDevice(descriptor, &callback); + + // TODO: FUTURE: Once crbug.com/dawn/1122 is fixed, we should process events here otherwise our + // callback would not be invoked: + //c.wgpuInstanceProcessEvents(adapter.instance) + + return response; } test "syntax" { @@ -172,5 +202,6 @@ test "syntax" { _ = RequestDeviceErrorCode; _ = RequestDeviceError; _ = RequestDeviceResponse; + _ = RequestDeviceCallback; _ = requestDevice; } diff --git a/gpu/src/NativeInstance.zig b/gpu/src/NativeInstance.zig index fd368052..d5ed36a9 100644 --- a/gpu/src/NativeInstance.zig +++ b/gpu/src/NativeInstance.zig @@ -12,6 +12,7 @@ const Adapter = @import("Adapter.zig"); const RequestDeviceErrorCode = Adapter.RequestDeviceErrorCode; const RequestDeviceError = Adapter.RequestDeviceError; const RequestDeviceResponse = Adapter.RequestDeviceResponse; +const RequestDeviceCallback = Adapter.RequestDeviceCallback; const Device = @import("Device.zig"); const Surface = @import("Surface.zig"); @@ -230,7 +231,6 @@ fn wrapAdapter(adapter: c.WGPUAdapter) Adapter { .ptr = adapter.?, .vtable = &adapter_vtable, - .request_device_frame_size = @frameSize(adapter_vtable.requestDevice), }; } @@ -246,7 +246,11 @@ const adapter_vtable = Adapter.VTable{ } }).release, .requestDevice = (struct { - pub fn requestDevice(ptr: *anyopaque, descriptor: *const Device.Descriptor) callconv(.Async) RequestDeviceResponse { + pub fn requestDevice( + ptr: *anyopaque, + descriptor: *const Device.Descriptor, + callback: *RequestDeviceCallback, + ) void { const adapter = @ptrCast(c.WGPUAdapter, @alignCast(@alignOf(c.WGPUAdapter), ptr)); const required_limits = if (descriptor.required_limits) |l| c.WGPURequiredLimits{ @@ -262,14 +266,13 @@ const adapter_vtable = Adapter.VTable{ .requiredLimits = if (required_limits) |*l| l else null, }; - const callback = (struct { - pub fn callback(status: c.WGPURequestDeviceStatus, device: c.WGPUDevice, message: [*c]const u8, userdata: ?*anyopaque) callconv(.C) void { - const _callback_response = @ptrCast(*Adapter.RequestDeviceResponse, @alignCast(@alignOf(*Adapter.RequestDeviceResponse), userdata)); + const cCallback = (struct { + pub fn cCallback(status: c.WGPURequestDeviceStatus, device: c.WGPUDevice, message: [*c]const u8, userdata: ?*anyopaque) callconv(.C) void { + const callback_info = @ptrCast(*RequestDeviceCallback, @alignCast(@alignOf(*RequestDeviceCallback), userdata.?)); - // Store the response into a field on the native instance for later reading. - _callback_response.* = if (status == c.WGPURequestDeviceStatus_Success) .{ + const response = if (status == c.WGPURequestDeviceStatus_Success) RequestDeviceResponse{ .device = wrapDevice(device.?), - } else .{ + } else RequestDeviceResponse{ .err = Adapter.RequestDeviceError{ .message = std.mem.span(message), .code = switch (status) { @@ -279,18 +282,12 @@ const adapter_vtable = Adapter.VTable{ }, }, }; + + callback_info.type_erased_callback(callback_info.type_erased_ctx, response); } - }).callback; + }).cCallback; - var callback_response: Adapter.RequestDeviceResponse = undefined; - c.wgpuAdapterRequestDevice(adapter, &desc, callback, &callback_response); - // TODO: Once crbug.com/dawn/1122 is fixed, we should process events here otherwise our - // callback will not be invoked. - // c.wgpuInstanceProcessEvents(native.instance) - suspend {} // must suspend so that async caller can resume - - // Return the response, asserting the callback has executed at this point. - return callback_response; + c.wgpuAdapterRequestDevice(adapter, &desc, cCallback, callback); } }).requestDevice, }; diff --git a/gpu/src/main.zig b/gpu/src/main.zig index 462de8a6..e5e4bc25 100644 --- a/gpu/src/main.zig +++ b/gpu/src/main.zig @@ -49,10 +49,15 @@ pub const RequestAdapterErrorCode = Interface.RequestAdapterErrorCode; pub const RequestAdapterError = Interface.RequestAdapterError; pub const RequestAdapterResponse = Interface.RequestAdapterResponse; +pub const Adapter = @import("Adapter.zig"); +pub const RequestDeviceErrorCode = Adapter.RequestDeviceErrorCode; +pub const RequestDeviceError = Adapter.RequesatDeviceError; +pub const RequestDeviceCallback = Adapter.RequestDeviceCallback; +pub const RequestDeviceResponse = Adapter.RequestDeviceResponse; + pub const NativeInstance = @import("NativeInstance.zig"); // Interfaces -pub const Adapter = @import("Adapter.zig"); pub const Device = @import("Device.zig"); pub const Surface = @import("Surface.zig"); pub const Queue = @import("Queue.zig");