diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml index 539377a..47de931 100644 --- a/.github/workflows/artifacts.yml +++ b/.github/workflows/artifacts.yml @@ -33,6 +33,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust_target }} + - name: Compile bindings and target checks + run: zig build --build-file build.tests.zig check -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all - name: Build static and dynamic libraries run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -p dist/wgpu-linux-${{ matrix.arch }} - uses: actions/upload-artifact@v4 @@ -75,6 +77,8 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.rust_target }} + - name: Compile bindings and target checks + run: zig build --build-file build.tests.zig check -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all - name: Build static and dynamic libraries run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -p dist/${{ matrix.artifact }} - uses: actions/upload-artifact@v4 @@ -114,6 +118,8 @@ jobs: with: toolchain: ${{ matrix.rust_toolchain }} targets: ${{ matrix.rust_target }} + - name: Compile bindings and target checks + run: zig build --build-file build.tests.zig check -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all - name: Build static and dynamic libraries run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -p dist/${{ matrix.artifact }} - uses: actions/upload-artifact@v4 @@ -153,6 +159,8 @@ jobs: run: | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --install "ndk;27.0.12077973" echo "ANDROID_NDK_HOME=$ANDROID_SDK_ROOT/ndk/27.0.12077973" >> "$GITHUB_ENV" + - name: Compile bindings and target checks + run: zig build --build-file build.tests.zig check -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all - name: Build static and dynamic libraries run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -p dist/wgpu-android-${{ matrix.abi }} - uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 0e22644..7830bda 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,53 @@ # wgpu_native_zig + Zig bindings for [wgpu-native](https://github.com/gfx-rs/wgpu-native) Requires Zig 0.16.x. This package exposes two modules: `wgpu-c` and `wgpu`. -`wgpu-c` is just `wgpu.h` (and by extension `webgpu.h`) run through `translate-c`, so as close to wgpu-native's original C API as is possible in Zig. +`wgpu-c` is `wgpu.h` (and therefore `webgpu.h`) translated directly by Zig, so it +tracks wgpu-native's C API without a second handwritten declaration layer. `wgpu` is a Zig-friendly wrapper over the bindings generated directly from `wgpu.h` and `webgpu.h`. The generated declarations are also available through `wgpu.raw`; wrapper methods and raw calls therefore share the headers as their single ABI source of truth. +### Binding coverage + +The pinned wgpu-native `v29.0.0.0` headers currently declare 226 functions. The +`wgpu` wrapper exposes every function with a usable v29 implementation, while +`wgpu.raw` and `wgpu-c` expose all function and type declarations produced by +`translate-c`. A compile-time audit checks this partition, rejects handwritten +`extern fn wgpu...` declarations, and fails when a future header update adds an +unclassified function or type. + +The following v29 API groups intentionally remain raw-only because their upstream +implementations panic, are blocked, or always return an unavailable result: + +- `wgpuGetProcAddress` and the currently unimplemented `*SetLabel` functions. +- Async pipeline creation, shader compilation info, device-lost futures, `waitAny`, + WGSL-language feature queries, and global instance-feature enumeration. +- Buffer map-state and mapped-range copy helpers. +- External-texture lifecycle functions. +- Device adapter-info lookup and texture binding-view-dimension lookup. +- The native Metal command-queue accessor, which always returns null in v29. + +Supported wgpu-native extensions are available as regular Zig methods, including +`Queue.getTimestampPeriod`, graphics-debugger capture control, and the borrowed Metal +device/texture accessors. Platform-native pointers are optional and must not be released +by the caller. + ## Adding this package to your build + Add the package to your dependencies, either with: + ```sh -zig fetch --save https://github.com/bronter/wgpu_native_zig/archive/refs/tags/v7.0.0.tar.gz +zig fetch --save https://github.com/openharmony-zig/wgpu_native_zig/archive/refs/tags/v7.0.0.tar.gz ``` + or by manually adding to your `build.zig.zon`: + ```zig .{ // ...other stuff @@ -24,15 +55,17 @@ or by manually adding to your `build.zig.zon`: // ...other dependencies .wgpu_native_zig = .{ // You can either use a commit hash: - .url="https://github.com/bronter/wgpu_native_zig/archive/.tar.gz", + .url="https://github.com/openharmony-zig/wgpu_native_zig/archive/.tar.gz", // or a tagged release: - // .url = "https://github.com/bronter/wgpu_native_zig/archive/refs/tags/v7.0.0.tar.gz` + // .url = "https://github.com/openharmony-zig/wgpu_native_zig/archive/refs/tags/v7.0.0.tar.gz` .hash="" } } } ``` + Then, in `build.zig` add: + ```zig const wgpu_native_dep = b.dependency("wgpu_native_zig", .{}); @@ -43,8 +76,10 @@ Then, in `build.zig` add: ``` ### Building on Windows + Windows x86_64 has two options for ABI: GNU and MSVC. For i686 and aarch64, only the MSVC option is available. If you need to specify the build target, you can do that with: + ```zig const target = b.standardTargetOptions(.{ .default_target = .{ @@ -53,11 +88,15 @@ const target = b.standardTargetOptions(.{ } }); ``` + Or, specify it with your build command. For example, the triangle example in this repository can be run like so: + ```sh zig build --build-file build.examples.zig run-triangle-example -Dtarget=x86_64-windows-msvc ``` + Either way, pass the resolved target to the dependency like so: + ```zig const wgpu_native_dep = b.dependency("wgpu_native_zig", .{ .target = target @@ -65,6 +104,7 @@ const wgpu_native_dep = b.dependency("wgpu_native_zig", .{ ``` When using static linking with MSVC, you might encounter duplicate symbol errors. If so, try + ```zig if (target.result.abi == .msvc) { // "exe" here is the *std.Build.Step.Compile from b.addExecutable() (or b.addTest()) @@ -72,18 +112,23 @@ if (target.result.abi == .msvc) { exe.bundle_ubsan_rt = false; } ``` + An example of using `wgpu-native-zig` with static linking on Windows can be found at [wgpu-native-zig-windows-test](https://github.com/bronter/wgpu-native-zig-windows-test). ### Dynamic linking + Dynamic linking can be made to work, though it is a bit messy to use. When you initialize your `wgpu_native_dep`, add the option for dynamic linking like so: + ```zig const wgpu_native_dep = b.dependency("wgpu_native_zig", .{ // Defaults to .static if you don't specify .link_mode = .dynamic }); ``` + Then add the following with your install step dependencies: + ```zig const lib_dir = wgpu_native_dep.namedWriteFiles("lib").getDirectory(); @@ -163,14 +208,14 @@ because upstream `wgpu-native` does not publish OpenHarmony archives. ### Supported artifact targets -| Platform | Architectures / ABIs | Source build | Published prebuilt | -| --- | --- | --- | --- | -| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | Yes, with `ANDROID_NDK_HOME` | Yes | -| iOS | arm64 device, arm64 simulator, x86_64 simulator | Yes, on macOS | Yes | -| Linux | aarch64, x86_64 (GNU); aarch64, x86_64 (musl) | Yes | GNU targets | -| macOS | aarch64, x86_64 | Yes, on macOS | Yes | -| Windows | aarch64/x86/x86_64 MSVC, x86/x86_64 GNU | Yes, on Windows | All except x86 GNU | -| OpenHarmony | arm64-v8a, armeabi-v7a, x86_64 | Yes, with `OHOS_NDK_HOME` | Local CI artifact | +| Platform | Architectures / ABIs | Source build | Published prebuilt | +| ----------- | ----------------------------------------------- | ---------------------------- | ------------------ | +| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | Yes, with `ANDROID_NDK_HOME` | Yes | +| iOS | arm64 device, arm64 simulator, x86_64 simulator | Yes, on macOS | Yes | +| Linux | aarch64, x86_64 (GNU); aarch64, x86_64 (musl) | Yes | GNU targets | +| macOS | aarch64, x86_64 | Yes, on macOS | Yes | +| Windows | aarch64/x86/x86_64 MSVC, x86/x86_64 GNU | Yes, on Windows | All except x86 GNU | +| OpenHarmony | arm64-v8a, armeabi-v7a, x86_64 | Yes, with `OHOS_NDK_HOME` | Local CI artifact | The OpenHarmony commands are: @@ -189,31 +234,36 @@ zig build --build-file build.tests.zig check \ ``` The target-artifact workflow builds the complete matrix on Linux x86_64/aarch64, macOS -arm64/Intel, Windows, Android, and OpenHarmony runners. Every artifact prefix contains -both link modes and the matching headers. - +arm64/Intel, Windows, Android, and OpenHarmony runners. Every target runs the +binding/ABI audit before packaging. Desktop targets compile all wrapper tests, while +Android, OpenHarmony, and arm64 iOS targets build a Zig link probe. Zig 0.16 cannot link +the historical `x86_64-apple-ios` Mach-O platform marker as an explicit simulator dylib, +so that one target runs the binding/ABI audit without the link probe. Every artifact +prefix contains both link modes and the matching headers. ## How the `wgpu` module differs from `wgpu-c` -* Names are shortened to remove redundancy. - * For example `wgpu.WGPUSurfaceDescriptor` becomes `wgpu.SurfaceDescriptor` -* C pointers (`[*c]`) are replaced with more specific pointer types. - * For example `[*c]const u8` is replaced with `?[*:0]const u8`. -* Pointers to opaque structs are made explicit (and only optional when they need to be). - * For example `wgpu.WGPUAdapter` from `webgpu.h` would instead be expressed as `*wgpu.Adapter` or `?*wgpu.Adapter`, depending on the context. -* Methods are expressed as decls inside of structs - * For example + +- Names are shortened to remove redundancy. + - For example `wgpu.WGPUSurfaceDescriptor` becomes `wgpu.SurfaceDescriptor` +- C pointers (`[*c]`) are replaced with more specific pointer types. + - For example `[*c]const u8` is replaced with `?[*:0]const u8`. +- Pointers to opaque structs are made explicit (and only optional when they need to be). + - For example `wgpu.WGPUAdapter` from `webgpu.h` would instead be expressed as `*wgpu.Adapter` or `?*wgpu.Adapter`, depending on the context. +- Methods are expressed as decls inside of structs + - For example ```zig wgpu.wgpuInstanceCreateSurface(instance: WGPUInstance, descriptor: [*c]const WGPUSurfaceDescriptor) WGPUSurface - ``` + ``` becomes ```zig Instance.createSurface(self: *Instance, descriptor: *const SurfaceDescriptor) ?*Surface ``` -* Certain asynchronous methods such as requestAdapter and requestDevice are provided with wrapper methods. - * For example, requesting an adapter with a callback looks something like +- Callback-based operations provide synchronous helpers for adapter/device requests, + buffer mapping, submitted queue work, and device error scopes. + - For example, requesting an adapter with a callback looks something like ```zig fn handleRequestAdapter( - status: RequestAdapterStatus, + status: Instance.RequestAdapterStatus, adapter: ?*Adapter, message: StringView, userdata1: ?*anyopaque, @@ -233,17 +283,16 @@ both link modes and the matching headers. } var adapter_ptr: ?*Adapter = null; var completed = false; - const request_adapter_info = RequestAdapterInfo { + const request_adapter_info = Instance.RequestAdapterCallbackInfo { .callback = handleRequestAdapter, .userdata1 = @ptrCast(&adapter_ptr), .userdata2 = @ptrCast(&completed), } const ra_future = instance.requestAdapter(null, request_adapter_info); - // There is currently no way to use a `Future`, - // it's supposed to be passed into `Instance.waitAny()`, - // which is unimplemented as of `wgpu_native` v24.0.3.1. - _ = ra_future; + // wgpu-native v29 does not implement Instance.waitAny(), so drive + // allow_process_events callbacks with Instance.processEvents(). + _ = ra_future; instance.processEvents(); while(!completed) { @@ -254,54 +303,100 @@ both link modes and the matching headers. whereas the non-callback version looks like ```zig // The wrapper methods use polling, so 200_000_000 is the polling interval in nanoseconds. - const response = try instance.requestAdapterSync(io, null, 200_000_000); + var response = try instance.requestAdapterSync(allocator, io, null, 200_000_000); + defer response.deinit(allocator); const adapter_ptr: ?*Adapter = switch (response.status) { - .success => response.adapter, + .success => response.takeAdapter(), else => blk: { - std.log.err("{s}\n", .{response.message}); + std.log.err("{s}\n", .{response.message orelse "adapter request failed"}); break :blk null; } }; ``` -* Chained structs are provided with inline functions for constructing them, which come in two forms depending on whether or not the chained struct is likely to always be required. - * For required chained structs, you can either write them explicitely: + The synchronous responses own their copied callback messages and any returned + handle. Call `deinit()` on every response, and use `takeAdapter()` or + `takeDevice()` to transfer a successful handle out of it. The other helpers + follow the same lifetime model: + `Buffer.mapSync()`, `Queue.onSubmittedWorkDoneSync()`, and + `Device.popErrorScopeSync()`. + If the supplied `Io` is cancelled, the synchronous wrapper finishes draining + the native callback before returning the cancellation error, yielding the + polling thread between `processEvents()` calls. This is required because + wgpu-native v29 has no `waitAny()` implementation and the callback userdata + must remain alive until completion. +- Output structures whose members are allocated by wgpu-native use pointer-based + `deinit()` methods. In particular, `SupportedFeatures`, `AdapterInfo`, and + `SurfaceCapabilities` clear their owned pointers, counts, and strings after + releasing them, so a deferred `deinit()` cannot observe stale members. + `SupportedFeatures.slice()` and the `SurfaceCapabilities.*Slice()` accessors + expose their pointer/count arrays as bounded read-only slices. +- `Instance.enumerateAdapters()` allocates an `AdapterList` that owns every returned + adapter. Deinitialize the list to release all remaining adapters, or transfer one: + ```zig + var adapters = try instance.enumerateAdapters(allocator, null); + defer adapters.deinit(allocator); + + const adapter = adapters.takeAdapter(0).?; + defer adapter.release(); + ``` +- `SurfaceTexture` owns the texture returned by `Surface.getCurrentTexture()`. + Use `defer surface_texture.deinit()` to release it automatically, or call + `takeTexture()` to transfer the texture to code that will release it. +- Wrapper methods use slices where the C API uses a pointer/count pair. + `Queue.writeBuffer()` and `Queue.writeTexture()` take byte slices, + `setBindGroup()` takes a dynamic-offset slice, and `setImmediates()` takes a + byte slice. Buffer mapped-range accessors return bounded byte slices and resolve + `WGPU_WHOLE_MAP_SIZE` against the buffer size. +- ABI-compatible descriptors retain their C pointer/count fields, while `init()` + and chainable `withXxx()` helpers accept borrowed slices and synchronize both + fields. The slice must remain alive until the native call returns. +- Chained structs are provided with inline functions for constructing them, which come in two forms depending on whether or not the chained struct is likely to always be required. + - For required chained structs, you can either write them explicitely: ```zig - SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceDescriptorFromXlibWindow { - .chain = ChainedStruct { - .s_type = SType.surface_descriptor_from_xlib_window, - }, - .display = display, - .window = window, - }), - .label = "xlib_surface_descriptor", + const source = SurfaceSourceXlibWindow{ + .display = display, + .window = window, + }; + const descriptor = SurfaceDescriptor{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice("xlib_surface_descriptor"), }; ``` - or use a function to construct them: + or use a function to construct the root descriptor: ```zig - // Here the descriptors from SurfaceDescriptor and SurfaceDescriptorFromXlibWindow have been merged, - // so just pass in an anonymous struct with the things that you need; default values will take care of the rest. - surfaceDescriptorFromXlibWindow(.{ - .label = "xlib_surface_descriptor", + const source = SurfaceSourceXlibWindow{ .display = display, - .window = window - }); + .window = window, + }; + const descriptor = surfaceDescriptorFromXlibWindow( + &source, + "xlib_surface_descriptor", + ); ``` - * For optional chained structs, you can either write them explicitely like in the example above, or you can use a method of the parent struct instance to add them, for example: + - For optional chained structs, create the extension separately and attach it + with `withExtras()`: ```zig - &(SurfaceConfiguration { - .device = device, - // other stuff - }).withDesiredMaxFrameLatency(2); + const extras = SurfaceConfigurationExtras{ + .desired_maximum_frame_latency = 2, + }; + const configuration = (SurfaceConfiguration{ + .device = device, + // other fields + }).withExtras(&extras); ``` -* `WGPUBool` is replaced with `bool` whenever possible. - * This means it is replaced with `bool` in wrapper method parameters and return values, but not in structs that preserve the C ABI. + The source or extras value must remain alive until the native API call that + consumes the descriptor has returned. +- `WGPUBool` is replaced with `bool` whenever possible. + - This means it is replaced with `bool` in wrapper method parameters and return values, but not in structs that preserve the C ABI. +- Callback types that belong to one handle are scoped under that handle. For example, + use `Instance.RequestAdapterCallbackInfo`, `Adapter.RequestDeviceCallbackInfo`, + `Buffer.MapCallbackInfo`, and `Device.PopErrorScopeCallbackInfo`. +- Wrapper names retain meaningful WebGPU prefixes. For example, + `WGPUTextureSampleType` is exposed as `TextureSampleType`. ## TODO -* Cleanup/organization: - * If types are only tied to a specific opaque struct, they should be decls inside that struct. - * There are many things that seem to be in the wrong file. - * For example a lot of what is in `pipeline.zig` is actually only used by `Device`, and should probably be in `device.zig` instead. - * Since pointers to opaque structs are made explicit, it would be more consistent if pointers to callback functions are explicit as well. -* Port [wgpu-native-examples](https://github.com/samdauwe/webgpu-native-examples) using wrapper code, as a basic form of documentation. + +- Expand headless coverage for limit queries, textures, samplers, query sets, and + render bundles. +- Port [wgpu-native-examples](https://github.com/samdauwe/webgpu-native-examples) using wrapper code, as a basic form of documentation. diff --git a/build/Library.zig b/build/Library.zig index 61e2f53..72b53b2 100644 --- a/build/Library.zig +++ b/build/Library.zig @@ -23,12 +23,24 @@ pub const Result = struct { return self.platform.kind == .ohos; } + pub fn isAndroid(self: Result) bool { + return self.platform.kind == .android; + } + + pub fn isIos(self: Result) bool { + return self.target.result.os.tag == .ios; + } + pub fn linkModule( self: Result, b: *std.Build, mod: *std.Build.Module, ) void { - mod.link_libcpp = true; + if (self.platform.kind != .apple and + self.platform.kind != .android) + { + mod.link_libcpp = true; + } Platform.configureModule( b, self.platform, @@ -45,10 +57,8 @@ pub const Result = struct { pub fn linkTestModule( self: Result, - b: *std.Build, mod: *std.Build.Module, ) void { - self.linkModule(b, mod); Platform.configureTest( self.platform, mod, @@ -126,7 +136,6 @@ pub fn build(b: *std.Build, options: Options) ?Result { .wgpu_mod = wgpu_mod, .wgpu_c_mod = wgpu_c_mod, }; - result.linkModule(b, wgpu_mod); result.linkModule(b, wgpu_c_mod); install(b, artifact); return result; diff --git a/build/platform/android.zig b/build/platform/android.zig index 8f1e117..0e39e68 100644 --- a/build/platform/android.zig +++ b/build/platform/android.zig @@ -42,26 +42,8 @@ pub fn configureSource( cargo: *std.Build.Step.Run, config: types.Config, ) void { - const ndk_root = b.graph.environ_map.get("ANDROID_NDK_HOME") orelse - b.graph.environ_map.get("ANDROID_NDK_ROOT") orelse - std.debug.panic( - "Building {s} requires ANDROID_NDK_HOME or ANDROID_NDK_ROOT", - .{config.rust_target}, - ); - const host_dir = switch (b.graph.host.result.os.tag) { - .linux => "linux-x86_64", - .macos => "darwin-x86_64", - .windows => "windows-x86_64", - else => std.debug.panic("Unsupported Android NDK host", .{}), - }; - const toolchain_root = b.pathJoin(&.{ - ndk_root, - "toolchains", - "llvm", - "prebuilt", - host_dir, - }); - const api_level = b.graph.environ_map.get("ANDROID_API_LEVEL") orelse "21"; + const toolchain_root = toolchainRoot(b, config); + const api_level = apiLevel(b, config); const clang_target = config.clang_target.?; const linker = b.pathJoin(&.{ toolchain_root, @@ -84,16 +66,63 @@ pub fn configureSource( } pub fn configureModule( - _: *std.Build, - _: types.Config, - _: *std.Build.Module, + b: *std.Build, + config: types.Config, + mod: *std.Build.Module, _: std.builtin.LinkMode, -) void {} +) void { + // Zig does not bundle an Android libc. Link the NDK runtime and platform + // stubs directly instead of requesting Zig's libc/libc++ builds. + mod.link_libc = false; + mod.link_libcpp = false; + + const target_library_dir: std.Build.LazyPath = .{ + .cwd_relative = b.pathJoin(&.{ + toolchainRoot(b, config), + "sysroot", + "usr", + "lib", + systemIncludeTarget(config), + }), + }; + const platform_library_dir = target_library_dir.path( + b, + apiLevel(b, config), + ); + mod.addLibraryPath(target_library_dir); + mod.addLibraryPath(platform_library_dir); + + mod.addObjectFile(target_library_dir.path(b, "libc++_static.a")); + mod.addObjectFile(target_library_dir.path(b, "libc++abi.a")); + inline for ([_][]const u8{ + "libc.so", + "libm.so", + "libdl.so", + "libandroid.so", + "liblog.so", + }) |library| { + mod.addObjectFile(platform_library_dir.path(b, library)); + } +} pub fn configureTranslateC( - _: types.Config, - _: *std.Build.Step.TranslateC, -) void {} + config: types.Config, + translate_c: *std.Build.Step.TranslateC, +) void { + const b = translate_c.step.owner; + const sysroot = b.pathJoin(&.{ toolchainRoot(b, config), "sysroot" }); + translate_c.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ + sysroot, + "usr", + "include", + }) }); + translate_c.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ + sysroot, + "usr", + "include", + systemIncludeTarget(config), + }) }); +} pub fn configureCompile(_: types.Config, _: *std.Build.Step.Compile) void {} @@ -102,3 +131,42 @@ pub fn configureTest( _: *std.Build.Module, _: std.builtin.LinkMode, ) void {} + +fn toolchainRoot(b: *std.Build, config: types.Config) []const u8 { + const ndk_root = b.graph.environ_map.get("ANDROID_NDK_HOME") orelse + b.graph.environ_map.get("ANDROID_NDK_ROOT") orelse + std.debug.panic( + "Building {s} requires ANDROID_NDK_HOME or ANDROID_NDK_ROOT", + .{config.rust_target}, + ); + const host_dir = switch (b.graph.host.result.os.tag) { + .linux => "linux-x86_64", + .macos => "darwin-x86_64", + .windows => "windows-x86_64", + else => std.debug.panic("Unsupported Android NDK host", .{}), + }; + return b.pathJoin(&.{ + ndk_root, + "toolchains", + "llvm", + "prebuilt", + host_dir, + }); +} + +fn apiLevel(b: *std.Build, config: types.Config) []const u8 { + return b.graph.environ_map.get("ANDROID_API_LEVEL") orelse b.fmt( + "{d}", + .{config.target.result.os.version_range.linux.android}, + ); +} + +fn systemIncludeTarget(config: types.Config) []const u8 { + return switch (config.target.result.cpu.arch) { + .aarch64 => "aarch64-linux-android", + .arm => "arm-linux-androideabi", + .x86 => "i686-linux-android", + .x86_64 => "x86_64-linux-android", + else => unreachable, + }; +} diff --git a/build/platform/apple.zig b/build/platform/apple.zig index a8d0783..3a20e2a 100644 --- a/build/platform/apple.zig +++ b/build/platform/apple.zig @@ -76,11 +76,23 @@ pub fn configureModule( ) void { // Explicit Apple targets do not inherit native SDK search paths. if (config.apple_sdk_root) |sdk_root| { - mod.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ + // linkSystemLibrary("c++") is normalized to Zig's bundled libc++. + // Apple targets must instead link the SDK-provided text stub directly. + mod.link_libcpp = false; + mod.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ sdk_root, "usr", - "lib", + "include", }) }); + const sdk_library_dir: std.Build.LazyPath = .{ + .cwd_relative = b.pathJoin(&.{ + sdk_root, + "usr", + "lib", + }), + }; + mod.addLibraryPath(sdk_library_dir); + mod.addObjectFile(sdk_library_dir.path(b, "libc++.tbd")); mod.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sdk_root, "System", @@ -94,9 +106,23 @@ pub fn configureModule( } pub fn configureTranslateC( - _: types.Config, - _: *std.Build.Step.TranslateC, -) void {} + config: types.Config, + translate_c: *std.Build.Step.TranslateC, +) void { + const sdk_root = config.apple_sdk_root orelse return; + const b = translate_c.step.owner; + translate_c.addSystemIncludePath(.{ .cwd_relative = b.pathJoin(&.{ + sdk_root, + "usr", + "include", + }) }); + translate_c.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ + sdk_root, + "System", + "Library", + "Frameworks", + }) }); +} pub fn configureCompile(_: types.Config, _: *std.Build.Step.Compile) void {} diff --git a/examples/bmp.zig b/examples/bmp.zig index f3a3e42..5ba506e 100644 --- a/examples/bmp.zig +++ b/examples/bmp.zig @@ -1,6 +1,16 @@ const std = @import("std"); -pub fn write24BitBMP(io: std.Io, file_name: []const u8, comptime width: u32, comptime height: u32, bgra_data: *[width * height * 4]u8) !void { +pub fn write24BitBMP( + io: std.Io, + file_name: []const u8, + comptime width: u32, + comptime height: u32, + bgra_data: []const u8, +) !void { + if (bgra_data.len != width * height * 4) { + return error.InvalidImageDataLength; + } + const file = try std.Io.Dir.cwd().createFile(io, file_name, .{}); defer file.close(io); diff --git a/examples/triangle/triangle.zig b/examples/triangle/triangle.zig index e637ba3..0131737 100644 --- a/examples/triangle/triangle.zig +++ b/examples/triangle/triangle.zig @@ -10,26 +10,22 @@ const output_extent = wgpu.Extent3D{ const output_bytes_per_row = 4 * output_extent.width; const output_size = output_bytes_per_row * output_extent.height; -fn handleBufferMap(status: wgpu.MapAsyncStatus, _: wgpu.StringView, userdata1: ?*anyopaque, _: ?*anyopaque) callconv(.c) void { - std.log.info("buffer_map status={x:.8}\n", .{@intFromEnum(status)}); - const complete: *bool = @ptrCast(@alignCast(userdata1)); - complete.* = true; -} - // Based off of headless triangle example from https://github.com/eliemichel/LearnWebGPU-Code/tree/step030-headless pub fn main(init: std.process.Init) !void { const instance = wgpu.Instance.create(null).?; defer instance.release(); - const adapter_request = try instance.requestAdapterSync(init.io, &wgpu.RequestAdapterOptions{}, 0); + var adapter_request = try instance.requestAdapterSync(init.gpa, init.io, &wgpu.RequestAdapterOptions{}, 0); + defer adapter_request.deinit(init.gpa); const adapter = switch (adapter_request.status) { - .success => adapter_request.adapter.?, + .success => adapter_request.takeAdapter().?, else => return error.NoAdapter, }; defer adapter.release(); - const device_request = try adapter.requestDeviceSync( + var device_request = try adapter.requestDeviceSync( + init.gpa, init.io, instance, &wgpu.DeviceDescriptor{ @@ -37,8 +33,9 @@ pub fn main(init: std.process.Init) !void { }, 0, ); + defer device_request.deinit(init.gpa); const device = switch (device_request.status) { - .success => device_request.device.?, + .success => device_request.takeDevice().?, else => return error.NoDevice, }; defer device.release(); @@ -62,9 +59,11 @@ pub fn main(init: std.process.Init) !void { .array_layer_count = 1, }).?; - const shader_module = device.createShaderModule(&wgpu.shaderModuleWGSLDescriptor(.{ - .code = @embedFile("./shader.wgsl"), - })).?; + const shader_source = wgpu.ShaderSourceWGSL{ + .code = wgpu.StringView.fromSlice(@embedFile("./shader.wgsl")), + }; + const shader_descriptor = wgpu.shaderModuleWGSLDescriptor(&shader_source, "triangle.wgsl"); + const shader_module = device.createShaderModule(&shader_descriptor).?; defer shader_module.release(); const staging_buffer = device.createBuffer(&wgpu.BufferDescriptor{ @@ -92,6 +91,8 @@ pub fn main(init: std.process.Init) !void { }, }, }; + var fragment_state = wgpu.FragmentState.init(shader_module, color_targets); + fragment_state.entry_point = wgpu.StringView.fromSlice("fs_main"); const pipeline = device.createRenderPipeline(&wgpu.RenderPipelineDescriptor{ .vertex = wgpu.VertexState{ @@ -99,7 +100,7 @@ pub fn main(init: std.process.Init) !void { .entry_point = wgpu.StringView.fromSlice("vs_main"), }, .primitive = wgpu.PrimitiveState{}, - .fragment = &wgpu.FragmentState{ .module = shader_module, .entry_point = wgpu.StringView.fromSlice("fs_main"), .target_count = color_targets.len, .targets = color_targets.ptr }, + .fragment = &fragment_state, .multisample = wgpu.MultisampleState{}, }).?; defer pipeline.release(); @@ -116,10 +117,8 @@ pub fn main(init: std.process.Init) !void { .view = next_texture, .clear_value = wgpu.Color{}, }}; - const render_pass = encoder.beginRenderPass(&wgpu.RenderPassDescriptor{ - .color_attachment_count = color_attachments.len, - .color_attachments = color_attachments.ptr, - }).?; + const render_pass_descriptor = wgpu.RenderPassDescriptor.init(color_attachments); + const render_pass = encoder.beginRenderPass(&render_pass_descriptor).?; render_pass.setPipeline(pipeline); render_pass.draw(3, 1, 0, 0); @@ -152,21 +151,21 @@ pub fn main(init: std.process.Init) !void { queue.submit(&[_]*const wgpu.CommandBuffer{command_buffer}); - var buffer_map_complete = false; - _ = staging_buffer.mapAsync(wgpu.MapModes.read, 0, output_size, wgpu.BufferMapCallbackInfo{ - .callback = handleBufferMap, - .userdata1 = @ptrCast(&buffer_map_complete), - }); - instance.processEvents(); - while (!buffer_map_complete) { - instance.processEvents(); - } - // _ = device.poll(true, null); - - const buf: [*]u8 = @ptrCast(@alignCast(staging_buffer.getMappedRange(0, output_size).?)); + var map_response = try staging_buffer.mapSync( + init.gpa, + init.io, + instance, + wgpu.Buffer.MapModes.read, + 0, + output_size, + 0, + ); + defer map_response.deinit(init.gpa); + if (map_response.status != .success) return error.BufferMapFailed; + + const output = staging_buffer.getConstMappedRange(0, output_size).?; defer staging_buffer.unmap(); - const output = buf[0..output_size]; try bmp.write24BitBMP(init.io, "examples/output/triangle.bmp", output_extent.width, output_extent.height, output); } } diff --git a/src/adapter.zig b/src/adapter.zig index 1b4fdb9..a6c367e 100644 --- a/src/adapter.zig +++ b/src/adapter.zig @@ -7,10 +7,12 @@ const ChainedStructOut = _chained_struct.ChainedStructOut; const _misc = @import("misc.zig"); const WGPUBool = _misc.WGPUBool; -const FeatureName = _misc.FeatureName; const StringView = _misc.StringView; const Status = _misc.Status; -const SupportedFeatures = _misc.SupportedFeatures; + +const _feature = @import("feature.zig"); +const FeatureName = _feature.FeatureName; +const SupportedFeatures = _feature.SupportedFeatures; const Limits = @import("limits.zig").Limits; @@ -21,10 +23,6 @@ const Instance = @import("instance.zig").Instance; const _device = @import("device.zig"); const Device = _device.Device; const DeviceDescriptor = _device.DeviceDescriptor; -const RequestDeviceCallback = _device.RequestDeviceCallback; -const RequestDeviceCallbackInfo = _device.RequestDeviceCallbackInfo; -const RequestDeviceStatus = _device.RequestDeviceStatus; -const RequestDeviceResponse = _device.RequestDeviceResponse; const _async = @import("async.zig"); const CallbackMode = _async.CallbackMode; @@ -41,6 +39,7 @@ pub const AdapterType = enum(u32) { integrated_gpu = 0x00000002, cpu = 0x00000003, unknown = 0x00000004, + _, }; pub const BackendType = enum(u32) { @@ -93,58 +92,82 @@ pub const RequestAdapterWebXROptions = extern struct { xr_compatible: WGPUBool = @intFromBool(false), }; -pub const RequestAdapterStatus = enum(u32) { - success = 0x00000001, - callback_cancelled = 0x00000002, - unavailable = 0x00000003, - @"error" = 0x00000004, +pub const AdapterInfo = extern struct { + next_in_chain: ?*ChainedStructOut = null, + vendor: StringView = .{}, + architecture: StringView = .{}, + device: StringView = .{}, + description: StringView = .{}, + backend_type: BackendType = .undefined, + adapter_type: AdapterType = @enumFromInt(0), + vendor_id: u32 = 0, + device_id: u32 = 0, + subgroup_min_size: u32 = 0, + subgroup_max_size: u32 = 0, + + pub inline fn deinit(self: *AdapterInfo) void { + raw.call(void, "wgpuAdapterInfoFreeMembers", .{self.*}); + self.vendor = .{}; + self.architecture = .{}; + self.device = .{}; + self.description = .{}; + } }; -pub const RequestAdapterCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, +pub const Adapter = opaque { + pub const RequestDeviceStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + @"error" = 0x00000003, + }; - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, + pub const RequestDeviceCallback = *const fn ( + status: RequestDeviceStatus, + device: ?*Device, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; - callback: RequestAdapterCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; + pub const RequestDeviceCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, -// TODO: This should maybe be relocated to instance.zig; it is only used there. -pub const RequestAdapterCallback = *const fn ( - status: RequestAdapterStatus, - adapter: ?*Adapter, - message: StringView, - userdata1: ?*anyopaque, - userdata2: ?*anyopaque, -) callconv(.c) void; - -pub const RequestAdapterResponse = struct { - status: RequestAdapterStatus, - message: ?[]const u8, - adapter: ?*Adapter, -}; + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, -pub const AdapterInfo = extern struct { - next_in_chain: ?*ChainedStructOut = null, - vendor: StringView, - architecture: StringView, - device: StringView, - description: StringView, - backend_type: BackendType, - adapter_type: AdapterType, - vendor_id: u32, - device_id: u32, - subgroup_min_size: u32, - subgroup_max_size: u32, - - pub inline fn freeMembers(self: AdapterInfo) void { - raw.call(void, "wgpuAdapterInfoFreeMembers", .{self}); - } -}; + callback: RequestDeviceCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const RequestDeviceResponse = struct { + status: RequestDeviceStatus, + message: ?[]const u8, + device: ?*Device, + + pub fn deinit(self: *RequestDeviceResponse, allocator: std.mem.Allocator) void { + if (self.message) |message| allocator.free(message); + if (self.device) |device| device.release(); + self.message = null; + self.device = null; + } + + pub fn takeDevice(self: *RequestDeviceResponse) ?*Device { + const device = self.device; + self.device = null; + return device; + } + }; + + pub const RequestDeviceSyncError = std.Io.Cancelable || std.mem.Allocator.Error; + + const RequestDeviceSyncState = struct { + allocator: std.mem.Allocator, + response: RequestDeviceResponse = undefined, + message_error: ?std.mem.Allocator.Error = null, + completed: bool = false, + }; -pub const Adapter = opaque { pub inline fn getFeatures(self: *Adapter, features: *SupportedFeatures) void { raw.call(void, "wgpuAdapterGetFeatures", .{ self, features }); } @@ -158,46 +181,63 @@ pub const Adapter = opaque { return raw.call(WGPUBool, "wgpuAdapterHasFeature", .{ self, feature }) != 0; } - fn defaultDeviceCallback(status: RequestDeviceStatus, device: ?*Device, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void { - const ud_response: *RequestDeviceResponse = @ptrCast(@alignCast(userdata1)); - ud_response.* = RequestDeviceResponse{ + fn defaultDeviceCallback(status: RequestDeviceStatus, device: ?*Device, message: StringView, userdata1: ?*anyopaque, _: ?*anyopaque) callconv(.c) void { + const state: *RequestDeviceSyncState = @ptrCast(@alignCast(userdata1)); + state.response = .{ .status = status, - .message = message.toSlice(), + .message = null, .device = device, }; - - const completed: *bool = @ptrCast(@alignCast(userdata2)); - completed.* = true; + state.response.message = _async.copyCallbackMessage( + state.allocator, + message, + ) catch |err| { + state.message_error = err; + state.completed = true; + return; + }; + state.completed = true; } - // This is a synchronous wrapper that handles asynchronous (callback) logic. - // It uses polling to see when the request has been fulfilled, so needs a polling interval parameter. + // This is a synchronous wrapper that handles asynchronous (callback) logic. The returned + // response owns its message and device until deinit() or takeDevice() is called. pub fn requestDeviceSync( self: *Adapter, + allocator: std.mem.Allocator, io: std.Io, instance: *Instance, descriptor: ?*const DeviceDescriptor, polling_interval_nanoseconds: u64, - ) std.Io.Cancelable!RequestDeviceResponse { - var response: RequestDeviceResponse = undefined; - var completed = false; + ) RequestDeviceSyncError!RequestDeviceResponse { + var state = RequestDeviceSyncState{ .allocator = allocator }; const callback_info = RequestDeviceCallbackInfo{ .callback = defaultDeviceCallback, - .userdata1 = @ptrCast(&response), - .userdata2 = @ptrCast(&completed), + .userdata1 = @ptrCast(&state), }; const device_future = raw.call(Future, "wgpuAdapterRequestDevice", .{ self, descriptor, callback_info }); // TODO: Revisit once Instance.waitAny() is implemented in wgpu-native, // it takes in futures and returns when one of them completes. _ = device_future; - instance.processEvents(); - while (!completed) { - try io.sleep(.fromNanoseconds(polling_interval_nanoseconds), .awake); - instance.processEvents(); - } + var wait_error: ?std.Io.Cancelable = null; + _async.waitForCallback( + instance, + &state.completed, + io, + polling_interval_nanoseconds, + ) catch |err| { + wait_error = err; + }; - return response; + if (state.message_error) |err| { + state.response.deinit(allocator); + return err; + } + if (wait_error) |err| { + state.response.deinit(allocator); + return err; + } + return state.response; } pub inline fn requestDevice(self: *Adapter, descriptor: ?*const DeviceDescriptor, callback_info: RequestDeviceCallbackInfo) Future { @@ -216,19 +256,59 @@ test "can request device" { const instance = Instance.create(null).?; defer instance.release(); - const adapter_response = try instance.requestAdapterSync(std.testing.io, null, 200_000_000); + var adapter_response = try instance.requestAdapterSync(testing.allocator, testing.io, null, 200_000_000); + defer adapter_response.deinit(testing.allocator); const adapter: ?*Adapter = switch (adapter_response.status) { - .success => adapter_response.adapter, + .success => adapter_response.takeAdapter(), else => null, }; if (adapter == null) return error.SkipZigTest; defer adapter.?.release(); - const device_response = try adapter.?.requestDeviceSync(std.testing.io, instance, null, 200_000_000); + + var features = SupportedFeatures{}; + adapter.?.getFeatures(&features); + defer features.deinit(); + try testing.expect(features.feature_count == 0 or features.features != null); + features.deinit(); + try testing.expectEqual(0, features.feature_count); + try testing.expectEqual(null, features.features); + + var info = AdapterInfo{}; + const info_status = adapter.?.getInfo(&info); + defer info.deinit(); + try testing.expectEqual(Status.success, info_status); + info.deinit(); + try testing.expectEqual(null, info.vendor.toSlice()); + try testing.expectEqual(null, info.architecture.toSlice()); + try testing.expectEqual(null, info.device.toSlice()); + try testing.expectEqual(null, info.description.toSlice()); + + var device_response = try adapter.?.requestDeviceSync(testing.allocator, testing.io, instance, null, 200_000_000); + defer device_response.deinit(testing.allocator); const device: ?*Device = switch (device_response.status) { - .success => device_response.device, + .success => device_response.takeDevice(), else => null, }; if (device == null) return error.SkipZigTest; defer device.?.release(); try testing.expect(device != null); } + +test "synchronous device callback copies its message" { + const testing = @import("std").testing; + + var callback_message = [_]u8{ 'o', 'l', 'd' }; + var state = Adapter.RequestDeviceSyncState{ .allocator = testing.allocator }; + Adapter.defaultDeviceCallback( + .@"error", + null, + StringView.fromSlice(&callback_message), + @ptrCast(&state), + null, + ); + defer state.response.deinit(testing.allocator); + + callback_message[0] = 'n'; + try testing.expect(state.completed); + try testing.expectEqualStrings("old", state.response.message.?); +} diff --git a/src/async.zig b/src/async.zig index 7afa79e..87bf25f 100644 --- a/src/async.zig +++ b/src/async.zig @@ -1,4 +1,8 @@ -const WGPUBool = @import("misc.zig").WGPUBool; +const std = @import("std"); + +const _misc = @import("misc.zig"); +const StringView = _misc.StringView; +const WGPUBool = _misc.WGPUBool; // // The callback mode controls how a callback for an asynchronous operation may be fired. @@ -64,3 +68,93 @@ pub const FutureWaitInfo = extern struct { // Whether or not the future completed. completed: WGPUBool, }; + +/// Copies a callback StringView whose storage is owned by the native +/// implementation. The caller owns the returned slice. +pub fn copyCallbackMessage( + allocator: std.mem.Allocator, + message: StringView, +) std.mem.Allocator.Error!?[]const u8 { + const slice = message.toSlice() orelse return null; + return try allocator.dupe(u8, slice); +} + +/// Drives an `allow_process_events` callback to completion. +/// +/// If `io` is cancelled, the error is returned only after the callback has +/// completed. This keeps callback userdata valid for its full native lifetime. +/// The drain phase yields the current thread between event-processing calls so +/// cancellation does not turn into a busy loop. +pub fn waitForCallback( + event_source: anytype, + completed: *const bool, + io: std.Io, + polling_interval_nanoseconds: u64, +) std.Io.Cancelable!void { + var cancellation_error: ?std.Io.Cancelable = null; + + event_source.processEvents(); + while (!completed.*) { + if (cancellation_error == null) { + io.sleep(.fromNanoseconds(polling_interval_nanoseconds), .awake) catch |err| { + cancellation_error = err; + }; + } else { + std.Thread.yield() catch std.atomic.spinLoopHint(); + } + event_source.processEvents(); + } + + if (cancellation_error) |err| return err; +} + +test "waitForCallback drives events until completion" { + const testing = std.testing; + + var completed = false; + var event_source = TestEventSource{ .completed = &completed }; + + try waitForCallback(&event_source, &completed, testing.io, 0); + + try testing.expect(completed); + try testing.expectEqual(2, event_source.process_count); +} + +test "waitForCallback drains events before returning cancellation" { + const testing = std.testing; + + var completed = false; + var event_source = TestEventSource{ + .completed = &completed, + .complete_after = 3, + }; + var vtable = testing.io.vtable.*; + vtable.sleep = cancelSleep; + const canceled_io = std.Io{ + .userdata = null, + .vtable = &vtable, + }; + + try testing.expectError( + error.Canceled, + waitForCallback(&event_source, &completed, canceled_io, 0), + ); + + try testing.expect(completed); + try testing.expectEqual(3, event_source.process_count); +} + +const TestEventSource = struct { + completed: *bool, + process_count: usize = 0, + complete_after: usize = 2, + + fn processEvents(self: *TestEventSource) void { + self.process_count += 1; + if (self.process_count == self.complete_after) self.completed.* = true; + } +}; + +fn cancelSleep(_: ?*anyopaque, _: std.Io.Timeout) std.Io.Cancelable!void { + return error.Canceled; +} diff --git a/src/bind_group.zig b/src/bind_group.zig index c10ebd3..d264871 100644 --- a/src/bind_group.zig +++ b/src/bind_group.zig @@ -18,7 +18,7 @@ const TextureView = _texture.TextureView; const TextureBindingLayout = _texture.TextureBindingLayout; const StorageTextureBindingLayout = _texture.StorageTextureBindingLayout; const StorageTextureAccess = _texture.StorageTextureAccess; -const SampleType = _texture.SampleType; +const TextureSampleType = _texture.TextureSampleType; const ShaderStage = @import("shader.zig").ShaderStage; @@ -26,15 +26,10 @@ const _misc = @import("misc.zig"); const WGPU_WHOLE_SIZE = _misc.WGPU_WHOLE_SIZE; const StringView = _misc.StringView; -pub const ExternalTexture = opaque { - pub inline fn addRef(self: *ExternalTexture) void { - raw.call(void, "wgpuExternalTextureAddRef", .{self}); - } - - pub inline fn release(self: *ExternalTexture) void { - raw.call(void, "wgpuExternalTextureRelease", .{self}); - } -}; +// wgpu-native v29 declares ExternalTexture lifecycle functions, but their +// implementations panic. Keep the handle type for binding descriptors while +// exposing those functions only through `wgpu.raw`. +pub const ExternalTexture = opaque {}; pub const ExternalTextureBindingLayout = extern struct { chain: ChainedStruct = .{ @@ -70,18 +65,16 @@ pub const BindGroupLayoutEntry = extern struct { .type = SamplerBindingType.binding_not_used, }, texture: TextureBindingLayout = TextureBindingLayout{ - .sample_type = SampleType.binding_not_used, + .sample_type = TextureSampleType.binding_not_used, }, storage_texture: StorageTextureBindingLayout = StorageTextureBindingLayout{ .access = StorageTextureAccess.binding_not_used, }, - pub inline fn withCount(self: BindGroupLayoutEntry, count: u32) BindGroupLayoutEntry { - var bgle = self; - bgle.next_in_chain = @ptrCast(&BindGroupLayoutEntryExtras{ - .count = count, - }); - return bgle; + pub inline fn withExtras(self: BindGroupLayoutEntry, extras: *const BindGroupLayoutEntryExtras) BindGroupLayoutEntry { + var entry = self; + entry.next_in_chain = @ptrCast(extras); + return entry; } }; @@ -90,6 +83,14 @@ pub const BindGroupLayoutDescriptor = extern struct { label: StringView = StringView{}, entry_count: usize, entries: [*]const BindGroupLayoutEntry, + + /// Initializes a descriptor that borrows `entries`. + pub inline fn init(entries: []const BindGroupLayoutEntry) BindGroupLayoutDescriptor { + return .{ + .entry_count = entries.len, + .entries = entries.ptr, + }; + } }; pub const BindGroupLayout = opaque { @@ -111,12 +112,48 @@ pub const BindGroupEntryExtras = extern struct { chain: ChainedStruct = ChainedStruct{ .s_type = SType.bind_group_entry_extras, }, - buffers: ?[*]const *Buffer, + buffers: ?[*]const *Buffer = null, buffer_count: usize = 0, - samplers: ?[*]const *Sampler, + samplers: ?[*]const *Sampler = null, sampler_count: usize = 0, - texture_views: ?[*]const *TextureView, + texture_views: ?[*]const *TextureView = null, texture_view_count: usize = 0, + + /// Returns extras that borrow `buffers`. + pub inline fn withBuffers( + self: BindGroupEntryExtras, + buffers: []const *Buffer, + ) BindGroupEntryExtras { + var extras = self; + extras.buffer_count = buffers.len; + extras.buffers = if (buffers.len == 0) null else buffers.ptr; + return extras; + } + + /// Returns extras that borrow `samplers`. + pub inline fn withSamplers( + self: BindGroupEntryExtras, + samplers: []const *Sampler, + ) BindGroupEntryExtras { + var extras = self; + extras.sampler_count = samplers.len; + extras.samplers = if (samplers.len == 0) null else samplers.ptr; + return extras; + } + + /// Returns extras that borrow `texture_views`. + pub inline fn withTextureViews( + self: BindGroupEntryExtras, + texture_views: []const *TextureView, + ) BindGroupEntryExtras { + var extras = self; + extras.texture_view_count = texture_views.len; + extras.texture_views = if (texture_views.len == 0) + null + else + texture_views.ptr; + return extras; + } }; pub const BindGroupEntry = extern struct { @@ -128,10 +165,10 @@ pub const BindGroupEntry = extern struct { sampler: ?*Sampler = null, texture_view: ?*TextureView = null, - pub inline fn withNativeExtras(self: BindGroupEntry, extras: *BindGroupEntryExtras) BindGroupEntry { - var bge = self; - bge.next_in_chain = @ptrCast(extras); - return bge; + pub inline fn withExtras(self: BindGroupEntry, extras: *const BindGroupEntryExtras) BindGroupEntry { + var entry = self; + entry.next_in_chain = @ptrCast(extras); + return entry; } }; @@ -141,6 +178,18 @@ pub const BindGroupDescriptor = extern struct { layout: *BindGroupLayout, entry_count: usize, entries: [*]const BindGroupEntry, + + /// Initializes a descriptor that borrows `entries`. + pub inline fn init( + layout: *BindGroupLayout, + entries: []const BindGroupEntry, + ) BindGroupDescriptor { + return .{ + .layout = layout, + .entry_count = entries.len, + .entries = entries.ptr, + }; + } }; pub const BindGroup = opaque { diff --git a/src/binding_audit.zig b/src/binding_audit.zig new file mode 100644 index 0000000..fb958c0 --- /dev/null +++ b/src/binding_audit.zig @@ -0,0 +1,331 @@ +const std = @import("std"); +const header = @import("wgpu-header"); +const wrapper = @import("wgpu-wrapper"); + +const wrapper_sources = .{ + .{ "adapter.zig", @embedFile("adapter.zig") }, + .{ "async.zig", @embedFile("async.zig") }, + .{ "bind_group.zig", @embedFile("bind_group.zig") }, + .{ "buffer.zig", @embedFile("buffer.zig") }, + .{ "chained_struct.zig", @embedFile("chained_struct.zig") }, + .{ "command_encoder.zig", @embedFile("command_encoder.zig") }, + .{ "copy.zig", @embedFile("copy.zig") }, + .{ "device.zig", @embedFile("device.zig") }, + .{ "feature.zig", @embedFile("feature.zig") }, + .{ "instance.zig", @embedFile("instance.zig") }, + .{ "limits.zig", @embedFile("limits.zig") }, + .{ "log.zig", @embedFile("log.zig") }, + .{ "misc.zig", @embedFile("misc.zig") }, + .{ "pipeline.zig", @embedFile("pipeline.zig") }, + .{ "query_set.zig", @embedFile("query_set.zig") }, + .{ "queue.zig", @embedFile("queue.zig") }, + .{ "raw.zig", @embedFile("raw.zig") }, + .{ "render_bundle.zig", @embedFile("render_bundle.zig") }, + .{ "root.zig", @embedFile("root.zig") }, + .{ "sampler.zig", @embedFile("sampler.zig") }, + .{ "shader.zig", @embedFile("shader.zig") }, + .{ "surface.zig", @embedFile("surface.zig") }, + .{ "texture.zig", @embedFile("texture.zig") }, +}; + +// These functions are declared by the pinned v29 headers but do not provide a +// usable implementation in wgpu-native v29.0.0.0. Most panic through Rust's +// `unimplemented!()`; the native Metal command queue accessor logs a warning +// and always returns null. They remain available through `wgpu.raw`, but are +// intentionally not promoted to the Zig-friendly wrapper. +const unavailable_v29_functions = [_][]const u8{ + "wgpuBindGroupLayoutSetLabel", + "wgpuBindGroupSetLabel", + "wgpuBufferGetMapState", + "wgpuBufferReadMappedRange", + "wgpuBufferSetLabel", + "wgpuBufferWriteMappedRange", + "wgpuCommandBufferSetLabel", + "wgpuCommandEncoderSetLabel", + "wgpuComputePassEncoderSetLabel", + "wgpuComputePipelineSetLabel", + "wgpuDeviceCreateComputePipelineAsync", + "wgpuDeviceCreateRenderPipelineAsync", + "wgpuDeviceGetAdapterInfo", + "wgpuDeviceGetLostFuture", + "wgpuDeviceSetLabel", + "wgpuExternalTextureAddRef", + "wgpuExternalTextureRelease", + "wgpuExternalTextureSetLabel", + "wgpuGetInstanceFeatures", + "wgpuGetProcAddress", + "wgpuHasInstanceFeature", + "wgpuInstanceGetWGSLLanguageFeatures", + "wgpuInstanceHasWGSLLanguageFeature", + "wgpuInstanceWaitAny", + "wgpuPipelineLayoutSetLabel", + "wgpuQuerySetSetLabel", + "wgpuQueueGetNativeMetalCommandQueue", + "wgpuQueueSetLabel", + "wgpuRenderBundleEncoderSetLabel", + "wgpuRenderBundleSetLabel", + "wgpuRenderPassEncoderSetLabel", + "wgpuRenderPipelineSetLabel", + "wgpuSamplerSetLabel", + "wgpuShaderModuleGetCompilationInfo", + "wgpuShaderModuleSetLabel", + "wgpuSupportedInstanceFeaturesFreeMembers", + "wgpuSupportedWGSLLanguageFeaturesFreeMembers", + "wgpuSurfaceSetLabel", + "wgpuTextureGetTextureBindingViewDimension", + "wgpuTextureSetLabel", + "wgpuTextureViewSetLabel", +}; + +pub fn validate() void { + var wrapper_functions: [256][]const u8 = undefined; + var wrapper_function_count: usize = 0; + for (wrapper_sources) |source| { + validateWrapperSource( + source[0], + source[1], + &wrapper_functions, + &wrapper_function_count, + ); + } + + for (unavailable_v29_functions) |name| { + if (!@hasDecl(header, name)) { + @compileError("v29 unavailable-function list contains missing header function " ++ name); + } + } + + var header_function_count: usize = 0; + for (std.meta.declarations(header)) |declaration| { + if (!std.mem.startsWith(u8, declaration.name, "wgpu")) continue; + if (@typeInfo(@TypeOf(@field(header, declaration.name))) != .@"fn") continue; + header_function_count += 1; + } + + const covered_function_count = + wrapper_function_count + unavailable_v29_functions.len; + if (covered_function_count != header_function_count) { + @compileError(std.fmt.comptimePrint( + "v29 function coverage mismatch: {d} wrapper + {d} unavailable != {d} header functions", + .{ + wrapper_function_count, + unavailable_v29_functions.len, + header_function_count, + }, + )); + } + + validateTypes(); + validateCallbacks(); +} + +fn validateTypes() void { + for (std.meta.declarations(header)) |declaration| { + if (!std.mem.startsWith(u8, declaration.name, "WGPU")) continue; + if (std.mem.indexOfScalar(u8, declaration.name, '_') != null) continue; + if (std.mem.startsWith(u8, declaration.name, "WGPUProc")) continue; + if (std.mem.endsWith(u8, declaration.name, "Impl")) continue; + + const header_declaration = @field(header, declaration.name); + if (@TypeOf(header_declaration) != type) continue; + + _ = wrapperType(declaration.name); + } +} + +fn wrapperType(comptime c_name: []const u8) type { + if (std.mem.eql(u8, c_name, "WGPUBufferMapState")) + return wrapper.Buffer.MapState; + if (std.mem.eql(u8, c_name, "WGPUMapMode")) + return wrapper.Buffer.MapMode; + if (std.mem.eql(u8, c_name, "WGPUMapAsyncStatus")) + return wrapper.Buffer.MapAsyncStatus; + if (std.mem.eql(u8, c_name, "WGPUBufferMapCallback")) + return wrapper.Buffer.MapCallback; + if (std.mem.eql(u8, c_name, "WGPUBufferMapCallbackInfo")) + return wrapper.Buffer.MapCallbackInfo; + if (std.mem.eql(u8, c_name, "WGPURequestAdapterStatus") or + std.mem.eql(u8, c_name, "WGPURequestAdapterCallback") or + std.mem.eql(u8, c_name, "WGPURequestAdapterCallbackInfo")) + { + return @field(wrapper.Instance, c_name["WGPU".len..]); + } + if (std.mem.eql(u8, c_name, "WGPURequestDeviceStatus") or + std.mem.eql(u8, c_name, "WGPURequestDeviceCallback") or + std.mem.eql(u8, c_name, "WGPURequestDeviceCallbackInfo")) + { + return @field(wrapper.Adapter, c_name["WGPU".len..]); + } + if (std.mem.eql(u8, c_name, "WGPUCreatePipelineAsyncStatus") or + std.mem.startsWith(u8, c_name, "WGPUCreateComputePipelineAsync") or + std.mem.startsWith(u8, c_name, "WGPUCreateRenderPipelineAsync")) + { + return @field(wrapper.Device, c_name["WGPU".len..]); + } + if (std.mem.eql(u8, c_name, "WGPUDeviceLostReason") or + std.mem.eql(u8, c_name, "WGPUDeviceLostCallback") or + std.mem.eql(u8, c_name, "WGPUDeviceLostCallbackInfo") or + std.mem.eql(u8, c_name, "WGPUErrorType") or + std.mem.eql(u8, c_name, "WGPUErrorFilter") or + std.mem.eql(u8, c_name, "WGPUUncapturedErrorCallback") or + std.mem.eql(u8, c_name, "WGPUUncapturedErrorCallbackInfo") or + std.mem.eql(u8, c_name, "WGPUPopErrorScopeStatus") or + std.mem.eql(u8, c_name, "WGPUPopErrorScopeCallback") or + std.mem.eql(u8, c_name, "WGPUPopErrorScopeCallbackInfo")) + { + return @field(wrapper.Device, c_name["WGPU".len..]); + } + if (std.mem.eql(u8, c_name, "WGPUQueueWorkDoneStatus")) + return wrapper.Queue.WorkDoneStatus; + if (std.mem.eql(u8, c_name, "WGPUQueueWorkDoneCallback")) + return wrapper.Queue.WorkDoneCallback; + if (std.mem.eql(u8, c_name, "WGPUQueueWorkDoneCallbackInfo")) + return wrapper.Queue.WorkDoneCallbackInfo; + if (std.mem.eql(u8, c_name, "WGPUCompilationInfoRequestStatus") or + std.mem.eql(u8, c_name, "WGPUCompilationMessageType") or + std.mem.eql(u8, c_name, "WGPUCompilationMessage") or + std.mem.eql(u8, c_name, "WGPUCompilationInfo") or + std.mem.eql(u8, c_name, "WGPUCompilationInfoCallback") or + std.mem.eql(u8, c_name, "WGPUCompilationInfoCallbackInfo")) + { + return @field(wrapper.ShaderModule, c_name["WGPU".len..]); + } + + const wrapper_name = wrapperTypeName(c_name); + if (!@hasDecl(wrapper, wrapper_name)) { + @compileError(std.fmt.comptimePrint( + "{s} has no wgpu wrapper type ({s})", + .{ c_name, wrapper_name }, + )); + } + return @field(wrapper, wrapper_name); +} + +fn wrapperTypeName(comptime c_name: []const u8) []const u8 { + if (std.mem.eql(u8, c_name, "WGPUBool")) return "WGPUBool"; + if (std.mem.eql(u8, c_name, "WGPUFlags")) return "WGPUFlags"; + if (std.mem.eql(u8, c_name, "WGPUInstanceEnumerateAdapterOptions")) + return "EnumerateAdapterOptions"; + if (std.mem.eql(u8, c_name, "WGPUNativeFeature")) + return "FeatureName"; + if (std.mem.eql(u8, c_name, "WGPUNativeLimits")) + return "WGPUNativeLimits"; + if (std.mem.eql(u8, c_name, "WGPUNativeQueryType")) + return "QueryType"; + if (std.mem.eql(u8, c_name, "WGPUNativeSType")) + return "SType"; + if (std.mem.eql(u8, c_name, "WGPUNativeSurfaceGetCurrentTextureStatus")) + return "GetCurrentTextureStatus"; + if (std.mem.eql(u8, c_name, "WGPUNativeTextureFormat")) + return "TextureFormat"; + if (std.mem.eql(u8, c_name, "WGPURenderPassColorAttachment")) + return "ColorAttachment"; + if (std.mem.eql(u8, c_name, "WGPURenderPassDepthStencilAttachment")) + return "DepthStencilAttachment"; + if (std.mem.eql(u8, c_name, "WGPUStringView")) return "StringView"; + if (std.mem.eql(u8, c_name, "WGPUSurfaceGetCurrentTextureStatus")) + return "GetCurrentTextureStatus"; + if (std.mem.eql(u8, c_name, "WGPUTextureViewDimension")) + return "ViewDimension"; + return c_name[4..]; +} + +fn validateCallbacks() void { + for (std.meta.declarations(header)) |declaration| { + if (!std.mem.startsWith(u8, declaration.name, "WGPU")) continue; + if (!std.mem.endsWith(u8, declaration.name, "Callback")) continue; + if (std.mem.startsWith(u8, declaration.name, "WGPUProc")) continue; + + const HeaderFn = callbackFunctionType(@field(header, declaration.name)); + const WrapperFn = callbackFunctionType(wrapperType(declaration.name)); + const header_info = @typeInfo(HeaderFn).@"fn"; + const wrapper_info = @typeInfo(WrapperFn).@"fn"; + + if (std.meta.activeTag(header_info.calling_convention) != + std.meta.activeTag(wrapper_info.calling_convention)) + { + @compileError(declaration.name ++ " has a mismatched calling convention"); + } + if (header_info.params.len != wrapper_info.params.len) { + @compileError(declaration.name ++ " has a mismatched parameter count"); + } + for (header_info.params, wrapper_info.params, 0..) |header_param, wrapper_param, index| { + const HeaderParam = header_param.type.?; + const WrapperParam = wrapper_param.type.?; + if (@sizeOf(HeaderParam) != @sizeOf(WrapperParam) or + @alignOf(HeaderParam) != @alignOf(WrapperParam)) + { + @compileError(std.fmt.comptimePrint( + "{s} parameter {d} has a mismatched ABI", + .{ declaration.name, index }, + )); + } + } + + const HeaderReturn = header_info.return_type.?; + const WrapperReturn = wrapper_info.return_type.?; + if (@sizeOf(HeaderReturn) != @sizeOf(WrapperReturn) or + @alignOf(HeaderReturn) != @alignOf(WrapperReturn)) + { + @compileError(declaration.name ++ " has a mismatched return ABI"); + } + } +} + +fn callbackFunctionType(comptime Callback: type) type { + return switch (@typeInfo(Callback)) { + .optional => |optional| callbackFunctionType(optional.child), + .pointer => |pointer| callbackFunctionType(pointer.child), + .@"fn" => Callback, + else => @compileError(@typeName(Callback) ++ " is not a callback function pointer"), + }; +} + +fn isUnavailableV29Function(comptime name: []const u8) bool { + inline for (unavailable_v29_functions) |unavailable| { + if (std.mem.eql(u8, name, unavailable)) return true; + } + return false; +} + +fn validateWrapperSource( + comptime file_name: []const u8, + comptime source: []const u8, + wrapper_functions: *[256][]const u8, + wrapper_function_count: *usize, +) void { + if (std.mem.indexOf(u8, source, "extern fn wgpu") != null) { + @compileError(file_name ++ " contains a handwritten wgpu extern"); + } + + var cursor: usize = 0; + while (std.mem.indexOfPos(u8, source, cursor, "raw.call(")) |call_start| { + const name_start = std.mem.indexOfPos(u8, source, call_start, "\"wgpu") orelse + @compileError(file_name ++ " contains an invalid raw.call"); + const name_end = std.mem.indexOfScalarPos( + u8, + source, + name_start + 1, + '"', + ) orelse @compileError(file_name ++ " contains an unterminated function name"); + const name = source[name_start + 1 .. name_end]; + if (!@hasDecl(header, name)) { + @compileError(file_name ++ " references missing header function " ++ name); + } + if (isUnavailableV29Function(name)) { + @compileError(file_name ++ " exposes unavailable wgpu-native v29 function " ++ name); + } + var already_registered = false; + for (wrapper_functions[0..wrapper_function_count.*]) |registered| { + if (std.mem.eql(u8, registered, name)) { + already_registered = true; + break; + } + } + if (!already_registered) { + wrapper_functions[wrapper_function_count.*] = name; + wrapper_function_count.* += 1; + } + cursor = name_end + 1; + } +} diff --git a/src/buffer.zig b/src/buffer.zig index 9645e55..926d0ed 100644 --- a/src/buffer.zig +++ b/src/buffer.zig @@ -1,3 +1,5 @@ +const std = @import("std"); + const _misc = @import("misc.zig"); const WGPUBool = _misc.WGPUBool; const WGPUFlags = _misc.WGPUFlags; @@ -43,39 +45,6 @@ pub const BufferUsages = struct { pub const query_resolve = @as(BufferUsage, 0x0000000000000200); }; -pub const BufferMapState = enum(u32) { - unmapped = 0x00000001, - pending = 0x00000002, - mapped = 0x00000003, -}; - -pub const MapMode = WGPUFlags; -pub const MapModes = struct { - pub const none = @as(MapMode, 0x0000000000000000); - pub const read = @as(MapMode, 0x0000000000000001); - pub const write = @as(MapMode, 0x0000000000000002); -}; - -pub const MapAsyncStatus = enum(u32) { - success = 0x00000001, - callback_cancelled = 0x00000002, - @"error" = 0x00000003, - aborted = 0x00000004, -}; - -pub const BufferMapCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, - - callback: BufferMapCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; - -pub const BufferMapCallback = *const fn (status: MapAsyncStatus, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void; - pub const BufferDescriptor = extern struct { next_in_chain: ?*const ChainedStruct = null, label: StringView = StringView{}, @@ -85,6 +54,89 @@ pub const BufferDescriptor = extern struct { }; pub const Buffer = opaque { + pub const MapState = enum(u32) { + unmapped = 0x00000001, + pending = 0x00000002, + mapped = 0x00000003, + }; + + pub const MapMode = WGPUFlags; + pub const MapModes = struct { + pub const none = @as(MapMode, 0x0000000000000000); + pub const read = @as(MapMode, 0x0000000000000001); + pub const write = @as(MapMode, 0x0000000000000002); + }; + + pub const MapAsyncStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + @"error" = 0x00000003, + aborted = 0x00000004, + }; + + pub const MapCallback = *const fn ( + status: MapAsyncStatus, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const MapCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + + callback: MapCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const MapResponse = struct { + status: MapAsyncStatus, + message: ?[]const u8, + + pub fn deinit( + self: *MapResponse, + allocator: std.mem.Allocator, + ) void { + if (self.message) |message| allocator.free(message); + self.message = null; + } + }; + + pub const MapSyncError = + std.Io.Cancelable || std.mem.Allocator.Error; + + const MapSyncState = struct { + allocator: std.mem.Allocator, + response: MapResponse = undefined, + message_error: ?std.mem.Allocator.Error = null, + completed: bool = false, + }; + + fn defaultMapCallback( + status: MapAsyncStatus, + message: StringView, + userdata1: ?*anyopaque, + _: ?*anyopaque, + ) callconv(.c) void { + const state: *MapSyncState = @ptrCast(@alignCast(userdata1)); + state.response = .{ + .status = status, + .message = null, + }; + state.response.message = _async.copyCallbackMessage( + state.allocator, + message, + ) catch |err| { + state.message_error = err; + state.completed = true; + return; + }; + state.completed = true; + } + pub inline fn destroy(self: *Buffer) void { raw.call(void, "wgpuBufferDestroy", .{self}); } @@ -95,7 +147,7 @@ pub const Buffer = opaque { // size // Byte size of the range to get. The returned pointer is valid for exactly this many bytes. // - // Returns a const pointer to beginning of the mapped range. + // Returns a const byte slice covering the mapped range. // It must not be written; writing to this range causes undefined behavior. // Returns `NULL` with ImplementationDefinedLogging if: // @@ -104,13 +156,23 @@ pub const Buffer = opaque { // (JS does not allow this because const ranges do not exist.) // // wgpu-native translates a size of WGPU_WHOLE_MAP_SIZE to "None" internally - pub inline fn getConstMappedRange(self: *Buffer, offset: usize, size: usize) ?*const anyopaque { - return raw.call(?*const anyopaque, "wgpuBufferGetConstMappedRange", .{ self, offset, size }); + pub inline fn getConstMappedRange( + self: *Buffer, + offset: usize, + size: usize, + ) ?[]const u8 { + const length = self.mappedRangeLength(offset, size) orelse return null; + const data = raw.call( + ?*const anyopaque, + "wgpuBufferGetConstMappedRange", + .{ self, offset, size }, + ) orelse return null; + return @as([*]const u8, @ptrCast(data))[0..length]; } // Unimplemented as of wgpu-native v29.0.0.0, // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs - // pub inline fn getMapState(self: *Buffer) BufferMapState { + // pub inline fn getMapState(self: *Buffer) MapState { // return wgpuBufferGetMapState(self); // } @@ -120,15 +182,37 @@ pub const Buffer = opaque { // size // Byte size of the range to get. The returned pointer is valid for exactly this many bytes. // - // Returns a mutable pointer to beginning of the mapped range. + // Returns a mutable byte slice covering the mapped range. // Returns `NULL` with ImplementationDefinedLogging if: // // - There is any content-timeline error as defined in the WebGPU specification for `getMappedRange()` (alignments, overlaps, etc.) // - The buffer is not mapped with MapMode.write. // // wgpu-native translates a size of WGPU_WHOLE_MAP_SIZE to "None" internally - pub inline fn getMappedRange(self: *Buffer, offset: usize, size: usize) ?*anyopaque { - return raw.call(?*anyopaque, "wgpuBufferGetMappedRange", .{ self, offset, size }); + pub inline fn getMappedRange( + self: *Buffer, + offset: usize, + size: usize, + ) ?[]u8 { + const length = self.mappedRangeLength(offset, size) orelse return null; + const data = raw.call( + ?*anyopaque, + "wgpuBufferGetMappedRange", + .{ self, offset, size }, + ) orelse return null; + return @as([*]u8, @ptrCast(data))[0..length]; + } + + fn mappedRangeLength( + self: *Buffer, + offset: usize, + size: usize, + ) ?usize { + if (size != WGPU_WHOLE_MAP_SIZE) return size; + const buffer_size = std.math.cast(usize, self.getSize()) orelse + return null; + if (offset > buffer_size) return null; + return buffer_size - offset; } pub inline fn getSize(self: *Buffer) u64 { @@ -138,10 +222,49 @@ pub const Buffer = opaque { return raw.call(BufferUsage, "wgpuBufferGetUsage", .{self}); } - pub inline fn mapAsync(self: *Buffer, mode: MapMode, offset: usize, size: usize, callback_info: BufferMapCallbackInfo) Future { + pub inline fn mapAsync(self: *Buffer, mode: MapMode, offset: usize, size: usize, callback_info: MapCallbackInfo) Future { return raw.call(Future, "wgpuBufferMapAsync", .{ self, mode, offset, size, callback_info }); } + /// Maps a buffer while safely driving an allow_process_events callback. + /// The returned response owns its copied message until deinit() is called. + pub fn mapSync( + self: *Buffer, + allocator: std.mem.Allocator, + io: std.Io, + event_source: anytype, + mode: MapMode, + offset: usize, + size: usize, + polling_interval_nanoseconds: u64, + ) MapSyncError!MapResponse { + var state = MapSyncState{ .allocator = allocator }; + _ = self.mapAsync(mode, offset, size, .{ + .callback = defaultMapCallback, + .userdata1 = @ptrCast(&state), + }); + + var wait_error: ?std.Io.Cancelable = null; + _async.waitForCallback( + event_source, + &state.completed, + io, + polling_interval_nanoseconds, + ) catch |err| { + wait_error = err; + }; + + if (state.message_error) |err| { + state.response.deinit(allocator); + return err; + } + if (wait_error) |err| { + state.response.deinit(allocator); + return err; + } + return state.response; + } + // Unimplemented as of wgpu-native v29.0.0.0, // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs // pub inline fn setLabel(self: *Buffer, label: []const u8) void { @@ -158,3 +281,19 @@ pub const Buffer = opaque { raw.call(void, "wgpuBufferRelease", .{self}); } }; + +test "synchronous map callback copies its message" { + var callback_message = [_]u8{ 'o', 'l', 'd' }; + var state = Buffer.MapSyncState{ .allocator = std.testing.allocator }; + Buffer.defaultMapCallback( + .@"error", + StringView.fromSlice(&callback_message), + @ptrCast(&state), + null, + ); + defer state.response.deinit(std.testing.allocator); + + callback_message[0] = 'n'; + try std.testing.expect(state.completed); + try std.testing.expectEqualStrings("old", state.response.message.?); +} diff --git a/src/command_encoder.zig b/src/command_encoder.zig index 9997622..af1dad9 100644 --- a/src/command_encoder.zig +++ b/src/command_encoder.zig @@ -8,10 +8,13 @@ const SType = _chained_struct.SType; const Buffer = @import("buffer.zig").Buffer; const QuerySet = @import("query_set.zig").QuerySet; +const _copy = @import("copy.zig"); +const TexelCopyBufferInfo = _copy.TexelCopyBufferInfo; +const TexelCopyTextureInfo = _copy.TexelCopyTextureInfo; + const _texture = @import("texture.zig"); +const TextureFormat = _texture.TextureFormat; const TextureView = _texture.TextureView; -const TexelCopyBufferInfo = _texture.TexelCopyBufferInfo; -const TexelCopyTextureInfo = _texture.TexelCopyTextureInfo; const Extent3D = _texture.Extent3D; const _misc = @import("misc.zig"); @@ -26,11 +29,111 @@ const _pipeline = @import("pipeline.zig"); const ComputePipeline = _pipeline.ComputePipeline; const RenderPipeline = _pipeline.RenderPipeline; -const RenderBundle = @import("render_bundle.zig").RenderBundle; +const _render_bundle = @import("render_bundle.zig"); +const RenderBundleDescriptor = _render_bundle.RenderBundleDescriptor; +const RenderBundle = _render_bundle.RenderBundle; pub const WGPU_DEPTH_SLICE_UNDEFINED = U32_MAX; pub const WGPU_QUERY_SET_INDEX_UNDEFINED = U32_MAX; +pub const RenderBundleEncoderDescriptor = extern struct { + next_in_chain: ?*const ChainedStruct = null, + label: StringView = .{}, + color_format_count: usize, + color_formats: [*]const TextureFormat, + depth_stencil_format: TextureFormat = .undefined, + sample_count: u32 = 1, + depth_read_only: WGPUBool = @intFromBool(false), + stencil_read_only: WGPUBool = @intFromBool(false), + + /// Initializes a descriptor that borrows `color_formats`. + pub inline fn init( + color_formats: []const TextureFormat, + ) RenderBundleEncoderDescriptor { + return .{ + .color_format_count = color_formats.len, + .color_formats = color_formats.ptr, + }; + } +}; + +pub const RenderBundleEncoder = opaque { + pub inline fn draw(self: *RenderBundleEncoder, vertex_count: u32, instance_count: u32, first_vertex: u32, first_instance: u32) void { + raw.call(void, "wgpuRenderBundleEncoderDraw", .{ self, vertex_count, instance_count, first_vertex, first_instance }); + } + pub inline fn drawIndexed(self: *RenderBundleEncoder, index_count: u32, instance_count: u32, first_index: u32, base_vertex: i32, first_instance: u32) void { + raw.call(void, "wgpuRenderBundleEncoderDrawIndexed", .{ self, index_count, instance_count, first_index, base_vertex, first_instance }); + } + pub inline fn drawIndexedIndirect(self: *RenderBundleEncoder, indirect_buffer: *Buffer, indirect_offset: u64) void { + raw.call(void, "wgpuRenderBundleEncoderDrawIndexedIndirect", .{ self, indirect_buffer, indirect_offset }); + } + pub inline fn drawIndirect(self: *RenderBundleEncoder, indirect_buffer: *Buffer, indirect_offset: u64) void { + raw.call(void, "wgpuRenderBundleEncoderDrawIndirect", .{ self, indirect_buffer, indirect_offset }); + } + pub inline fn finish(self: *RenderBundleEncoder, descriptor: ?*const RenderBundleDescriptor) ?*RenderBundle { + return raw.call(?*RenderBundle, "wgpuRenderBundleEncoderFinish", .{ self, descriptor }); + } + pub inline fn insertDebugMarker(self: *RenderBundleEncoder, marker_label: []const u8) void { + raw.call(void, "wgpuRenderBundleEncoderInsertDebugMarker", .{ self, StringView.fromSlice(marker_label) }); + } + pub inline fn popDebugGroup(self: *RenderBundleEncoder) void { + raw.call(void, "wgpuRenderBundleEncoderPopDebugGroup", .{self}); + } + pub inline fn pushDebugGroup(self: *RenderBundleEncoder, group_label: []const u8) void { + raw.call(void, "wgpuRenderBundleEncoderPushDebugGroup", .{ self, StringView.fromSlice(group_label) }); + } + pub inline fn setBindGroup( + self: *RenderBundleEncoder, + group_index: u32, + group: ?*BindGroup, + dynamic_offsets: []const u32, + ) void { + raw.call(void, "wgpuRenderBundleEncoderSetBindGroup", .{ + self, + group_index, + group, + dynamic_offsets.len, + dynamic_offsets.ptr, + }); + } + pub inline fn setIndexBuffer(self: *RenderBundleEncoder, buffer: *Buffer, format: IndexFormat, offset: u64, size: u64) void { + raw.call(void, "wgpuRenderBundleEncoderSetIndexBuffer", .{ self, buffer, format, offset, size }); + } + + // Unimplemented as of wgpu-native v29.0.0.0, + // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs + // pub inline fn setLabel(self: *RenderBundleEncoder, label: []const u8) void { + // wgpuRenderBundleEncoderSetLabel(self, StringView.fromSlice(label)); + // } + + pub inline fn setPipeline(self: *RenderBundleEncoder, pipeline: *RenderPipeline) void { + raw.call(void, "wgpuRenderBundleEncoderSetPipeline", .{ self, pipeline }); + } + pub inline fn setVertexBuffer(self: *RenderBundleEncoder, slot: u32, buffer: ?*Buffer, offset: u64, size: u64) void { + raw.call(void, "wgpuRenderBundleEncoderSetVertexBuffer", .{ self, slot, buffer, offset, size }); + } + pub inline fn addRef(self: *RenderBundleEncoder) void { + raw.call(void, "wgpuRenderBundleEncoderAddRef", .{self}); + } + pub inline fn release(self: *RenderBundleEncoder) void { + raw.call(void, "wgpuRenderBundleEncoderRelease", .{self}); + } + + // wgpu-native + pub inline fn setImmediates( + self: *RenderBundleEncoder, + offset: u32, + data: []const u8, + ) void { + raw.call(void, "wgpuRenderBundleEncoderSetImmediates", .{ + self, + offset, + @as(u32, @intCast(data.len)), + data.ptr, + }); + } +}; + pub const PassTimestampWrites = extern struct { next_in_chain: ?*const ChainedStruct = null, query_set: *QuerySet, @@ -70,8 +173,19 @@ pub const ComputePassEncoder = opaque { pub inline fn pushDebugGroup(self: *ComputePassEncoder, group_label: []const u8) void { raw.call(void, "wgpuComputePassEncoderPushDebugGroup", .{ self, StringView.fromSlice(group_label) }); } - pub inline fn setBindGroup(self: *ComputePassEncoder, group_index: u32, group: *BindGroup, dynamic_offset_count: usize, dynamic_offsets: ?[*]const u32) void { - raw.call(void, "wgpuComputePassEncoderSetBindGroup", .{ self, group_index, group, dynamic_offset_count, dynamic_offsets }); + pub inline fn setBindGroup( + self: *ComputePassEncoder, + group_index: u32, + group: ?*BindGroup, + dynamic_offsets: []const u32, + ) void { + raw.call(void, "wgpuComputePassEncoderSetBindGroup", .{ + self, + group_index, + group, + dynamic_offsets.len, + dynamic_offsets.ptr, + }); } // Unimplemented as of wgpu-native v29.0.0.0, @@ -91,8 +205,17 @@ pub const ComputePassEncoder = opaque { } // wgpu-native - pub inline fn setImmediates(self: *ComputePassEncoder, offset: u32, size_bytes: u32, data: *const anyopaque) void { - raw.call(void, "wgpuComputePassEncoderSetImmediates", .{ self, offset, size_bytes, data }); + pub inline fn setImmediates( + self: *ComputePassEncoder, + offset: u32, + data: []const u8, + ) void { + raw.call(void, "wgpuComputePassEncoderSetImmediates", .{ + self, + offset, + @as(u32, @intCast(data.len)), + data.ptr, + }); } pub inline fn beginPipelineStatisticsQuery(self: *ComputePassEncoder, query_set: *QuerySet, query_index: u32) void { raw.call(void, "wgpuComputePassEncoderBeginPipelineStatisticsQuery", .{ self, query_set, query_index }); @@ -161,12 +284,19 @@ pub const RenderPassDescriptor = extern struct { occlusion_query_set: ?*QuerySet = null, timestamp_writes: ?*const PassTimestampWrites = null, - pub inline fn withMaxDrawCount(self: RenderPassDescriptor, max_draw_count: u64) RenderPassDescriptor { - var descriptor = self; - descriptor.next_in_chain = @ptrCast(&RenderPassMaxDrawCount{ - .max_draw_count = max_draw_count, - }); + /// Initializes a descriptor that borrows `color_attachments`. + pub inline fn init( + color_attachments: []const ColorAttachment, + ) RenderPassDescriptor { + return .{ + .color_attachment_count = color_attachments.len, + .color_attachments = color_attachments.ptr, + }; + } + pub inline fn withExtras(self: RenderPassDescriptor, extras: *const RenderPassMaxDrawCount) RenderPassDescriptor { + var descriptor = self; + descriptor.next_in_chain = @ptrCast(extras); return descriptor; } }; @@ -207,8 +337,19 @@ pub const RenderPassEncoder = opaque { pub inline fn pushDebugGroup(self: *RenderPassEncoder, group_label: []const u8) void { raw.call(void, "wgpuRenderPassEncoderPushDebugGroup", .{ self, StringView.fromSlice(group_label) }); } - pub inline fn setBindGroup(self: *RenderPassEncoder, group_index: u32, group: *BindGroup, dynamic_offset_count: usize, dynamic_offsets: ?[*]const u32) void { - raw.call(void, "wgpuRenderPassEncoderSetBindGroup", .{ self, group_index, group, dynamic_offset_count, dynamic_offsets }); + pub inline fn setBindGroup( + self: *RenderPassEncoder, + group_index: u32, + group: ?*BindGroup, + dynamic_offsets: []const u32, + ) void { + raw.call(void, "wgpuRenderPassEncoderSetBindGroup", .{ + self, + group_index, + group, + dynamic_offsets.len, + dynamic_offsets.ptr, + }); } pub inline fn setBlendConstant(self: *RenderPassEncoder, color: *const Color) void { raw.call(void, "wgpuRenderPassEncoderSetBlendConstant", .{ self, color }); @@ -232,7 +373,7 @@ pub const RenderPassEncoder = opaque { pub inline fn setStencilReference(self: *RenderPassEncoder, stencil_reference: u32) void { raw.call(void, "wgpuRenderPassEncoderSetStencilReference", .{ self, stencil_reference }); } - pub inline fn setVertexBuffer(self: *RenderPassEncoder, slot: u32, buffer: *Buffer, offset: u64, size: u64) void { + pub inline fn setVertexBuffer(self: *RenderPassEncoder, slot: u32, buffer: ?*Buffer, offset: u64, size: u64) void { raw.call(void, "wgpuRenderPassEncoderSetVertexBuffer", .{ self, slot, buffer, offset, size }); } pub inline fn setViewport(self: *RenderPassEncoder, x: f32, y: f32, width: f32, height: f32, min_depth: f32, max_depth: f32) void { @@ -246,8 +387,17 @@ pub const RenderPassEncoder = opaque { } // wgpu-native - pub inline fn setImmediates(self: *RenderPassEncoder, offset: u32, size_bytes: u32, data: *const anyopaque) void { - raw.call(void, "wgpuRenderPassEncoderSetImmediates", .{ self, offset, size_bytes, data }); + pub inline fn setImmediates( + self: *RenderPassEncoder, + offset: u32, + data: []const u8, + ) void { + raw.call(void, "wgpuRenderPassEncoderSetImmediates", .{ + self, + offset, + @as(u32, @intCast(data.len)), + data.ptr, + }); } pub inline fn multiDrawIndirect(self: *RenderPassEncoder, buffer: *Buffer, offset: u64, count: u32) void { raw.call(void, "wgpuRenderPassEncoderMultiDrawIndirect", .{ self, buffer, offset, count }); diff --git a/src/copy.zig b/src/copy.zig new file mode 100644 index 0000000..6dedf60 --- /dev/null +++ b/src/copy.zig @@ -0,0 +1,32 @@ +const Buffer = @import("buffer.zig").Buffer; +const U32_MAX = @import("misc.zig").U32_MAX; + +const _texture = @import("texture.zig"); +const Texture = _texture.Texture; +const TextureAspect = _texture.TextureAspect; + +pub const WGPU_COPY_STRIDE_UNDEFINED = U32_MAX; + +pub const Origin3D = extern struct { + x: u32 = 0, + y: u32 = 0, + z: u32 = 0, +}; + +pub const TexelCopyTextureInfo = extern struct { + texture: *Texture, + mip_level: u32 = 0, + origin: Origin3D, + aspect: TextureAspect = .all, +}; + +pub const TexelCopyBufferLayout = extern struct { + offset: u64 = 0, + bytes_per_row: u32 = WGPU_COPY_STRIDE_UNDEFINED, + rows_per_image: u32 = WGPU_COPY_STRIDE_UNDEFINED, +}; + +pub const TexelCopyBufferInfo = extern struct { + layout: TexelCopyBufferLayout, + buffer: *Buffer, +}; diff --git a/src/device.zig b/src/device.zig index c9ab8ff..1e7b4a3 100644 --- a/src/device.zig +++ b/src/device.zig @@ -7,10 +7,12 @@ const SType = _chained_struct.SType; const _misc = @import("misc.zig"); const WGPUBool = _misc.WGPUBool; -const FeatureName = _misc.FeatureName; const StringView = _misc.StringView; const Status = _misc.Status; -const SupportedFeatures = _misc.SupportedFeatures; + +const _feature = @import("feature.zig"); +const FeatureName = _feature.FeatureName; +const SupportedFeatures = _feature.SupportedFeatures; const _async = @import("async.zig"); const CallbackMode = _async.CallbackMode; @@ -43,20 +45,17 @@ const CommandEncoder = _command_encoder.CommandEncoder; const _pipeline = @import("pipeline.zig"); const ComputePipelineDescriptor = _pipeline.ComputePipelineDescriptor; const ComputePipeline = _pipeline.ComputePipeline; -const CreateComputePipelineAsyncCallbackInfo = _pipeline.CreateComputePipelineAsyncCallbackInfo; const PipelineLayoutDescriptor = _pipeline.PipelineLayoutDescriptor; const PipelineLayout = _pipeline.PipelineLayout; const RenderPipelineDescriptor = _pipeline.RenderPipelineDescriptor; const RenderPipeline = _pipeline.RenderPipeline; -const CreateRenderPipelineAsyncCallbackInfo = _pipeline.CreateRenderPipelineAsyncCallbackInfo; const _query_set = @import("query_set.zig"); const QuerySetDescriptor = _query_set.QuerySetDescriptor; const QuerySet = _query_set.QuerySet; -const _render_bundle = @import("render_bundle.zig"); -const RenderBundleEncoderDescriptor = _render_bundle.RenderBundleEncoderDescriptor; -const RenderBundleEncoder = _render_bundle.RenderBundleEncoder; +const RenderBundleEncoderDescriptor = _command_encoder.RenderBundleEncoderDescriptor; +const RenderBundleEncoder = _command_encoder.RenderBundleEncoder; const _sampler = @import("sampler.zig"); const SamplerDescriptor = _sampler.SamplerDescriptor; @@ -71,69 +70,14 @@ const _texture = @import("texture.zig"); const TextureDescriptor = _texture.TextureDescriptor; const Texture = _texture.Texture; -pub const DeviceLostReason = enum(u32) { - unknown = 0x00000001, - destroyed = 0x00000002, - callback_cancelled = 0x00000003, - failed_creation = 0x00000004, -}; - -pub const DeviceLostCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // Apparently in the webgpu header this has no (valid) default: https://github.com/webgpu-native/webgpu-headers/pull/471 - // As of wgpu-native v24.0.3.1, Instance.waitAny() has not been implemented, but Instance.processEvents() has, - // so the safest mode to use currently is probably CallbackMode.allow_process_events. - // If you really know what you're doing, CallbackMode.allow_spontaneous could also work as an option here. - // TODO: Revisit this if/when Instance.waitAny() is implemented in wgpu-native - mode: CallbackMode = CallbackMode.allow_process_events, - callback: DeviceLostCallback = defaultDeviceLostCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; - -// `device` is a reference to the device which was lost. If, and only if, the `reason` is DeviceLostReason.failed_creation, `device` is a non-null pointer to a null Device. -pub const DeviceLostCallback = *const fn (device: *const ?*Device, reason: DeviceLostReason, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void; -pub fn defaultDeviceLostCallback(device: *const ?*Device, reason: DeviceLostReason, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void { - _ = device; - _ = userdata1; - _ = userdata2; - - // Without a device you can't really do much of anything, so do a panic here by default. - // For better error handling, implement DeviceLostCallback with your own error handling logic. - // Remember you can pass pointers in through the userdata fields of the DeviceLostCallbackInfo struct; - // you could pass in a simple pointer to a bool or something more complex like a struct. - std.debug.panic("Device lost: reason={s} message=\"{s}\"\n", .{ @tagName(reason), message.toSlice() orelse "" }); -} +/// Borrowed backend-native `id` returned by wgpu-native. +pub const NativeMetalDevice = opaque {}; pub const DeviceExtras = extern struct { chain: ChainedStruct = ChainedStruct{ .s_type = SType.device_extras, }, - trace_path: StringView, -}; - -pub const ErrorType = enum(u32) { - no_error = 0x00000001, - validation = 0x00000002, - out_of_memory = 0x00000003, - internal = 0x00000004, - unknown = 0x00000005, -}; - -pub const UncapturedErrorCallback = *const fn (device: ?*Device, error_type: ErrorType, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void; - -pub const ErrorFilter = enum(u32) { - validation = 0x00000001, - out_of_memory = 0x00000002, - internal = 0x00000003, -}; - -pub const UncapturedErrorCallbackInfo = extern struct { - next_in_chain: ?*const ChainedStruct = null, - callback: ?UncapturedErrorCallback = null, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, + trace_path: StringView = .{}, }; pub const DeviceDescriptor = extern struct { @@ -141,85 +85,223 @@ pub const DeviceDescriptor = extern struct { label: StringView = StringView{}, required_feature_count: usize = 0, required_features: [*]const FeatureName = &[0]FeatureName{}, - required_limits: ?*const Limits, + required_limits: ?*const Limits = null, default_queue: QueueDescriptor = QueueDescriptor{}, - device_lost_callback_info: DeviceLostCallbackInfo = DeviceLostCallbackInfo{}, - uncaptured_error_callback_info: UncapturedErrorCallbackInfo = UncapturedErrorCallbackInfo{}, - - pub inline fn withTracePath(self: DeviceDescriptor, trace_path: []const u8) DeviceDescriptor { - var dd = self; - dd.next_in_chain = @ptrCast(&DeviceExtras{ - .trace_path = StringView.fromSlice(trace_path), - }); - return dd; + device_lost_callback_info: Device.DeviceLostCallbackInfo = .{}, + uncaptured_error_callback_info: Device.UncapturedErrorCallbackInfo = .{}, + + /// Returns a descriptor that borrows `features` until the native call returns. + pub inline fn withRequiredFeatures( + self: DeviceDescriptor, + features: []const FeatureName, + ) DeviceDescriptor { + var descriptor = self; + descriptor.required_feature_count = features.len; + descriptor.required_features = features.ptr; + return descriptor; } -}; -pub const RequestDeviceStatus = enum(u32) { - success = 0x00000001, - callback_cancelled = 0x00000002, - @"error" = 0x00000003, -}; - -// TODO: This probably belongs in adapter.zig -pub const RequestDeviceCallback = *const fn (status: RequestDeviceStatus, device: ?*Device, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void; - -pub const RequestDeviceResponse = struct { - status: RequestDeviceStatus, - message: ?[]const u8, - device: ?*Device, + pub inline fn withExtras(self: DeviceDescriptor, extras: *const DeviceExtras) DeviceDescriptor { + var descriptor = self; + descriptor.next_in_chain = @ptrCast(extras); + return descriptor; + } }; -pub const RequestDeviceCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, - - callback: RequestDeviceCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; +// wgpu-native -pub const PopErrorScopeStatus = enum(u32) { - success = 0x00000001, // The error scope stack was successfully popped and a result was reported. - callback_cancelled = 0x00000002, - @"error" = 0x00000003, // The error scope stack could not be popped, because it was empty. -}; +pub const Device = opaque { + pub const DeviceLostReason = enum(u32) { + unknown = 0x00000001, + destroyed = 0x00000002, + callback_cancelled = 0x00000003, + failed_creation = 0x00000004, + }; + + // `device` is a reference to the device which was lost. If, and only if, + // `reason` is `failed_creation`, it points to a null Device handle. + pub const DeviceLostCallback = *const fn ( + device: *const ?*Device, + reason: DeviceLostReason, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const DeviceLostCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + callback: DeviceLostCallback = defaultDeviceLostCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub fn defaultDeviceLostCallback( + device: *const ?*Device, + reason: DeviceLostReason, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void { + _ = device; + _ = userdata1; + _ = userdata2; + std.debug.panic( + "Device lost: reason={s} message=\"{s}\"\n", + .{ @tagName(reason), message.toSlice() orelse "" }, + ); + } -// status -// See PopErrorScopeStatus. -// -// error_type -// The type of the error caught by the scope, or ErrorType.no_error if there was none. -// If the `status` is not PopErrorScopeStatus.success, this is ErrorType.no_error. -// -// message -// If the `type` is not ErrorType.no_error, this is a non-empty string; -// otherwise, this is an empty string. -// -pub const PopErrorScopeCallback = *const fn ( - status: PopErrorScopeStatus, - error_type: ErrorType, - message: StringView, - userdata1: ?*anyopaque, - userdata2: ?*anyopaque, -) callconv(.c) void; - -pub const PopErrorScopeCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, - - callback: PopErrorScopeCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; + pub const ErrorType = enum(u32) { + no_error = 0x00000001, + validation = 0x00000002, + out_of_memory = 0x00000003, + internal = 0x00000004, + unknown = 0x00000005, + }; + + pub const UncapturedErrorCallback = *const fn ( + device: ?*Device, + error_type: ErrorType, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const ErrorFilter = enum(u32) { + validation = 0x00000001, + out_of_memory = 0x00000002, + internal = 0x00000003, + }; + + pub const UncapturedErrorCallbackInfo = extern struct { + next_in_chain: ?*const ChainedStruct = null, + callback: ?UncapturedErrorCallback = null, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const PopErrorScopeStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + @"error" = 0x00000003, + }; + + pub const PopErrorScopeCallback = *const fn ( + status: PopErrorScopeStatus, + error_type: ErrorType, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const PopErrorScopeCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + + callback: PopErrorScopeCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const PopErrorScopeResponse = struct { + status: PopErrorScopeStatus, + error_type: ErrorType, + message: ?[]const u8, + + pub fn deinit( + self: *PopErrorScopeResponse, + allocator: std.mem.Allocator, + ) void { + if (self.message) |message| allocator.free(message); + self.message = null; + } + }; + + pub const PopErrorScopeSyncError = + std.Io.Cancelable || std.mem.Allocator.Error; + + const PopErrorScopeSyncState = struct { + allocator: std.mem.Allocator, + response: PopErrorScopeResponse = undefined, + message_error: ?std.mem.Allocator.Error = null, + completed: bool = false, + }; + + fn defaultPopErrorScopeCallback( + status: PopErrorScopeStatus, + error_type: ErrorType, + message: StringView, + userdata1: ?*anyopaque, + _: ?*anyopaque, + ) callconv(.c) void { + const state: *PopErrorScopeSyncState = + @ptrCast(@alignCast(userdata1)); + state.response = .{ + .status = status, + .error_type = error_type, + .message = null, + }; + state.response.message = _async.copyCallbackMessage( + state.allocator, + message, + ) catch |err| { + state.message_error = err; + state.completed = true; + return; + }; + state.completed = true; + } -// wgpu-native + pub const CreatePipelineAsyncStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + validation_error = 0x00000003, + internal_error = 0x00000004, + }; + + pub const CreateComputePipelineAsyncCallback = *const fn ( + status: CreatePipelineAsyncStatus, + pipeline: ?*ComputePipeline, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const CreateComputePipelineAsyncCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + + callback: CreateComputePipelineAsyncCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const CreateRenderPipelineAsyncCallback = *const fn ( + status: CreatePipelineAsyncStatus, + pipeline: ?*RenderPipeline, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const CreateRenderPipelineAsyncCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + + callback: CreateRenderPipelineAsyncCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; -pub const Device = opaque { pub inline fn createBindGroup(self: *Device, descriptor: *const BindGroupDescriptor) ?*BindGroup { return raw.call(?*BindGroup, "wgpuDeviceCreateBindGroup", .{ self, descriptor }); } @@ -229,7 +311,7 @@ pub const Device = opaque { pub inline fn createBuffer(self: *Device, descriptor: *const BufferDescriptor) ?*Buffer { return raw.call(?*Buffer, "wgpuDeviceCreateBuffer", .{ self, descriptor }); } - pub inline fn createCommandEncoder(self: *Device, descriptor: *const CommandEncoderDescriptor) ?*CommandEncoder { + pub inline fn createCommandEncoder(self: *Device, descriptor: ?*const CommandEncoderDescriptor) ?*CommandEncoder { return raw.call(?*CommandEncoder, "wgpuDeviceCreateCommandEncoder", .{ self, descriptor }); } pub inline fn createComputePipeline(self: *Device, descriptor: *const ComputePipelineDescriptor) ?*ComputePipeline { @@ -261,7 +343,7 @@ pub const Device = opaque { // return wgpuDeviceCreateRenderPipelineAsync(self, descriptor, callback_info); // } - pub inline fn createSampler(self: *Device, descriptor: *const SamplerDescriptor) ?*Sampler { + pub inline fn createSampler(self: *Device, descriptor: ?*const SamplerDescriptor) ?*Sampler { return raw.call(?*Sampler, "wgpuDeviceCreateSampler", .{ self, descriptor }); } pub inline fn createShaderModule(self: *Device, descriptor: *const ShaderModuleDescriptor) ?*ShaderModule { @@ -297,13 +379,50 @@ pub const Device = opaque { pub inline fn getQueue(self: *Device) ?*Queue { return raw.call(?*Queue, "wgpuDeviceGetQueue", .{self}); } - pub inline fn hasFeature(self: *Device, feature: FeatureName) WGPUBool { - return raw.call(WGPUBool, "wgpuDeviceHasFeature", .{ self, feature }); + pub inline fn hasFeature(self: *Device, feature: FeatureName) bool { + return raw.call(WGPUBool, "wgpuDeviceHasFeature", .{ self, feature }) != 0; } pub inline fn popErrorScope(self: *Device, callback_info: PopErrorScopeCallbackInfo) Future { return raw.call(Future, "wgpuDevicePopErrorScope", .{ self, callback_info }); } + + /// Pops an error scope while safely driving an allow_process_events + /// callback. The response owns its copied message until deinit(). + pub fn popErrorScopeSync( + self: *Device, + allocator: std.mem.Allocator, + io: std.Io, + event_source: anytype, + polling_interval_nanoseconds: u64, + ) PopErrorScopeSyncError!PopErrorScopeResponse { + var state = PopErrorScopeSyncState{ .allocator = allocator }; + _ = self.popErrorScope(.{ + .callback = defaultPopErrorScopeCallback, + .userdata1 = @ptrCast(&state), + }); + + var wait_error: ?std.Io.Cancelable = null; + _async.waitForCallback( + event_source, + &state.completed, + io, + polling_interval_nanoseconds, + ) catch |err| { + wait_error = err; + }; + + if (state.message_error) |err| { + state.response.deinit(allocator); + return err; + } + if (wait_error) |err| { + state.response.deinit(allocator); + return err; + } + return state.response; + } + pub inline fn pushErrorScope(self: *Device, filter: ErrorFilter) void { raw.call(void, "wgpuDevicePushErrorScope", .{ self, filter }); } @@ -328,6 +447,38 @@ pub const Device = opaque { pub inline fn createShaderModuleSpirV(self: *Device, descriptor: *const ShaderModuleDescriptorSpirV) ?*ShaderModule { return raw.call(?*ShaderModule, "wgpuDeviceCreateShaderModuleSpirV", .{ self, descriptor }); } + + /// Returns a borrowed Metal device when this device uses the Metal backend. + /// The pointer remains valid only while `self` is alive and must not be released. + pub inline fn getNativeMetalDevice(self: *Device) ?*NativeMetalDevice { + return raw.call(?*NativeMetalDevice, "wgpuDeviceGetNativeMetalDevice", .{self}); + } + + /// Starts a platform graphics-debugger capture when supported. + pub inline fn startGraphicsDebuggerCapture(self: *Device) bool { + return raw.call(WGPUBool, "wgpuDeviceStartGraphicsDebuggerCapture", .{self}) != 0; + } + + pub inline fn stopGraphicsDebuggerCapture(self: *Device) void { + raw.call(void, "wgpuDeviceStopGraphicsDebuggerCapture", .{self}); + } }; -// TODO: Test methods of Device (as long as they can be tested headlessly: see https://eliemichel.github.io/LearnWebGPU/advanced-techniques/headless.html) +test "synchronous error-scope callback copies its message" { + var callback_message = [_]u8{ 'o', 'l', 'd' }; + var state = Device.PopErrorScopeSyncState{ + .allocator = std.testing.allocator, + }; + Device.defaultPopErrorScopeCallback( + .@"error", + .validation, + StringView.fromSlice(&callback_message), + @ptrCast(&state), + null, + ); + defer state.response.deinit(std.testing.allocator); + + callback_message[0] = 'n'; + try std.testing.expect(state.completed); + try std.testing.expectEqualStrings("old", state.response.message.?); +} diff --git a/src/feature.zig b/src/feature.zig new file mode 100644 index 0000000..3d7fd9d --- /dev/null +++ b/src/feature.zig @@ -0,0 +1,78 @@ +const raw = @import("raw.zig"); +const sliceFromOptional = @import("misc.zig").sliceFromOptional; + +pub const FeatureName = enum(u32) { + core_features_and_limits = 0x00000001, + depth_clip_control = 0x00000002, + depth32_float_stencil8 = 0x00000003, + texture_compression_bc = 0x00000004, + texture_compression_bc_sliced_3d = 0x00000005, + texture_compression_etc2 = 0x00000006, + texture_compression_astc = 0x00000007, + texture_compression_astc_sliced_3d = 0x00000008, + timestamp_query = 0x00000009, + indirect_first_instance = 0x0000000A, + shader_f16 = 0x0000000B, + rg11b10_ufloat_renderable = 0x0000000C, + bgra8_unorm_storage = 0x0000000D, + float32_filterable = 0x0000000E, + float32_blendable = 0x0000000F, + clip_distances = 0x00000010, + dual_source_blending = 0x00000011, + subgroups = 0x00000012, + texture_formats_tier_1 = 0x00000013, + texture_formats_tier_2 = 0x00000014, + primitive_index = 0x00000015, + texture_component_swizzle = 0x00000016, + + // wgpu-native extras + immediates = 0x00030001, + texture_adapter_specific_format_features = 0x00030002, + multi_draw_indirect_count = 0x00030004, + vertex_writable_storage = 0x00030005, + texture_binding_array = 0x00030006, + sampled_texture_and_storage_buffer_array_non_uniform_indexing = 0x00030007, + pipeline_statistics_query = 0x00030008, + storage_resource_binding_array = 0x00030009, + partially_bound_binding_array = 0x0003000A, + texture_format_16bit_norm = 0x0003000B, + texture_compression_astc_hdr = 0x0003000C, + mappable_primary_buffers = 0x0003000E, + buffer_binding_array = 0x0003000F, + uniform_buffer_and_storage_texture_array_non_uniform_indexing = 0x00030010, + polygon_mode_line = 0x00030013, + polygon_mode_point = 0x00030014, + conservative_rasterization = 0x00030015, + spirv_shader_passthrough = 0x00030017, + vertex_attribute_64bit = 0x00030019, + texture_format_nv12 = 0x0003001A, + ray_query = 0x0003001C, + shader_f64 = 0x0003001D, + shader_i16 = 0x0003001E, + shader_early_depth_test = 0x00030020, + subgroup = 0x00030021, + subgroup_vertex = 0x00030022, + subgroup_barrier = 0x00030023, + timestamp_query_inside_encoders = 0x00030024, + timestamp_query_inside_passes = 0x00030025, + shader_int64 = 0x00030026, +}; + +pub const SupportedFeatures = extern struct { + feature_count: usize = 0, + features: ?[*]const FeatureName = null, + + pub inline fn slice(self: *const SupportedFeatures) []const FeatureName { + return sliceFromOptional( + FeatureName, + self.features, + self.feature_count, + ); + } + + pub inline fn deinit(self: *SupportedFeatures) void { + raw.call(void, "wgpuSupportedFeaturesFreeMembers", .{self.*}); + self.feature_count = 0; + self.features = null; + } +}; diff --git a/src/global.zig b/src/global.zig deleted file mode 100644 index 2df7d8f..0000000 --- a/src/global.zig +++ /dev/null @@ -1,18 +0,0 @@ -// const StringView = @import("misc.zig").StringView; - -// Generic function return type for wgpuGetProcAddress -// pub const Proc = *const fn() callconv(.c) void; - -// Supposedly getProcAddress is a global function, but it doesn't seem like it should work without being tied to a Device? -// Could be it's one of those functions that's meant to be called with null the first time, TODO: look into that. -// -// Regardless, apparently the reason it exists is because different devices have different drivers and therefore different procs, -// so you need to get the version of the proc that is meant for that particular device. -// -// Although this function appears in webgpu.h, it is currently unimplemented in wgpu-native, -// (https://github.com/gfx-rs/wgpu-native/blob/trunk/src/unimplemented.rs) -// so I'm leaving it here in case it gets implemented eventually, but commented out until/unless that happens. -// extern fn wgpuGetProcAddress(proc_name: StringView) ?Proc; -// pub inline fn getProcAddress(proc_name: StringView) ?Proc { -// return wgpuGetProcAddress(proc_name); -// } diff --git a/src/instance.zig b/src/instance.zig index aec5ad2..10cca89 100644 --- a/src/instance.zig +++ b/src/instance.zig @@ -8,10 +8,6 @@ const SType = _chained_struct.SType; const _adapter = @import("adapter.zig"); const Adapter = _adapter.Adapter; const RequestAdapterOptions = _adapter.RequestAdapterOptions; -const RequestAdapterCallbackInfo = _adapter.RequestAdapterCallbackInfo; -const RequestAdapterCallback = _adapter.RequestAdapterCallback; -const RequestAdapterStatus = _adapter.RequestAdapterStatus; -const RequestAdapterResponse = _adapter.RequestAdapterResponse; const BackendType = _adapter.BackendType; const _surface = @import("surface.zig"); @@ -20,11 +16,12 @@ const SurfaceDescriptor = _surface.SurfaceDescriptor; const _misc = @import("misc.zig"); const WGPUFlags = _misc.WGPUFlags; -const WGPUBool = _misc.WGPUBool; const StringView = _misc.StringView; const Status = _misc.Status; +const sliceFromOptional = _misc.sliceFromOptional; const _async = @import("async.zig"); +const CallbackMode = _async.CallbackMode; const Future = _async.Future; const WaitStatus = _async.WaitStatus; const FutureWaitInfo = _async.FutureWaitInfo; @@ -144,10 +141,16 @@ pub const InstanceFeatureName = enum(u32) { pub const SupportedInstanceFeatures = extern struct { feature_count: usize = 0, - features: [*]const InstanceFeatureName = &[0]InstanceFeatureName{}, - - pub inline fn freeMembers(self: SupportedInstanceFeatures) void { - raw.call(void, "wgpuSupportedInstanceFeaturesFreeMembers", .{self}); + features: ?[*]const InstanceFeatureName = null, + + pub inline fn slice( + self: *const SupportedInstanceFeatures, + ) []const InstanceFeatureName { + return sliceFromOptional( + InstanceFeatureName, + self.features, + self.feature_count, + ); } }; @@ -162,10 +165,21 @@ pub const InstanceDescriptor = extern struct { required_features: [*]const InstanceFeatureName = &[0]InstanceFeatureName{}, required_limits: ?*const InstanceLimits = null, - pub inline fn withNativeExtras(self: InstanceDescriptor, extras: *InstanceExtras) InstanceDescriptor { - var id = self; - id.next_in_chain = @ptrCast(extras); - return id; + /// Returns a descriptor that borrows `features` until the native call returns. + pub inline fn withRequiredFeatures( + self: InstanceDescriptor, + features: []const InstanceFeatureName, + ) InstanceDescriptor { + var descriptor = self; + descriptor.required_feature_count = features.len; + descriptor.required_features = features.ptr; + return descriptor; + } + + pub inline fn withExtras(self: InstanceDescriptor, extras: *const InstanceExtras) InstanceDescriptor { + var descriptor = self; + descriptor.next_in_chain = @ptrCast(extras); + return descriptor; } }; @@ -182,8 +196,18 @@ pub const WGSLLanguageFeatureName = enum(u32) { }; pub const SupportedWGSLLanguageFeatures = extern struct { - feature_count: usize, - features: [*]const WGSLLanguageFeatureName, + feature_count: usize = 0, + features: ?[*]const WGSLLanguageFeatureName = null, + + pub inline fn slice( + self: *const SupportedWGSLLanguageFeatures, + ) []const WGSLLanguageFeatureName { + return sliceFromOptional( + WGSLLanguageFeatureName, + self.features, + self.feature_count, + ); + } // Unimplemented as of wgpu-native v29.0.0.0, // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs @@ -229,26 +253,93 @@ pub const EnumerateAdapterOptions = extern struct { backends: InstanceBackend, }; +pub const AdapterList = struct { + adapters: []?*Adapter = &.{}, + + pub fn deinit( + self: *AdapterList, + allocator: std.mem.Allocator, + ) void { + for (self.adapters) |adapter| { + if (adapter) |value| value.release(); + } + if (self.adapters.len != 0) allocator.free(self.adapters); + self.adapters = &.{}; + } + + pub fn takeAdapter(self: *AdapterList, index: usize) ?*Adapter { + const adapter = self.adapters[index]; + self.adapters[index] = null; + return adapter; + } +}; + // wgpu-native pub const Instance = opaque { + pub const RequestAdapterStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + unavailable = 0x00000003, + @"error" = 0x00000004, + }; + + pub const RequestAdapterCallback = *const fn ( + status: RequestAdapterStatus, + adapter: ?*Adapter, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const RequestAdapterCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + + callback: RequestAdapterCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const RequestAdapterResponse = struct { + status: RequestAdapterStatus, + message: ?[]const u8, + adapter: ?*Adapter, + + pub fn deinit(self: *RequestAdapterResponse, allocator: std.mem.Allocator) void { + if (self.message) |message| allocator.free(message); + if (self.adapter) |adapter| adapter.release(); + self.message = null; + self.adapter = null; + } + + pub fn takeAdapter(self: *RequestAdapterResponse) ?*Adapter { + const adapter = self.adapter; + self.adapter = null; + return adapter; + } + }; + + pub const RequestAdapterSyncError = std.Io.Cancelable || std.mem.Allocator.Error; + + const RequestAdapterSyncState = struct { + allocator: std.mem.Allocator, + response: RequestAdapterResponse = undefined, + message_error: ?std.mem.Allocator.Error = null, + completed: bool = false, + }; + // This is a global function, but it creates an instance so I put it here. pub inline fn create(descriptor: ?*const InstanceDescriptor) ?*Instance { return raw.call(?*Instance, "wgpuCreateInstance", .{descriptor}); } - pub inline fn getFeatures(features: *SupportedInstanceFeatures) void { - raw.call(void, "wgpuGetInstanceFeatures", .{features}); - } - pub inline fn getLimits(limits: *InstanceLimits) Status { return raw.call(Status, "wgpuGetInstanceLimits", .{limits}); } - pub inline fn hasFeature(feature: InstanceFeatureName) bool { - return raw.call(WGPUBool, "wgpuHasInstanceFeature", .{feature}) != 0; - } - pub inline fn createSurface(self: *Instance, descriptor: *const SurfaceDescriptor) ?*Surface { return raw.call(?*Surface, "wgpuInstanceCreateSurface", .{ self, descriptor }); } @@ -270,45 +361,62 @@ pub const Instance = opaque { raw.call(void, "wgpuInstanceProcessEvents", .{self}); } - fn defaultAdapterCallback(status: RequestAdapterStatus, adapter: ?*Adapter, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void { - const ud_response: *RequestAdapterResponse = @ptrCast(@alignCast(userdata1)); - ud_response.* = RequestAdapterResponse{ + fn defaultAdapterCallback(status: RequestAdapterStatus, adapter: ?*Adapter, message: StringView, userdata1: ?*anyopaque, _: ?*anyopaque) callconv(.c) void { + const state: *RequestAdapterSyncState = @ptrCast(@alignCast(userdata1)); + state.response = .{ .status = status, - .message = message.toSlice(), + .message = null, .adapter = adapter, }; - - const completed: *bool = @ptrCast(@alignCast(userdata2)); - completed.* = true; + state.response.message = _async.copyCallbackMessage( + state.allocator, + message, + ) catch |err| { + state.message_error = err; + state.completed = true; + return; + }; + state.completed = true; } - // This is a synchronous wrapper that handles asynchronous (callback) logic. - // It uses polling to see when the request has been fulfilled, so needs a polling interval parameter. + // This is a synchronous wrapper that handles asynchronous (callback) logic. The returned + // response owns its message and adapter until deinit() or takeAdapter() is called. pub fn requestAdapterSync( self: *Instance, + allocator: std.mem.Allocator, io: std.Io, options: ?*const RequestAdapterOptions, polling_interval_nanoseconds: u64, - ) std.Io.Cancelable!RequestAdapterResponse { - var response: RequestAdapterResponse = undefined; - var completed = false; + ) RequestAdapterSyncError!RequestAdapterResponse { + var state = RequestAdapterSyncState{ .allocator = allocator }; const callback_info = RequestAdapterCallbackInfo{ .callback = defaultAdapterCallback, - .userdata1 = @ptrCast(&response), - .userdata2 = @ptrCast(&completed), + .userdata1 = @ptrCast(&state), }; const adapter_future = raw.call(Future, "wgpuInstanceRequestAdapter", .{ self, options, callback_info }); // TODO: Revisit once Instance.waitAny() is implemented in wgpu-native, // it takes in futures and returns when one of them completes. _ = adapter_future; - self.processEvents(); - while (!completed) { - try io.sleep(.fromNanoseconds(polling_interval_nanoseconds), .awake); - self.processEvents(); - } + var wait_error: ?std.Io.Cancelable = null; + _async.waitForCallback( + self, + &state.completed, + io, + polling_interval_nanoseconds, + ) catch |err| { + wait_error = err; + }; - return response; + if (state.message_error) |err| { + state.response.deinit(allocator); + return err; + } + if (wait_error) |err| { + state.response.deinit(allocator); + return err; + } + return state.response; } pub inline fn requestAdapter(self: *Instance, options: ?*const RequestAdapterOptions, callback_info: RequestAdapterCallbackInfo) Future { @@ -334,9 +442,32 @@ pub const Instance = opaque { pub inline fn generateReport(self: *Instance, report: *GlobalReport) void { raw.call(void, "wgpuGenerateReport", .{ self, report }); } - pub inline fn enumerateAdapters(self: *Instance, options: ?*EnumerateAdapterOptions, adapters: ?[*]*Adapter) usize { + fn enumerateAdaptersRaw( + self: *Instance, + options: ?*const EnumerateAdapterOptions, + adapters: ?[*]?*Adapter, + ) usize { return raw.call(usize, "wgpuInstanceEnumerateAdapters", .{ self, options, adapters }); } + + /// Enumerates adapters and owns every returned adapter handle. + /// Call AdapterList.deinit(), or take individual handles with takeAdapter(). + pub fn enumerateAdapters( + self: *Instance, + allocator: std.mem.Allocator, + options: ?*const EnumerateAdapterOptions, + ) std.mem.Allocator.Error!AdapterList { + const count = self.enumerateAdaptersRaw(options, null); + if (count == 0) return .{}; + + const adapters = try allocator.alloc(?*Adapter, count); + errdefer allocator.free(adapters); + @memset(adapters, null); + + const written = self.enumerateAdaptersRaw(options, adapters.ptr); + std.debug.assert(written == count); + return .{ .adapters = adapters }; + } }; test "can create instance (and release it afterwards)" { @@ -352,12 +483,49 @@ test "can request adapter" { const instance = Instance.create(null).?; defer instance.release(); - const response = try instance.requestAdapterSync(std.testing.io, null, 200_000_000); + var response = try instance.requestAdapterSync(testing.allocator, testing.io, null, 200_000_000); + defer response.deinit(testing.allocator); const adapter: ?*Adapter = switch (response.status) { - .success => response.adapter, + .success => response.takeAdapter(), else => null, }; if (adapter == null) return error.SkipZigTest; defer adapter.?.release(); try testing.expect(response.status == .success); } + +test "synchronous adapter callback copies its message" { + const testing = @import("std").testing; + + var callback_message = [_]u8{ 'o', 'l', 'd' }; + var state = Instance.RequestAdapterSyncState{ .allocator = testing.allocator }; + Instance.defaultAdapterCallback( + .@"error", + null, + StringView.fromSlice(&callback_message), + @ptrCast(&state), + null, + ); + defer state.response.deinit(testing.allocator); + + callback_message[0] = 'n'; + try testing.expect(state.completed); + try testing.expectEqualStrings("old", state.response.message.?); +} + +test "enumerated adapter ownership can be transferred" { + const testing = std.testing; + + const instance = Instance.create(null).?; + defer instance.release(); + + var adapters = try instance.enumerateAdapters(testing.allocator, null); + defer adapters.deinit(testing.allocator); + + for (adapters.adapters) |adapter| try testing.expect(adapter != null); + if (adapters.adapters.len != 0) { + const adapter = adapters.takeAdapter(0).?; + defer adapter.release(); + try testing.expectEqual(null, adapters.adapters[0]); + } +} diff --git a/src/misc.zig b/src/misc.zig index 8a7ce1d..11d2f72 100644 --- a/src/misc.zig +++ b/src/misc.zig @@ -10,6 +10,15 @@ pub const WGPU_WHOLE_SIZE = U64_MAX; pub const WGPUBool = u32; pub const WGPUFlags = u64; +pub fn sliceFromOptional( + comptime T: type, + items: ?[*]const T, + count: usize, +) []const T { + if (count == 0) return &.{}; + return items.?[0..count]; +} + // Status code returned (synchronously) from many operations. // Generally indicates an invalid input like an unknown enum value or OutStructChainError. pub const Status = enum(u32) { @@ -23,77 +32,6 @@ pub const OptionalBool = enum(u32) { undefined = 0x00000002, }; -// Used by both device and adapter -// FeatureName and Limits are clearly related -// but idk if they should go in device.zig, adapter.zig, or their own separate file. -// So they're going in the "miscellaneous" pile for now. -pub const FeatureName = enum(u32) { - core_features_and_limits = 0x00000001, - depth_clip_control = 0x00000002, - depth32_float_stencil8 = 0x00000003, - texture_compression_bc = 0x00000004, - texture_compression_bc_sliced_3d = 0x00000005, - texture_compression_etc2 = 0x00000006, - texture_compression_astc = 0x00000007, - texture_compression_astc_sliced_3d = 0x00000008, - timestamp_query = 0x00000009, - indirect_first_instance = 0x0000000A, - shader_f16 = 0x0000000B, - rg11b10_ufloat_renderable = 0x0000000C, - bgra8_unorm_storage = 0x0000000D, - float32_filterable = 0x0000000E, - float32_blendable = 0x0000000F, - clip_distances = 0x00000010, - dual_source_blending = 0x00000011, - subgroups = 0x00000012, - texture_formats_tier_1 = 0x00000013, - texture_formats_tier_2 = 0x00000014, - primitive_index = 0x00000015, - texture_component_swizzle = 0x00000016, - - // wgpu-native extras - immediates = 0x00030001, - texture_adapter_specific_format_features = 0x00030002, - multi_draw_indirect_count = 0x00030004, - vertex_writable_storage = 0x00030005, - texture_binding_array = 0x00030006, - sampled_texture_and_storage_buffer_array_non_uniform_indexing = 0x00030007, - pipeline_statistics_query = 0x00030008, - storage_resource_binding_array = 0x00030009, - partially_bound_binding_array = 0x0003000A, - texture_format_16bit_norm = 0x0003000B, - texture_compression_astc_hdr = 0x0003000C, - mappable_primary_buffers = 0x0003000E, - buffer_binding_array = 0x0003000F, - uniform_buffer_and_storage_texture_array_non_uniform_indexing = 0x00030010, - polygon_mode_line = 0x00030013, - polygon_mode_point = 0x00030014, - conservative_rasterization = 0x00030015, - spirv_shader_passthrough = 0x00030017, - vertex_attribute_64bit = 0x00030019, - texture_format_nv12 = 0x0003001A, - ray_query = 0x0003001C, - shader_f64 = 0x0003001D, - shader_i16 = 0x0003001E, - shader_early_depth_test = 0x00030020, - subgroup = 0x00030021, - subgroup_vertex = 0x00030022, - subgroup_barrier = 0x00030023, - timestamp_query_inside_encoders = 0x00030024, - timestamp_query_inside_passes = 0x00030025, - shader_int64 = 0x00030026, -}; - -pub const SupportedFeatures = extern struct { - feature_count: usize, - features: [*]const FeatureName, - - // Frees array members of SupportedFeatures which were allocated by the API. - pub inline fn freeMembers(self: SupportedFeatures) void { - raw.call(void, "wgpuSupportedFeaturesFreeMembers", .{self}); - } -}; - pub const IndexFormat = enum(u32) { undefined = 0x00000000, // Indicates no value is passed for this argument. uint16 = 0x00000001, @@ -149,18 +87,29 @@ pub const StringView = extern struct { } pub fn toSlice(self: StringView) ?[]const u8 { - const data = self.data orelse return null; + const data = self.data orelse return nullDataToSlice(self.length) catch { + std.debug.panic( + "invalid StringView: null data with non-zero length {d}", + .{self.length}, + ); + }; - // test if null-terminated string if (self.length == WGPU_STRLEN) { - // Returns the slice up to, but not including, the null terminator - // I feel like there should be a builtin for this or something, but I don't see one in the docs. - // Maybe there's a simpler way to do it and I'm just overthinking it. return std.mem.sliceTo(@as([*:0]const u8, @ptrCast(data)), 0); } return data[0..self.length]; } + + const NullDataError = error{InvalidLength}; + + fn nullDataToSlice(length: usize) NullDataError!?[]const u8 { + return switch (length) { + WGPU_STRLEN => null, + 0 => "", + else => error.InvalidLength, + }; + } }; test "StringView can be constructed from slice" { @@ -191,7 +140,13 @@ test "slice can be constructed from null-terminated StringView" { try std.testing.expectEqualSlices(u8, "test", sv.toSlice().?); } -test "StringView.toSlice returns null if data is null" { +test "StringView.toSlice distinguishes null from an empty string" { + const empty = StringView{ + .data = null, + .length = 0, + }; + try std.testing.expectEqualSlices(u8, "", empty.toSlice().?); + const sv = StringView{ .data = null, .length = WGPU_STRLEN, @@ -199,3 +154,10 @@ test "StringView.toSlice returns null if data is null" { try std.testing.expectEqual(null, sv.toSlice()); } + +test "StringView rejects null data with a non-zero explicit length" { + try std.testing.expectError( + error.InvalidLength, + StringView.nullDataToSlice(1), + ); +} diff --git a/src/pipeline.zig b/src/pipeline.zig index 2ef9164..5aef942 100644 --- a/src/pipeline.zig +++ b/src/pipeline.zig @@ -16,9 +16,6 @@ const WGPUFlags = _misc.WGPUFlags; const StringView = _misc.StringView; const OptionalBool = _misc.OptionalBool; -const _async = @import("async.zig"); -const CallbackMode = _async.CallbackMode; - const TextureFormat = @import("texture.zig").TextureFormat; pub const PipelineLayoutExtras = extern struct { @@ -35,12 +32,20 @@ pub const PipelineLayoutDescriptor = extern struct { bind_group_layouts: [*]const *BindGroupLayout, immediate_size: u32 = 0, - pub inline fn withImmediateDataSize(self: PipelineLayoutDescriptor, immediate_data_size: u32) PipelineLayoutDescriptor { - var pld = self; - pld.next_in_chain = @ptrCast(&PipelineLayoutExtras{ - .immediate_data_size = immediate_data_size, - }); - return pld; + /// Initializes a descriptor that borrows `bind_group_layouts`. + pub inline fn init( + bind_group_layouts: []const *BindGroupLayout, + ) PipelineLayoutDescriptor { + return .{ + .bind_group_layout_count = bind_group_layouts.len, + .bind_group_layouts = bind_group_layouts.ptr, + }; + } + + pub inline fn withExtras(self: PipelineLayoutDescriptor, extras: *const PipelineLayoutExtras) PipelineLayoutDescriptor { + var descriptor = self; + descriptor.next_in_chain = @ptrCast(extras); + return descriptor; } }; @@ -72,6 +77,17 @@ pub const ComputeState = extern struct { entry_point: StringView = StringView{}, constant_count: usize = 0, constants: [*]const ConstantEntry = &[0]ConstantEntry{}, + + /// Returns state that borrows `constants`. + pub inline fn withConstants( + self: ComputeState, + constants: []const ConstantEntry, + ) ComputeState { + var state = self; + state.constant_count = constants.len; + state.constants = constants.ptr; + return state; + } }; pub const ComputePipelineDescriptor = extern struct { @@ -81,33 +97,6 @@ pub const ComputePipelineDescriptor = extern struct { compute: ComputeState, }; -pub const CreatePipelineAsyncStatus = enum(u32) { - success = 0x00000001, - callback_cancelled = 0x00000002, - validation_error = 0x00000003, - internal_error = 0x00000004, -}; - -pub const CreateComputePipelineAsyncCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, - - callback: CreateComputePipelineAsyncCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; - -// TODO: This should probably be in device.zig, as well as its RenderPipeline counterpart -pub const CreateComputePipelineAsyncCallback = *const fn ( - status: CreatePipelineAsyncStatus, - pipeline: ?*ComputePipeline, - message: StringView, - userdata1: ?*anyopaque, - userdata2: ?*anyopaque, -) callconv(.c) void; - pub const ComputePipeline = opaque { pub inline fn getBindGroupLayout(self: *ComputePipeline, group_index: u32) ?*BindGroupLayout { return raw.call(?*BindGroupLayout, "wgpuComputePipelineGetBindGroupLayout", .{ self, group_index }); @@ -195,6 +184,18 @@ pub const VertexBufferLayout = extern struct { array_stride: u64, attribute_count: usize, attributes: [*]const VertexAttribute, + + /// Initializes a layout that borrows `attributes`. + pub inline fn init( + array_stride: u64, + attributes: []const VertexAttribute, + ) VertexBufferLayout { + return .{ + .array_stride = array_stride, + .attribute_count = attributes.len, + .attributes = attributes.ptr, + }; + } }; pub const VertexState = extern struct { @@ -205,6 +206,28 @@ pub const VertexState = extern struct { constants: [*]const ConstantEntry = &[0]ConstantEntry{}, buffer_count: usize = 0, buffers: [*]const VertexBufferLayout = &[0]VertexBufferLayout{}, + + /// Returns state that borrows `constants`. + pub inline fn withConstants( + self: VertexState, + constants: []const ConstantEntry, + ) VertexState { + var state = self; + state.constant_count = constants.len; + state.constants = constants.ptr; + return state; + } + + /// Returns state that borrows `buffers`. + pub inline fn withBuffers( + self: VertexState, + buffers: []const VertexBufferLayout, + ) VertexState { + var state = self; + state.buffer_count = buffers.len; + state.buffers = buffers.ptr; + return state; + } }; pub const PrimitiveTopology = enum(u32) { @@ -393,6 +416,40 @@ pub const FragmentState = extern struct { constants: [*]const ConstantEntry = &[0]ConstantEntry{}, target_count: usize, targets: [*]const ColorTargetState, + + /// Initializes fragment state that borrows `targets`. + pub inline fn init( + module: *ShaderModule, + targets: []const ColorTargetState, + ) FragmentState { + return .{ + .module = module, + .target_count = targets.len, + .targets = targets.ptr, + }; + } + + /// Returns state that borrows `constants`. + pub inline fn withConstants( + self: FragmentState, + constants: []const ConstantEntry, + ) FragmentState { + var state = self; + state.constant_count = constants.len; + state.constants = constants.ptr; + return state; + } + + /// Returns state that borrows `targets`. + pub inline fn withTargets( + self: FragmentState, + targets: []const ColorTargetState, + ) FragmentState { + var state = self; + state.target_count = targets.len; + state.targets = targets.ptr; + return state; + } }; pub const RenderPipelineDescriptor = extern struct { @@ -424,22 +481,3 @@ pub const RenderPipeline = opaque { raw.call(void, "wgpuRenderPipelineRelease", .{self}); } }; - -pub const CreateRenderPipelineAsyncCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, - - callback: CreateRenderPipelineAsyncCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; - -pub const CreateRenderPipelineAsyncCallback = *const fn ( - status: CreatePipelineAsyncStatus, - pipeline: ?*RenderPipeline, - message: StringView, - userdata1: ?*anyopaque, - userdata2: ?*anyopaque, -) callconv(.c) void; diff --git a/src/query_set.zig b/src/query_set.zig index ff607fc..bf0d431 100644 --- a/src/query_set.zig +++ b/src/query_set.zig @@ -27,6 +27,16 @@ pub const QuerySetDescriptorExtras = extern struct { }, pipeline_statistics: [*]const PipelineStatisticName, pipeline_statistic_count: usize, + + /// Initializes extras that borrow `pipeline_statistics`. + pub inline fn init( + pipeline_statistics: []const PipelineStatisticName, + ) QuerySetDescriptorExtras { + return .{ + .pipeline_statistics = pipeline_statistics.ptr, + .pipeline_statistic_count = pipeline_statistics.len, + }; + } }; pub const QuerySetDescriptor = extern struct { @@ -35,13 +45,10 @@ pub const QuerySetDescriptor = extern struct { type: QueryType, count: u32, - pub inline fn withPipelineStatistics(self: QuerySetDescriptor, pipeline_statistic_count: usize, pipeline_statistics: [*]const PipelineStatisticName) QuerySetDescriptor { - var qsd = self; - qsd.next_in_chain = @ptrCast(&QuerySetDescriptorExtras{ - .pipeline_statistics = pipeline_statistics, - .pipeline_statistic_count = pipeline_statistic_count, - }); - return qsd; + pub inline fn withExtras(self: QuerySetDescriptor, extras: *const QuerySetDescriptorExtras) QuerySetDescriptor { + var descriptor = self; + descriptor.next_in_chain = @ptrCast(extras); + return descriptor; } }; diff --git a/src/queue.zig b/src/queue.zig index a3729f8..e9499cf 100644 --- a/src/queue.zig +++ b/src/queue.zig @@ -1,11 +1,15 @@ +const std = @import("std"); + const raw = @import("raw.zig"); const ChainedStruct = @import("chained_struct.zig").ChainedStruct; const CommandBuffer = @import("command_encoder.zig").CommandBuffer; const Buffer = @import("buffer.zig").Buffer; +const _copy = @import("copy.zig"); +const TexelCopyTextureInfo = _copy.TexelCopyTextureInfo; +const TexelCopyBufferLayout = _copy.TexelCopyBufferLayout; + const _texture = @import("texture.zig"); -const TexelCopyTextureInfo = _texture.TexelCopyTextureInfo; -const TexelCopyBufferLayout = _texture.TexelCopyBufferLayout; const Extent3D = _texture.Extent3D; const _async = @import("async.zig"); @@ -21,32 +25,118 @@ pub const QueueDescriptor = extern struct { label: StringView = StringView{}, }; -pub const WorkDoneStatus = enum(u32) { - success = 0x00000001, - callback_cancelled = 0x00000002, - @"error" = 0x00000003, -}; - -pub const QueueWorkDoneCallbackInfo = extern struct { - next_in_chain: ?*ChainedStruct = null, - - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, - - callback: QueueWorkDoneCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; - -pub const QueueWorkDoneCallback = *const fn (status: WorkDoneStatus, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void; - // wgpu-native pub const Queue = opaque { - pub inline fn onSubmittedWorkDone(self: *Queue, callback_info: QueueWorkDoneCallbackInfo) Future { + pub const WorkDoneStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + @"error" = 0x00000003, + }; + + pub const WorkDoneCallback = *const fn ( + status: WorkDoneStatus, + message: StringView, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; + + pub const WorkDoneCallbackInfo = extern struct { + next_in_chain: ?*ChainedStruct = null, + + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, + + callback: WorkDoneCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; + + pub const WorkDoneResponse = struct { + status: WorkDoneStatus, + message: ?[]const u8, + + pub fn deinit( + self: *WorkDoneResponse, + allocator: std.mem.Allocator, + ) void { + if (self.message) |message| allocator.free(message); + self.message = null; + } + }; + + pub const WorkDoneSyncError = + std.Io.Cancelable || std.mem.Allocator.Error; + + const WorkDoneSyncState = struct { + allocator: std.mem.Allocator, + response: WorkDoneResponse = undefined, + message_error: ?std.mem.Allocator.Error = null, + completed: bool = false, + }; + + fn defaultWorkDoneCallback( + status: WorkDoneStatus, + message: StringView, + userdata1: ?*anyopaque, + _: ?*anyopaque, + ) callconv(.c) void { + const state: *WorkDoneSyncState = @ptrCast(@alignCast(userdata1)); + state.response = .{ + .status = status, + .message = null, + }; + state.response.message = _async.copyCallbackMessage( + state.allocator, + message, + ) catch |err| { + state.message_error = err; + state.completed = true; + return; + }; + state.completed = true; + } + + pub inline fn onSubmittedWorkDone(self: *Queue, callback_info: WorkDoneCallbackInfo) Future { return raw.call(Future, "wgpuQueueOnSubmittedWorkDone", .{ self, callback_info }); } + /// Waits for previously submitted work while safely driving an + /// allow_process_events callback. The response owns its copied message. + pub fn onSubmittedWorkDoneSync( + self: *Queue, + allocator: std.mem.Allocator, + io: std.Io, + event_source: anytype, + polling_interval_nanoseconds: u64, + ) WorkDoneSyncError!WorkDoneResponse { + var state = WorkDoneSyncState{ .allocator = allocator }; + _ = self.onSubmittedWorkDone(.{ + .callback = defaultWorkDoneCallback, + .userdata1 = @ptrCast(&state), + }); + + var wait_error: ?std.Io.Cancelable = null; + _async.waitForCallback( + event_source, + &state.completed, + io, + polling_interval_nanoseconds, + ) catch |err| { + wait_error = err; + }; + + if (state.message_error) |err| { + state.response.deinit(allocator); + return err; + } + if (wait_error) |err| { + state.response.deinit(allocator); + return err; + } + return state.response; + } + // Unimplemented as of wgpu-native v29.0.0.0, // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs // pub inline fn setLabel(self: *Queue, label: []const u8) void { @@ -57,12 +147,36 @@ pub const Queue = opaque { raw.call(void, "wgpuQueueSubmit", .{ self, commands.len, commands.ptr }); } - pub inline fn writeBuffer(self: *Queue, buffer: *Buffer, buffer_offset: u64, data: *const anyopaque, size: usize) void { - raw.call(void, "wgpuQueueWriteBuffer", .{ self, buffer, buffer_offset, data, size }); + pub inline fn writeBuffer( + self: *Queue, + buffer: *Buffer, + buffer_offset: u64, + data: []const u8, + ) void { + raw.call(void, "wgpuQueueWriteBuffer", .{ + self, + buffer, + buffer_offset, + data.ptr, + data.len, + }); } - pub inline fn writeTexture(self: *Queue, destination: *const TexelCopyTextureInfo, data: *const anyopaque, data_size: usize, data_layout: *const TexelCopyBufferLayout, write_size: *const Extent3D) void { - raw.call(void, "wgpuQueueWriteTexture", .{ self, destination, data, data_size, data_layout, write_size }); + pub inline fn writeTexture( + self: *Queue, + destination: *const TexelCopyTextureInfo, + data: []const u8, + data_layout: *const TexelCopyBufferLayout, + write_size: *const Extent3D, + ) void { + raw.call(void, "wgpuQueueWriteTexture", .{ + self, + destination, + data.ptr, + data.len, + data_layout, + write_size, + }); } pub inline fn addRef(self: *Queue) void { raw.call(void, "wgpuQueueAddRef", .{self}); @@ -75,4 +189,25 @@ pub const Queue = opaque { pub inline fn submitForIndex(self: *Queue, commands: []const *const CommandBuffer) SubmissionIndex { return raw.call(SubmissionIndex, "wgpuQueueSubmitForIndex", .{ self, commands.len, commands.ptr }); } + + /// Returns the number of nanoseconds represented by one timestamp-query tick. + pub inline fn getTimestampPeriod(self: *Queue) f32 { + return raw.call(f32, "wgpuQueueGetTimestampPeriod", .{self}); + } }; + +test "synchronous work-done callback copies its message" { + var callback_message = [_]u8{ 'o', 'l', 'd' }; + var state = Queue.WorkDoneSyncState{ .allocator = std.testing.allocator }; + Queue.defaultWorkDoneCallback( + .@"error", + StringView.fromSlice(&callback_message), + @ptrCast(&state), + null, + ); + defer state.response.deinit(std.testing.allocator); + + callback_message[0] = 'n'; + try std.testing.expect(state.completed); + try std.testing.expectEqualStrings("old", state.response.message.?); +} diff --git a/src/raw.zig b/src/raw.zig index 2488cd2..04115f6 100644 --- a/src/raw.zig +++ b/src/raw.zig @@ -43,7 +43,7 @@ fn convert(comptime To: type, value: anytype) To { else => conversionError(To, From), }, .int => switch (@typeInfo(From)) { - .@"enum" => @intFromEnum(value), + .@"enum" => @intCast(@intFromEnum(value)), .int, .comptime_int => @intCast(value), else => conversionError(To, From), }, @@ -59,51 +59,13 @@ fn conversionError(comptime To: type, comptime From: type) noreturn { )); } -const wrapper_sources = .{ - .{ "adapter.zig", @embedFile("adapter.zig") }, - .{ "bind_group.zig", @embedFile("bind_group.zig") }, - .{ "buffer.zig", @embedFile("buffer.zig") }, - .{ "command_encoder.zig", @embedFile("command_encoder.zig") }, - .{ "device.zig", @embedFile("device.zig") }, - .{ "instance.zig", @embedFile("instance.zig") }, - .{ "log.zig", @embedFile("log.zig") }, - .{ "misc.zig", @embedFile("misc.zig") }, - .{ "pipeline.zig", @embedFile("pipeline.zig") }, - .{ "query_set.zig", @embedFile("query_set.zig") }, - .{ "queue.zig", @embedFile("queue.zig") }, - .{ "render_bundle.zig", @embedFile("render_bundle.zig") }, - .{ "sampler.zig", @embedFile("sampler.zig") }, - .{ "shader.zig", @embedFile("shader.zig") }, - .{ "surface.zig", @embedFile("surface.zig") }, - .{ "texture.zig", @embedFile("texture.zig") }, -}; - -test "all wrapper calls are declared by wgpu headers" { - comptime { - @setEvalBranchQuota(10_000_000); - for (wrapper_sources) |source| validateWrapperSource(source[0], source[1]); - } -} - -fn validateWrapperSource(comptime file_name: []const u8, comptime source: []const u8) void { - if (std.mem.indexOf(u8, source, "extern fn wgpu") != null) { - @compileError(file_name ++ " contains a handwritten wgpu extern"); - } +test "convert enum to signed C integer" { + const Value = enum(u32) { + maximum = std.math.maxInt(c_int), + }; - var cursor: usize = 0; - while (std.mem.indexOfPos(u8, source, cursor, "raw.call(")) |call_start| { - const name_start = std.mem.indexOfPos(u8, source, call_start, "\"wgpu") orelse - @compileError(file_name ++ " contains an invalid raw.call"); - const name_end = std.mem.indexOfScalarPos( - u8, - source, - name_start + 1, - '"', - ) orelse @compileError(file_name ++ " contains an unterminated function name"); - const name = source[name_start + 1 .. name_end]; - if (!@hasDecl(header, name)) { - @compileError(file_name ++ " references missing header function " ++ name); - } - cursor = name_end + 1; - } + try std.testing.expectEqual( + std.math.maxInt(c_int), + convert(c_int, Value.maximum), + ); } diff --git a/src/render_bundle.zig b/src/render_bundle.zig index ab8d422..289b5d3 100644 --- a/src/render_bundle.zig +++ b/src/render_bundle.zig @@ -1,84 +1,6 @@ const raw = @import("raw.zig"); -const _misc = @import("misc.zig"); -const WGPUBool = _misc.WGPUBool; -const IndexFormat = _misc.IndexFormat; -const StringView = _misc.StringView; - +const StringView = @import("misc.zig").StringView; const ChainedStruct = @import("chained_struct.zig").ChainedStruct; -const TextureFormat = @import("texture.zig").TextureFormat; -const Buffer = @import("buffer.zig").Buffer; -const BindGroup = @import("bind_group.zig").BindGroup; -const RenderPipeline = @import("pipeline.zig").RenderPipeline; -pub const RenderBundleEncoderDescriptor = extern struct { - next_in_chain: ?*const ChainedStruct = null, - label: StringView = StringView{}, - color_format_count: usize, - color_formats: [*]const TextureFormat, - depth_stencil_format: TextureFormat = TextureFormat.undefined, - sample_count: u32 = 1, - depth_read_only: WGPUBool = @intFromBool(false), - stencil_read_only: WGPUBool = @intFromBool(false), -}; - -// wgpu-native - -// TODO: This is very similar to CommandEncoder; should it go in the same file? There's a lot of duplicated import code. -pub const RenderBundleEncoder = opaque { - pub inline fn draw(self: *RenderBundleEncoder, vertex_count: u32, instance_count: u32, first_vertex: u32, first_instance: u32) void { - raw.call(void, "wgpuRenderBundleEncoderDraw", .{ self, vertex_count, instance_count, first_vertex, first_instance }); - } - pub inline fn drawIndexed(self: *RenderBundleEncoder, index_count: u32, instance_count: u32, first_index: u32, base_vertex: i32, first_instance: u32) void { - raw.call(void, "wgpuRenderBundleEncoderDrawIndexed", .{ self, index_count, instance_count, first_index, base_vertex, first_instance }); - } - pub inline fn drawIndexedIndirect(self: *RenderBundleEncoder, indirect_buffer: *Buffer, indirect_offset: u64) void { - raw.call(void, "wgpuRenderBundleEncoderDrawIndexedIndirect", .{ self, indirect_buffer, indirect_offset }); - } - pub inline fn drawIndirect(self: *RenderBundleEncoder, indirect_buffer: *Buffer, indirect_offset: u64) void { - raw.call(void, "wgpuRenderBundleEncoderDrawIndirect", .{ self, indirect_buffer, indirect_offset }); - } - pub inline fn finish(self: *RenderBundleEncoder, descriptor: *const RenderBundleDescriptor) ?*RenderBundle { - return raw.call(?*RenderBundle, "wgpuRenderBundleEncoderFinish", .{ self, descriptor }); - } - pub inline fn insertDebugMarker(self: *RenderBundleEncoder, marker_label: []const u8) void { - raw.call(void, "wgpuRenderBundleEncoderInsertDebugMarker", .{ self, StringView.fromSlice(marker_label) }); - } - pub inline fn popDebugGroup(self: *RenderBundleEncoder) void { - raw.call(void, "wgpuRenderBundleEncoderPopDebugGroup", .{self}); - } - pub inline fn pushDebugGroup(self: *RenderBundleEncoder, group_label: []const u8) void { - raw.call(void, "wgpuRenderBundleEncoderPushDebugGroup", .{ self, StringView.fromSlice(group_label) }); - } - pub inline fn setBindGroup(self: *RenderBundleEncoder, group_index: u32, group: *BindGroup, dynamic_offset_count: usize, dynamic_offsets: ?[*]const u32) void { - raw.call(void, "wgpuRenderBundleEncoderSetBindGroup", .{ self, group_index, group, dynamic_offset_count, dynamic_offsets }); - } - pub inline fn setIndexBuffer(self: *RenderBundleEncoder, buffer: *Buffer, format: IndexFormat, offset: u64, size: u64) void { - raw.call(void, "wgpuRenderBundleEncoderSetIndexBuffer", .{ self, buffer, format, offset, size }); - } - - // Unimplemented as of wgpu-native v29.0.0.0, - // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs - // pub inline fn setLabel(self: *RenderBundleEncoder, label: []const u8) void { - // wgpuRenderBundleEncoderSetLabel(self, StringView.fromSlice(label)); - // } - - pub inline fn setPipeline(self: *RenderBundleEncoder, pipeline: *RenderPipeline) void { - raw.call(void, "wgpuRenderBundleEncoderSetPipeline", .{ self, pipeline }); - } - pub inline fn setVertexBuffer(self: *RenderBundleEncoder, slot: u32, buffer: *Buffer, offset: u64, size: u64) void { - raw.call(void, "wgpuRenderBundleEncoderSetVertexBuffer", .{ self, slot, buffer, offset, size }); - } - pub inline fn addRef(self: *RenderBundleEncoder) void { - raw.call(void, "wgpuRenderBundleEncoderAddRef", .{self}); - } - pub inline fn release(self: *RenderBundleEncoder) void { - raw.call(void, "wgpuRenderBundleEncoderRelease", .{self}); - } - - // wgpu-native - pub inline fn setImmediates(self: *RenderBundleEncoder, offset: u32, size_bytes: u32, data: *const anyopaque) void { - raw.call(void, "wgpuRenderBundleEncoderSetImmediates", .{ self, offset, size_bytes, data }); - } -}; pub const RenderBundleDescriptor = extern struct { next_in_chain: ?*const ChainedStruct = null, diff --git a/src/root.zig b/src/root.zig index 2ffb104..fb7f28c 100644 --- a/src/root.zig +++ b/src/root.zig @@ -9,14 +9,16 @@ pub const WGPUBool = _misc.WGPUBool; pub const WGPUFlags = _misc.WGPUFlags; pub const Status = _misc.Status; pub const OptionalBool = _misc.OptionalBool; -pub const FeatureName = _misc.FeatureName; -pub const SupportedFeatures = _misc.SupportedFeatures; pub const IndexFormat = _misc.IndexFormat; pub const CompareFunction = _misc.CompareFunction; pub const getVersion = _misc.getVersion; pub const WGPU_STRLEN = _misc.WGPU_STRLEN; pub const StringView = _misc.StringView; +const _feature = @import("feature.zig"); +pub const FeatureName = _feature.FeatureName; +pub const SupportedFeatures = _feature.SupportedFeatures; + const _adapter = @import("adapter.zig"); pub const PowerPreference = _adapter.PowerPreference; pub const AdapterType = _adapter.AdapterType; @@ -24,10 +26,6 @@ pub const BackendType = _adapter.BackendType; pub const FeatureLevel = _adapter.FeatureLevel; pub const RequestAdapterOptions = _adapter.RequestAdapterOptions; pub const RequestAdapterWebXROptions = _adapter.RequestAdapterWebXROptions; -pub const RequestAdapterStatus = _adapter.RequestAdapterStatus; -pub const RequestAdapterCallbackInfo = _adapter.RequestAdapterCallbackInfo; -pub const RequestAdapterCallback = _adapter.RequestAdapterCallback; -pub const RequestAdapterResponse = _adapter.RequestAdapterResponse; pub const AdapterInfo = _adapter.AdapterInfo; pub const Adapter = _adapter.Adapter; @@ -50,12 +48,6 @@ pub const BufferBindingType = _buffer.BufferBindingType; pub const BufferBindingLayout = _buffer.BufferBindingLayout; pub const BufferUsage = _buffer.BufferUsage; pub const BufferUsages = _buffer.BufferUsages; -pub const BufferMapState = _buffer.BufferMapState; -pub const MapMode = _buffer.MapMode; -pub const MapModes = _buffer.MapModes; -pub const MapAsyncStatus = _buffer.MapAsyncStatus; -pub const BufferMapCallbackInfo = _buffer.BufferMapCallbackInfo; -pub const BufferMapCallback = _buffer.BufferMapCallback; pub const BufferDescriptor = _buffer.BufferDescriptor; pub const Buffer = _buffer.Buffer; @@ -82,25 +74,13 @@ pub const RenderPassEncoder = _command_encoder.RenderPassEncoder; pub const CommandBufferDescriptor = _command_encoder.CommandBufferDescriptor; pub const CommandBuffer = _command_encoder.CommandBuffer; pub const CommandEncoder = _command_encoder.CommandEncoder; +pub const RenderBundleEncoderDescriptor = _command_encoder.RenderBundleEncoderDescriptor; +pub const RenderBundleEncoder = _command_encoder.RenderBundleEncoder; const _device = @import("device.zig"); -pub const DeviceLostReason = _device.DeviceLostReason; -pub const DeviceLostCallbackInfo = _device.DeviceLostCallbackInfo; -pub const DeviceLostCallback = _device.DeviceLostCallback; -pub const defaultDeviceLostCallback = _device.defaultDeviceLostCallback; +pub const NativeMetalDevice = _device.NativeMetalDevice; pub const DeviceExtras = _device.DeviceExtras; -pub const ErrorType = _device.ErrorType; -pub const UncapturedErrorCallback = _device.UncapturedErrorCallback; -pub const ErrorFilter = _device.ErrorFilter; -pub const UncapturedErrorCallbackInfo = _device.UncapturedErrorCallbackInfo; pub const DeviceDescriptor = _device.DeviceDescriptor; -pub const RequestDeviceStatus = _device.RequestDeviceStatus; -pub const RequestDeviceCallback = _device.RequestDeviceCallback; -pub const RequestDeviceResponse = _device.RequestDeviceResponse; -pub const RequestDeviceCallbackInfo = _device.RequestDeviceCallbackInfo; -pub const PopErrorScopeStatus = _device.PopErrorScopeStatus; -pub const PopErrorScopeCallback = _device.PopErrorScopeCallback; -pub const PopErrorScopeCallbackInfo = _device.PopErrorScopeCallbackInfo; pub const Device = _device.Device; const _instance = @import("instance.zig"); @@ -130,6 +110,7 @@ pub const RegistryReport = _instance.RegistryReport; pub const HubReport = _instance.HubReport; pub const GlobalReport = _instance.GlobalReport; pub const EnumerateAdapterOptions = _instance.EnumerateAdapterOptions; +pub const AdapterList = _instance.AdapterList; pub const Instance = _instance.Instance; const _limits = @import("limits.zig"); @@ -152,9 +133,6 @@ pub const PipelineLayout = _pipeline.PipelineLayout; pub const ConstantEntry = _pipeline.ConstantEntry; pub const ComputeState = _pipeline.ComputeState; pub const ComputePipelineDescriptor = _pipeline.ComputePipelineDescriptor; -pub const CreatePipelineAsyncStatus = _pipeline.CreatePipelineAsyncStatus; -pub const CreateComputePipelineAsyncCallbackInfo = _pipeline.CreateComputePipelineAsyncCallbackInfo; -pub const CreateComputePipelineAsyncCallback = _pipeline.CreateComputePipelineAsyncCallback; pub const ComputePipeline = _pipeline.ComputePipeline; pub const VertexStepMode = _pipeline.VertexStepMode; pub const VertexFormat = _pipeline.VertexFormat; @@ -181,8 +159,6 @@ pub const ColorTargetState = _pipeline.ColorTargetState; pub const FragmentState = _pipeline.FragmentState; pub const RenderPipelineDescriptor = _pipeline.RenderPipelineDescriptor; pub const RenderPipeline = _pipeline.RenderPipeline; -pub const CreateRenderPipelineAsyncCallbackInfo = _pipeline.CreateRenderPipelineAsyncCallbackInfo; -pub const CreateRenderPipelineAsyncCallback = _pipeline.CreateRenderPipelineAsyncCallback; const _query_set = @import("query_set.zig"); pub const QueryType = _query_set.QueryType; @@ -194,14 +170,9 @@ pub const QuerySet = _query_set.QuerySet; const _queue = @import("queue.zig"); pub const SubmissionIndex = _queue.SubmissionIndex; pub const QueueDescriptor = _queue.QueueDescriptor; -pub const WorkDoneStatus = _queue.WorkDoneStatus; -pub const QueueWorkDoneCallbackInfo = _queue.QueueWorkDoneCallbackInfo; -pub const QueueWorkDoneCallback = _queue.QueueWorkDoneCallback; pub const Queue = _queue.Queue; const _render_bundle = @import("render_bundle.zig"); -pub const RenderBundleEncoderDescriptor = _render_bundle.RenderBundleEncoderDescriptor; -pub const RenderBundleEncoder = _render_bundle.RenderBundleEncoder; pub const RenderBundleDescriptor = _render_bundle.RenderBundleDescriptor; pub const RenderBundle = _render_bundle.RenderBundle; @@ -220,43 +191,28 @@ pub const ShaderStages = _shader.ShaderStages; pub const ShaderModuleDescriptor = _shader.ShaderModuleDescriptor; pub const ShaderModuleDescriptorSpirV = _shader.ShaderModuleDescriptorSpirV; pub const ShaderSourceSPIRV = _shader.ShaderSourceSPIRV; -pub const ShaderModuleSPIRVMergedDescriptor = _shader.ShaderModuleSPIRVMergedDescriptor; pub const shaderModuleSPIRVDescriptor = _shader.shaderModuleSPIRVDescriptor; pub const ShaderSourceWGSL = _shader.ShaderSourceWGSL; -pub const ShaderModuleWGSLMergedDescriptor = _shader.ShaderModuleWGSLMergedDescriptor; pub const shaderModuleWGSLDescriptor = _shader.shaderModuleWGSLDescriptor; pub const ShaderDefine = _shader.ShaderDefine; pub const ShaderSourceGLSL = _shader.ShaderSourceGLSL; -pub const ShaderModuleGLSLMergedDescriptor = _shader.ShaderModuleGLSLMergedDescriptor; pub const shaderModuleGLSLDescriptor = _shader.shaderModuleGLSLDescriptor; -pub const CompilationInfoRequestStatus = _shader.CompilationInfoRequestStatus; -pub const CompilationMessageType = _shader.CompilationMessageType; -pub const CompilationMessage = _shader.CompilationMessage; -pub const CompilationInfo = _shader.CompilationInfo; -pub const CompilationInfoCallback = _shader.CompilationInfoCallback; -pub const CompilationInfoCallbackInfo = _shader.CompilationInfoCallbackInfo; pub const ShaderModule = _shader.ShaderModule; const _surface = @import("surface.zig"); pub const SurfaceDescriptor = _surface.SurfaceDescriptor; pub const SurfaceSourceAndroidNativeWindow = _surface.SurfaceSourceAndroidNativeWindow; -pub const MergedSurfaceDescriptorFromAndroidWindow = _surface.MergedSurfaceDescriptorFromAndroidWindow; pub const surfaceDescriptorFromAndroidNativeWindow = _surface.surfaceDescriptorFromAndroidNativeWindow; pub const SurfaceSourceMetalLayer = _surface.SurfaceSourceMetalLayer; -pub const MergedSurfaceDescriptorFromMetalLayer = _surface.MergedSurfaceDescriptorFromMetalLayer; pub const surfaceDescriptorFromMetalLayer = _surface.surfaceDescriptorFromMetalLayer; pub const SurfaceSourceWaylandSurface = _surface.SurfaceSourceWaylandSurface; -pub const MergedSurfaceDescriptorFromWaylandSurface = _surface.MergedSurfaceDescriptorFromWaylandSurface; pub const surfaceDescriptorFromWaylandSurface = _surface.surfaceDescriptorFromWaylandSurface; pub const SurfaceSourceWindowsHWND = _surface.SurfaceSourceWindowsHWND; pub const SurfaceSourceSwapChainPanel = _surface.SurfaceSourceSwapChainPanel; -pub const MergedSurfaceDescriptorFromWindowsHWND = _surface.MergedSurfaceDescriptorFromWindowsHWND; pub const surfaceDescriptorFromWindowsHWND = _surface.surfaceDescriptorFromWindowsHWND; pub const SurfaceSourceXCBWindow = _surface.SurfaceSourceXCBWindow; -pub const MergedSurfaceDescriptorFromXcbWindow = _surface.MergedSurfaceDescriptorFromXcbWindow; pub const surfaceDescriptorFromXcbWindow = _surface.surfaceDescriptorFromXcbWindow; pub const SurfaceSourceXlibWindow = _surface.SurfaceSourceXlibWindow; -pub const MergedSurfaceDescriptorFromXlibWindow = _surface.MergedSurfaceDescriptorFromXlibWindow; pub const surfaceDescriptorFromXlibWindow = _surface.surfaceDescriptorFromXlibWindow; pub const CompositeAlphaMode = _surface.CompositeAlphaMode; pub const PresentMode = _surface.PresentMode; @@ -273,7 +229,6 @@ pub const Surface = _surface.Surface; const _texture = @import("texture.zig"); pub const WGPU_ARRAY_LAYER_COUNT_UNDEFINED = _texture.WGPU_ARRAY_LAYER_COUNT_UNDEFINED; pub const WGPU_MIP_LEVEL_COUNT_UNDEFINED = _texture.WGPU_MIP_LEVEL_COUNT_UNDEFINED; -pub const WGPU_COPY_STRIDE_UNDEFINED = _texture.WGPU_COPY_STRIDE_UNDEFINED; pub const TextureFormat = _texture.TextureFormat; pub const TextureUsage = _texture.TextureUsage; pub const TextureUsages = _texture.TextureUsages; @@ -283,7 +238,7 @@ pub const ComponentSwizzle = _texture.ComponentSwizzle; pub const TextureComponentSwizzle = _texture.TextureComponentSwizzle; pub const TextureComponentSwizzleDescriptor = _texture.TextureComponentSwizzleDescriptor; pub const TextureView = _texture.TextureView; -pub const SampleType = _texture.SampleType; +pub const TextureSampleType = _texture.TextureSampleType; pub const ViewDimension = _texture.ViewDimension; pub const TextureBindingViewDimension = _texture.TextureBindingViewDimension; pub const TextureBindingLayout = _texture.TextureBindingLayout; @@ -292,11 +247,15 @@ pub const StorageTextureBindingLayout = _texture.StorageTextureBindingLayout; pub const TextureDimension = _texture.TextureDimension; pub const Extent3D = _texture.Extent3D; pub const TextureDescriptor = _texture.TextureDescriptor; +pub const NativeMetalTexture = _texture.NativeMetalTexture; pub const Texture = _texture.Texture; -pub const Origin3D = _texture.Origin3D; -pub const TexelCopyTextureInfo = _texture.TexelCopyTextureInfo; -pub const TexelCopyBufferLayout = _texture.TexelCopyBufferLayout; -pub const TexelCopyBufferInfo = _texture.TexelCopyBufferInfo; + +const _copy = @import("copy.zig"); +pub const WGPU_COPY_STRIDE_UNDEFINED = _copy.WGPU_COPY_STRIDE_UNDEFINED; +pub const Origin3D = _copy.Origin3D; +pub const TexelCopyTextureInfo = _copy.TexelCopyTextureInfo; +pub const TexelCopyBufferLayout = _copy.TexelCopyBufferLayout; +pub const TexelCopyBufferInfo = _copy.TexelCopyBufferInfo; const _async = @import("async.zig"); pub const CallbackMode = _async.CallbackMode; diff --git a/src/shader.zig b/src/shader.zig index aa73d33..52a8e4e 100644 --- a/src/shader.zig +++ b/src/shader.zig @@ -31,6 +31,14 @@ pub const ShaderModuleDescriptorSpirV = extern struct { label: StringView = StringView{}, source_size: u32, source: [*]const u32, + + /// Initializes a descriptor that borrows `source`. + pub inline fn init(source: []const u32) ShaderModuleDescriptorSpirV { + return .{ + .source_size = @intCast(source.len), + .source = source.ptr, + }; + } }; pub const ShaderSourceSPIRV = extern struct { @@ -39,37 +47,32 @@ pub const ShaderSourceSPIRV = extern struct { }, code_size: u32, code: [*]const u32, + + /// Initializes a chained source that borrows `code`. + pub inline fn init(code: []const u32) ShaderSourceSPIRV { + return .{ + .code_size = @intCast(code.len), + .code = code.ptr, + }; + } }; -pub const ShaderModuleSPIRVMergedDescriptor = struct { - label: []const u8 = "", - code_size: u32, - code: [*]const u32, -}; -pub inline fn shaderModuleSPIRVDescriptor(descriptor: ShaderModuleSPIRVMergedDescriptor) ShaderModuleDescriptor { - return ShaderModuleDescriptor{ - .next_in_chain = @ptrCast(&ShaderSourceSPIRV{ - .code_size = descriptor.code_size, - .code = descriptor.code, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn shaderModuleSPIRVDescriptor(source: *const ShaderSourceSPIRV, label: []const u8) ShaderModuleDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } pub const ShaderSourceWGSL = extern struct { chain: ChainedStruct = ChainedStruct{ .s_type = SType.shader_source_wgsl, }, code: StringView }; -pub const ShaderModuleWGSLMergedDescriptor = struct { - label: []const u8 = "", - code: []const u8, -}; pub inline fn shaderModuleWGSLDescriptor( - descriptor: ShaderModuleWGSLMergedDescriptor, + source: *const ShaderSourceWGSL, + label: []const u8, ) ShaderModuleDescriptor { - return ShaderModuleDescriptor{ - .next_in_chain = @ptrCast(&ShaderSourceWGSL{ - .code = StringView.fromSlice(descriptor.code), - }), - .label = StringView.fromSlice(descriptor.label), + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -84,80 +87,85 @@ pub const ShaderSourceGLSL = extern struct { stage: ShaderStage, code: StringView, define_count: u32 = 0, - defines: ?[*]ShaderDefine = null, -}; -pub const ShaderModuleGLSLMergedDescriptor = struct { - label: []const u8 = "", - stage: ShaderStage, - code: []const u8, - define_count: u32 = 0, - defines: ?[*]ShaderDefine = null, + defines: ?[*]const ShaderDefine = null, + + /// Returns a source that borrows `defines`. + pub inline fn withDefines( + self: ShaderSourceGLSL, + defines: []const ShaderDefine, + ) ShaderSourceGLSL { + var source = self; + source.define_count = @intCast(defines.len); + source.defines = if (defines.len == 0) null else defines.ptr; + return source; + } }; pub inline fn shaderModuleGLSLDescriptor( - descriptor: ShaderModuleGLSLMergedDescriptor, + source: *const ShaderSourceGLSL, + label: []const u8, ) ShaderModuleDescriptor { - return ShaderModuleDescriptor{ - .next_in_chain = @ptrCast(&ShaderSourceGLSL{ - .stage = descriptor.stage, - .code = StringView.fromSlice(descriptor.code), - .define_count = descriptor.define_count, - .defines = descriptor.defines, - }), - .label = StringView.fromSlice(descriptor.label), + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } -pub const CompilationInfoRequestStatus = enum(u32) { - success = 0x00000001, - callback_cancelled = 0x00000002, -}; +pub const ShaderModule = opaque { + pub const CompilationInfoRequestStatus = enum(u32) { + success = 0x00000001, + callback_cancelled = 0x00000002, + }; -pub const CompilationMessageType = enum(u32) { - @"error" = 0x00000001, - warning = 0x00000002, - info = 0x00000003, -}; + pub const CompilationMessageType = enum(u32) { + @"error" = 0x00000001, + warning = 0x00000002, + info = 0x00000003, + }; -pub const CompilationMessage = extern struct { - next_in_chain: ?*const ChainedStruct = null, - message: StringView, + pub const CompilationMessage = extern struct { + next_in_chain: ?*const ChainedStruct = null, + message: StringView, - // Severity level of the message. - type: CompilationMessageType, + // Severity level of the message. + type: CompilationMessageType, - // Line number where the message is attached, starting at 1. - line_num: u64, + // Line number where the message is attached, starting at 1. + line_num: u64, - // Offset in UTF-8 code units (bytes) from the beginning of the line, starting at 1. - line_pos: u64, + // Offset in UTF-8 code units (bytes) from the beginning of the line, starting at 1. + line_pos: u64, - // Offset in UTF-8 code units (bytes) from the beginning of the shader code, starting at 0. - offset: u64, + // Offset in UTF-8 code units (bytes) from the beginning of the shader code, starting at 0. + offset: u64, - // Length in UTF-8 code units (bytes) of the span the message corresponds to. - length: u64, -}; + // Length in UTF-8 code units (bytes) of the span the message corresponds to. + length: u64, + }; -pub const CompilationInfo = extern struct { - next_in_chain: ?*const ChainedStruct = null, - message_count: usize, - messages: [*]const CompilationMessage, -}; + pub const CompilationInfo = extern struct { + next_in_chain: ?*const ChainedStruct = null, + message_count: usize, + messages: [*]const CompilationMessage, + }; -pub const CompilationInfoCallback = *const fn (status: CompilationInfoRequestStatus, compilationInfo: ?*const CompilationInfo, userdata1: ?*anyopaque, userdata2: ?*anyopaque) callconv(.c) void; + pub const CompilationInfoCallback = *const fn ( + status: CompilationInfoRequestStatus, + compilation_info: ?*const CompilationInfo, + userdata1: ?*anyopaque, + userdata2: ?*anyopaque, + ) callconv(.c) void; -pub const CompilationInfoCallbackInfo = extern struct { - next_in_chain: ?*const ChainedStruct = null, + pub const CompilationInfoCallbackInfo = extern struct { + next_in_chain: ?*const ChainedStruct = null, - // TODO: Revisit this default if/when Instance.waitAny() is implemented. - mode: CallbackMode = CallbackMode.allow_process_events, + // TODO: Revisit this default if/when Instance.waitAny() is implemented. + mode: CallbackMode = .allow_process_events, - callback: CompilationInfoCallback, - userdata1: ?*anyopaque = null, - userdata2: ?*anyopaque = null, -}; + callback: CompilationInfoCallback, + userdata1: ?*anyopaque = null, + userdata2: ?*anyopaque = null, + }; -pub const ShaderModule = opaque { // Unimplemented as of wgpu-native v29.0.0.0, // see https://github.com/gfx-rs/wgpu-native/blob/d2e3330ade4ae1bb238d76b485926f067e7ee64c/src/unimplemented.rs // pub inline fn getCompilationInfo(self: *ShaderModule, callback_info: CompilationInfoCallbackInfo) Future { diff --git a/src/surface.zig b/src/surface.zig index e269275..be1f240 100644 --- a/src/surface.zig +++ b/src/surface.zig @@ -19,6 +19,7 @@ const Device = _device.Device; const _misc = @import("misc.zig"); const WGPUBool = _misc.WGPUBool; const StringView = _misc.StringView; +const sliceFromOptional = _misc.sliceFromOptional; const Status = _misc.Status; // The root descriptor for the creation of an Surface with Instance.createSurface(). @@ -39,16 +40,10 @@ pub const SurfaceSourceAndroidNativeWindow = extern struct { // The pointer to the [`ANativeWindow`](https://developer.android.com/ndk/reference/group/a-native-window) that will be wrapped by the Surface. window: *anyopaque, }; -pub const MergedSurfaceDescriptorFromAndroidWindow = struct { - label: []const u8 = "", - window: *anyopaque, -}; -pub inline fn surfaceDescriptorFromAndroidNativeWindow(descriptor: MergedSurfaceDescriptorFromAndroidWindow) SurfaceDescriptor { - return SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceSourceAndroidNativeWindow{ - .window = descriptor.window, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn surfaceDescriptorFromAndroidNativeWindow(source: *const SurfaceSourceAndroidNativeWindow, label: []const u8) SurfaceDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -61,16 +56,10 @@ pub const SurfaceSourceMetalLayer = extern struct { // The pointer to the [`CAMetalLayer`](https://developer.apple.com/documentation/quartzcore/cametallayer?language=objc) that will be wrapped by the Surface. layer: *anyopaque, }; -pub const MergedSurfaceDescriptorFromMetalLayer = struct { - label: []const u8 = "", - layer: *anyopaque, -}; -pub inline fn surfaceDescriptorFromMetalLayer(descriptor: MergedSurfaceDescriptorFromMetalLayer) SurfaceDescriptor { - return SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceSourceMetalLayer{ - .layer = descriptor.layer, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn surfaceDescriptorFromMetalLayer(source: *const SurfaceSourceMetalLayer, label: []const u8) SurfaceDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -86,18 +75,10 @@ pub const SurfaceSourceWaylandSurface = extern struct { // A [`wl_surface`](https://wayland.freedesktop.org/docs/html/apa.html#protocol-spec-wl_surface) that will be wrapped by the Surface surface: *anyopaque, }; -pub const MergedSurfaceDescriptorFromWaylandSurface = struct { - label: []const u8 = "", - display: *anyopaque, - surface: *anyopaque, -}; -pub inline fn surfaceDescriptorFromWaylandSurface(descriptor: MergedSurfaceDescriptorFromWaylandSurface) SurfaceDescriptor { - return SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceSourceWaylandSurface{ - .display = descriptor.display, - .surface = descriptor.surface, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn surfaceDescriptorFromWaylandSurface(source: *const SurfaceSourceWaylandSurface, label: []const u8) SurfaceDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -114,18 +95,10 @@ pub const SurfaceSourceWindowsHWND = extern struct { // The [`HWND`](https://learn.microsoft.com/en-us/windows/apps/develop/ui-input/retrieve-hwnd) that will be wrapped by the Surface. hwnd: *anyopaque, }; -pub const MergedSurfaceDescriptorFromWindowsHWND = struct { - label: []const u8 = "", - hinstance: *anyopaque, - hwnd: *anyopaque, -}; -pub inline fn surfaceDescriptorFromWindowsHWND(descriptor: MergedSurfaceDescriptorFromWindowsHWND) SurfaceDescriptor { - return SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceSourceWindowsHWND{ - .hinstance = descriptor.hinstance, - .hwnd = descriptor.hwnd, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn surfaceDescriptorFromWindowsHWND(source: *const SurfaceSourceWindowsHWND, label: []const u8) SurfaceDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -141,18 +114,10 @@ pub const SurfaceSourceXCBWindow = extern struct { // The `xcb_window_t` for the window that will be wrapped by the Surface. window: u32, }; -pub const MergedSurfaceDescriptorFromXcbWindow = struct { - label: []const u8 = "", - connection: *anyopaque, - window: u32, -}; -pub inline fn surfaceDescriptorFromXcbWindow(descriptor: MergedSurfaceDescriptorFromXcbWindow) SurfaceDescriptor { - return SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceSourceXCBWindow{ - .connection = descriptor.connection, - .window = descriptor.window, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn surfaceDescriptorFromXcbWindow(source: *const SurfaceSourceXCBWindow, label: []const u8) SurfaceDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -168,18 +133,10 @@ pub const SurfaceSourceXlibWindow = extern struct { // The [`Window`](https://www.x.org/releases/current/doc/libX11/libX11/libX11.html#Creating_Windows) that will be wrapped by the Surface. window: u64, }; -pub const MergedSurfaceDescriptorFromXlibWindow = struct { - label: []const u8 = "", - display: *anyopaque, - window: u64, -}; -pub inline fn surfaceDescriptorFromXlibWindow(descriptor: MergedSurfaceDescriptorFromXlibWindow) SurfaceDescriptor { - return SurfaceDescriptor{ - .next_in_chain = @ptrCast(&SurfaceSourceXlibWindow{ - .display = descriptor.display, - .window = descriptor.window, - }), - .label = StringView.fromSlice(descriptor.label), +pub inline fn surfaceDescriptorFromXlibWindow(source: *const SurfaceSourceXlibWindow, label: []const u8) SurfaceDescriptor { + return .{ + .next_in_chain = &source.chain, + .label = StringView.fromSlice(label), }; } @@ -290,12 +247,21 @@ pub const SurfaceConfiguration = extern struct { // When and in which order the surface's frames will be shown on the screen. present_mode: PresentMode = PresentMode.fifo, - pub inline fn withDesiredMaxFrameLatency(self: SurfaceConfiguration, desired_max_frame_latency: u32) SurfaceConfiguration { - var sc = self; - sc.next_in_chain = @ptrCast(&SurfaceConfigurationExtras{ - .desired_maximum_frame_latency = desired_max_frame_latency, - }); - return sc; + /// Returns a configuration that borrows `view_formats`. + pub inline fn withViewFormats( + self: SurfaceConfiguration, + view_formats: []const TextureFormat, + ) SurfaceConfiguration { + var configuration = self; + configuration.view_format_count = view_formats.len; + configuration.view_formats = view_formats.ptr; + return configuration; + } + + pub inline fn withExtras(self: SurfaceConfiguration, extras: *const SurfaceConfigurationExtras) SurfaceConfiguration { + var configuration = self; + configuration.next_in_chain = @ptrCast(extras); + return configuration; } }; @@ -305,25 +271,58 @@ pub const SurfaceCapabilities = extern struct { // The bit set of supported TextureUsage bits. // Guaranteed to contain TextureUsage.render_attachment. - usages: TextureUsage, + usages: TextureUsage = TextureUsages.none, // A list of supported TextureFormat values, in order of preference. - format_count: usize, - formats: [*]const TextureFormat, + format_count: usize = 0, + formats: ?[*]const TextureFormat = null, // A list of supported PresentMode values. // Guaranteed to contain PresentMode.fifo. - present_mode_count: usize, - present_modes: [*]const PresentMode, + present_mode_count: usize = 0, + present_modes: ?[*]const PresentMode = null, // A list of supported CompositeAlphaMode values. // CompositeAlphaMode.auto will be an alias for the first element and will never be present in this array. - alpha_mode_count: usize, - alpha_modes: [*]const CompositeAlphaMode, + alpha_mode_count: usize = 0, + alpha_modes: ?[*]const CompositeAlphaMode = null, + + pub inline fn formatsSlice( + self: *const SurfaceCapabilities, + ) []const TextureFormat { + return sliceFromOptional(TextureFormat, self.formats, self.format_count); + } + + pub inline fn presentModesSlice( + self: *const SurfaceCapabilities, + ) []const PresentMode { + return sliceFromOptional( + PresentMode, + self.present_modes, + self.present_mode_count, + ); + } + + pub inline fn alphaModesSlice( + self: *const SurfaceCapabilities, + ) []const CompositeAlphaMode { + return sliceFromOptional( + CompositeAlphaMode, + self.alpha_modes, + self.alpha_mode_count, + ); + } // Frees array members of SurfaceCapabilities which were allocated by the API. - pub inline fn freeMembers(self: SurfaceCapabilities) void { - raw.call(void, "wgpuSurfaceCapabilitiesFreeMembers", .{self}); + pub inline fn deinit(self: *SurfaceCapabilities) void { + raw.call(void, "wgpuSurfaceCapabilitiesFreeMembers", .{self.*}); + self.usages = TextureUsages.none; + self.format_count = 0; + self.formats = null; + self.present_mode_count = 0; + self.present_modes = null; + self.alpha_mode_count = 0; + self.alpha_modes = null; } }; @@ -350,18 +349,30 @@ pub const GetCurrentTextureStatus = enum(u32) { // wgpu-native extension: the surface is currently occluded. occluded = 0x00030001, + _, }; // Queried each frame from a Surface to get a Texture to render to along with some metadata. pub const SurfaceTexture = extern struct { - next_in_chain: ?*ChainedStructOut, + next_in_chain: ?*ChainedStructOut = null, // The Texture representing the frame that will be shown on the surface. // It is ReturnedWithOwnership from Surface.getCurrentTexture(). - texture: ?*Texture, + texture: ?*Texture = null, // Whether the call to Surface.getCurrentTexture() succeeded and a hint as to why it might not have. - status: GetCurrentTextureStatus, + status: GetCurrentTextureStatus = @enumFromInt(0), + + pub inline fn deinit(self: *SurfaceTexture) void { + if (self.texture) |texture| texture.release(); + self.texture = null; + } + + pub inline fn takeTexture(self: *SurfaceTexture) ?*Texture { + const texture = self.texture; + self.texture = null; + return texture; + } }; pub const Surface = opaque { @@ -376,7 +387,7 @@ pub const Surface = opaque { // // capabilities // The structure to fill capabilities in. - // It may contain memory allocations so `capabilities.freeMembers()` must be called to avoid memory leaks. + // It may contain memory allocations so `capabilities.deinit()` must be called to avoid memory leaks. // // Return value indicates if there was an OutStructChainError. // diff --git a/src/texture.zig b/src/texture.zig index 0de3e89..c939565 100644 --- a/src/texture.zig +++ b/src/texture.zig @@ -9,9 +9,6 @@ const U32_MAX = _misc.U32_MAX; pub const WGPU_ARRAY_LAYER_COUNT_UNDEFINED = U32_MAX; pub const WGPU_MIP_LEVEL_COUNT_UNDEFINED = U32_MAX; -pub const WGPU_COPY_STRIDE_UNDEFINED = U32_MAX; - -const Buffer = @import("buffer.zig").Buffer; pub const TextureFormat = enum(u32) { undefined = 0x00000000, // Indicates no value is passed for this argument. @@ -139,9 +136,6 @@ pub const TextureUsages = struct { pub const transient_attachment = @as(TextureUsage, 0x0000000000000020); }; -// TODO: Like a lot of things in this file, this breaks from the wrapper code convention by having an unneeded prefix ("Texture") -// in front of the name, even though "Aspect" is exclusively used in TextureAspect. I've done this because just calling -// it "Aspect" seems like it'd confuse people thinking it is an aspect ratio or something, but should it just be "Aspect"? pub const TextureAspect = enum(u32) { undefined = 0x00000000, // Indicates no value is passed for this argument. all = 0x00000001, @@ -201,8 +195,7 @@ pub const TextureView = opaque { } }; -// TODO: Should this maybe go in sampler.zig instead? -pub const SampleType = enum(u32) { +pub const TextureSampleType = enum(u32) { // Indicates that this TextureBindingLayout member of its parent BindGroupLayoutEntry is not used. binding_not_used = 0x00000000, @@ -235,7 +228,7 @@ pub const TextureBindingViewDimension = extern struct { pub const TextureBindingLayout = extern struct { next_in_chain: ?*const ChainedStruct = null, - sample_type: SampleType = SampleType.undefined, + sample_type: TextureSampleType = .undefined, view_dimension: ViewDimension = ViewDimension.@"2d", multisampled: WGPUBool = @intFromBool(false), }; @@ -283,8 +276,22 @@ pub const TextureDescriptor = extern struct { sample_count: u32 = 1, view_format_count: usize = 0, view_formats: [*]const TextureFormat = &[_]TextureFormat{}, + + /// Returns a descriptor that borrows `view_formats`. + pub inline fn withViewFormats( + self: TextureDescriptor, + view_formats: []const TextureFormat, + ) TextureDescriptor { + var descriptor = self; + descriptor.view_format_count = view_formats.len; + descriptor.view_formats = view_formats.ptr; + return descriptor; + } }; +/// Borrowed backend-native `id` returned by wgpu-native. +pub const NativeMetalTexture = opaque {}; + pub const Texture = opaque { pub inline fn createView(self: *Texture, descriptor: ?*const TextureViewDescriptor) ?*TextureView { return raw.call(?*TextureView, "wgpuTextureCreateView", .{ self, descriptor }); @@ -329,30 +336,10 @@ pub const Texture = opaque { pub inline fn release(self: *Texture) void { raw.call(void, "wgpuTextureRelease", .{self}); } -}; - -pub const Origin3D = extern struct { - x: u32 = 0, - y: u32 = 0, - z: u32 = 0, -}; -pub const TexelCopyTextureInfo = extern struct { - texture: *Texture, - mip_level: u32 = 0, - origin: Origin3D, - aspect: TextureAspect = TextureAspect.all, -}; - -pub const TexelCopyBufferLayout = extern struct { - offset: u64 = 0, - bytes_per_row: u32 = WGPU_COPY_STRIDE_UNDEFINED, - rows_per_image: u32 = WGPU_COPY_STRIDE_UNDEFINED, -}; - -// Seems a little weird to put this in texture.zig, -// but it seems to have more to do with images/textures than with buffers. -pub const TexelCopyBufferInfo = extern struct { - layout: TexelCopyBufferLayout, - buffer: *Buffer, + /// Returns a borrowed Metal texture when this texture uses the Metal backend. + /// The pointer remains valid only while `self` is alive and must not be released. + pub inline fn getNativeMetalTexture(self: *Texture) ?*NativeMetalTexture { + return raw.call(?*NativeMetalTexture, "wgpuTextureGetNativeMetalTexture", .{self}); + } }; diff --git a/tests/abi.zig b/tests/abi.zig index 5e99d9e..2086abe 100644 --- a/tests/abi.zig +++ b/tests/abi.zig @@ -2,6 +2,16 @@ const std = @import("std"); const wgpu = @import("wgpu"); const c = @import("wgpu-c"); +const wrapper_namespaces = .{ + wgpu, + wgpu.Instance, + wgpu.Adapter, + wgpu.Buffer, + wgpu.Device, + wgpu.Queue, + wgpu.ShaderModule, +}; + fn cTypeName(comptime zig_name: []const u8) []const u8 { if (std.mem.eql(u8, zig_name, "ColorAttachment")) return "WGPURenderPassColorAttachment"; @@ -11,12 +21,16 @@ fn cTypeName(comptime zig_name: []const u8) []const u8 { return "WGPUInstanceEnumerateAdapterOptions"; if (std.mem.eql(u8, zig_name, "GetCurrentTextureStatus")) return "WGPUSurfaceGetCurrentTextureStatus"; - if (std.mem.eql(u8, zig_name, "SampleType")) - return "WGPUTextureSampleType"; + if (std.mem.eql(u8, zig_name, "MapState")) + return "WGPUBufferMapState"; + if (std.mem.eql(u8, zig_name, "MapCallbackInfo")) + return "WGPUBufferMapCallbackInfo"; if (std.mem.eql(u8, zig_name, "ViewDimension")) return "WGPUTextureViewDimension"; if (std.mem.eql(u8, zig_name, "WorkDoneStatus")) return "WGPUQueueWorkDoneStatus"; + if (std.mem.eql(u8, zig_name, "WorkDoneCallbackInfo")) + return "WGPUQueueWorkDoneCallbackInfo"; if (std.mem.startsWith(u8, zig_name, "WGPU")) return zig_name; return "WGPU" ++ zig_name; @@ -64,154 +78,224 @@ test "all wrapper methods compile against the C headers" { } } -test "pure Zig structs match the C ABI" { +test "wrapper types match the C ABI" { comptime { - @setEvalBranchQuota(10_000_000); - for (std.meta.declarations(wgpu)) |declaration| { - const zig_declaration = @field(wgpu, declaration.name); - if (@TypeOf(zig_declaration) != type) continue; + @setEvalBranchQuota(50_000_000); + for (wrapper_namespaces) |namespace| { + validateNamespaceAbi(namespace); + } + } +} - const c_name = cTypeName(declaration.name); - if (!@hasDecl(c, c_name)) continue; +test "wrapper defaults and pointer qualifiers match header semantics" { + comptime { + const descriptor = wgpu.DeviceDescriptor{}; + if (descriptor.required_limits != null) { + @compileError("DeviceDescriptor.required_limits must default to null"); + } + const extras = wgpu.DeviceExtras{}; + if (extras.trace_path.data != null or + extras.trace_path.length != wgpu.WGPU_STRLEN) + { + @compileError("DeviceExtras.trace_path must use the StringView initializer"); + } - const ZigType = zig_declaration; - const CType = @field(c, c_name); - if (@TypeOf(CType) != type) continue; + const instance_features = wgpu.SupportedInstanceFeatures{}; + if (instance_features.feature_count != 0 or + instance_features.features != null) + { + @compileError("SupportedInstanceFeatures must use the C initializer defaults"); + } + const wgsl_features = wgpu.SupportedWGSLLanguageFeatures{}; + if (wgsl_features.feature_count != 0 or wgsl_features.features != null) { + @compileError("SupportedWGSLLanguageFeatures must use the C initializer defaults"); + } - switch (@typeInfo(ZigType)) { - .@"struct", .@"union", .@"enum" => {}, - else => continue, - } - switch (@typeInfo(CType)) { - .@"struct", .@"union", .@"enum", .int => {}, - else => continue, - } + requireOptionalConstManyPointer( + @FieldType(wgpu.ShaderSourceGLSL, "defines"), + wgpu.ShaderDefine, + "ShaderSourceGLSL.defines", + ); + const enumerate_info = @typeInfo(@TypeOf( + wgpu.Instance.enumerateAdapters, + )).@"fn"; + requireOptionalConstOnePointer( + enumerate_info.params[2].type.?, + wgpu.EnumerateAdapterOptions, + "Instance.enumerateAdapters options", + ); + } +} - if (@typeInfo(ZigType) == .@"enum") { - const zig_fields = @typeInfo(ZigType).@"enum".fields; - var c_values: [256]u64 = undefined; - var c_names: [256][]const u8 = undefined; - var c_value_count = 0; - for (std.meta.declarations(c)) |c_declaration| { - if (!isEnumConstant( - c_declaration.name, - declaration.name, - c_name, - )) continue; - const c_value = @field(c, c_declaration.name); - if (@TypeOf(c_value) == type) continue; - - c_values[c_value_count] = @intCast(c_value); - c_names[c_value_count] = c_declaration.name; - c_value_count += 1; - } - for ( - c_values[0..c_value_count], - c_names[0..c_value_count], - ) |c_value, c_declaration_name| { - var found = false; - for (zig_fields) |zig_field| { - const zig_value: u64 = @intFromEnum( - @field(ZigType, zig_field.name), - ); - if (zig_value == c_value) found = true; - } - if (!found) { - @compileError(std.fmt.comptimePrint( - "{s} is missing {s} ({d})", - .{ declaration.name, c_declaration_name, c_value }, - )); - } - } - for (zig_fields) |zig_field| { - const zig_value: u64 = @intFromEnum( - @field(ZigType, zig_field.name), - ); - var found = false; - for (c_values[0..c_value_count]) |c_value| { - if (zig_value == c_value) found = true; - } - if (!found) { - @compileError(std.fmt.comptimePrint( - "{s}.{s} ({d}) is not present in the C API", - .{ declaration.name, zig_field.name, zig_value }, - )); - } - } - } +fn requireOptionalConstManyPointer( + comptime Pointer: type, + comptime Child: type, + comptime name: []const u8, +) void { + const optional = switch (@typeInfo(Pointer)) { + .optional => |info| info, + else => @compileError(name ++ " must be optional"), + }; + const pointer = switch (@typeInfo(optional.child)) { + .pointer => |info| info, + else => @compileError(name ++ " must contain a pointer"), + }; + if (pointer.size != .many or !pointer.is_const or pointer.child != Child) { + @compileError(name ++ " must be a const many-item pointer"); + } +} - if (@sizeOf(ZigType) != @sizeOf(CType)) { - @compileError(std.fmt.comptimePrint( - "{s} has size {d}, but {s} has size {d}", - .{ - declaration.name, - @sizeOf(ZigType), - c_name, - @sizeOf(CType), - }, - )); - } - if (@alignOf(ZigType) != @alignOf(CType)) { - @compileError(std.fmt.comptimePrint( - "{s} has alignment {d}, but {s} has alignment {d}", - .{ - declaration.name, - @alignOf(ZigType), - c_name, - @alignOf(CType), - }, - )); - } +fn requireOptionalConstOnePointer( + comptime Pointer: type, + comptime Child: type, + comptime name: []const u8, +) void { + const optional = switch (@typeInfo(Pointer)) { + .optional => |info| info, + else => @compileError(name ++ " must be optional"), + }; + const pointer = switch (@typeInfo(optional.child)) { + .pointer => |info| info, + else => @compileError(name ++ " must contain a pointer"), + }; + if (pointer.size != .one or !pointer.is_const or pointer.child != Child) { + @compileError(name ++ " must be a const single-item pointer"); + } +} - if (@typeInfo(ZigType) == .@"struct" and - @typeInfo(CType) == .@"struct") - { - const zig_fields = @typeInfo(ZigType).@"struct".fields; - const c_fields = @typeInfo(CType).@"struct".fields; - if (zig_fields.len != c_fields.len) { - @compileError(std.fmt.comptimePrint( - "{s} has {d} fields, but {s} has {d}", - .{ - declaration.name, - zig_fields.len, - c_name, - c_fields.len, - }, - )); - } - for (zig_fields, 0..) |zig_field, index| { - if (index >= c_fields.len) continue; - const c_field = c_fields[index]; - if (@offsetOf(ZigType, zig_field.name) != - @offsetOf(CType, c_field.name)) - { - @compileError(std.fmt.comptimePrint( - "{s}.{s} has offset {d}, but {s}.{s} has offset {d}", - .{ - declaration.name, - zig_field.name, - @offsetOf(ZigType, zig_field.name), - c_name, - c_field.name, - @offsetOf(CType, c_field.name), - }, - )); - } - if (@sizeOf(zig_field.type) != @sizeOf(c_field.type)) { - @compileError(std.fmt.comptimePrint( - "{s}.{s} has size {d}, but {s}.{s} has size {d}", - .{ - declaration.name, - zig_field.name, - @sizeOf(zig_field.type), - c_name, - c_field.name, - @sizeOf(c_field.type), - }, - )); - } - } - } +fn validateNamespaceAbi(comptime namespace: type) void { + for (std.meta.declarations(namespace)) |declaration| { + const ZigType = @field(namespace, declaration.name); + if (@TypeOf(ZigType) != type) continue; + + const c_name = cTypeName(declaration.name); + if (!@hasDecl(c, c_name)) continue; + const CType = @field(c, c_name); + if (@TypeOf(CType) != type) continue; + + switch (@typeInfo(ZigType)) { + .@"struct", .@"union", .@"enum" => {}, + else => continue, + } + switch (@typeInfo(CType)) { + .@"struct", .@"union", .@"enum", .int => {}, + else => continue, + } + + if (@sizeOf(ZigType) != @sizeOf(CType) or + @alignOf(ZigType) != @alignOf(CType)) + { + @compileError(std.fmt.comptimePrint( + "{s}.{s} does not match the ABI of {s}", + .{ @typeName(namespace), declaration.name, c_name }, + )); + } + + if (@typeInfo(ZigType) == .@"enum") { + validateEnumValues( + namespace, + declaration.name, + c_name, + ZigType, + ); + } + if (@typeInfo(ZigType) == .@"struct" and + @typeInfo(CType) == .@"struct") + { + validateStructFields( + namespace, + declaration.name, + c_name, + ZigType, + CType, + ); + } + } +} + +fn validateEnumValues( + comptime namespace: type, + comptime zig_name: []const u8, + comptime c_name: []const u8, + comptime ZigType: type, +) void { + const zig_fields = @typeInfo(ZigType).@"enum".fields; + for (std.meta.declarations(c)) |c_declaration| { + if (!isEnumConstant(c_declaration.name, zig_name, c_name)) continue; + const c_value = @field(c, c_declaration.name); + if (@TypeOf(c_value) == type) continue; + + var found = false; + for (zig_fields) |zig_field| { + const zig_value: u64 = @intFromEnum( + @field(ZigType, zig_field.name), + ); + if (zig_value == @as(u64, @intCast(c_value))) found = true; + } + if (!found) { + @compileError(std.fmt.comptimePrint( + "{s}.{s} is missing {s}", + .{ @typeName(namespace), zig_name, c_declaration.name }, + )); + } + } + + for (zig_fields) |zig_field| { + const zig_value: u64 = @intFromEnum( + @field(ZigType, zig_field.name), + ); + var found = false; + for (std.meta.declarations(c)) |c_declaration| { + if (!isEnumConstant(c_declaration.name, zig_name, c_name)) continue; + const c_value = @field(c, c_declaration.name); + if (@TypeOf(c_value) == type) continue; + if (zig_value == @as(u64, @intCast(c_value))) found = true; + } + if (!found) { + @compileError(std.fmt.comptimePrint( + "{s}.{s}.{s} is not present in {s}", + .{ @typeName(namespace), zig_name, zig_field.name, c_name }, + )); + } + } +} + +fn validateStructFields( + comptime namespace: type, + comptime zig_name: []const u8, + comptime c_name: []const u8, + comptime ZigType: type, + comptime CType: type, +) void { + const zig_fields = @typeInfo(ZigType).@"struct".fields; + const c_fields = @typeInfo(CType).@"struct".fields; + if (zig_fields.len != c_fields.len) { + @compileError(std.fmt.comptimePrint( + "{s}.{s} has {d} fields, but {s} has {d}", + .{ + @typeName(namespace), + zig_name, + zig_fields.len, + c_name, + c_fields.len, + }, + )); + } + for (zig_fields, c_fields) |zig_field, c_field| { + if (@offsetOf(ZigType, zig_field.name) != + @offsetOf(CType, c_field.name) or + @sizeOf(zig_field.type) != @sizeOf(c_field.type)) + { + @compileError(std.fmt.comptimePrint( + "{s}.{s}.{s} does not match {s}.{s}", + .{ + @typeName(namespace), + zig_name, + zig_field.name, + c_name, + c_field.name, + }, + )); } } } diff --git a/tests/bindings.zig b/tests/bindings.zig new file mode 100644 index 0000000..d55d5f8 --- /dev/null +++ b/tests/bindings.zig @@ -0,0 +1,8 @@ +const audit = @import("binding-audit"); + +test "wrapper covers every implemented wgpu-native v29 function" { + comptime { + @setEvalBranchQuota(10_000_000); + audit.validate(); + } +} diff --git a/tests/build.zig b/tests/build.zig index 8e89e0b..a4f8d40 100644 --- a/tests/build.zig +++ b/tests/build.zig @@ -7,17 +7,51 @@ pub fn build(b: *std.Build) void { "check", "Compile the bindings and all target-compatible tests", ); + const audit_step = b.step( + "audit", + "Compile the binding coverage and ABI audits", + ); + + bindingTest(b, library, audit_step); + abiTest(b, library, audit_step); + check_step.dependOn(audit_step); - if (library.isOhos()) { - linkProbe(b, library, check_step); + if (library.isOhos() or library.isAndroid() or library.isIos()) { + // Rust's legacy x86_64-apple-ios target emits the historical + // x86_64-ios Mach-O platform marker. Zig 0.16's linker rejects that + // marker for an explicit simulator output, so this target can run the + // compile-time audits but not a Zig link probe. + if (!library.isIos() or + library.target.result.cpu.arch != .x86_64) + { + linkProbe(b, library, check_step); + } return; } unitTests(b, library, check_step); - abiTest(b, library, check_step); computeTests(b, library, check_step); } +fn bindingTest( + b: *std.Build, + library: Library.Result, + check_step: *std.Build.Step, +) void { + const test_mod = b.createModule(.{ + .root_source_file = b.path("tests/bindings.zig"), + .target = library.target, + .optimize = library.optimize, + }); + test_mod.addImport("binding-audit", bindingAuditModule(b, library)); + const test_exe = b.addTest(.{ + .name = "bindings-test", + .root_module = test_mod, + }); + library.configureCompile(test_exe); + check_step.dependOn(&test_exe.step); +} + fn abiTest( b: *std.Build, library: Library.Result, @@ -44,27 +78,33 @@ fn unitTests( check_step: *std.Build.Step, ) void { const unit_test_step = b.step("test", "Run unit tests"); - const test_files = [_][:0]const u8{ - "src/raw.zig", - "src/instance.zig", - "src/adapter.zig", - "src/pipeline.zig", - }; - comptime var test_names: [test_files.len][:0]const u8 = test_files; - comptime for (test_files, 0..) |test_file, index| { - test_names[index] = test_file[4..(test_file.len - 4)] ++ "-test"; + const unit_tests = .{ + .{ .path = "src/raw.zig", .name = "raw-test" }, + .{ .path = "src/misc.zig", .name = "misc-test" }, + .{ .path = "src/async.zig", .name = "async-test" }, + .{ .path = "src/instance.zig", .name = "instance-test" }, + .{ .path = "src/adapter.zig", .name = "adapter-test" }, + .{ .path = "src/buffer.zig", .name = "buffer-test" }, + .{ .path = "src/device.zig", .name = "device-test" }, + .{ .path = "src/pipeline.zig", .name = "pipeline-test" }, + .{ .path = "src/queue.zig", .name = "queue-test" }, + .{ .path = "tests/lifetimes.zig", .name = "lifetimes-test" }, + .{ .path = "tests/ownership.zig", .name = "ownership-test" }, }; - for (test_files, test_names) |test_file, test_name| { + inline for (unit_tests) |unit_test| { const test_mod = b.createModule(.{ - .root_source_file = b.path(test_file), + .root_source_file = b.path(unit_test.path), .target = library.target, .optimize = library.optimize, }); test_mod.addImport("wgpu-header", library.wgpu_c_mod); - library.linkTestModule(b, test_mod); + if (std.mem.startsWith(u8, unit_test.path, "tests/")) { + test_mod.addImport("wgpu", library.wgpu_mod); + } + library.linkTestModule(test_mod); const test_exe = b.addTest(.{ - .name = test_name, + .name = unit_test.name, .root_module = test_mod, }); library.configureCompile(test_exe); @@ -136,3 +176,17 @@ fn linkProbe( _ = probe.getEmittedBin(); check_step.dependOn(&probe.step); } + +fn bindingAuditModule( + b: *std.Build, + library: Library.Result, +) *std.Build.Module { + const audit_mod = b.createModule(.{ + .root_source_file = b.path("src/binding_audit.zig"), + .target = library.target, + .optimize = library.optimize, + }); + audit_mod.addImport("wgpu-header", library.wgpu_c_mod); + audit_mod.addImport("wgpu-wrapper", library.wgpu_mod); + return audit_mod; +} diff --git a/tests/compute.zig b/tests/compute.zig index d942da0..e31332d 100644 --- a/tests/compute.zig +++ b/tests/compute.zig @@ -3,12 +3,6 @@ const testing = std.testing; const wgpu = @import("wgpu"); -fn handleBufferMap(status: wgpu.MapAsyncStatus, _: wgpu.StringView, userdata1: ?*anyopaque, _: ?*anyopaque) callconv(.c) void { - std.log.info("buffer_map status={x:.8}\n", .{@intFromEnum(status)}); - const completed: *bool = @ptrCast(@alignCast(userdata1)); - completed.* = true; -} - fn compute_collatz() ![4]u32 { const numbers = [_]u32{ 1, 2, 3, 4 }; const numbers_size = @sizeOf(@TypeOf(numbers)); @@ -17,27 +11,45 @@ fn compute_collatz() ![4]u32 { const instance = wgpu.Instance.create(null).?; defer instance.release(); - const adapter_response = try instance.requestAdapterSync(std.testing.io, null, 200_000_000); + var adapter_response = try instance.requestAdapterSync(testing.allocator, testing.io, null, 200_000_000); + defer adapter_response.deinit(testing.allocator); const adapter = switch (adapter_response.status) { - .success => adapter_response.adapter.?, + .success => adapter_response.takeAdapter().?, else => return error.NoAdapter, }; defer adapter.release(); - const device_response = try adapter.requestDeviceSync(std.testing.io, instance, null, 200_000_000); + var device_response = try adapter.requestDeviceSync(testing.allocator, testing.io, instance, null, 200_000_000); + defer device_response.deinit(testing.allocator); const device = switch (device_response.status) { - .success => device_response.device.?, + .success => device_response.takeDevice().?, else => return error.NoDevice, }; defer device.release(); + device.pushErrorScope(.validation); + var error_scope = try device.popErrorScopeSync( + testing.allocator, + testing.io, + instance, + 200_000_000, + ); + defer error_scope.deinit(testing.allocator); + try testing.expectEqual( + wgpu.Device.PopErrorScopeStatus.success, + error_scope.status, + ); + try testing.expectEqual(wgpu.Device.ErrorType.no_error, error_scope.error_type); + const queue = device.getQueue().?; defer queue.release(); + try testing.expect(queue.getTimestampPeriod() > 0); - const shader_module = device.createShaderModule(&wgpu.shaderModuleWGSLDescriptor(.{ - .label = "compute.wgsl", - .code = @embedFile("./compute.wgsl"), - })).?; + const shader_source = wgpu.ShaderSourceWGSL{ + .code = wgpu.StringView.fromSlice(@embedFile("./compute.wgsl")), + }; + const shader_descriptor = wgpu.shaderModuleWGSLDescriptor(&shader_source, "compute.wgsl"); + const shader_module = device.createShaderModule(&shader_descriptor).?; defer shader_module.release(); const staging_buffer = device.createBuffer(&wgpu.BufferDescriptor{ @@ -68,17 +80,16 @@ fn compute_collatz() ![4]u32 { const bind_group_layout = compute_pipeline.getBindGroupLayout(0).?; defer bind_group_layout.release(); - const bind_group = device.createBindGroup(&wgpu.BindGroupDescriptor{ - .label = wgpu.StringView.fromSlice("bind_group"), - .layout = bind_group_layout, - .entry_count = 1, - .entries = &[_]wgpu.BindGroupEntry{wgpu.BindGroupEntry{ - .binding = 0, - .buffer = storage_buffer, - .offset = 0, - .size = numbers_size, - }}, - }).?; + const bind_group_entries = [_]wgpu.BindGroupEntry{wgpu.BindGroupEntry{ + .binding = 0, + .buffer = storage_buffer, + .offset = 0, + .size = numbers_size, + }}; + var bind_group_descriptor = + wgpu.BindGroupDescriptor.init(bind_group_layout, &bind_group_entries); + bind_group_descriptor.label = wgpu.StringView.fromSlice("bind_group"); + const bind_group = device.createBindGroup(&bind_group_descriptor).?; defer bind_group.release(); const command_encoder = device.createCommandEncoder(&wgpu.CommandEncoderDescriptor{ @@ -91,7 +102,7 @@ fn compute_collatz() ![4]u32 { }).?; compute_pass_encoder.setPipeline(compute_pipeline); - compute_pass_encoder.setBindGroup(0, bind_group, 0, null); + compute_pass_encoder.setBindGroup(0, bind_group, &.{}); compute_pass_encoder.dispatchWorkgroups(numbers_length, 1, 1); compute_pass_encoder.end(); @@ -105,20 +116,35 @@ fn compute_collatz() ![4]u32 { }).?; defer command_buffer.release(); - queue.writeBuffer(storage_buffer, 0, &numbers, numbers_size); + queue.writeBuffer(storage_buffer, 0, std.mem.asBytes(&numbers)); queue.submit(&[_]*const wgpu.CommandBuffer{command_buffer}); - var buffer_map_complete = false; - _ = staging_buffer.mapAsync(wgpu.MapModes.read, 0, numbers_size, wgpu.BufferMapCallbackInfo{ - .callback = handleBufferMap, - .userdata1 = @ptrCast(&buffer_map_complete), - }); - instance.processEvents(); - while (!buffer_map_complete) { - instance.processEvents(); - } - - const buf: [*]u32 = @ptrCast(@alignCast(staging_buffer.getMappedRange(0, numbers_size).?)); + var work_done = try queue.onSubmittedWorkDoneSync( + testing.allocator, + testing.io, + instance, + 200_000_000, + ); + defer work_done.deinit(testing.allocator); + try testing.expectEqual(wgpu.Queue.WorkDoneStatus.success, work_done.status); + + var map_response = try staging_buffer.mapSync( + testing.allocator, + testing.io, + instance, + wgpu.Buffer.MapModes.read, + 0, + numbers_size, + 200_000_000, + ); + defer map_response.deinit(testing.allocator); + try testing.expectEqual(wgpu.Buffer.MapAsyncStatus.success, map_response.status); + + const mapped = staging_buffer.getConstMappedRange( + 0, + wgpu.WGPU_WHOLE_MAP_SIZE, + ).?; + const buf: [*]const u32 = @ptrCast(@alignCast(mapped.ptr)); defer staging_buffer.unmap(); const ret = [4]u32{ buf[0], buf[1], buf[2], buf[3] }; diff --git a/tests/lifetimes.zig b/tests/lifetimes.zig new file mode 100644 index 0000000..b5c45a6 --- /dev/null +++ b/tests/lifetimes.zig @@ -0,0 +1,307 @@ +const std = @import("std"); +const testing = std.testing; + +const wgpu = @import("wgpu"); + +fn expectChain(expected: *const wgpu.ChainedStruct, actual: ?*const wgpu.ChainedStruct) !void { + try testing.expect(actual != null); + try testing.expectEqual(@intFromPtr(expected), @intFromPtr(actual.?)); +} + +fn expectPointer(expected: anytype, actual: anytype) !void { + try testing.expectEqual(@intFromPtr(expected), @intFromPtr(actual)); +} + +test "shader descriptor helpers borrow caller-owned sources" { + const spirv_code = [_]u32{0}; + const spirv_source = wgpu.ShaderSourceSPIRV.init(&spirv_code); + const spirv_descriptor = wgpu.shaderModuleSPIRVDescriptor(&spirv_source, "SPIR-V"); + try expectChain(&spirv_source.chain, spirv_descriptor.next_in_chain); + + const wgsl_source = wgpu.ShaderSourceWGSL{ + .code = wgpu.StringView.fromSlice("@compute @workgroup_size(1) fn main() {}"), + }; + const wgsl_descriptor = wgpu.shaderModuleWGSLDescriptor(&wgsl_source, "WGSL"); + try expectChain(&wgsl_source.chain, wgsl_descriptor.next_in_chain); + + const glsl_source = wgpu.ShaderSourceGLSL{ + .stage = wgpu.ShaderStages.vertex, + .code = wgpu.StringView.fromSlice("void main() {}"), + }; + const glsl_descriptor = wgpu.shaderModuleGLSLDescriptor(&glsl_source, "GLSL"); + try expectChain(&glsl_source.chain, glsl_descriptor.next_in_chain); +} + +test "surface descriptor helpers borrow caller-owned sources" { + const native_pointer: *anyopaque = @ptrFromInt(1); + + const android_source = wgpu.SurfaceSourceAndroidNativeWindow{ .window = native_pointer }; + const android_descriptor = wgpu.surfaceDescriptorFromAndroidNativeWindow(&android_source, "Android"); + try expectChain(&android_source.chain, android_descriptor.next_in_chain); + + const metal_source = wgpu.SurfaceSourceMetalLayer{ .layer = native_pointer }; + const metal_descriptor = wgpu.surfaceDescriptorFromMetalLayer(&metal_source, "Metal"); + try expectChain(&metal_source.chain, metal_descriptor.next_in_chain); + + const wayland_source = wgpu.SurfaceSourceWaylandSurface{ + .display = native_pointer, + .surface = native_pointer, + }; + const wayland_descriptor = wgpu.surfaceDescriptorFromWaylandSurface(&wayland_source, "Wayland"); + try expectChain(&wayland_source.chain, wayland_descriptor.next_in_chain); + + const windows_source = wgpu.SurfaceSourceWindowsHWND{ + .hinstance = native_pointer, + .hwnd = native_pointer, + }; + const windows_descriptor = wgpu.surfaceDescriptorFromWindowsHWND(&windows_source, "Windows"); + try expectChain(&windows_source.chain, windows_descriptor.next_in_chain); + + const xcb_source = wgpu.SurfaceSourceXCBWindow{ + .connection = native_pointer, + .window = 1, + }; + const xcb_descriptor = wgpu.surfaceDescriptorFromXcbWindow(&xcb_source, "XCB"); + try expectChain(&xcb_source.chain, xcb_descriptor.next_in_chain); + + const xlib_source = wgpu.SurfaceSourceXlibWindow{ + .display = native_pointer, + .window = 1, + }; + const xlib_descriptor = wgpu.surfaceDescriptorFromXlibWindow(&xlib_source, "Xlib"); + try expectChain(&xlib_source.chain, xlib_descriptor.next_in_chain); +} + +test "withExtras helpers borrow caller-owned extensions" { + const instance_extras = wgpu.InstanceExtras{ + .backends = wgpu.InstanceBackends.all, + .flags = wgpu.InstanceFlags.default, + .dx12_shader_compiler = .undefined, + .gles3_minor_version = .automatic, + .gl_fence_behavior = .gl_fence_behaviour_normal, + .dxc_max_shader_model = .dxc_max_shader_model_v6_0, + }; + const instance_descriptor = (wgpu.InstanceDescriptor{}).withExtras(&instance_extras); + try expectChain(&instance_extras.chain, instance_descriptor.next_in_chain); + + const device_extras = wgpu.DeviceExtras{ .trace_path = wgpu.StringView{} }; + const device_descriptor = (wgpu.DeviceDescriptor{ .required_limits = null }).withExtras(&device_extras); + try expectChain(&device_extras.chain, device_descriptor.next_in_chain); + + const pipeline_extras = wgpu.PipelineLayoutExtras{ .immediate_data_size = 16 }; + const pipeline_descriptor = + wgpu.PipelineLayoutDescriptor.init(&.{}).withExtras(&pipeline_extras); + try expectChain(&pipeline_extras.chain, pipeline_descriptor.next_in_chain); + + const layout_entry_extras = wgpu.BindGroupLayoutEntryExtras{ .count = 2 }; + const layout_entry = (wgpu.BindGroupLayoutEntry{ + .binding = 0, + .visibility = wgpu.ShaderStages.compute, + }).withExtras(&layout_entry_extras); + try expectChain(&layout_entry_extras.chain, layout_entry.next_in_chain); + + const bind_group_entry_extras = wgpu.BindGroupEntryExtras{}; + const bind_group_entry = (wgpu.BindGroupEntry{ .binding = 0 }).withExtras(&bind_group_entry_extras); + try expectChain(&bind_group_entry_extras.chain, bind_group_entry.next_in_chain); + + const render_pass_extras = wgpu.RenderPassMaxDrawCount{ .max_draw_count = 1 }; + const render_pass_descriptor = + wgpu.RenderPassDescriptor.init(&.{}).withExtras(&render_pass_extras); + try expectChain(&render_pass_extras.chain, render_pass_descriptor.next_in_chain); + + const statistics = [_]wgpu.PipelineStatisticName{.compute_shader_invocations}; + const query_extras = wgpu.QuerySetDescriptorExtras.init(&statistics); + const query_descriptor = (wgpu.QuerySetDescriptor{ + .type = .pipeline_statistics, + .count = 1, + }).withExtras(&query_extras); + try expectChain(&query_extras.chain, query_descriptor.next_in_chain); + + const surface_extras = wgpu.SurfaceConfigurationExtras{ + .desired_maximum_frame_latency = 2, + }; + const surface_configuration = (wgpu.SurfaceConfiguration{ + .device = @ptrFromInt(1), + .format = .rgba8_unorm, + .width = 1, + .height = 1, + }).withExtras(&surface_extras); + try expectChain(&surface_extras.chain, surface_configuration.next_in_chain); +} + +test "descriptor slice helpers synchronize borrowed pointer-count pairs" { + const instance_features = [_]wgpu.InstanceFeatureName{ + .shader_source_spirv, + .multiple_devices_per_adapter, + }; + const instance_descriptor = + (wgpu.InstanceDescriptor{}).withRequiredFeatures(&instance_features); + try testing.expectEqual(instance_features.len, instance_descriptor.required_feature_count); + try expectPointer(instance_features[0..].ptr, instance_descriptor.required_features); + + const device_features = [_]wgpu.FeatureName{ + .depth_clip_control, + .timestamp_query, + }; + const device_descriptor = + (wgpu.DeviceDescriptor{}).withRequiredFeatures(&device_features); + try testing.expectEqual(device_features.len, device_descriptor.required_feature_count); + try expectPointer(device_features[0..].ptr, device_descriptor.required_features); + + const layout_entries = [_]wgpu.BindGroupLayoutEntry{.{ + .binding = 0, + .visibility = wgpu.ShaderStages.compute, + }}; + const bind_group_layout_descriptor = + wgpu.BindGroupLayoutDescriptor.init(&layout_entries); + try testing.expectEqual(layout_entries.len, bind_group_layout_descriptor.entry_count); + try expectPointer(layout_entries[0..].ptr, bind_group_layout_descriptor.entries); + + const bind_group_layout: *wgpu.BindGroupLayout = @ptrFromInt(0x1000); + const bind_group_entries = [_]wgpu.BindGroupEntry{.{ .binding = 0 }}; + const bind_group_descriptor = + wgpu.BindGroupDescriptor.init(bind_group_layout, &bind_group_entries); + try testing.expectEqual(bind_group_entries.len, bind_group_descriptor.entry_count); + try expectPointer(bind_group_entries[0..].ptr, bind_group_descriptor.entries); + + const buffer: *wgpu.Buffer = @ptrFromInt(0x2000); + const sampler: *wgpu.Sampler = @ptrFromInt(0x3000); + const texture_view: *wgpu.TextureView = @ptrFromInt(0x4000); + const buffers = [_]*wgpu.Buffer{buffer}; + const samplers = [_]*wgpu.Sampler{sampler}; + const texture_views = [_]*wgpu.TextureView{texture_view}; + const bind_group_entry_extras = (wgpu.BindGroupEntryExtras{}) + .withBuffers(&buffers) + .withSamplers(&samplers) + .withTextureViews(&texture_views); + try testing.expectEqual(buffers.len, bind_group_entry_extras.buffer_count); + try expectPointer(buffers[0..].ptr, bind_group_entry_extras.buffers.?); + try testing.expectEqual(samplers.len, bind_group_entry_extras.sampler_count); + try expectPointer(samplers[0..].ptr, bind_group_entry_extras.samplers.?); + try testing.expectEqual(texture_views.len, bind_group_entry_extras.texture_view_count); + try expectPointer(texture_views[0..].ptr, bind_group_entry_extras.texture_views.?); + + const empty_bind_group_entry_extras = (wgpu.BindGroupEntryExtras{}) + .withBuffers(&.{}) + .withSamplers(&.{}) + .withTextureViews(&.{}); + try testing.expectEqual(null, empty_bind_group_entry_extras.buffers); + try testing.expectEqual(null, empty_bind_group_entry_extras.samplers); + try testing.expectEqual(null, empty_bind_group_entry_extras.texture_views); + + const bind_group_layouts = [_]*wgpu.BindGroupLayout{bind_group_layout}; + const pipeline_layout_descriptor = + wgpu.PipelineLayoutDescriptor.init(&bind_group_layouts); + try testing.expectEqual( + bind_group_layouts.len, + pipeline_layout_descriptor.bind_group_layout_count, + ); + try expectPointer( + bind_group_layouts[0..].ptr, + pipeline_layout_descriptor.bind_group_layouts, + ); + + const shader_module: *wgpu.ShaderModule = @ptrFromInt(0x5000); + const constants = [_]wgpu.ConstantEntry{.{ + .key = wgpu.StringView.fromSlice("constant"), + .value = 1, + }}; + const compute_state = (wgpu.ComputeState{ + .module = shader_module, + }).withConstants(&constants); + try testing.expectEqual(constants.len, compute_state.constant_count); + try expectPointer(constants[0..].ptr, compute_state.constants); + + const attributes = [_]wgpu.VertexAttribute{.{ + .format = .float32x4, + .offset = 0, + .shader_location = 0, + }}; + const vertex_buffer = wgpu.VertexBufferLayout.init(16, &attributes); + try testing.expectEqual(attributes.len, vertex_buffer.attribute_count); + try expectPointer(attributes[0..].ptr, vertex_buffer.attributes); + + const vertex_buffers = [_]wgpu.VertexBufferLayout{vertex_buffer}; + const vertex_state = (wgpu.VertexState{ + .module = shader_module, + }).withConstants(&constants).withBuffers(&vertex_buffers); + try testing.expectEqual(constants.len, vertex_state.constant_count); + try expectPointer(constants[0..].ptr, vertex_state.constants); + try testing.expectEqual(vertex_buffers.len, vertex_state.buffer_count); + try expectPointer(vertex_buffers[0..].ptr, vertex_state.buffers); + + const targets = [_]wgpu.ColorTargetState{.{ .format = .rgba8_unorm }}; + const fragment_state = wgpu.FragmentState.init(shader_module, &.{}) + .withTargets(&targets) + .withConstants(&constants); + try testing.expectEqual(constants.len, fragment_state.constant_count); + try expectPointer(constants[0..].ptr, fragment_state.constants); + try testing.expectEqual(targets.len, fragment_state.target_count); + try expectPointer(targets[0..].ptr, fragment_state.targets); + + const color_formats = [_]wgpu.TextureFormat{ .rgba8_unorm, .bgra8_unorm }; + const render_bundle_descriptor = + wgpu.RenderBundleEncoderDescriptor.init(&color_formats); + try testing.expectEqual(color_formats.len, render_bundle_descriptor.color_format_count); + try expectPointer(color_formats[0..].ptr, render_bundle_descriptor.color_formats); + + const color_attachments = [_]wgpu.ColorAttachment{.{ .view = texture_view }}; + const render_pass_descriptor = wgpu.RenderPassDescriptor.init(&color_attachments); + try testing.expectEqual( + color_attachments.len, + render_pass_descriptor.color_attachment_count, + ); + try expectPointer( + color_attachments[0..].ptr, + render_pass_descriptor.color_attachments, + ); + + const texture_descriptor = (wgpu.TextureDescriptor{ + .usage = wgpu.TextureUsages.render_attachment, + .size = .{}, + .format = .rgba8_unorm, + }).withViewFormats(&color_formats); + try testing.expectEqual(color_formats.len, texture_descriptor.view_format_count); + try expectPointer(color_formats[0..].ptr, texture_descriptor.view_formats); + + const device: *wgpu.Device = @ptrFromInt(0x6000); + const surface_configuration = (wgpu.SurfaceConfiguration{ + .device = device, + .format = .rgba8_unorm, + .width = 1, + .height = 1, + }).withViewFormats(&color_formats); + try testing.expectEqual(color_formats.len, surface_configuration.view_format_count); + try expectPointer(color_formats[0..].ptr, surface_configuration.view_formats); + + const spirv_words = [_]u32{ 0x07230203, 0 }; + const native_spirv = wgpu.ShaderModuleDescriptorSpirV.init(&spirv_words); + try testing.expectEqual(@as(u32, spirv_words.len), native_spirv.source_size); + try expectPointer(spirv_words[0..].ptr, native_spirv.source); + + const chained_spirv = wgpu.ShaderSourceSPIRV.init(&spirv_words); + try testing.expectEqual(@as(u32, spirv_words.len), chained_spirv.code_size); + try expectPointer(spirv_words[0..].ptr, chained_spirv.code); + + const defines = [_]wgpu.ShaderDefine{.{ + .name = wgpu.StringView.fromSlice("VALUE"), + .value = wgpu.StringView.fromSlice("1"), + }}; + const glsl_source = (wgpu.ShaderSourceGLSL{ + .stage = wgpu.ShaderStages.compute, + .code = wgpu.StringView.fromSlice("void main() {}"), + }).withDefines(&defines); + try testing.expectEqual(@as(u32, defines.len), glsl_source.define_count); + try expectPointer(defines[0..].ptr, glsl_source.defines.?); + const glsl_source_without_defines = glsl_source.withDefines(&.{}); + try testing.expectEqual(@as(u32, 0), glsl_source_without_defines.define_count); + try testing.expectEqual(null, glsl_source_without_defines.defines); + + const statistics = [_]wgpu.PipelineStatisticName{ + .vertex_shader_invocations, + .fragment_shader_invocations, + }; + const query_extras = wgpu.QuerySetDescriptorExtras.init(&statistics); + try testing.expectEqual(statistics.len, query_extras.pipeline_statistic_count); + try expectPointer(statistics[0..].ptr, query_extras.pipeline_statistics); +} diff --git a/tests/ownership.zig b/tests/ownership.zig new file mode 100644 index 0000000..3125673 --- /dev/null +++ b/tests/ownership.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const testing = std.testing; + +const wgpu = @import("wgpu"); + +test "empty SurfaceCapabilities can be deinitialized repeatedly" { + var capabilities = wgpu.SurfaceCapabilities{}; + + try testing.expectEqual(0, capabilities.formatsSlice().len); + try testing.expectEqual(0, capabilities.presentModesSlice().len); + try testing.expectEqual(0, capabilities.alphaModesSlice().len); + capabilities.deinit(); + capabilities.deinit(); + + try testing.expectEqual(wgpu.TextureUsages.none, capabilities.usages); + try testing.expectEqual(0, capabilities.format_count); + try testing.expectEqual(null, capabilities.formats); + try testing.expectEqual(0, capabilities.present_mode_count); + try testing.expectEqual(null, capabilities.present_modes); + try testing.expectEqual(0, capabilities.alpha_mode_count); + try testing.expectEqual(null, capabilities.alpha_modes); +} + +test "owned output arrays expose bounded slices" { + const features = [_]wgpu.FeatureName{ + .core_features_and_limits, + .depth_clip_control, + }; + const supported_features = wgpu.SupportedFeatures{ + .feature_count = features.len, + .features = &features, + }; + try testing.expectEqualSlices( + wgpu.FeatureName, + &features, + supported_features.slice(), + ); + + const formats = [_]wgpu.TextureFormat{ .rgba8_unorm, .bgra8_unorm }; + const present_modes = [_]wgpu.PresentMode{ .fifo, .mailbox }; + const alpha_modes = [_]wgpu.CompositeAlphaMode{.@"opaque"}; + const capabilities = wgpu.SurfaceCapabilities{ + .format_count = formats.len, + .formats = &formats, + .present_mode_count = present_modes.len, + .present_modes = &present_modes, + .alpha_mode_count = alpha_modes.len, + .alpha_modes = &alpha_modes, + }; + try testing.expectEqualSlices( + wgpu.TextureFormat, + &formats, + capabilities.formatsSlice(), + ); + try testing.expectEqualSlices( + wgpu.PresentMode, + &present_modes, + capabilities.presentModesSlice(), + ); + try testing.expectEqualSlices( + wgpu.CompositeAlphaMode, + &alpha_modes, + capabilities.alphaModesSlice(), + ); +} + +test "SurfaceTexture transfers texture ownership explicitly" { + const texture: *wgpu.Texture = @ptrFromInt(1); + var surface_texture = wgpu.SurfaceTexture{ + .texture = texture, + .status = .success_optimal, + }; + + try testing.expectEqual(texture, surface_texture.takeTexture()); + try testing.expectEqual(null, surface_texture.texture); + + // The transferred texture is no longer released by SurfaceTexture. + surface_texture.deinit(); +} + +test "empty SurfaceTexture can be deinitialized repeatedly" { + var surface_texture = wgpu.SurfaceTexture{}; + + surface_texture.deinit(); + surface_texture.deinit(); + + try testing.expectEqual(null, surface_texture.texture); +}