From eb625d1a537244619a42a9e313027d42fd846115 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 28 Jul 2026 12:25:54 +0200 Subject: [PATCH 1/4] :wrench: --- .github/workflows/build-dawn.yml | 33 ++++++++++++++++--------- .gitmodules | 2 +- externals/dawn | 2 +- packages/webgpu/CONTRIBUTING.md | 25 ++++++++++--------- packages/webgpu/package.json | 3 ++- packages/webgpu/scripts/install-dawn.ts | 4 +-- 6 files changed, 42 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-dawn.yml b/.github/workflows/build-dawn.yml index 81d83cb75..7b07bdcca 100644 --- a/.github/workflows/build-dawn.yml +++ b/.github/workflows/build-dawn.yml @@ -18,28 +18,39 @@ jobs: - name: Checkout repository uses: actions/checkout@v6.0.2 - - name: Determine Dawn branch metadata + - name: Determine Dawn version metadata id: dawn_meta run: | set -eo pipefail - branch=$(git config -f .gitmodules submodule.externals/dawn.branch) - if [ -z "$branch" ]; then - echo "Could not determine Dawn branch from .gitmodules" >&2 + version=$(node -p "require('./packages/webgpu/package.json').dawn") + commit=$(node -p "require('./packages/webgpu/package.json').dawnCommit") + if [ -z "$version" ] || [ "$version" = "undefined" ]; then + echo "Could not determine Dawn version from package.json ('dawn' field)" >&2 exit 1 fi - slug=${branch//\//-} - echo "branch=$branch" >> "$GITHUB_OUTPUT" + if [ -z "$commit" ] || [ "$commit" = "undefined" ]; then + echo "Could not determine Dawn commit from package.json ('dawnCommit' field)" >&2 + exit 1 + fi + gitlink=$(git ls-tree HEAD externals/dawn | awk '{print $3}') + if [ "$gitlink" != "$commit" ]; then + echo "Submodule gitlink ($gitlink) does not match package.json dawnCommit ($commit)." >&2 + echo "Update the externals/dawn submodule and the dawnCommit field together." >&2 + exit 1 + fi + slug=${version//\//-} + echo "branch=$version" >> "$GITHUB_OUTPUT" echo "branch_slug=$slug" >> "$GITHUB_OUTPUT" - name: Compute release metadata id: release_meta run: | - tag="dawn-${DAWN_BRANCH_SLUG}" + tag="dawn-${DAWN_VERSION_SLUG}" echo "tag_name=$tag" >> "$GITHUB_OUTPUT" - echo "release_name=Dawn ${DAWN_BRANCH}" >> "$GITHUB_OUTPUT" + echo "release_name=Dawn ${DAWN_VERSION}" >> "$GITHUB_OUTPUT" env: - DAWN_BRANCH: ${{ steps.dawn_meta.outputs.branch }} - DAWN_BRANCH_SLUG: ${{ steps.dawn_meta.outputs.branch_slug }} + DAWN_VERSION: ${{ steps.dawn_meta.outputs.branch }} + DAWN_VERSION_SLUG: ${{ steps.dawn_meta.outputs.branch_slug }} - name: Create GitHub release id: create_release @@ -49,7 +60,7 @@ jobs: with: tag_name: ${{ steps.release_meta.outputs.tag_name }} name: ${{ steps.release_meta.outputs.release_name }} - body: "Dawn prebuilt binaries for version ${{ steps.dawn_meta.outputs.branch }}" + body: "Dawn prebuilt binaries for ${{ steps.dawn_meta.outputs.branch }} (Dawn commit pinned in package.json dawnCommit)" draft: false prerelease: true generate_release_notes: false diff --git a/.gitmodules b/.gitmodules index 609c52db0..81c45037a 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "externals/dawn"] path = externals/dawn url = https://dawn.googlesource.com/dawn - branch = chromium/7849 + branch = main diff --git a/externals/dawn b/externals/dawn index cbfe412bf..63f25feec 160000 --- a/externals/dawn +++ b/externals/dawn @@ -1 +1 @@ -Subproject commit cbfe412bf6c88a23f6936b7219e1129d7d0a1620 +Subproject commit 63f25feec51e9351fb25222b6d5de1af791d7c4f diff --git a/packages/webgpu/CONTRIBUTING.md b/packages/webgpu/CONTRIBUTING.md index c3368aab5..45464e40b 100644 --- a/packages/webgpu/CONTRIBUTING.md +++ b/packages/webgpu/CONTRIBUTING.md @@ -23,28 +23,31 @@ The Expo config plugin lives in `plugin/src` and is compiled to `plugin/build` b ## Upgrading Dawn -The Dawn version is pinned in two places that must stay in sync: +The Dawn version tracks the one shipped by `@shopify/react-native-skia` Graphite builds: the pin is the exact Dawn commit from the Skia milestone's DEPS file (`third_party/externals/dawn` in Skia's DEPS). It is recorded in two places that must stay in sync: -- `.gitmodules` → `submodule.externals/dawn.branch` (e.g. `chromium/7849`) -- `packages/webgpu/package.json` → the `"dawn"` field (same value, e.g. `chromium/7849`) +- the `externals/dawn` submodule gitlink (the commit the submodule points at) +- `packages/webgpu/package.json` → `"dawn"` (a human-readable label, e.g. `skia-m150`) and `"dawnCommit"` (the exact commit hash) -`yarn install-dawn` downloads **prebuilt** binaries from a GitHub release tagged `dawn-` (e.g. `dawn-chromium-7849`); the release host is configured at the top of `scripts/install-dawn.ts`. `yarn build-dawn` builds the same binaries from the submodule source instead. +The **Build Dawn** workflow verifies the gitlink matches `dawnCommit` and fails otherwise. -Steps to bump to a new Dawn version (`chromium/`): +`yarn install-dawn` downloads **prebuilt** binaries from a GitHub release on this repo tagged `dawn-` (e.g. `dawn-skia-m150`). `yarn build-dawn` builds the same binaries from the submodule source instead. -1. **Point the submodule at the new branch.** Update both `.gitmodules` and the `"dawn"` field in `package.json` to `chromium/`, then move the submodule to the new tip: +Steps to bump to a new Dawn version (new Skia milestone `m`): + +1. **Find the Dawn commit** in the Skia milestone's `DEPS` file (`third_party/externals/dawn` entry). + +2. **Point the submodule at that commit** and update `package.json` (`"dawn": "skia-m"`, `"dawnCommit": ""`): ```sh - git submodule set-branch --branch chromium/ externals/dawn - git submodule update --remote externals/dawn + cd externals/dawn && git fetch origin && git checkout && cd ../.. ``` -2. **Publish prebuilt binaries.** Trigger the **Build Dawn** workflow (`.github/workflows/build-dawn.yml`, `workflow_dispatch`). It reads the branch from `.gitmodules`, builds Android + Apple, and creates the `dawn-chromium-` release with the headers, the Android `.so`s, and the Apple `.xcframework`. (To build locally instead, run `yarn build-dawn`; this requires the Android NDK and Xcode toolchains.) +3. **Publish prebuilt binaries.** Trigger the **Build Dawn** workflow (`.github/workflows/build-dawn.yml`, `workflow_dispatch`). It builds Android + Apple from the submodule and creates the `dawn-skia-m` release with the headers, the Android `.so`s, and the Apple `.xcframework`. (To build locally instead, run `yarn build-dawn`; this requires the Android NDK and Xcode toolchains.) -3. **Pull the new binaries** once the release exists: +4. **Pull the new binaries** once the release exists: ```sh cd packages/webgpu && yarn install-dawn ``` -4. **Verify and commit.** Build and run the example app, then commit the submodule bump together with the updated `.gitmodules` and `package.json`. \ No newline at end of file +5. **Verify and commit.** Build and run the example app, then commit the submodule bump together with the updated `package.json`. \ No newline at end of file diff --git a/packages/webgpu/package.json b/packages/webgpu/package.json index d58fd6b80..cbab17a17 100644 --- a/packages/webgpu/package.json +++ b/packages/webgpu/package.json @@ -22,7 +22,8 @@ "app.plugin.js", "plugin/build/**" ], - "dawn": "chromium/7849", + "dawn": "skia-m150", + "dawnCommit": "63f25feec51e9351fb25222b6d5de1af791d7c4f", "scripts": { "test": "NODE_OPTIONS='--experimental-require-module' jest -i", "test:ref": "REFERENCE=true NODE_OPTIONS='--experimental-require-module' jest -i", diff --git a/packages/webgpu/scripts/install-dawn.ts b/packages/webgpu/scripts/install-dawn.ts index e6d827756..70f7c4aa0 100644 --- a/packages/webgpu/scripts/install-dawn.ts +++ b/packages/webgpu/scripts/install-dawn.ts @@ -75,7 +75,7 @@ if (!dawnVersion) { } // Parse the dawn version to construct the release tag -// Format: "chromium/7472" -> "dawn-chromium-7472" +// Format: "skia-m150" -> "dawn-skia-m150" const releaseTag = `dawn-${dawnVersion.replace("/", "-")}`; const releaseUrl = `https://github.com/wcandillon/react-native-webgpu/releases/tag/${releaseTag}`; @@ -171,7 +171,7 @@ const assetNames: { [key: string]: string } = { }; for (const [index, asset] of assets.entries()) { - const assetUrl = `https://github.com/Shopify/react-native-skia/releases/download/${releaseTag}/${asset.name}`; + const assetUrl = `https://github.com/wcandillon/react-native-webgpu/releases/download/${releaseTag}/${asset.name}`; const tarPath = join(libsDir, asset.name); const displayName = assetNames[asset.name] || asset.name; From c5d6810e8bdc6c05540de6063f2d37a6be70c75b Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 28 Jul 2026 12:35:16 +0200 Subject: [PATCH 2/4] :wrench: --- packages/webgpu/CONTRIBUTING.md | 8 ++++---- packages/webgpu/package.json | 2 +- packages/webgpu/scripts/install-dawn.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/webgpu/CONTRIBUTING.md b/packages/webgpu/CONTRIBUTING.md index 45464e40b..b98847007 100644 --- a/packages/webgpu/CONTRIBUTING.md +++ b/packages/webgpu/CONTRIBUTING.md @@ -26,23 +26,23 @@ The Expo config plugin lives in `plugin/src` and is compiled to `plugin/build` b The Dawn version tracks the one shipped by `@shopify/react-native-skia` Graphite builds: the pin is the exact Dawn commit from the Skia milestone's DEPS file (`third_party/externals/dawn` in Skia's DEPS). It is recorded in two places that must stay in sync: - the `externals/dawn` submodule gitlink (the commit the submodule points at) -- `packages/webgpu/package.json` → `"dawn"` (a human-readable label, e.g. `skia-m150`) and `"dawnCommit"` (the exact commit hash) +- `packages/webgpu/package.json` → `"dawn"` (a human-readable label, e.g. `chrome-m150`; Skia milestones mirror Chrome milestones) and `"dawnCommit"` (the exact commit hash) The **Build Dawn** workflow verifies the gitlink matches `dawnCommit` and fails otherwise. -`yarn install-dawn` downloads **prebuilt** binaries from a GitHub release on this repo tagged `dawn-` (e.g. `dawn-skia-m150`). `yarn build-dawn` builds the same binaries from the submodule source instead. +`yarn install-dawn` downloads **prebuilt** binaries from a GitHub release on this repo tagged `dawn-` (e.g. `dawn-chrome-m150`). `yarn build-dawn` builds the same binaries from the submodule source instead. Steps to bump to a new Dawn version (new Skia milestone `m`): 1. **Find the Dawn commit** in the Skia milestone's `DEPS` file (`third_party/externals/dawn` entry). -2. **Point the submodule at that commit** and update `package.json` (`"dawn": "skia-m"`, `"dawnCommit": ""`): +2. **Point the submodule at that commit** and update `package.json` (`"dawn": "chrome-m"`, `"dawnCommit": ""`): ```sh cd externals/dawn && git fetch origin && git checkout && cd ../.. ``` -3. **Publish prebuilt binaries.** Trigger the **Build Dawn** workflow (`.github/workflows/build-dawn.yml`, `workflow_dispatch`). It builds Android + Apple from the submodule and creates the `dawn-skia-m` release with the headers, the Android `.so`s, and the Apple `.xcframework`. (To build locally instead, run `yarn build-dawn`; this requires the Android NDK and Xcode toolchains.) +3. **Publish prebuilt binaries.** Trigger the **Build Dawn** workflow (`.github/workflows/build-dawn.yml`, `workflow_dispatch`). It builds Android + Apple from the submodule and creates the `dawn-chrome-m` release with the headers, the Android `.so`s, and the Apple `.xcframework`. (To build locally instead, run `yarn build-dawn`; this requires the Android NDK and Xcode toolchains.) 4. **Pull the new binaries** once the release exists: diff --git a/packages/webgpu/package.json b/packages/webgpu/package.json index cbab17a17..f3b2b765f 100644 --- a/packages/webgpu/package.json +++ b/packages/webgpu/package.json @@ -22,7 +22,7 @@ "app.plugin.js", "plugin/build/**" ], - "dawn": "skia-m150", + "dawn": "chrome-m150", "dawnCommit": "63f25feec51e9351fb25222b6d5de1af791d7c4f", "scripts": { "test": "NODE_OPTIONS='--experimental-require-module' jest -i", diff --git a/packages/webgpu/scripts/install-dawn.ts b/packages/webgpu/scripts/install-dawn.ts index 70f7c4aa0..345f6a3b6 100644 --- a/packages/webgpu/scripts/install-dawn.ts +++ b/packages/webgpu/scripts/install-dawn.ts @@ -75,7 +75,7 @@ if (!dawnVersion) { } // Parse the dawn version to construct the release tag -// Format: "skia-m150" -> "dawn-skia-m150" +// Format: "chrome-m150" -> "dawn-chrome-m150" const releaseTag = `dawn-${dawnVersion.replace("/", "-")}`; const releaseUrl = `https://github.com/wcandillon/react-native-webgpu/releases/tag/${releaseTag}`; From bc74f28e3724b6be633a9e534168b19cedc0a08a Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 28 Jul 2026 14:35:33 +0200 Subject: [PATCH 3/4] :wrench: --- apps/example/ios/Podfile.lock | 2 +- packages/webgpu/apple/WebGPUView.mm | 3 +-- packages/webgpu/cpp/rnwgpu/api/GPU.cpp | 21 ++++++++++++++++++ packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h | 24 +++++++++++++++++++++ packages/webgpu/react-native-webgpu.podspec | 5 +++++ packages/webgpu/src/importDevice.ts | 22 +++++++++++++++++++ packages/webgpu/src/index.tsx | 4 ++++ packages/webgpu/src/main/index.tsx | 1 + 8 files changed, 79 insertions(+), 3 deletions(-) create mode 100644 packages/webgpu/src/importDevice.ts diff --git a/apps/example/ios/Podfile.lock b/apps/example/ios/Podfile.lock index 9b93bfb63..943738687 100644 --- a/apps/example/ios/Podfile.lock +++ b/apps/example/ios/Podfile.lock @@ -3074,7 +3074,7 @@ SPEC CHECKSUMS: React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460 react-native-skia: fc73e9bdc46ebb420a98c9c2be29fee80f565e79 - react-native-webgpu: cc416064f9c8a68c6fde764a6c36305676f5fd65 + react-native-webgpu: 77c012dcaf2ba43b77e34c9ea237fab24ff1e0fb React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3 React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510 diff --git a/packages/webgpu/apple/WebGPUView.mm b/packages/webgpu/apple/WebGPUView.mm index 651030bf1..33e563740 100644 --- a/packages/webgpu/apple/WebGPUView.mm +++ b/packages/webgpu/apple/WebGPUView.mm @@ -6,8 +6,7 @@ #import #import "MetalView.h" -#import "RCTFabricComponentsPlugins.h" -#import "Utils.h" +#import using namespace facebook::react; diff --git a/packages/webgpu/cpp/rnwgpu/api/GPU.cpp b/packages/webgpu/cpp/rnwgpu/api/GPU.cpp index 92939b28c..85c0c5bee 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPU.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPU.cpp @@ -1,5 +1,7 @@ #include "GPU.h" +#include + #include #include #include @@ -14,6 +16,25 @@ namespace rnwgpu { GPU::GPU(jsi::Runtime & /*runtime*/) : NativeObject(CLASS_NAME) { + // If a Graphite build of @shopify/react-native-skia is loaded in this + // process, adopt its wgpu::Instance instead of creating our own, so + // Graphite's device and every JS-created device live on one instance. That + // makes pointer-based handoff (RNWebGPU.importDevice of Skia's device) + // sound: the async pump serves the shared instance, so callbacks on + // imported devices settle. Discovered at runtime via dlsym so there is no + // build-time coupling and native-module init order does not matter. Both + // packages must link the same single Dawn copy (they vendor the same + // artifact) for the handle to be meaningful. + using GetInstanceFn = WGPUInstance (*)(); + if (auto getSkiaInstance = reinterpret_cast( + dlsym(RTLD_DEFAULT, "rnskia_getWGPUInstance"))) { + if (WGPUInstance external = getSkiaInstance()) { + wgpuInstanceAddRef(external); + _instance = wgpu::Instance::Acquire(external); + return; + } + } + static const auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; wgpu::InstanceDescriptor instanceDesc{.requiredFeatureCount = 1, .requiredFeatures = &kTimedWaitAny}; diff --git a/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h b/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h index 9670a4509..f5ebdd8e6 100644 --- a/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h +++ b/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h @@ -9,6 +9,8 @@ #include "Canvas.h" #include "GPU.h" #include "GPUCanvasContext.h" +#include "GPUDevice.h" +#include "rnwgpu/async/RuntimeContext.h" #include "ImageBitmap.h" #include "PlatformContext.h" #include "VideoFrame.h" @@ -78,6 +80,26 @@ class RNWebGPU : public NativeObject { return ctx; } + // Wrap an externally created WGPUDevice (passed as a BigInt pointer, e.g. + // Skia's Graphite device from Skia.getNativeDevice()) in a GPUDevice. + // AddRefs the handle, so the original owner keeps its reference. Only sound + // because the process links a single Dawn copy and the GPU instance is + // shared with the exporter (see the GPU constructor): the calling runtime's + // pump serves the instance the device lives on, so its async callbacks + // settle normally. + std::shared_ptr importDevice(jsi::Runtime &runtime, + void *pointer) { + if (pointer == nullptr) { + throw std::runtime_error( + "importDevice: expected a non-null WGPUDevice pointer (BigInt)"); + } + auto raw = reinterpret_cast(pointer); + wgpuDeviceAddRef(raw); + wgpu::Device device = wgpu::Device::Acquire(raw); + auto ctx = async::RuntimeContext::getOrCreate(runtime, _gpu->get()); + return std::make_shared(device, ctx, "Imported Device"); + } + jsi::Value createImageBitmap(jsi::Runtime &runtime, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) { @@ -285,6 +307,8 @@ class RNWebGPU : public NativeObject { &RNWebGPU::createTestVideoFrame); installMethod(runtime, prototype, "createVideoFrameFromNativeBuffer", &RNWebGPU::createVideoFrameFromNativeBuffer); + installMethodWithRuntime(runtime, prototype, "importDevice", + &RNWebGPU::importDevice); installMethod(runtime, prototype, "createVideoPlayer", &RNWebGPU::createVideoPlayer); installMethod(runtime, prototype, "writeTestVideoFile", diff --git a/packages/webgpu/react-native-webgpu.podspec b/packages/webgpu/react-native-webgpu.podspec index 69e169e82..12857ffad 100644 --- a/packages/webgpu/react-native-webgpu.podspec +++ b/packages/webgpu/react-native-webgpu.podspec @@ -28,6 +28,11 @@ Pod::Spec.new do |s| s.pod_target_xcconfig = { 'HEADER_SEARCH_PATHS' => '$(PODS_TARGET_SRCROOT)/cpp', + # Xcode's all-target headermaps let same-named headers leak across pods + # (e.g. @shopify/react-native-skia keeps a jsi/ helper layer with + # identical relative header paths). Resolve includes strictly through our + # own search paths instead. + 'USE_HEADERMAP' => 'NO', } # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0. diff --git a/packages/webgpu/src/importDevice.ts b/packages/webgpu/src/importDevice.ts new file mode 100644 index 000000000..80be5693b --- /dev/null +++ b/packages/webgpu/src/importDevice.ts @@ -0,0 +1,22 @@ +/** + * Wraps an externally created WGPUDevice pointer into a GPUDevice. + * + * The canonical use case is adopting the Graphite device from a + * @shopify/react-native-skia Graphite build: + * + * ```ts + * const device = importDevice(Skia.getNativeDevice()); + * ``` + * + * This is only sound when the exporting library links the same single Dawn + * copy as react-native-webgpu and shares its wgpu::Instance (react-native-skia + * Graphite builds do both). + */ +export const importDevice = (pointer: bigint): GPUDevice => { + if (typeof RNWebGPU === "undefined") { + throw new Error( + "react-native-webgpu is not installed natively; importDevice is unavailable", + ); + } + return RNWebGPU.importDevice(pointer); +}; diff --git a/packages/webgpu/src/index.tsx b/packages/webgpu/src/index.tsx index 7d7ab3536..090e11014 100644 --- a/packages/webgpu/src/index.tsx +++ b/packages/webgpu/src/index.tsx @@ -58,6 +58,10 @@ declare global { // on Android) into a NativeVideoFrame. Matches the shape used by libraries // that emit NativeBuffer (e.g. react-native-vision-camera). createVideoFrameFromNativeBuffer: (pointer: bigint) => NativeVideoFrame; + // Wrap an externally created WGPUDevice pointer (e.g. Skia's Graphite + // device from Skia.getNativeDevice()) into a GPUDevice. Requires the + // exporter to share this process's single Dawn instance. + importDevice: (pointer: bigint) => GPUDevice; createVideoPlayer: ( path: string, pixelFormat?: NativeVideoPixelFormat, diff --git a/packages/webgpu/src/main/index.tsx b/packages/webgpu/src/main/index.tsx index fa39b927e..4c942f5e6 100644 --- a/packages/webgpu/src/main/index.tsx +++ b/packages/webgpu/src/main/index.tsx @@ -6,6 +6,7 @@ export * from "../Offscreen"; export * from "../WebGPUViewNativeComponent"; export * from "../hooks"; export * from "../GPUDeviceProvider"; +export * from "../importDevice"; export * from "../formats"; export { default as WebGPUModule } from "../NativeWebGPUModule"; From bb09fe4dd3022070e2699f38e873f0b34e61e5b1 Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 28 Jul 2026 14:42:47 +0200 Subject: [PATCH 4/4] :wrench: --- .../docs/getting-started/native-api.mdx | 12 +++++ apps/docs/content/docs/integrations/meta.json | 1 + .../docs/integrations/react-native-skia.mdx | 49 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 apps/docs/content/docs/integrations/react-native-skia.mdx diff --git a/apps/docs/content/docs/getting-started/native-api.mdx b/apps/docs/content/docs/getting-started/native-api.mdx index 0b0e78ff5..a055d7cc9 100644 --- a/apps/docs/content/docs/getting-started/native-api.mdx +++ b/apps/docs/content/docs/getting-started/native-api.mdx @@ -93,6 +93,18 @@ if (!device.features.has("rnwebgpu/native-texture" as GPUFeatureName)) { See [Vision Camera](/docs/integrations/vision-camera) for an end-to-end example applying WGSL effects to live camera frames, and [Native Extensions](/api/gpu-device-extensions) for the full API. +## Importing an external device + +`importDevice()` wraps a `WGPUDevice` created by another native library into a fully functional `GPUDevice`. The pointer is passed as a `BigInt`; the native side takes a reference, so the original owner keeps its own. + +```tsx +import { importDevice } from "react-native-webgpu"; + +const device = importDevice(pointer); +``` + +This is only sound when the exporting library links the exact same Dawn build as react-native-webgpu and shares its `wgpu::Instance`; otherwise the imported device's async callbacks would settle on an instance nobody pumps. The canonical exporter is a Graphite build of `@shopify/react-native-skia`, which satisfies both conditions automatically; see [React Native Skia](/docs/integrations/react-native-skia). + ## See also - [Canvas](/docs/getting-started/canvas) - the rendering surface and frame loop diff --git a/apps/docs/content/docs/integrations/meta.json b/apps/docs/content/docs/integrations/meta.json index a67122666..53e13aef8 100644 --- a/apps/docs/content/docs/integrations/meta.json +++ b/apps/docs/content/docs/integrations/meta.json @@ -4,6 +4,7 @@ "introduction", "worklets", "vision-camera", + "react-native-skia", "typegpu", "three-js", "react-three-fiber", diff --git a/apps/docs/content/docs/integrations/react-native-skia.mdx b/apps/docs/content/docs/integrations/react-native-skia.mdx new file mode 100644 index 000000000..526a9aacb --- /dev/null +++ b/apps/docs/content/docs/integrations/react-native-skia.mdx @@ -0,0 +1,49 @@ +--- +title: React Native Skia +description: Share one Dawn and one GPU device with Skia's Graphite renderer. +--- + +react-native-webgpu and [`@shopify/react-native-skia`](https://shopify.github.io/react-native-skia/) work alongside each other nicely. + +With the default (Ganesh) releases of react-native-skia there is no coupling at all: any version of either package can be installed together, and they simply don't interact. + +Graphite builds of react-native-skia (the `@next` release channel) render through Dawn, the same WebGPU implementation react-native-webgpu is built on, and the two packages are designed to share it: + +- **One Dawn copy.** Both packages vendor the exact same prebuilt Dawn artifact, pinned to the Dawn revision of the Skia milestone (for example `dawn-chrome-m150`). When both are installed, react-native-skia detects react-native-webgpu at `pod install` time and defers to its copy, so the process contains exactly one Dawn. A build-time guard compares the two packages' Dawn tags and fails the native build on a mismatch instead of letting two incompatible copies coexist. +- **One `wgpu::Instance`.** When a Graphite build of react-native-skia is present, react-native-webgpu adopts Skia's instance automatically at startup (discovered at runtime, independent of native-module initialization order). Graphite's device and every device you create through `navigator.gpu` live on the same instance. +- **The WebGPU API lives here.** Graphite builds of react-native-skia do not install `navigator.gpu` or any WebGPU JS API; react-native-webgpu is the single WebGPU API surface. If you want to write WebGPU code in an app that uses Skia, install react-native-webgpu alongside it. + + +Because a Graphite build links react-native-webgpu's Dawn copy, the two packages must be built against the same Dawn tag; this is what makes sharing an instance and importing devices sound. Releases are kept in lockstep, and the native build fails with an explicit message if the tags ever differ, so a mismatch surfaces at build time rather than as undefined behavior at runtime. Default (Ganesh) releases of react-native-skia have no such constraint. + + +## Rendering with Skia's device + +`Skia.getNativeDevice()` returns the raw pointer of Graphite's `wgpu::Device` as a `BigInt`, and [`importDevice()`](/docs/getting-started/native-api#importing-an-external-device) wraps it into a regular `GPUDevice`: + +```tsx +import { Skia } from "@shopify/react-native-skia"; +import { Canvas, importDevice } from "react-native-webgpu"; + +const device = importDevice(Skia.getNativeDevice()); +// Use it like any GPUDevice: create pipelines, submit work, +// render to a react-native-webgpu . +``` + +Because the device is shared with Graphite, GPU resources you create with it live on the same device Skia renders with, which is the foundation for zero-copy interop between the two worlds. + +A few things to know: + +- **Lifetime.** The device is owned by Skia's Graphite context and is valid for the lifetime of the process. `importDevice` takes its own reference. +- **Device loss.** Skia treats the loss of its device as fatal. Treat an imported device accordingly: there is no point requesting a replacement from `navigator.gpu` on loss. +- **You can still create your own devices.** `navigator.gpu.requestAdapter()` and `requestDevice()` work as usual; devices you create are siblings of Graphite's device on the shared instance. Use a dedicated device when you want your GPU work isolated from Skia's command queue. +- **Threading.** The imported device follows the same [threading model](/docs/getting-started/native-api#threading-model) as any other device. Note that Graphite records and submits its own work on the device concurrently; Dawn synchronizes device access internally. + +## Zero-copy texture sharing + +Sharing textures between Skia images and WebGPU textures on the shared device (the equivalent of Skia's previous `MakeTextureFromImage` / `MakeImageFromTexture`) is being rebuilt on top of this interop and will be documented here when it ships. In the meantime, platform surfaces (`CVPixelBuffer` / `AHardwareBuffer`) can cross the boundary through [`importSharedTextureMemory`](/api/gpu-device-extensions#importsharedtexturememory). + +## See also + +- [Native APIs](/docs/getting-started/native-api) - `importDevice` and the threading model +- [Native Extensions](/api/gpu-device-extensions) - native texture interop on `GPUDevice`