From 792f192c7c9fc73490a294d48c61dcfa4ed9d57e Mon Sep 17 00:00:00 2001 From: William Candillon Date: Tue, 28 Jul 2026 15:56:05 +0200 Subject: [PATCH] :wrench: --- .../docs/integrations/react-native-skia.mdx | 23 ++++++++++++++++++- packages/webgpu/cpp/rnwgpu/api/GPUTexture.h | 8 +++++++ packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h | 16 +++++++++++++ packages/webgpu/src/importDevice.ts | 20 ++++++++++++++++ packages/webgpu/src/index.tsx | 11 +++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/integrations/react-native-skia.mdx b/apps/docs/content/docs/integrations/react-native-skia.mdx index 526a9aacb..463408906 100644 --- a/apps/docs/content/docs/integrations/react-native-skia.mdx +++ b/apps/docs/content/docs/integrations/react-native-skia.mdx @@ -41,7 +41,28 @@ A few things to know: ## 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). +Textures cross the package boundary the same way the device does: as raw pointers on the shared device. + +**WebGPU texture as a Skia image.** Every `GPUTexture` exposes a non-spec `nativePointer` (a `BigInt` of the `WGPUTexture` handle, borrowed: keep the `GPUTexture` alive while the pointer is in use). `Skia.Image.MakeImageFromNativeTexture` wraps it into an `SkImage` without copying: + +```tsx +const texture = device.createTexture({ ... }); +// render into it with WebGPU, then draw it with Skia: +const image = Skia.Image.MakeImageFromNativeTexture(texture.nativePointer); +``` + +**Skia image as a WebGPU texture.** `Skia.Image.MakeNativeTextureFromImage` uploads an `SkImage` into a texture on the shared device and returns a `+1` pointer; `adoptTexture()` wraps it into a `GPUTexture` that owns that reference: + +```tsx +import { adoptTexture } from "react-native-webgpu"; + +const texture = adoptTexture(Skia.Image.MakeNativeTextureFromImage(image)); +// sample it in a render pipeline via texture.createView() +``` + +Adopt each pointer exactly once: the returned `GPUTexture` releases the reference when destroyed. + +Platform surfaces (`CVPixelBuffer` / `AHardwareBuffer`) can also cross the boundary through [`importSharedTextureMemory`](/api/gpu-device-extensions#importsharedtexturememory), or as external textures via `RNWebGPU.createVideoFrameFromNativeBuffer` + `importExternalTexture`. ## See also diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h b/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h index 772cf3788..8e8fad238 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h @@ -42,6 +42,12 @@ class GPUTexture : public NativeObject { wgpu::TextureFormat getFormat(); double getUsage(); + // Non-spec RN extension: the raw WGPUTexture handle as a BigInt, for + // pointer-based interop (e.g. Skia's Image.MakeImageFromNativeTexture). + // Borrowed: this GPUTexture keeps ownership; the consumer must retain the + // JS object (or AddRef natively) for as long as it uses the pointer. + void *getNativePointer() { return _instance.Get(); } + std::string getLabel() { return _label; } void setLabel(const std::string &label) { _label = label; @@ -50,6 +56,8 @@ class GPUTexture : public NativeObject { static void definePrototype(jsi::Runtime &runtime, jsi::Object &prototype) { installGetter(runtime, prototype, "__brand", &GPUTexture::getBrand); + installGetter(runtime, prototype, "nativePointer", + &GPUTexture::getNativePointer); installMethod(runtime, prototype, "createView", &GPUTexture::createView); installMethod(runtime, prototype, "destroy", &GPUTexture::destroy); installGetter(runtime, prototype, "width", &GPUTexture::getWidth); diff --git a/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h b/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h index f5ebdd8e6..e1e7a3cbc 100644 --- a/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h +++ b/packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h @@ -10,6 +10,7 @@ #include "GPU.h" #include "GPUCanvasContext.h" #include "GPUDevice.h" +#include "GPUTexture.h" #include "rnwgpu/async/RuntimeContext.h" #include "ImageBitmap.h" #include "PlatformContext.h" @@ -100,6 +101,20 @@ class RNWebGPU : public NativeObject { return std::make_shared(device, ctx, "Imported Device"); } + // Wrap an externally created WGPUTexture in a GPUTexture, TAKING OWNERSHIP + // of one reference (unlike importDevice, which borrows): the returned + // GPUTexture releases it when destroyed. Pair with producers that return a + // +1 pointer, e.g. Skia.Image.MakeNativeTextureFromImage(). + std::shared_ptr adoptTexture(void *pointer) { + if (pointer == nullptr) { + throw std::runtime_error( + "adoptTexture: expected a non-null WGPUTexture pointer (BigInt)"); + } + wgpu::Texture texture = + wgpu::Texture::Acquire(reinterpret_cast(pointer)); + return std::make_shared(texture, "Adopted Texture"); + } + jsi::Value createImageBitmap(jsi::Runtime &runtime, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) { @@ -309,6 +324,7 @@ class RNWebGPU : public NativeObject { &RNWebGPU::createVideoFrameFromNativeBuffer); installMethodWithRuntime(runtime, prototype, "importDevice", &RNWebGPU::importDevice); + installMethod(runtime, prototype, "adoptTexture", &RNWebGPU::adoptTexture); installMethod(runtime, prototype, "createVideoPlayer", &RNWebGPU::createVideoPlayer); installMethod(runtime, prototype, "writeTestVideoFile", diff --git a/packages/webgpu/src/importDevice.ts b/packages/webgpu/src/importDevice.ts index 80be5693b..fd6f3ac2b 100644 --- a/packages/webgpu/src/importDevice.ts +++ b/packages/webgpu/src/importDevice.ts @@ -20,3 +20,23 @@ export const importDevice = (pointer: bigint): GPUDevice => { } return RNWebGPU.importDevice(pointer); }; + +/** + * Wraps an externally created WGPUTexture pointer into a GPUTexture, taking + * ownership of one reference: the returned texture releases it when destroyed. + * + * Pair with producers that hand out a +1 reference, such as + * `Skia.Image.MakeNativeTextureFromImage()`: + * + * ```ts + * const texture = adoptTexture(Skia.Image.MakeNativeTextureFromImage(image)); + * ``` + */ +export const adoptTexture = (pointer: bigint): GPUTexture => { + if (typeof RNWebGPU === "undefined") { + throw new Error( + "react-native-webgpu is not installed natively; adoptTexture is unavailable", + ); + } + return RNWebGPU.adoptTexture(pointer); +}; diff --git a/packages/webgpu/src/index.tsx b/packages/webgpu/src/index.tsx index 090e11014..5db6b3d01 100644 --- a/packages/webgpu/src/index.tsx +++ b/packages/webgpu/src/index.tsx @@ -62,6 +62,10 @@ declare global { // device from Skia.getNativeDevice()) into a GPUDevice. Requires the // exporter to share this process's single Dawn instance. importDevice: (pointer: bigint) => GPUDevice; + // Wrap an externally created WGPUTexture pointer into a GPUTexture, + // taking ownership of one reference (pair with producers that return a + // +1 pointer, e.g. Skia.Image.MakeNativeTextureFromImage()). + adoptTexture: (pointer: bigint) => GPUTexture; createVideoPlayer: ( path: string, pixelFormat?: NativeVideoPixelFormat, @@ -69,6 +73,13 @@ declare global { writeTestVideoFile: () => string; }; + interface GPUTexture { + // Non-spec RN extension: raw WGPUTexture handle as a BigInt for + // pointer-based interop (e.g. Skia.Image.MakeImageFromNativeTexture). + // Borrowed: keep this GPUTexture alive while the pointer is in use. + readonly nativePointer: bigint; + } + interface GPUDevice { importSharedTextureMemory( descriptor: GPUSharedTextureMemoryDescriptor,