-
Notifications
You must be signed in to change notification settings - Fork 19
API changes to make certain things feel more natural in Zig #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Copying over from #23 : I'm still thinking about how I want to wrap the callbacks so that we can apply the same changes there that we do everywhere else. We might be able to leverage the fact that we can pass in types so that the callback doesn't have to do all that weird It might also be worth revisiting methods like |
If I want to help convert all the null returns to error unions, what would be your guideline for what the error types are? |
I'm open to suggestion, but the way I've been doing it so far is to create a public error union named after the opaque struct the methods belong to, so for errors coming from I also tried to name the specific error similar to the method that generated it, so if the method has With the method that used |
I just want it to be be somewhat obvious which method generated an error, so that if I have an application that has a |
Taking a look at what you did in Instance, as I try to bring similar changes for Device, I notice that you used []u8 (variable) slices for strings. This causes issues, as Zig treats strings (with quotes) as []const u8, thus making them unstorable in []u8. I checked with this dummy code: const SomeStruct = struct {
a: []u8,
};
pub fn main() !void {
const d = SomeStruct{
.a = "aca",
};
@import("std").debug.print("{s}\n", .{d.a});
}
// outputs:
\\\ slice.zig:7:10: error: expected type '[]u8', found '*const [3:0]u8'
\\\ .a = "aca",
\\\ ~^~~~~~~~~
\\\ slice.zig:7:10: note: cast discards const qualifier |
Yeah, I should've run tests on that, will update it to use |
Similar initial work as Instance structs/methods, but for Device
More idiomatic Adapter structs and methods
I've figured out how to do callbacks like this: // In adapter.zig:
pub fn MakeRequestAdapterCallbackTrampoline(
comptime UserDataPointerType: type,
) type {
const CallbackType = *const fn(RequestAdapterStatus, ?*Adapter, ?[]const u8, UserDataPointerType) void;
// The compiler doesn't seem to like it if I don't have the function inside of a struct
return struct {
pub fn callback(status: RequestAdapterStatus, adapter: ?*Adapter, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.C) void {
const wrapped_callback: CallbackType = @ptrCast(userdata2);
const userdata: UserDataPointerType = @ptrCast(@alignCast(userdata1));
wrapped_callback(status, adapter, message.toSlice(), userdata);
}
};
}
//...
// In instance.zig, inside of Instance
// requestAdapter2 is just a temporary name so that I don't break existing tests
pub fn requestAdapter2(
self: *Instance,
mode: ?CallbackMode,
userdata: anytype,
callback: *const fn(RequestAdapterStatus, ?*Adapter, ?[]const u8, @TypeOf(userdata)) void,
options: ?RequestAdapterOptions,
) Future {
if (@typeInfo(@TypeOf(userdata)) != .pointer) {
@compileError("userdata should be a pointer type");
}
const Trampoline = MakeRequestAdapterCallbackTrampoline(@TypeOf(userdata));
const callback_info = RequestAdapterCallbackInfo {
.mode = mode orelse CallbackMode.allow_process_events,
.callback = Trampoline.callback,
.userdata1 = @ptrCast(userdata),
.userdata2 = @constCast(@ptrCast(callback)),
};
if (options) |o| {
return wgpuInstanceRequestAdapter(self, &o, callback_info);
} else {
return wgpuInstanceRequestAdapter(self, null, callback_info);
}
}
//...later on when we actually use it
fn adapterRequestHandler(status: RequestAdapterStatus, adapter: ?*Adapter, message: ?[]const u8, userdata: *?*Adapter) void {
_ = message;
if (status == .success) {
userdata.* = adapter;
}
}
test "requestAdapter with custom userdata type" {
const instance = try Instance.create(null);
var maybe_adapter: ?*Adapter = null;
_ = instance.requestAdapter2(null, &maybe_adapter, adapterRequestHandler, null);
try std.testing.expect(maybe_adapter != null);
} It comes at a price though: in order to avoid having to allocate memory for a second function pointer, I had to dedicate I have a bit of work to do to for |
I believe that the 2nd userdata is meant to be used by engines that use webgpu anyway, so yeah Another thing you could do, if you really want to keep 2 userdatas (as this is still a webgpu interface), is put them in a tuple of opaque pointers, and pass that as parameter 1 |
Side note, |
I just noticed that I forgot one WGPUBool in the Adapter PR (in But it makes me think, considering more and more extern structs are getting a Zig interface and a layer of conversion in the function calls, maybe it would be a good idea to make that conversion layer into struct methods? Something like this: pub const DeviceDescriptor = struct {
...
pub fn toWgpu(self: DeviceDescriptor) WGPUDeviceDescriptor { ... }
}; |
Yeah, that would certainly cut down on the clutter in the opaque struct methods. |
…native_zig into idiomatic-zig
So I started working on Not necessarily a problem, but I wonder if having to pass in an allocator for things that previously didn't require it would be frustrating to users? Even if I kept the chained struct format, we'd probably run into a similar issue with lists somewhere else, so now is probably a good time to put some thought into it. I've thought about writing a wrapper around Pros:
Cons:
|
Implement toWGPU() method for structs in adapter.zig device.zig
Another con I see to doing this is we stray further away from the native webgpu api, which, if I wanted that, I would have just gone with the Zig-Gamedev implementation. Using .toWGPU was my initial idea to do some separation of concerns, but if we keep in mind that the motive here is to convert idiomatic zig structs to what WebGPU expects, then I have a few ideas: We can create a slice using just the pointer and the length and inversely, as long as the original variables are not deallocated ( Maybe there would be a .toAllocatedWGPU and a .toUnsafeWGPU, where the latter does exactly that. That way, there would be an implementation for both preferences (which seems to be in line with what Zig's std tends to do), and the interface could just use .toUnsafeWGPU internally. |
I'd agree with not straying too far from the native webgpu api. The issue is with slices of wrapper structs. If I have an array of |
So as far as I can tell, we either have to avoid slices of wrapper structs with different sizes than the native structs, or pass in an allocator. Or if the native structs are the same size or smaller, we might be able to re-use the buffer, but only if we allow it to be mutable. |
Also you're right that |
Oohh, now I understand the issue... Well, not only would you have to avoid different sizes than the native structs, but you'd also have to avoid different layouts. As in, if a zig slice stores the pointer first but wgpu-native stores the length first, we're cooked. Another issue is that this is extra work at runtime for allocating and copying or converting the same data... |
Yeah, pretty much. Luckily most of the slices |
Yes, but bind groups and layouts are what users will use the most, as they are required for rendering. Ideally, if we want this to feel like it's the same but in Zig, we would hide any allocation done for conversion. Maybe the library could have a fixed buffer allocator ready inside. It's not ideal, but it'd be a way to avoid forcing the user to pass an allocator. Generally, I never liked the idea of passing an allocator to a function that is only ever gonna use it internally and never return any heap allocated data. |
Another option would be to use a pattern similar to what we have for shader module descriptors in fn bindGroupEntry(entry: WrappedBindGroupEntry) BindGroupEntry {
//...
} Then BindGroupDescriptor can take a slice of const bind_group = device.createBindGroup(&BindGroupDescriptor {
//...other stuff in the descriptor
entries: &[_]BindGroupEntry {
bindGroupEntry(.{
//...
}),
bindGroupEntry(.{
//...
}),
},
}); |
I guess that can work. It does fall in line with the pattern that you'd used everywhere. Bonus point if it is trivial enough that it can be inlined and optimized out into constructing the original struct |
I could make the function |
Well, it's something I found out by using a debugger, finding out that a lot of unnecessary jumps (especially switches) were dissipated even in debug mode I don't remember the intricate details, and I have yet to figure out how to debug a zig executable in vscode on windows |
I'm just gonna quote @filipcro's suggestion from the original issue
I will mention, in case you (or anyone) didn't know: If you do a packed struct but the original api's flags don't line up, you will need to count the bits and add useless paddings to the struct. Otherwise, you won't be able to properly bit cast it |
Fortunately, it appears that all flag structs' bits are adjacent, meaning they only require one padding at the end (I could be wrong) |
Yeah, the padding thing is a bit annoying, though I think it makes sense since Zig tries not to have any hidden behavior and you'd want to be able to see what the rest of the bits are explicitly set to. I think you're right about all the flag bits being adjacent. Some flags have been added or removed over time, but when that happens the rest of the flags are changed so that there aren't any gaps. The enums sometimes have gaps, but for those it doesn't really matter. |
Using packed structs instead of ORed consts
…turedErrorCallbackInfo
I added some functions to construct the If all goes well though, I wonder if it might be worthwhile to try to separate out that logic for |
…o() and fix related bugs
Add init() method so that we don't have both a function and a struct with the same name
Related to the discussion in #23:
wgpu.Status
Procs
structs since they are unused and difficult to maintainWGPUFlags