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` 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";