React Native WebGPU

Native Extensions

Native interop extensions to GPUDevice and the WebGPU globals.

React Native WebGPU adds a handful of native extensions on top of the WebGPU spec.

importSharedTextureMemory

Imports a native buffer (IOSurface / AHardwareBuffer) for zero-copy sampling. Nothing has to be requested: requestDevice() enables the Dawn features behind native frame import whenever the adapter supports them (see Native textures).

const  = await .();

// frame from createVideoPlayer(..., "bgra8") or Vision Camera
const  = .({
  : .,
  : "camera-frame",
});

const  = .();

.(, /* initialized */ true);
// sample texture in render pass…
.();

.();
.();
.();

Use beginAccess(texture, true) when the producer has already written pixels (typical for video/camera).

memory.destroy() releases the import right away rather than when the object is garbage collected, which can take a while on a runtime importing a frame per vsync. End the access and destroy the textures created from it first: destroying during an access is a validation error, and so is using the memory afterwards.

importSharedFence

Import native GPU sync fences for producer/consumer handoff:

// Wait for the producer's GPU work before using the texture
const  = .({ ..., : "frame-ready" });
// 1n for a sync fd, the event value for an MTLSharedEvent
.(, true, [{ , : 1n }]);
// beginAccess() keeps its own reference: release the fence right away
.();

// … render using texture …

// Each fence signals once that GPU work is done. Export it only to hand it to
// native code: the handle is a duplicate that code has to close, and there is
// no JS API to close one. With no consumer, just destroy the fence.
const {  } = .();
for (const { : ,  } of ) {
  const { ,  } = .();
  (, , , );
  .();
}

destroy() releases a fence and its native handle right away rather than when the object is garbage collected. An exported handle is a duplicate, so it stays valid; using the fence afterwards is a validation error, and export() then throws.

createNativeBufferExternalTexture / createNativeBufferTexture

A decoder or a camera cycles through the same few buffers, but importExternalTexture() imports the buffer again on every frame. These two methods import a buffer once; each frame is then bracketed with beginAccess() / endAccess() on the wrapper.

// Once per buffer, keyed by the buffer: the import, the texture and the
// external texture (color conversion included).
let  = .(.);
if (!) {
   = .(, {
    : "clip-1",
  });
  .(., );
}

// Per frame: wait for the producer's fence, draw, hand the fence of the
// access back.
const  = [];
if (. !== null) {
  const  = .({
    : "sync-fd",
    : .,
  });
  .({ : , : 1n });
}
.();
.(({  }) => .());
(.); // ... queue.submit() ...
const  = .();
?..(); // or export() it for the producer

// When the producer drops the buffer
.();

beginAccess(fences?) makes the GPU wait for the fences before it reads the buffer; endAccess() returns one fence that signals once the GPU is done with it, or null. destroy() releases the import and the frame the wrapper was created from; an access left open is a validation error, and it is ended anyway so the producer gets its buffer back. The descriptor is that of importExternalTexture() without the source (colorSpace, rotation, mirrored, plus sourceColorSpace, cropOrigin and cropSize, see below); the same descriptor members apply to importExternalTexture().

createNativeBufferTexture(frame, { usage, label }) is the same wrapper over a color buffer (an RGBA AHardwareBuffer, a BGRA IOSurface) exposed as a regular GPUTexture; usage defaults to every usage the buffer allows. The external texture is sample-only, as on the web, and the only way to read a YUV frame; the texture is for a color buffer you also write to or copy from.

On Android, a buffer from another library (wrapped with createVideoFrameFromNativeBuffer) carries no color metadata: pass sourceColorSpace when you know it, or the conversion assumes what the platform reports for the buffer, BT.601 limited range by default.

Prop

Type

dawnToggles

Non-standard Dawn device toggles at creation:

const  = await .({
  : {
    : ["allow_unsafe_apis"],
    : ["use_dxc"],
  },
});

Prop

Type

On Android, the ignore_imported_ahardwarebuffer_vulkan_image_size toggle is enabled by default, so native buffers import on every driver. Pass it in disabledToggles to turn it off.

implicitDeviceSynchronization

By default, requestDevice() enables Dawn's implicit-device-synchronization feature, which lets a device created on the JS thread be used from the UI thread or a dedicated worklet runtime (see Thread safety).

Set implicitDeviceSynchronization: false on the device descriptor to opt out and skip the locking. Only do this for a device that is never touched from another runtime. The flag is a non-standard extension and is ignored on the web.

const  = await .({
  : false,
});
..("implicit-device-synchronization" as ); // false

The flag only controls the default injection: listing "implicit-device-synchronization" in requiredFeatures still enables the feature.

GPUExternalTexture extensions

Field / MethodDescription
sourceA NativeVideoFrame (from a video player, a camera buffer or createTestVideoFrame) instead of the web's HTMLVideoElement / VideoFrame
rotation0, 90, 180, or 270 degrees (camera orientation)
mirroredHorizontal flip for front camera
sourceColorSpaceOverrides the members of the frame's own color space (matrix, range, primaries, transfer), for a buffer that carries none
cropOrigin, cropSizeThe part of the frame the external texture shows, the whole frame by default
destroy()Release native surface access after the queue.submit() that sampled it

Usage with importExternalTexture (NV12 camera/video):

const externalTexture = device.importExternalTexture({
  source: nativeVideoFrame,
});

// Read orientation metadata in bind group / shader setup
console.log(externalTexture.rotation, externalTexture.mirrored);

// After submit that sampled the texture:
externalTexture.destroy();
nativeVideoFrame.release();

On Android, a YUV frame (what the video decoder and the camera produce) needs the opaque-ycbcr-android-for-external-texture device feature. requestDevice() enables it whenever the adapter supports it; a device created by another library and wrapped with importDevice() has to request it itself, as react-native-skia's does. The same applies to copyExternalImageToTexture() with a native frame.

textureSampleBaseClampToEdge returns RGB on both platforms, as on the web: the external texture converts a YUV frame while sampling, with the color space of the frame (its YCbCr matrix, range, primaries and transfer function). A video player's frames carry it. A buffer without one (a camera frame) falls back to what the platform reports for it, then to BT.709 limited range on Apple platforms and BT.601 limited range on Android; sourceColorSpace overrides either. The spec's colorSpace member selects the color space of the result, "srgb" (the default) or "display-p3".

copyExternalImageToTexture with a native frame

queue.copyExternalImageToTexture() also accepts a NativeVideoFrame as its source, the way the web API accepts a VideoFrame. The frame is rendered into the destination on the GPU through an external texture, so YUV frames come out as RGB, and the non-spec rotation and mirrored options of importExternalTexture apply. The destination needs GPUTextureUsage.RENDER_ATTACHMENT and an rgba8unorm, bgra8unorm (or -srgb), rgba16float or rgb10a2unorm format; the frame can be released as soon as the call returns.

const  = .({
  : [., .],
  : "rgba8unorm",
  : . | .,
});
..(
  { : ,  },
  {  },
  [., .],
);
.();

origin, flipY and the copy size work as on the web, in the upright frame: a frame rotated by 90 or 270 degrees has its width and height swapped. This is the path to hand a video or camera frame to a library that samples regular textures, such as React Native Skia; a shader of your own can sample the frame directly with importExternalTexture() and skip the copy.

Prop

Type

createImageBitmap

createImageBitmap mirrors its Web counterpart and also accepts a Uint8Array or ArrayBuffer as input.

const  = await ().(() => .());
const  = await ();

..(
  { :  },
  {  },
  { : ., : . },
);
.();

getPreferredHighBitDepthCanvasFormat

Returns the canvas texture format most likely to reach the display with more than 8 bits per channel on the current platform: "rgba16float" on iOS, "rgb10a2unorm" on Android. Unlike getPreferredCanvasFormat(), this helper is not part of the WebGPU standard, so it is exported as a regular function instead of a navigator.gpu method.

const  = ();
.({ , , : "opaque" });

The values you write stay sRGB-encoded exactly like on an 8-bit canvas: this is about precision, not HDR (no extended range). See High bit depth for the platform details and banding comparison.