Skip to content

Commit 9c84ecb

Browse files
authored
feat(πŸ“Έ): importExternalTexture part 0 (#372)
1 parent c4faa43 commit 9c84ecb

48 files changed

Lines changed: 2923 additions & 213 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

β€ŽREADME.mdβ€Ž

Lines changed: 57 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -226,19 +226,21 @@ device.queue.copyExternalImageToTexture(
226226

227227
React Native WebGPU exposes Dawn's `SharedTextureMemory` so you can import a native pixel surface (an `IOSurface`-backed `CVPixelBuffer` on iOS, an `AHardwareBuffer` on Android) as a sampleable `GPUTexture` without copying pixels through the CPU. This is the path you want for camera frames, video frames, or anything coming out of a hardware producer.
228228

229-
We expose a single umbrella feature name, `"rnwebgpu/shared-texture-memory"`. Request it at device creation.
229+
Like `importExternalTexture` on the web, this is **enabled by default**, there is nothing to request at device creation. The only thing to check is that the device supports it before importing. It always does on iOS/macOS; it can be missing on some Android drivers and emulators.
230230

231231
```tsx
232-
import type { VideoFrame } from "react-native-wgpu";
233-
234-
const FEATURE = "rnwebgpu/shared-texture-memory" as GPUFeatureName;
232+
import type { NativeVideoFrame } from "react-native-wgpu";
235233

236234
const adapter = await navigator.gpu.requestAdapter();
237-
const requiredFeatures = adapter!.features.has(FEATURE) ? [FEATURE] : [];
238-
const device = await adapter!.requestDevice({ requiredFeatures });
235+
const device = await adapter!.requestDevice();
236+
237+
// On by default when supported; this is the only check you need.
238+
if (!device.features.has("rnwebgpu/native-texture" as GPUFeatureName)) {
239+
return; // rare: some Android drivers/emulators can't import native surfaces
240+
}
239241

240-
// `frame` here is a VideoFrame whose .handle is the native surface
241-
// (IOSurfaceRef / AHardwareBuffer*). VideoFrames are produced by helpers
242+
// `frame` here is a NativeVideoFrame whose .handle is the native surface
243+
// (IOSurfaceRef / AHardwareBuffer*). NativeVideoFrames are produced by helpers
242244
// like RNWebGPU.createVideoPlayer or RNWebGPU.createTestVideoFrame, or by
243245
// any third-party module that hands you a compatible native pointer.
244246
const memory = device.importSharedTextureMemory({
@@ -257,6 +259,53 @@ frame.release();
257259

258260
`beginAccess`/`endAccess` bracket the GPU's read window on the shared surface. Pass `initialized: true` when the producer has already written meaningful pixels (the typical video/camera case) and `false` when the next pass will fully overwrite the texture.
259261

262+
### Importing External Textures
263+
264+
`GPUDevice.importExternalTexture` is the higher-level path for sampling a native surface. You hand it a `NativeVideoFrame` and get back a `GPUExternalTexture` that you bind as a `texture_external` and read with `textureSampleBaseClampToEdge`. It does two things for you on top of `SharedTextureMemory`:
265+
266+
- **Color conversion.** Camera and video surfaces are usually biplanar YUV (NV12), not RGB. An external texture carries the YUV→RGB matrix and the source/destination color-space transfer functions, so on the supported paths the sampler returns ready-to-use RGB in hardware. With raw `SharedTextureMemory` you would sample the luma/chroma planes and do that conversion by hand in WGSL. This is the main reason to prefer it for camera and video frames.
267+
- **Lifecycle.** It owns the `SharedTextureMemory` + `createTexture` + `beginAccess`/`endAccess` sequence internally, so you just import the frame and `destroy()` the result.
268+
269+
It builds on the same default-on capability as Shared Texture Memory above, so feature-detect the device the same way before importing.
270+
271+
> **Android note:** the hardware YUV→RGB conversion is fully automatic on iOS (NV12 `IOSurface`). On Android, camera frames arrive as an _opaque_ YCbCr `AHardwareBuffer`, and Dawn's Vulkan path forces an identity (`RGB_IDENTITY`) sampler conversion, so the external sample comes back as raw `[Y, Cb, Cr]`. You still get the zero-copy import and the rotation/mirror transform, but you need to apply the YUV→RGB matrix yourself in the shader. See the `CAMERA_PRELUDE` in the [VisionCamera example](/apps/example/src/VisionCamera/shaders.ts) for a ready-made BT.709 decode.
272+
273+
```tsx
274+
const adapter = await navigator.gpu.requestAdapter();
275+
const device = await adapter!.requestDevice();
276+
// Feature-detect as shown above before importing on unsupported hardware.
277+
278+
const render = () => {
279+
// A GPUExternalTexture expires once the queue work that used it is submitted,
280+
// so re-import one every frame.
281+
const externalTexture = device.importExternalTexture({
282+
source: frame, // a NativeVideoFrame
283+
label: "video-frame",
284+
});
285+
286+
const bindGroup = device.createBindGroup({
287+
layout: pipeline.getBindGroupLayout(0),
288+
entries: [
289+
{ binding: 0, resource: externalTexture },
290+
{ binding: 1, resource: sampler },
291+
],
292+
});
293+
294+
// ... encode a pass that samples `externalTexture`, then:
295+
device.queue.submit([encoder.finish()]);
296+
297+
// Release the surface's access window right after the submit that sampled it.
298+
externalTexture.destroy();
299+
context.present();
300+
};
301+
```
302+
303+
Camera frames arrive in the sensor's native orientation, so `importExternalTexture` also accepts non-spec `rotation` (`0` | `90` | `180` | `270`, in degrees) and `mirrored` (horizontal flip) options. Dawn bakes them into the sampling transform, so the shader sees an upright frame. They map directly onto VisionCamera's `frame.orientation` / `frame.isMirrored`.
304+
305+
#### Calling `destroy()`
306+
307+
A `GPUExternalTexture` keeps an open access window on the underlying native surface until the wrapper is destroyed. On the Web `importExternalTexture` is core and the lifetime is handled for you; here the window is tied to the JavaScript object's lifetime. Call `externalTexture.destroy()` right after the `queue.submit()` that sampled it (never before) to release the surface back to its producer immediately. `destroy()` is idempotent, and the surface is also released when the object is garbage-collected, but relying on GC can starve a producer's buffer pool (e.g. an `AVPlayer`'s recycled `IOSurface`s) and pile up GPU resources, so prefer the explicit call in a render loop.
308+
260309
### Reanimated Integration
261310

262311
React Native WebGPU supports running WebGPU rendering on the UI thread using [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/) and [React Native Worklets](https://github.com/margelo/react-native-worklets).

β€Žapps/example/src/App.tsxβ€Ž

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ import { AsyncStarvation } from "./Diagnostics/AsyncStarvation";
3737
import { DeviceLostHang } from "./Diagnostics/DeviceLostHang";
3838
import { StorageBufferVertices } from "./StorageBufferVertices";
3939
import { SharedTextureMemory } from "./SharedTextureMemory";
40-
import { Camera } from "./Camera";
40+
import { ImportExternalTexture } from "./ImportExternalTexture";
41+
import { VisionCamera } from "./VisionCamera";
4142

4243
// The two lines below are needed by three.js
4344
import "fast-text-encoding";
@@ -103,7 +104,11 @@ function App() {
103104
name="SharedTextureMemory"
104105
component={SharedTextureMemory}
105106
/>
106-
<Stack.Screen name="Camera" component={Camera} />
107+
<Stack.Screen
108+
name="ImportExternalTexture"
109+
component={ImportExternalTexture}
110+
/>
111+
<Stack.Screen name="VisionCamera" component={VisionCamera} />
107112
</Stack.Navigator>
108113
</NavigationContainer>
109114
</GestureHandlerRootView>

β€Žapps/example/src/Camera/Camera.tsxβ€Ž

Lines changed: 0 additions & 60 deletions
This file was deleted.

β€Žapps/example/src/Camera/index.tsβ€Ž

Lines changed: 0 additions & 1 deletion
This file was deleted.

β€Žapps/example/src/Home.tsxβ€Ž

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,8 +132,12 @@ export const examples = [
132132
title: "🎞️ Shared Texture Memory",
133133
},
134134
{
135-
screen: "Camera",
136-
title: "πŸ“· Camera",
135+
screen: "ImportExternalTexture",
136+
title: "🎬 Import External Texture",
137+
},
138+
{
139+
screen: "VisionCamera",
140+
title: "πŸ“· VisionCamera integration",
137141
},
138142
];
139143

0 commit comments

Comments
Β (0)