diff --git a/.github/workflows/artifacts.yml b/.github/workflows/artifacts.yml new file mode 100644 index 0000000..539377a --- /dev/null +++ b/.github/workflows/artifacts.yml @@ -0,0 +1,199 @@ +name: Target artifacts + +on: + workflow_dispatch: + push: + tags: + - "v*" + +permissions: + contents: read + +jobs: + linux: + name: Linux ${{ matrix.arch }} + strategy: + fail-fast: false + matrix: + include: + - arch: x86_64 + runner: ubuntu-latest + zig_target: x86_64-linux-gnu + rust_target: x86_64-unknown-linux-gnu + - arch: aarch64 + runner: ubuntu-24.04-arm + zig_target: aarch64-linux-gnu + rust_target: aarch64-unknown-linux-gnu + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + - 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 + with: + name: wgpu-linux-${{ matrix.arch }} + path: dist/wgpu-linux-${{ matrix.arch }} + + apple: + name: ${{ matrix.artifact }} + strategy: + fail-fast: false + matrix: + include: + - artifact: wgpu-macos-aarch64 + runner: macos-latest + zig_target: aarch64-macos + rust_target: aarch64-apple-darwin + - artifact: wgpu-macos-x86_64 + runner: macos-15-intel + zig_target: x86_64-macos + rust_target: x86_64-apple-darwin + - artifact: wgpu-ios-aarch64 + runner: macos-latest + zig_target: aarch64-ios + rust_target: aarch64-apple-ios + - artifact: wgpu-ios-aarch64-simulator + runner: macos-latest + zig_target: aarch64-ios-simulator + rust_target: aarch64-apple-ios-sim + - artifact: wgpu-ios-x86_64-simulator + runner: macos-15-intel + zig_target: x86_64-ios-simulator + rust_target: x86_64-apple-ios + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + - name: Build static and dynamic libraries + run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -p dist/${{ matrix.artifact }} + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: dist/${{ matrix.artifact }} + + windows: + name: ${{ matrix.artifact }} + strategy: + fail-fast: false + matrix: + include: + - artifact: wgpu-windows-aarch64-msvc + zig_target: aarch64-windows-msvc + rust_target: aarch64-pc-windows-msvc + rust_toolchain: stable-msvc + - artifact: wgpu-windows-x86-msvc + zig_target: x86-windows-msvc + rust_target: i686-pc-windows-msvc + rust_toolchain: stable-msvc + - artifact: wgpu-windows-x86_64-msvc + zig_target: x86_64-windows-msvc + rust_target: x86_64-pc-windows-msvc + rust_toolchain: stable-msvc + - artifact: wgpu-windows-x86_64-gnu + zig_target: x86_64-windows-gnu + rust_target: x86_64-pc-windows-gnu + rust_toolchain: stable-gnu + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: ${{ matrix.rust_toolchain }} + targets: ${{ matrix.rust_target }} + - name: Build static and dynamic libraries + run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -p dist/${{ matrix.artifact }} + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact }} + path: dist/${{ matrix.artifact }} + + android: + name: Android ${{ matrix.abi }} + strategy: + fail-fast: false + matrix: + include: + - abi: arm64-v8a + zig_target: aarch64-linux-android + rust_target: aarch64-linux-android + - abi: armeabi-v7a + zig_target: arm-linux-androideabi + rust_target: armv7-linux-androideabi + - abi: x86 + zig_target: x86-linux-android + rust_target: i686-linux-android + - abi: x86_64 + zig_target: x86_64-linux-android + rust_target: x86_64-linux-android + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + - name: Select Android NDK + shell: bash + 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: 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 + with: + name: wgpu-android-${{ matrix.abi }} + path: dist/wgpu-android-${{ matrix.abi }} + + ohos: + name: OpenHarmony ${{ matrix.abi }} + strategy: + fail-fast: false + matrix: + include: + - abi: arm64-v8a + zig_target: aarch64-linux-ohos + rust_target: aarch64-unknown-linux-ohos + - abi: armeabi-v7a + zig_target: arm-linux-ohoseabi + rust_target: armv7-unknown-linux-ohos + - abi: x86_64 + zig_target: x86_64-linux-ohos + rust_target: x86_64-unknown-linux-ohos + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Setup OpenHarmony SDK + uses: openharmony-rs/setup-ohos-sdk@v1.0.0 + with: + version: "5.0.0" + - name: Setup Zig with OpenHarmony targets + uses: openharmony-zig/setup-zig-ohos@v0.1.1 + with: + tag: "0.16.0" + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + - name: Compile and link the Zig binding probe + 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-ohos-${{ matrix.abi }} + - uses: actions/upload-artifact@v4 + with: + name: wgpu-ohos-${{ matrix.abi }} + path: dist/wgpu-ohos-${{ matrix.abi }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8969804 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,73 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + formatting: + name: Zig 0.16 formatting + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - name: Check formatting + shell: bash + run: | + git ls-files -z '*.zig' '*.zon' | xargs -0 zig fmt --check + + native: + name: ${{ matrix.name }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux x86_64 + runner: ubuntu-latest + zig_target: x86_64-linux-gnu + rust_target: x86_64-unknown-linux-gnu + - name: macOS arm64 + runner: macos-latest + zig_target: aarch64-macos + rust_target: aarch64-apple-darwin + - name: Windows x86_64 MSVC + runner: windows-latest + zig_target: x86_64-windows-msvc + rust_target: x86_64-pc-windows-msvc + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v4 + - uses: mlugg/setup-zig@v2 + with: + version: 0.16.0 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.rust_target }} + - name: Install Linux Vulkan software driver + if: runner.os == 'Linux' + shell: bash + run: | + sudo apt-get update + sudo apt-get install --yes mesa-vulkan-drivers + - name: Build wgpu-native from source + run: zig build -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all + - name: Compile bindings and tests + run: zig build --build-file build.tests.zig check -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all + - name: Run native unit tests + run: zig build --build-file build.tests.zig test -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast -j1 --summary all + - name: Verify prebuilt fallback + env: + WGPU_NATIVE_USE_PREBUILT: "1" + run: zig build --build-file build.tests.zig check -Dtarget=${{ matrix.zig_target }} -Doptimize=ReleaseFast --summary all diff --git a/README.md b/README.md index e19b54e..a636b17 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ 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 run-triangle-example -Dtarget=x86_64-windows-msvc +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 @@ -95,6 +95,101 @@ const install_dll = b.addInstallBinFile(dll_path, "wgpu_native.dll"); b.getInstallStep().dependOn(&install_dll.step); ``` +## Building `wgpu-native` + +`wgpu-native` v25.0.2.1 is built from its pinned source commit by default. The matching +`webgpu-headers` commit is pinned separately, so the generated C ABI does not drift when +an upstream branch changes. + +The source build requires Cargo, a Rust toolchain with the selected target installed, +and the platform SDK normally required by that target. For example: + +```sh +# Native source build. Installs the static library, dynamic library, and C headers. +zig build -Doptimize=ReleaseFast + +# Compile the bindings and link probes without running them. +zig build --build-file build.tests.zig check -Doptimize=ReleaseFast +``` + +Targets that cannot legally be built on every host still require their native toolchain: +iOS requires Xcode on macOS, MSVC requires Windows, and Android/OpenHarmony require their +respective NDKs. + +The root `build.zig` only builds `wgpu-native` and exposes the `wgpu`/`wgpu-c` binding +modules. `build/Library.zig` is the shared library entry point, while +`build/platform/root.zig` dispatches to the Android, Apple, Linux, OpenHarmony, or +Windows build implementation. Tests and examples are isolated behind +`build.tests.zig` and `build.examples.zig`; their implementation stays in the +corresponding directory. + +### Using published prebuilt libraries + +Set `WGPU_NATIVE_USE_PREBUILT=1` to skip the Cargo source build and use the published +`wgpu-native` archive for the selected target: + +```sh +WGPU_NATIVE_USE_PREBUILT=1 zig build --build-file build.tests.zig check \ + -Dtarget=x86_64-linux-gnu +``` + +The equivalent Zig build option is `-Duse_prebuilt=true`. Downstream packages can pass it +while resolving this dependency: + +```zig +const wgpu_native_dep = b.dependency("wgpu_native_zig", .{ + .target = target, + .optimize = optimize, + .use_prebuilt = true, +}); +``` + +`WGPU_NATIVE_PREBUILT_DIR=/path/to/prefix` uses a local artifact directory and also +implies prebuilt mode. The prefix must use the layout produced by this package: + +```text +prefix/ +├── include/webgpu/{webgpu.h,wgpu.h} +└── lib/ + ├── libwgpu_native.a + └── libwgpu_native.so +``` + +Use the platform-specific dynamic and import-library names on Apple and Windows. +OpenHarmony currently uses this local-directory mechanism when consuming CI artifacts, +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 | + +The OpenHarmony commands are: + +```sh +rustup target add \ + aarch64-unknown-linux-ohos \ + armv7-unknown-linux-ohos \ + x86_64-unknown-linux-ohos + +zig build --build-file build.tests.zig check \ + -Dtarget=aarch64-linux-ohos -Doptimize=ReleaseFast +zig build --build-file build.tests.zig check \ + -Dtarget=arm-linux-ohoseabi -Doptimize=ReleaseFast +zig build --build-file build.tests.zig check \ + -Dtarget=x86_64-linux-ohos -Doptimize=ReleaseFast +``` + +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. + ## How the `wgpu` module differs from `wgpu-c` * Names are shortened to remove redundancy. @@ -202,7 +297,6 @@ b.getInstallStep().dependOn(&install_dll.step); * This pretty much means, it is replaced with `bool` in the parameters and return values of methods, but not in structs or the parameters/return values of procs (which are supposed to be function pointers to things returned by `wgpuGetProcAddress`). ## TODO -* Test this on other machines with different OS/CPU. The package requires Zig 0.16.x. * Cleanup/organization: * If types are only tied to a specific opaque struct, they should be decls inside that struct. * The associated Procs struct should probably be a decl of the opaque struct as well. @@ -210,6 +304,5 @@ b.getInstallStep().dependOn(&install_dll.step); * 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. -* Custom-build `wgpu-native`; provided all the necessary tools/dependencies are present. * Bindgen using [the webgpu-headers yaml](https://github.com/webgpu-native/webgpu-headers/blob/main/webgpu.yml)? * The proc definitions are mainly there since they are also present in the webgpu headers and I didn't fully understand what they were for when I started working on this project. However, I know better now and they aren't really used for anything currently. They're supposed to be used with `wgpuGetProcAddress` but it's [unimplemented in `wgpu-native`](https://github.com/gfx-rs/wgpu-native/issues/223). They are a pain to update by hand, so maybe they should be removed for now and made optional once we have a working bindings generator? Like the bindgen could put them in a separate `wgpu-procs` module. diff --git a/build.examples.zig b/build.examples.zig new file mode 100644 index 0000000..3ffd71c --- /dev/null +++ b/build.examples.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const Examples = @import("examples/build.zig"); + +// Keep the package root at the repository while example build logic lives in examples/. +pub fn build(b: *std.Build) void { + Examples.build(b); +} diff --git a/build.tests.zig b/build.tests.zig new file mode 100644 index 0000000..f87476e --- /dev/null +++ b/build.tests.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const Tests = @import("tests/build.zig"); + +// Keep the package root at the repository while test build logic lives in tests/. +pub fn build(b: *std.Build) void { + Tests.build(b); +} diff --git a/build.zig b/build.zig index a10710c..20deae2 100644 --- a/build.zig +++ b/build.zig @@ -1,380 +1,6 @@ +const Library = @import("build/Library.zig"); const std = @import("std"); -fn link_windows_system_libraries(mod: *std.Build.Module, is_gnu: bool) void { - if (is_gnu) { - // For gnu, the linker needs the d3dcompiler dll since it can't find a suitable static lib - // (I'd guess it tries to search for something like "libd3dcompiler.a" instead of "d3dcompiler.lib"). - mod.linkSystemLibrary("d3dcompiler_47", .{}); - - // This seems to have something to do with the windows-result crate in wgpu-native's dependencies - mod.linkSystemLibrary("api-ms-win-core-winrt-error-l1-1-0", .{}); - } else { - mod.linkSystemLibrary("d3dcompiler", .{}); - - // GetClientRect is unresolved unless we link this for msvc - mod.linkSystemLibrary("user32", .{}); - - mod.linkSystemLibrary("RuntimeObject", .{}); - } - mod.linkSystemLibrary("opengl32", .{}); - mod.linkSystemLibrary("gdi32", .{}); - - // COM-related - mod.linkSystemLibrary("OleAut32", .{}); - mod.linkSystemLibrary("Ole32", .{}); - - // Apparently these are needed because of rust stdlib - mod.linkSystemLibrary("ws2_32", .{}); - mod.linkSystemLibrary("userenv", .{}); - - // Needed by windows-rs (wgpu-native dependency) - mod.linkSystemLibrary("propsys", .{}); -} - -fn link_mac_frameworks(mod: *std.Build.Module) void { - mod.linkFramework("Foundation", .{}); - mod.linkFramework("QuartzCore", .{}); - mod.linkFramework("Metal", .{}); -} - -const WGPUBuildContext = struct { - link_mode: std.builtin.LinkMode, - target: std.Build.ResolvedTarget, - optimize: std.builtin.OptimizeMode, - is_windows: bool, - is_mac: bool, - wgpu_dep: *std.Build.Dependency, - libwgpu_path: ?std.Build.LazyPath, - install_lib_dir: []const u8, - wgpu_mod: *std.Build.Module, - wgpu_c_mod: *std.Build.Module, - - fn init(b: *std.Build) ?WGPUBuildContext { - const link_mode = b.option(std.builtin.LinkMode, "link_mode", "Use static linking instead of dynamic linking.") orelse .static; - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. - const target = b.standardTargetOptions(.{}); - - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. - const optimize = b.standardOptimizeOption(.{}); - - const target_res = target.result; - const os_str = @tagName(target_res.os.tag); - const arch_str = @tagName(target_res.cpu.arch); - - const mode_str = switch (optimize) { - .Debug => "debug", - else => "release", - }; - const abi_str = switch (target_res.os.tag) { - .ios => switch (target_res.abi) { - .simulator => "_simulator", - else => "", - }, - .windows => switch (target_res.abi) { - .msvc => "_msvc", - else => "_gnu", - }, - else => "", - }; - const target_name_slices = [_][:0]const u8{ "wgpu_", os_str, "_", arch_str, abi_str, "_", mode_str }; - const maybe_target_name = std.mem.concatWithSentinel(b.allocator, u8, &target_name_slices, 0); - const target_name = maybe_target_name catch |err| { - std.debug.panic("Failed to format target name: {s}", .{@errorName(err)}); - }; - - // Check if we have a dependency matching our selected target. - for (b.available_deps) |dep| { - const name, _ = dep; - if (std.mem.eql(u8, name, target_name)) { - break; - } - } else { - std.debug.panic("Could not find dependency matching target {s}", .{target_name}); - } - - const wgpu_mod = b.addModule("wgpu", .{ - .root_source_file = b.path("src/root.zig"), - .target = target, - .optimize = optimize, - .link_libcpp = true, - }); - - const wgpu_dep = b.lazyDependency(target_name, .{}) orelse return null; - - const translate_step = b.addTranslateC(.{ - // wgpu.h imports webgpu.h, so we get the contents of both files, as well as a bunch of libc garbage. - .root_source_file = wgpu_dep.path("include/webgpu/wgpu.h"), - - .target = target, - .optimize = optimize, - }); - - const wgpu_c_mod = translate_step.addModule("wgpu-c"); - wgpu_c_mod.resolved_target = target; - wgpu_c_mod.link_libcpp = true; - - var libwgpu_path: ?std.Build.LazyPath = null; - var is_windows: bool = false; - var is_mac: bool = target_res.os.tag == .macos or target_res.os.tag == .ios; - - // TODO: This seems like it could be made smaller, lots of repetitive code here. - switch (target_res.os.tag) { - .windows => { - is_windows = true; - if (target_res.abi == .msvc) { - // I feel like libcpp should work, but it definitely does not on msvc. Fortunately libc does. - wgpu_mod.link_libcpp = false; - wgpu_c_mod.link_libcpp = false; - wgpu_mod.link_libc = true; - wgpu_c_mod.link_libc = true; - - if (link_mode == .static) { - libwgpu_path = wgpu_dep.path("lib/wgpu_native.lib"); - - link_windows_system_libraries(wgpu_mod, false); - link_windows_system_libraries(wgpu_c_mod, false); - } else { - libwgpu_path = wgpu_dep.path("lib/wgpu_native.dll.lib"); - - // Unfortunately, it seems only the local tests can access the dll this way. - // For dependees, it copies to the zig cache, which you can use for testing if you do some weird stuff with the install steps, - // but it never copies to the output folder. So not helpful if you need to distribute a binary with the dll alongside it. - const dll_install_file = b.addInstallLibFile(wgpu_dep.path("lib/wgpu_native.dll"), "wgpu_native.dll"); - b.getInstallStep().dependOn(&dll_install_file.step); - - // For dependees that need the dll file, this seems to be the only reliable way to propagate it through. - // In Zig 0.14 there seems to be some method for exposing LazyPaths to dependees, which might be a bit cleaner. - const writeFiles = b.addNamedWriteFiles("lib"); - _ = writeFiles.addCopyFile(wgpu_dep.path("lib/wgpu_native.dll"), "wgpu_native.dll"); - } - } else { - if (link_mode == .static) { - libwgpu_path = wgpu_dep.path("lib/libwgpu_native.a"); - - link_windows_system_libraries(wgpu_mod, true); - link_windows_system_libraries(wgpu_c_mod, true); - } else { - libwgpu_path = wgpu_dep.path("lib/libwgpu_native.dll.a"); - - const dll_install_file = b.addInstallLibFile(wgpu_dep.path("lib/wgpu_native.dll"), "wgpu_native.dll"); - b.getInstallStep().dependOn(&dll_install_file.step); - - const writeFiles = b.addNamedWriteFiles("lib"); - _ = writeFiles.addCopyFile(wgpu_dep.path("lib/wgpu_native.dll"), "wgpu_native.dll"); - } - } - }, - - // This only tries to account for linux/macos since we're using pre-compiled wgpu-native; - // need to think harder about this if I get custom builds working. - else => if (link_mode == .static) { - libwgpu_path = wgpu_dep.path("lib/libwgpu_native.a"); - } else if (target_res.os.tag == .macos or target_res.os.tag == .ios) { // TODO: This is just guesswork, need to test it somehow, but I don't have a mac. - is_mac = true; - const dylib_install_file = b.addInstallLibFile(wgpu_dep.path("lib/libwgpu_native.dylib"), "libwgpu_native.dylib"); - b.getInstallStep().dependOn(&dylib_install_file.step); - - const writeFiles = b.addNamedWriteFiles("lib"); - _ = writeFiles.addCopyFile(wgpu_dep.path("lib/libwgpu_native.dylib"), "libwgpu_native.dylib"); - } else { - const so_install_file = b.addInstallLibFile(wgpu_dep.path("lib/libwgpu_native.so"), "libwgpu_native.so"); - b.getInstallStep().dependOn(&so_install_file.step); - - const writeFiles = b.addNamedWriteFiles("lib"); - _ = writeFiles.addCopyFile(wgpu_dep.path("lib/libwgpu_native.so"), "libwgpu_native.so"); - }, - } - - if (libwgpu_path != null) { - wgpu_mod.addObjectFile(libwgpu_path.?); - wgpu_c_mod.addObjectFile(libwgpu_path.?); - } - - return WGPUBuildContext{ - .link_mode = link_mode, - .target = target, - .optimize = optimize, - .is_windows = is_windows, - .is_mac = is_mac, - .wgpu_dep = wgpu_dep, - .libwgpu_path = libwgpu_path, - .install_lib_dir = b.getInstallPath(.lib, ""), - .wgpu_mod = wgpu_mod, - .wgpu_c_mod = wgpu_c_mod, - }; - } -}; - -fn dynamic_link(context: *const WGPUBuildContext, c: *std.Build.Step.Compile, cmd: *std.Build.Step.Run) void { - if (!context.is_windows) { - c.root_module.addLibraryPath(context.wgpu_dep.path("lib")); - c.root_module.linkSystemLibrary("wgpu_native", .{}); - } - cmd.addPathDir(context.install_lib_dir); -} - -fn handle_rt(context: *const WGPUBuildContext, exe: *std.Build.Step.Compile) void { - if (context.is_windows and context.target.result.abi == .msvc) { - // We get duplicate symbol errors at link-time if we don't disable these; - exe.bundle_compiler_rt = false; - exe.bundle_ubsan_rt = false; - } -} - -fn triangle_example(b: *std.Build, context: *const WGPUBuildContext) void { - const bmp_mod = b.createModule(.{ - .root_source_file = b.path("examples/bmp.zig"), - }); - - const triangle_example_exe_mod = b.createModule(.{ - .root_source_file = b.path("examples/triangle/triangle.zig"), - .target = context.target, - .optimize = context.optimize, - }); - triangle_example_exe_mod.addImport("wgpu", context.wgpu_mod); - triangle_example_exe_mod.addImport("bmp", bmp_mod); - const triangle_example_exe = b.addExecutable(.{ - .name = "triangle-example", - .root_module = triangle_example_exe_mod, - }); - handle_rt(context, triangle_example_exe); - if (context.is_mac) { - link_mac_frameworks(triangle_example_exe.root_module); - } - - const run_triangle_cmd = b.addRunArtifact(triangle_example_exe); - - const run_triangle_step = b.step("run-triangle-example", "Run the triangle example"); - run_triangle_step.dependOn(&run_triangle_cmd.step); - - if (context.link_mode == .dynamic) { - dynamic_link(context, triangle_example_exe, run_triangle_cmd); - - run_triangle_cmd.step.dependOn(b.getInstallStep()); - } -} - -fn unit_tests(b: *std.Build, context: *const WGPUBuildContext) void { - const unit_test_step = b.step("test", "Run unit tests"); - if (context.is_windows) { - unit_test_step.dependOn(b.getInstallStep()); - } - - const test_files = [_][:0]const u8{ - "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, idx| { - const test_name = test_file[4..(test_file.len - 4)] ++ "-test"; - test_names[idx] = test_name; - }; - - for (test_files, test_names) |test_file, test_name| { - // TODO: Seems weird to have a mod for each unit test, should probably revisit this. - const test_mod = b.createModule(.{ - .root_source_file = b.path(test_file), - .target = context.target, - .optimize = context.optimize, - }); - const t = b.addTest(.{ - .name = test_name, - .root_module = test_mod, - }); - handle_rt(context, t); - if (context.libwgpu_path != null) { - t.root_module.addObjectFile(context.libwgpu_path.?); - } - if (context.is_windows) { - t.root_module.link_libc = true; - } else { - t.root_module.link_libcpp = true; - } - - const run_test = b.addRunArtifact(t); - - if (context.is_mac) { - link_mac_frameworks(t.root_module); - } - - if (context.link_mode == .dynamic) { - dynamic_link(context, t, run_test); - } else if (context.is_windows) { - if (context.target.result.abi == .gnu) { - link_windows_system_libraries(t.root_module, true); - - // TODO: Find out why this is only required here; seems suspicious - t.root_module.linkSystemLibrary("unwind", .{}); - } else { - link_windows_system_libraries(t.root_module, false); - } - } - - unit_test_step.dependOn(&run_test.step); - } -} - -fn compute_tests(b: *std.Build, context: *const WGPUBuildContext) void { - const compute_test_mod = b.createModule(.{ - .root_source_file = b.path("tests/compute.zig"), - .target = context.target, - .optimize = context.optimize, - }); - compute_test_mod.addImport("wgpu", context.wgpu_mod); - const compute_test = b.addTest(.{ - .name = "compute-test", - .root_module = compute_test_mod, - }); - handle_rt(context, compute_test); - - const run_compute_test = b.addRunArtifact(compute_test); - - const compute_test_c_mod = b.createModule(.{ - .root_source_file = b.path("tests/compute_c.zig"), - .target = context.target, - .optimize = context.optimize, - }); - compute_test_c_mod.addImport("wgpu-c", context.wgpu_c_mod); - const compute_test_c = b.addTest(.{ - .name = "compute-test-c", - .root_module = compute_test_c_mod, - }); - handle_rt(context, compute_test_c); - - const run_compute_test_c = b.addRunArtifact(compute_test_c); - - const compute_test_step = b.step("compute-tests", "Run compute shader tests"); - if (context.link_mode == .dynamic) { - dynamic_link(context, compute_test, run_compute_test); - dynamic_link(context, compute_test_c, run_compute_test_c); - - run_compute_test.step.dependOn(b.getInstallStep()); - run_compute_test_c.step.dependOn(b.getInstallStep()); - } - - if (context.is_mac) { - link_mac_frameworks(compute_test.root_module); - link_mac_frameworks(compute_test_c.root_module); - } - - compute_test_step.dependOn(&run_compute_test.step); - compute_test_step.dependOn(&run_compute_test_c.step); -} - -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. pub fn build(b: *std.Build) void { - const context = WGPUBuildContext.init(b) orelse return; - - compute_tests(b, &context); - unit_tests(b, &context); - - triangle_example(b, &context); + _ = Library.build(b, Library.standardOptions(b)); } diff --git a/build.zig.zon b/build.zig.zon index be60017..78f56f9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -16,6 +16,16 @@ // Once all dependencies are fetched, `zig build` no longer requires // internet connectivity. .dependencies = .{ + .wgpu_native_source = .{ + .url = "https://github.com/gfx-rs/wgpu-native/archive/af9074edf144efe4f1432b2e42c477429c4964c1.tar.gz", + .hash = "N-V-__8AAMkXCADZTmf51o8SSzRb6oYEb3rjtHl5duhu7Wzs", + .lazy = true, + }, + .webgpu_headers_source = .{ + .url = "https://github.com/webgpu-native/webgpu-headers/archive/bac520839ff5ed2e2b648ed540bd9ec45edbccbc.tar.gz", + .hash = "N-V-__8AAIEJBgDDDaQxHnH7W6qR_NeONW1g8R_Tt0Xe9n_u", + .lazy = true, + }, // See `zig fetch --save ` for a command-line interface for adding dependencies. //.example = .{ // // When updating this field to a new URL, be sure to delete the corresponding diff --git a/build/Library.zig b/build/Library.zig new file mode 100644 index 0000000..3f68b16 --- /dev/null +++ b/build/Library.zig @@ -0,0 +1,188 @@ +const std = @import("std"); +const Platform = @import("platform/root.zig"); +const WgpuNativeArtifact = @import("WgpuNativeArtifact.zig"); + +pub const Options = struct { + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + link_mode: std.builtin.LinkMode, + use_prebuilt: bool, +}; + +pub const Result = struct { + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + link_mode: std.builtin.LinkMode, + platform: Platform.Config, + artifact: WgpuNativeArtifact.Result, + install_lib_dir: []const u8, + wgpu_mod: *std.Build.Module, + wgpu_c_mod: *std.Build.Module, + + pub fn isOhos(self: Result) bool { + return self.platform.kind == .ohos; + } + + pub fn linkModule( + self: Result, + b: *std.Build, + mod: *std.Build.Module, + ) void { + mod.link_libcpp = true; + Platform.configureModule( + b, + self.platform, + mod, + self.link_mode, + ); + if (self.artifact.link_library) |library| { + mod.addObjectFile(library); + } else if (self.link_mode == .dynamic) { + mod.addLibraryPath(self.artifact.library_dir); + mod.linkSystemLibrary("wgpu_native", .{}); + } + } + + pub fn linkTestModule( + self: Result, + b: *std.Build, + mod: *std.Build.Module, + ) void { + self.linkModule(b, mod); + Platform.configureTest( + self.platform, + mod, + self.link_mode, + ); + } + + pub fn configureCompile( + self: Result, + compile: *std.Build.Step.Compile, + ) void { + Platform.configureCompile(self.platform, compile); + } + + pub fn configureRun( + self: Result, + b: *std.Build, + run: *std.Build.Step.Run, + ) void { + if (self.link_mode != .dynamic) return; + run.addPathDir(self.install_lib_dir); + run.step.dependOn(b.getInstallStep()); + } +}; + +pub fn standardOptions(b: *std.Build) Options { + return .{ + .target = b.standardTargetOptions(.{}), + .optimize = b.standardOptimizeOption(.{}), + .link_mode = b.option( + std.builtin.LinkMode, + "link_mode", + "Select static or dynamic wgpu-native linking", + ) orelse .static, + .use_prebuilt = b.option( + bool, + "use_prebuilt", + "Use published wgpu-native binaries instead of building from source", + ) orelse envFlag(b, "WGPU_NATIVE_USE_PREBUILT"), + }; +} + +pub fn build(b: *std.Build, options: Options) ?Result { + const platform = Platform.build(b, options.target); + const artifact = WgpuNativeArtifact.build( + b, + platform, + options.optimize, + options.link_mode, + options.use_prebuilt, + ) orelse return null; + + const wgpu_mod = b.addModule("wgpu", .{ + .root_source_file = b.path("src/root.zig"), + .target = options.target, + .optimize = options.optimize, + }); + const translate_step = b.addTranslateC(.{ + .root_source_file = artifact.header, + .target = options.target, + .optimize = options.optimize, + }); + Platform.configureTranslateC(platform, translate_step); + const wgpu_c_mod = translate_step.addModule("wgpu-c"); + wgpu_c_mod.resolved_target = options.target; + + var result: Result = .{ + .target = options.target, + .optimize = options.optimize, + .link_mode = options.link_mode, + .platform = platform, + .artifact = artifact, + .install_lib_dir = b.getInstallPath(.lib, ""), + .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; +} + +fn install(b: *std.Build, artifact: WgpuNativeArtifact.Result) void { + const install_static_library = b.addInstallLibFile( + artifact.static_library, + artifact.static_name, + ); + b.getInstallStep().dependOn(&install_static_library.step); + const install_dynamic_library = b.addInstallLibFile( + artifact.dynamic_library, + artifact.dynamic_name, + ); + b.getInstallStep().dependOn(&install_dynamic_library.step); + if (artifact.dynamic_import_library) |import_library| { + const install_import_library = b.addInstallLibFile( + import_library, + artifact.dynamic_import_name.?, + ); + b.getInstallStep().dependOn(&install_import_library.step); + } + const install_wgpu_header = b.addInstallHeaderFile( + artifact.header, + "webgpu/wgpu.h", + ); + b.getInstallStep().dependOn(&install_wgpu_header.step); + const install_webgpu_header = b.addInstallHeaderFile( + artifact.webgpu_header, + "webgpu/webgpu.h", + ); + b.getInstallStep().dependOn(&install_webgpu_header.step); + if (artifact.runtime_library) |runtime_library| { + const write_files = b.addNamedWriteFiles("lib"); + _ = write_files.addCopyFile(runtime_library, artifact.dynamic_name); + } +} + +fn envFlag(b: *std.Build, name: []const u8) bool { + const value = b.graph.environ_map.get(name) orelse return false; + if (std.mem.eql(u8, value, "1") or + std.ascii.eqlIgnoreCase(value, "true") or + std.ascii.eqlIgnoreCase(value, "yes") or + std.ascii.eqlIgnoreCase(value, "on")) + { + return true; + } + if (std.mem.eql(u8, value, "0") or + std.ascii.eqlIgnoreCase(value, "false") or + std.ascii.eqlIgnoreCase(value, "no") or + std.ascii.eqlIgnoreCase(value, "off")) + { + return false; + } + std.debug.panic( + "{s} must be one of 1/0, true/false, yes/no, or on/off; got '{s}'", + .{ name, value }, + ); +} diff --git a/build/WgpuNativeArtifact.zig b/build/WgpuNativeArtifact.zig new file mode 100644 index 0000000..fd43c61 --- /dev/null +++ b/build/WgpuNativeArtifact.zig @@ -0,0 +1,156 @@ +const std = @import("std"); +const Platform = @import("platform/root.zig"); + +pub const Result = struct { + header: std.Build.LazyPath, + webgpu_header: std.Build.LazyPath, + library_dir: std.Build.LazyPath, + link_library: ?std.Build.LazyPath, + runtime_library: ?std.Build.LazyPath, + static_library: std.Build.LazyPath, + static_name: []const u8, + dynamic_library: std.Build.LazyPath, + dynamic_name: []const u8, + dynamic_import_library: ?std.Build.LazyPath, + dynamic_import_name: ?[]const u8, +}; + +pub fn build( + b: *std.Build, + platform: Platform.Config, + optimize: std.builtin.OptimizeMode, + link_mode: std.builtin.LinkMode, + use_prebuilt: bool, +) ?Result { + const local_prebuilt_dir = b.graph.environ_map.get("WGPU_NATIVE_PREBUILT_DIR"); + if (use_prebuilt or local_prebuilt_dir != null) { + return buildPrebuilt( + b, + platform, + optimize, + link_mode, + local_prebuilt_dir, + ); + } + return buildSource(b, platform, optimize, link_mode); +} + +fn buildPrebuilt( + b: *std.Build, + platform: Platform.Config, + optimize: std.builtin.OptimizeMode, + link_mode: std.builtin.LinkMode, + local_prebuilt_dir: ?[]const u8, +) ?Result { + const root: std.Build.LazyPath = if (local_prebuilt_dir) |path| + .{ .cwd_relative = b.dupePath(path) } + else blk: { + const base = platform.prebuilt_base orelse std.debug.panic( + "No published wgpu-native archive exists for {s}; unset WGPU_NATIVE_USE_PREBUILT or provide WGPU_NATIVE_PREBUILT_DIR", + .{platform.rust_target}, + ); + const mode = if (optimize == .Debug) "debug" else "release"; + const dependency_name = b.fmt("{s}_{s}", .{ base, mode }); + const dependency = b.lazyDependency(dependency_name, .{}) orelse return null; + break :blk dependency.path(""); + }; + + return artifactFromPaths( + b, + platform, + link_mode, + root.path(b, "lib"), + root.path(b, "include/webgpu/wgpu.h"), + root.path(b, "include/webgpu/webgpu.h"), + ); +} + +fn buildSource( + b: *std.Build, + platform: Platform.Config, + optimize: std.builtin.OptimizeMode, + link_mode: std.builtin.LinkMode, +) ?Result { + const source_dependency = b.lazyDependency("wgpu_native_source", .{}); + const headers_dependency = b.lazyDependency("webgpu_headers_source", .{}); + if (source_dependency == null or headers_dependency == null) return null; + + const staged_source = b.addWriteFiles(); + _ = staged_source.addCopyDirectory(source_dependency.?.path(""), "", .{}); + _ = staged_source.addCopyDirectory( + headers_dependency.?.path(""), + "ffi/webgpu-headers", + .{}, + ); + _ = staged_source.addCopyFile( + headers_dependency.?.path("webgpu.h"), + "ffi/webgpu.h", + ); + const source_root = staged_source.getDirectory(); + + const cargo = b.addSystemCommand(&.{ + "cargo", + "build", + "--locked", + "--manifest-path", + }); + cargo.setName(b.fmt("build wgpu-native for {s}", .{platform.rust_target})); + cargo.addFileArg(source_root.path(b, "Cargo.toml")); + cargo.addArgs(&.{ "--target", platform.rust_target, "--target-dir" }); + const cargo_target_dir = cargo.addOutputDirectoryArg("wgpu-native-target"); + if (optimize != .Debug) cargo.addArg("--release"); + Platform.configureSource(b, cargo, platform); + + const profile = if (optimize == .Debug) "debug" else "release"; + const artifact_root = cargo_target_dir.path( + b, + b.fmt("{s}/{s}", .{ platform.rust_target, profile }), + ); + + return artifactFromPaths( + b, + platform, + link_mode, + artifact_root, + source_root.path(b, "ffi/wgpu.h"), + source_root.path(b, "ffi/webgpu.h"), + ); +} + +fn artifactFromPaths( + b: *std.Build, + platform: Platform.Config, + link_mode: std.builtin.LinkMode, + library_dir: std.Build.LazyPath, + header: std.Build.LazyPath, + webgpu_header: std.Build.LazyPath, +) Result { + const static_name = platform.static_name; + const dynamic_name = platform.dynamic_name; + const dynamic_import_name = platform.dynamic_import_name; + const static_library = library_dir.path(b, static_name); + const dynamic_library = library_dir.path(b, dynamic_name); + const dynamic_import_library = if (dynamic_import_name) |name| + library_dir.path(b, name) + else + null; + + return .{ + .header = header, + .webgpu_header = webgpu_header, + .library_dir = library_dir, + .link_library = if (link_mode == .static) + static_library + else if (dynamic_import_library != null) + dynamic_import_library + else + null, + .runtime_library = if (link_mode == .dynamic) dynamic_library else null, + .static_library = static_library, + .static_name = static_name, + .dynamic_library = dynamic_library, + .dynamic_name = dynamic_name, + .dynamic_import_library = dynamic_import_library, + .dynamic_import_name = dynamic_import_name, + }; +} diff --git a/build/platform/android.zig b/build/platform/android.zig new file mode 100644 index 0000000..8f1e117 --- /dev/null +++ b/build/platform/android.zig @@ -0,0 +1,104 @@ +const std = @import("std"); +const common = @import("common.zig"); +const types = @import("types.zig"); + +pub fn build(_: *std.Build, target: std.Build.ResolvedTarget) types.Config { + const result = target.result; + return switch (result.cpu.arch) { + .aarch64 => .{ + .kind = .android, + .target = target, + .rust_target = "aarch64-linux-android", + .prebuilt_base = "wgpu_android_aarch64", + .clang_target = "aarch64-linux-android", + }, + .arm => .{ + .kind = .android, + .target = target, + .rust_target = "armv7-linux-androideabi", + .prebuilt_base = "wgpu_android_armv7", + .clang_target = "armv7a-linux-androideabi", + }, + .x86 => .{ + .kind = .android, + .target = target, + .rust_target = "i686-linux-android", + .prebuilt_base = "wgpu_android_x86", + .clang_target = "i686-linux-android", + }, + .x86_64 => .{ + .kind = .android, + .target = target, + .rust_target = "x86_64-linux-android", + .prebuilt_base = "wgpu_android_x86_64", + .clang_target = "x86_64-linux-android", + }, + else => common.unsupportedTarget(result), + }; +} + +pub fn configureSource( + b: *std.Build, + 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 clang_target = config.clang_target.?; + const linker = b.pathJoin(&.{ + toolchain_root, + "bin", + b.fmt("{s}{s}-clang", .{ clang_target, api_level }), + }); + common.configureCargoLinker( + b, + cargo, + config.rust_target, + linker, + b.fmt("{s}++", .{linker}), + ); + common.configureBindgen( + b, + cargo, + clang_target, + b.pathJoin(&.{ toolchain_root, "sysroot" }), + ); +} + +pub fn configureModule( + _: *std.Build, + _: types.Config, + _: *std.Build.Module, + _: std.builtin.LinkMode, +) void {} + +pub fn configureTranslateC( + _: types.Config, + _: *std.Build.Step.TranslateC, +) void {} + +pub fn configureCompile(_: types.Config, _: *std.Build.Step.Compile) void {} + +pub fn configureTest( + _: types.Config, + _: *std.Build.Module, + _: std.builtin.LinkMode, +) void {} diff --git a/build/platform/apple.zig b/build/platform/apple.zig new file mode 100644 index 0000000..a8d0783 --- /dev/null +++ b/build/platform/apple.zig @@ -0,0 +1,115 @@ +const builtin = @import("builtin"); +const std = @import("std"); +const common = @import("common.zig"); +const types = @import("types.zig"); + +pub fn build(b: *std.Build, target: std.Build.ResolvedTarget) types.Config { + const result = target.result; + const sdk_root = findSdkRoot(b, result); + return switch (result.os.tag) { + .ios => switch (result.cpu.arch) { + .aarch64 => if (result.abi == .simulator) + .{ + .kind = .apple, + .target = target, + .rust_target = "aarch64-apple-ios-sim", + .prebuilt_base = "wgpu_ios_aarch64_simulator", + .dynamic_name = "libwgpu_native.dylib", + .apple_sdk_root = sdk_root, + } + else + .{ + .kind = .apple, + .target = target, + .rust_target = "aarch64-apple-ios", + .prebuilt_base = "wgpu_ios_aarch64", + .dynamic_name = "libwgpu_native.dylib", + .apple_sdk_root = sdk_root, + }, + .x86_64 => if (result.abi == .simulator) + .{ + .kind = .apple, + .target = target, + .rust_target = "x86_64-apple-ios", + .prebuilt_base = "wgpu_ios_x86_64_simulator", + .dynamic_name = "libwgpu_native.dylib", + .apple_sdk_root = sdk_root, + } + else + common.unsupportedTarget(result), + else => common.unsupportedTarget(result), + }, + .macos => switch (result.cpu.arch) { + .aarch64 => .{ + .kind = .apple, + .target = target, + .rust_target = "aarch64-apple-darwin", + .prebuilt_base = "wgpu_macos_aarch64", + .dynamic_name = "libwgpu_native.dylib", + .apple_sdk_root = sdk_root, + }, + .x86_64 => .{ + .kind = .apple, + .target = target, + .rust_target = "x86_64-apple-darwin", + .prebuilt_base = "wgpu_macos_x86_64", + .dynamic_name = "libwgpu_native.dylib", + .apple_sdk_root = sdk_root, + }, + else => common.unsupportedTarget(result), + }, + else => common.unsupportedTarget(result), + }; +} + +pub fn configureSource( + _: *std.Build, + _: *std.Build.Step.Run, + _: types.Config, +) void {} + +pub fn configureModule( + b: *std.Build, + config: types.Config, + mod: *std.Build.Module, + _: std.builtin.LinkMode, +) void { + // Explicit Apple targets do not inherit native SDK search paths. + if (config.apple_sdk_root) |sdk_root| { + mod.addLibraryPath(.{ .cwd_relative = b.pathJoin(&.{ + sdk_root, + "usr", + "lib", + }) }); + mod.addSystemFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ + sdk_root, + "System", + "Library", + "Frameworks", + }) }); + } + mod.linkFramework("Foundation", .{}); + mod.linkFramework("QuartzCore", .{}); + mod.linkFramework("Metal", .{}); +} + +pub fn configureTranslateC( + _: types.Config, + _: *std.Build.Step.TranslateC, +) void {} + +pub fn configureCompile(_: types.Config, _: *std.Build.Step.Compile) void {} + +pub fn configureTest( + _: types.Config, + _: *std.Build.Module, + _: std.builtin.LinkMode, +) void {} + +fn findSdkRoot(b: *std.Build, target: std.Target) ?[]const u8 { + if (b.graph.environ_map.get("SDKROOT")) |sdk_root| { + if (std.fs.path.isAbsolute(sdk_root)) return sdk_root; + } + if (!comptime builtin.os.tag.isDarwin()) return null; + return std.zig.system.darwin.getSdk(b.allocator, b.graph.io, &target); +} diff --git a/build/platform/common.zig b/build/platform/common.zig new file mode 100644 index 0000000..a3d038b --- /dev/null +++ b/build/platform/common.zig @@ -0,0 +1,59 @@ +const std = @import("std"); + +pub fn configureCargoLinker( + b: *std.Build, + cargo: *std.Build.Step.Run, + rust_target: []const u8, + linker: []const u8, + cxx: []const u8, +) void { + const cargo_target = envTargetName(b, rust_target, true); + const cc_target = envTargetName(b, rust_target, false); + cargo.setEnvironmentVariable( + b.fmt("CARGO_TARGET_{s}_LINKER", .{cargo_target}), + linker, + ); + cargo.setEnvironmentVariable(b.fmt("CC_{s}", .{cc_target}), linker); + cargo.setEnvironmentVariable(b.fmt("CXX_{s}", .{cc_target}), cxx); +} + +pub fn configureBindgen( + b: *std.Build, + cargo: *std.Build.Step.Run, + clang_target: []const u8, + sysroot: []const u8, +) void { + const existing = b.graph.environ_map.get("BINDGEN_EXTRA_CLANG_ARGS"); + const args = if (existing) |value| + b.fmt("{s} --target={s} --sysroot={s}", .{ value, clang_target, sysroot }) + else + b.fmt("--target={s} --sysroot={s}", .{ clang_target, sysroot }); + cargo.setEnvironmentVariable("BINDGEN_EXTRA_CLANG_ARGS", args); +} + +pub fn unsupportedTarget(target: std.Target) noreturn { + std.debug.panic( + "wgpu-native source builds do not support {s}-{s}-{s}", + .{ + @tagName(target.cpu.arch), + @tagName(target.os.tag), + @tagName(target.abi), + }, + ); +} + +fn envTargetName( + b: *std.Build, + target: []const u8, + uppercase: bool, +) []const u8 { + const result = b.allocator.dupe(u8, target) catch @panic("OOM"); + for (result) |*character| { + if (character.* == '-') { + character.* = '_'; + } else if (uppercase) { + character.* = std.ascii.toUpper(character.*); + } + } + return result; +} diff --git a/build/platform/linux.zig b/build/platform/linux.zig new file mode 100644 index 0000000..a026987 --- /dev/null +++ b/build/platform/linux.zig @@ -0,0 +1,66 @@ +const std = @import("std"); +const common = @import("common.zig"); +const types = @import("types.zig"); + +pub fn build(_: *std.Build, target: std.Build.ResolvedTarget) types.Config { + const result = target.result; + return switch (result.cpu.arch) { + .aarch64 => switch (result.abi) { + .gnu => .{ + .kind = .linux, + .target = target, + .rust_target = "aarch64-unknown-linux-gnu", + .prebuilt_base = "wgpu_linux_aarch64", + }, + .musl => .{ + .kind = .linux, + .target = target, + .rust_target = "aarch64-unknown-linux-musl", + .prebuilt_base = null, + }, + else => common.unsupportedTarget(result), + }, + .x86_64 => switch (result.abi) { + .gnu => .{ + .kind = .linux, + .target = target, + .rust_target = "x86_64-unknown-linux-gnu", + .prebuilt_base = "wgpu_linux_x86_64", + }, + .musl => .{ + .kind = .linux, + .target = target, + .rust_target = "x86_64-unknown-linux-musl", + .prebuilt_base = null, + }, + else => common.unsupportedTarget(result), + }, + else => common.unsupportedTarget(result), + }; +} + +pub fn configureSource( + _: *std.Build, + _: *std.Build.Step.Run, + _: types.Config, +) void {} + +pub fn configureModule( + _: *std.Build, + _: types.Config, + _: *std.Build.Module, + _: std.builtin.LinkMode, +) void {} + +pub fn configureTranslateC( + _: types.Config, + _: *std.Build.Step.TranslateC, +) void {} + +pub fn configureCompile(_: types.Config, _: *std.Build.Step.Compile) void {} + +pub fn configureTest( + _: types.Config, + _: *std.Build.Module, + _: std.builtin.LinkMode, +) void {} diff --git a/build/platform/ohos.zig b/build/platform/ohos.zig new file mode 100644 index 0000000..5b87651 --- /dev/null +++ b/build/platform/ohos.zig @@ -0,0 +1,106 @@ +const std = @import("std"); +const common = @import("common.zig"); +const types = @import("types.zig"); + +pub fn build(_: *std.Build, target: std.Build.ResolvedTarget) types.Config { + const result = target.result; + return switch (result.cpu.arch) { + .aarch64 => .{ + .kind = .ohos, + .target = target, + .rust_target = "aarch64-unknown-linux-ohos", + .prebuilt_base = null, + .clang_target = "aarch64-unknown-linux-ohos", + .ohos_library_target = "aarch64-linux-ohos", + }, + .arm => .{ + .kind = .ohos, + .target = target, + .rust_target = "armv7-unknown-linux-ohos", + .prebuilt_base = null, + .clang_target = "armv7-unknown-linux-ohos", + .ohos_library_target = "arm-linux-ohos", + }, + .x86_64 => .{ + .kind = .ohos, + .target = target, + .rust_target = "x86_64-unknown-linux-ohos", + .prebuilt_base = null, + .clang_target = "x86_64-unknown-linux-ohos", + .ohos_library_target = "x86_64-linux-ohos", + }, + else => common.unsupportedTarget(result), + }; +} + +pub fn configureSource( + b: *std.Build, + cargo: *std.Build.Step.Run, + config: types.Config, +) void { + const native_root = nativeRoot(b, config); + const clang_target = config.clang_target.?; + const linker = b.pathJoin(&.{ + native_root, + "llvm", + "bin", + b.fmt("{s}-clang", .{clang_target}), + }); + const cxx = b.pathJoin(&.{ + native_root, + "llvm", + "bin", + b.fmt("{s}-clang++", .{clang_target}), + }); + common.configureCargoLinker(b, cargo, config.rust_target, linker, cxx); + common.configureBindgen( + b, + cargo, + clang_target, + b.pathJoin(&.{ native_root, "sysroot" }), + ); +} + +pub fn configureModule( + b: *std.Build, + config: types.Config, + mod: *std.Build.Module, + _: std.builtin.LinkMode, +) void { + const library_dir: std.Build.LazyPath = .{ .cwd_relative = b.pathJoin(&.{ + nativeRoot(b, config), + "sysroot", + "usr", + "lib", + config.ohos_library_target.?, + }) }; + mod.addLibraryPath(library_dir); + mod.linkSystemLibrary("c", .{}); + mod.addObjectFile(library_dir.path(b, "libc.so")); +} + +pub fn configureTranslateC( + _: types.Config, + _: *std.Build.Step.TranslateC, +) void {} + +pub fn configureCompile(_: types.Config, _: *std.Build.Step.Compile) void {} + +pub fn configureTest( + _: types.Config, + _: *std.Build.Module, + _: std.builtin.LinkMode, +) void {} + +fn nativeRoot(b: *std.Build, config: types.Config) []const u8 { + const sdk_root = b.graph.environ_map.get("OHOS_NDK_HOME") orelse + b.graph.environ_map.get("OHOS_SDK_HOME") orelse + std.debug.panic( + "Building {s} requires OHOS_NDK_HOME or OHOS_SDK_HOME", + .{config.rust_target}, + ); + return if (std.mem.eql(u8, std.fs.path.basename(sdk_root), "native")) + sdk_root + else + b.pathJoin(&.{ sdk_root, "native" }); +} diff --git a/build/platform/root.zig b/build/platform/root.zig new file mode 100644 index 0000000..c6f2e37 --- /dev/null +++ b/build/platform/root.zig @@ -0,0 +1,95 @@ +const std = @import("std"); +const android = @import("android.zig"); +const apple = @import("apple.zig"); +const linux = @import("linux.zig"); +const ohos = @import("ohos.zig"); +const types = @import("types.zig"); +const windows = @import("windows.zig"); + +pub const Config = types.Config; +pub const Kind = types.Kind; + +pub fn build(b: *std.Build, target: std.Build.ResolvedTarget) Config { + const result = target.result; + if (result.abi == .ohos or result.abi == .ohoseabi) { + return ohos.build(b, target); + } + if (result.abi == .android or result.abi == .androideabi) { + return android.build(b, target); + } + return switch (result.os.tag) { + .ios, .macos => apple.build(b, target), + .linux => linux.build(b, target), + .windows => windows.build(b, target), + else => @import("common.zig").unsupportedTarget(result), + }; +} + +pub fn configureSource( + b: *std.Build, + cargo: *std.Build.Step.Run, + config: Config, +) void { + switch (config.kind) { + .android => android.configureSource(b, cargo, config), + .apple => apple.configureSource(b, cargo, config), + .linux => linux.configureSource(b, cargo, config), + .ohos => ohos.configureSource(b, cargo, config), + .windows => windows.configureSource(b, cargo, config), + } +} + +pub fn configureModule( + b: *std.Build, + config: Config, + mod: *std.Build.Module, + link_mode: std.builtin.LinkMode, +) void { + switch (config.kind) { + .android => android.configureModule(b, config, mod, link_mode), + .apple => apple.configureModule(b, config, mod, link_mode), + .linux => linux.configureModule(b, config, mod, link_mode), + .ohos => ohos.configureModule(b, config, mod, link_mode), + .windows => windows.configureModule(b, config, mod, link_mode), + } +} + +pub fn configureTranslateC( + config: Config, + translate_c: *std.Build.Step.TranslateC, +) void { + switch (config.kind) { + .android => android.configureTranslateC(config, translate_c), + .apple => apple.configureTranslateC(config, translate_c), + .linux => linux.configureTranslateC(config, translate_c), + .ohos => ohos.configureTranslateC(config, translate_c), + .windows => windows.configureTranslateC(config, translate_c), + } +} + +pub fn configureCompile( + config: Config, + compile: *std.Build.Step.Compile, +) void { + switch (config.kind) { + .android => android.configureCompile(config, compile), + .apple => apple.configureCompile(config, compile), + .linux => linux.configureCompile(config, compile), + .ohos => ohos.configureCompile(config, compile), + .windows => windows.configureCompile(config, compile), + } +} + +pub fn configureTest( + config: Config, + mod: *std.Build.Module, + link_mode: std.builtin.LinkMode, +) void { + switch (config.kind) { + .android => android.configureTest(config, mod, link_mode), + .apple => apple.configureTest(config, mod, link_mode), + .linux => linux.configureTest(config, mod, link_mode), + .ohos => ohos.configureTest(config, mod, link_mode), + .windows => windows.configureTest(config, mod, link_mode), + } +} diff --git a/build/platform/types.zig b/build/platform/types.zig new file mode 100644 index 0000000..772a360 --- /dev/null +++ b/build/platform/types.zig @@ -0,0 +1,22 @@ +const std = @import("std"); + +pub const Kind = enum { + android, + apple, + linux, + ohos, + windows, +}; + +pub const Config = struct { + kind: Kind, + target: std.Build.ResolvedTarget, + rust_target: []const u8, + prebuilt_base: ?[]const u8, + static_name: []const u8 = "libwgpu_native.a", + dynamic_name: []const u8 = "libwgpu_native.so", + dynamic_import_name: ?[]const u8 = null, + clang_target: ?[]const u8 = null, + ohos_library_target: ?[]const u8 = null, + apple_sdk_root: ?[]const u8 = null, +}; diff --git a/build/platform/windows.zig b/build/platform/windows.zig new file mode 100644 index 0000000..a403248 --- /dev/null +++ b/build/platform/windows.zig @@ -0,0 +1,131 @@ +const std = @import("std"); +const common = @import("common.zig"); +const types = @import("types.zig"); + +pub fn build(_: *std.Build, target: std.Build.ResolvedTarget) types.Config { + const result = target.result; + return switch (result.cpu.arch) { + .aarch64 => if (result.abi == .msvc) + .{ + .kind = .windows, + .target = target, + .rust_target = "aarch64-pc-windows-msvc", + .prebuilt_base = "wgpu_windows_aarch64_msvc", + .static_name = "wgpu_native.lib", + .dynamic_name = "wgpu_native.dll", + .dynamic_import_name = "wgpu_native.dll.lib", + } + else + common.unsupportedTarget(result), + .x86 => switch (result.abi) { + .msvc => .{ + .kind = .windows, + .target = target, + .rust_target = "i686-pc-windows-msvc", + .prebuilt_base = "wgpu_windows_x86_msvc", + .static_name = "wgpu_native.lib", + .dynamic_name = "wgpu_native.dll", + .dynamic_import_name = "wgpu_native.dll.lib", + }, + .gnu => .{ + .kind = .windows, + .target = target, + .rust_target = "i686-pc-windows-gnu", + .prebuilt_base = null, + .dynamic_name = "wgpu_native.dll", + .dynamic_import_name = "libwgpu_native.dll.a", + }, + else => common.unsupportedTarget(result), + }, + .x86_64 => switch (result.abi) { + .msvc => .{ + .kind = .windows, + .target = target, + .rust_target = "x86_64-pc-windows-msvc", + .prebuilt_base = "wgpu_windows_x86_64_msvc", + .static_name = "wgpu_native.lib", + .dynamic_name = "wgpu_native.dll", + .dynamic_import_name = "wgpu_native.dll.lib", + }, + .gnu => .{ + .kind = .windows, + .target = target, + .rust_target = "x86_64-pc-windows-gnu", + .prebuilt_base = "wgpu_windows_x86_64_gnu", + .dynamic_name = "wgpu_native.dll", + .dynamic_import_name = "libwgpu_native.dll.a", + }, + else => common.unsupportedTarget(result), + }, + else => common.unsupportedTarget(result), + }; +} + +pub fn configureSource( + _: *std.Build, + _: *std.Build.Step.Run, + _: types.Config, +) void {} + +pub fn configureModule( + _: *std.Build, + config: types.Config, + mod: *std.Build.Module, + link_mode: std.builtin.LinkMode, +) void { + const is_gnu = config.target.result.abi == .gnu; + if (!is_gnu) { + mod.link_libcpp = false; + mod.link_libc = true; + } + if (link_mode == .static) linkSystemLibraries(mod, is_gnu); +} + +pub fn configureTranslateC( + config: types.Config, + translate_c: *std.Build.Step.TranslateC, +) void { + if (config.target.result.abi == .msvc) { + // Zig 0.16 translate-c does not accept MSVC's `ui64` literal suffix. + // stdint.h guards SIZE_MAX, so define it using the portable builtin. + translate_c.defineCMacro("SIZE_MAX", "__SIZE_MAX__"); + } +} + +pub fn configureCompile( + config: types.Config, + compile: *std.Build.Step.Compile, +) void { + if (config.target.result.abi == .msvc) { + compile.bundle_compiler_rt = false; + compile.bundle_ubsan_rt = false; + } +} + +pub fn configureTest( + config: types.Config, + mod: *std.Build.Module, + link_mode: std.builtin.LinkMode, +) void { + if (link_mode == .static and config.target.result.abi == .gnu) { + mod.linkSystemLibrary("unwind", .{}); + } +} + +fn linkSystemLibraries(mod: *std.Build.Module, is_gnu: bool) void { + if (is_gnu) { + mod.linkSystemLibrary("d3dcompiler_47", .{}); + mod.linkSystemLibrary("api-ms-win-core-winrt-error-l1-1-0", .{}); + } else { + mod.linkSystemLibrary("d3dcompiler", .{}); + mod.linkSystemLibrary("user32", .{}); + mod.linkSystemLibrary("RuntimeObject", .{}); + } + mod.linkSystemLibrary("opengl32", .{}); + mod.linkSystemLibrary("gdi32", .{}); + mod.linkSystemLibrary("OleAut32", .{}); + mod.linkSystemLibrary("Ole32", .{}); + mod.linkSystemLibrary("ws2_32", .{}); + mod.linkSystemLibrary("userenv", .{}); + mod.linkSystemLibrary("propsys", .{}); +} diff --git a/examples/build.zig b/examples/build.zig new file mode 100644 index 0000000..e13984a --- /dev/null +++ b/examples/build.zig @@ -0,0 +1,31 @@ +const Library = @import("../build/Library.zig"); +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const library = Library.build(b, Library.standardOptions(b)) orelse return; + if (library.isOhos()) return; + + const bmp_mod = b.createModule(.{ + .root_source_file = b.path("examples/bmp.zig"), + }); + const triangle_mod = b.createModule(.{ + .root_source_file = b.path("examples/triangle/triangle.zig"), + .target = library.target, + .optimize = library.optimize, + }); + triangle_mod.addImport("wgpu", library.wgpu_mod); + triangle_mod.addImport("bmp", bmp_mod); + + const triangle = b.addExecutable(.{ + .name = "triangle-example", + .root_module = triangle_mod, + }); + library.configureCompile(triangle); + const check_step = b.step("check", "Compile all examples without running them"); + check_step.dependOn(&triangle.step); + + const run_triangle = b.addRunArtifact(triangle); + library.configureRun(b, run_triangle); + const run_step = b.step("run-triangle-example", "Run the triangle example"); + run_step.dependOn(&run_triangle.step); +} diff --git a/test-all b/test-all deleted file mode 100755 index cb5eb05..0000000 --- a/test-all +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/sh -zig build test compute-tests --summary all diff --git a/tests/build.zig b/tests/build.zig new file mode 100644 index 0000000..37a5df7 --- /dev/null +++ b/tests/build.zig @@ -0,0 +1,115 @@ +const Library = @import("../build/Library.zig"); +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const library = Library.build(b, Library.standardOptions(b)) orelse return; + const check_step = b.step( + "check", + "Compile the bindings and all target-compatible tests", + ); + + if (library.isOhos()) { + linkProbe(b, library, check_step); + return; + } + + unitTests(b, library, check_step); + computeTests(b, library, check_step); +} + +fn unitTests( + b: *std.Build, + library: Library.Result, + check_step: *std.Build.Step, +) void { + const unit_test_step = b.step("test", "Run unit tests"); + const test_files = [_][:0]const u8{ + "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"; + }; + + for (test_files, test_names) |test_file, test_name| { + const test_mod = b.createModule(.{ + .root_source_file = b.path(test_file), + .target = library.target, + .optimize = library.optimize, + }); + library.linkTestModule(b, test_mod); + const test_exe = b.addTest(.{ + .name = test_name, + .root_module = test_mod, + }); + library.configureCompile(test_exe); + check_step.dependOn(&test_exe.step); + + const run_test = b.addRunArtifact(test_exe); + library.configureRun(b, run_test); + unit_test_step.dependOn(&run_test.step); + } +} + +fn computeTests( + b: *std.Build, + library: Library.Result, + check_step: *std.Build.Step, +) void { + const compute_test_mod = b.createModule(.{ + .root_source_file = b.path("tests/compute.zig"), + .target = library.target, + .optimize = library.optimize, + }); + compute_test_mod.addImport("wgpu", library.wgpu_mod); + const compute_test = b.addTest(.{ + .name = "compute-test", + .root_module = compute_test_mod, + }); + library.configureCompile(compute_test); + check_step.dependOn(&compute_test.step); + const run_compute_test = b.addRunArtifact(compute_test); + library.configureRun(b, run_compute_test); + + const compute_test_c_mod = b.createModule(.{ + .root_source_file = b.path("tests/compute_c.zig"), + .target = library.target, + .optimize = library.optimize, + }); + compute_test_c_mod.addImport("wgpu-c", library.wgpu_c_mod); + const compute_test_c = b.addTest(.{ + .name = "compute-test-c", + .root_module = compute_test_c_mod, + }); + library.configureCompile(compute_test_c); + check_step.dependOn(&compute_test_c.step); + const run_compute_test_c = b.addRunArtifact(compute_test_c); + library.configureRun(b, run_compute_test_c); + + const compute_test_step = b.step("compute-tests", "Run compute shader tests"); + compute_test_step.dependOn(&run_compute_test.step); + compute_test_step.dependOn(&run_compute_test_c.step); +} + +fn linkProbe( + b: *std.Build, + library: Library.Result, + check_step: *std.Build.Step, +) void { + const probe_mod = b.createModule(.{ + .root_source_file = b.path("tests/link_probe.zig"), + .target = library.target, + .optimize = library.optimize, + }); + probe_mod.addImport("wgpu", library.wgpu_mod); + const probe = b.addLibrary(.{ + .name = "wgpu-native-zig-link-probe", + .root_module = probe_mod, + .linkage = .dynamic, + }); + library.configureCompile(probe); + _ = probe.getEmittedBin(); + check_step.dependOn(&probe.step); +} diff --git a/tests/link_probe.zig b/tests/link_probe.zig new file mode 100644 index 0000000..725b0b2 --- /dev/null +++ b/tests/link_probe.zig @@ -0,0 +1,7 @@ +const wgpu = @import("wgpu"); + +export fn wgpuNativeZigLinkProbe() callconv(.c) u32 { + const instance = wgpu.Instance.create(null) orelse return 0; + instance.release(); + return 1; +}