Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/docs/content/docs/getting-started/native-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/integrations/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"introduction",
"worklets",
"vision-camera",
"react-native-skia",
"typegpu",
"three-js",
"react-three-fiber",
Expand Down
49 changes: 49 additions & 0 deletions apps/docs/content/docs/integrations/react-native-skia.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Callout type="info" title="Versions">
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.
</Callout>

## 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 <Canvas />.
```

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`
2 changes: 1 addition & 1 deletion apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 1 addition & 2 deletions packages/webgpu/apple/WebGPUView.mm
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
#import <react/renderer/components/RNWgpuViewSpec/RCTComponentViewHelpers.h>

#import "MetalView.h"
#import "RCTFabricComponentsPlugins.h"
#import "Utils.h"
#import <React/RCTFabricComponentsPlugins.h>

using namespace facebook::react;

Expand Down
21 changes: 21 additions & 0 deletions packages/webgpu/cpp/rnwgpu/api/GPU.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#include "GPU.h"

#include <dlfcn.h>

#include <cstdio>
#include <memory>
#include <string>
Expand All @@ -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<GetInstanceFn>(
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};
Expand Down
24 changes: 24 additions & 0 deletions packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -78,6 +80,26 @@ class RNWebGPU : public NativeObject<RNWebGPU> {
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<GPUDevice> 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<WGPUDevice>(pointer);
wgpuDeviceAddRef(raw);
wgpu::Device device = wgpu::Device::Acquire(raw);
auto ctx = async::RuntimeContext::getOrCreate(runtime, _gpu->get());
return std::make_shared<GPUDevice>(device, ctx, "Imported Device");
}

jsi::Value createImageBitmap(jsi::Runtime &runtime,
const jsi::Value & /*thisVal*/,
const jsi::Value *args, size_t count) {
Expand Down Expand Up @@ -285,6 +307,8 @@ class RNWebGPU : public NativeObject<RNWebGPU> {
&RNWebGPU::createTestVideoFrame);
installMethod(runtime, prototype, "createVideoFrameFromNativeBuffer",
&RNWebGPU::createVideoFrameFromNativeBuffer);
installMethodWithRuntime(runtime, prototype, "importDevice",
&RNWebGPU::importDevice);
installMethod(runtime, prototype, "createVideoPlayer",
&RNWebGPU::createVideoPlayer);
installMethod(runtime, prototype, "writeTestVideoFile",
Expand Down
5 changes: 5 additions & 0 deletions packages/webgpu/react-native-webgpu.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
22 changes: 22 additions & 0 deletions packages/webgpu/src/importDevice.ts
Original file line number Diff line number Diff line change
@@ -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);
};
4 changes: 4 additions & 0 deletions packages/webgpu/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/webgpu/src/main/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Loading