React Native WebGPU

repository·main·Indexed 22 days ago

https://github.com/wcandillon/react-native-webgpu

A library that brings WebGPU capabilities to React Native applications using the Dawn engine. It provides a Canvas component for high-performance graphics programming, a GPUDeviceProvider for device sharing, and extensions for native texture memory import, shared fences, and high bit depth canvas formats.

Tokens
28.8K
Snippets
80
Records
95
Agent score
78%

What's inside react-native-webgpu

  1. Overview of React Native WebGPU integrations

    main

    React Native WebGPU is designed to be symmetric to the WebGPU web ecosystem. This allows many existing WebGPU libraries to run on React Native with minimal glue code (such as manual present() calls, Metro resolution, and worklet setup).

    Available integrations include:

    • Worklets: Run render loops on the UI thread or a dedicated worklet runtime, off the JS thread.
    • Vision Camera: Apply real-time WGSL effects to live camera frames via zero-copy importExternalTexture.
    • TypeGPU: Provides type-safe buffers, bind groups, and shaders written as TypeScript functions.
    • Three.js: Supports the WebGPURenderer from three/webgpu for scenes, cameras, and materials.
    • React Three Fiber: Provides Three.js via declarative JSX components.
    • TensorFlow.js: Enables on-device machine learning using the WebGPU backend.
  2. Import native textures (Zero-copy)

    main

    For high-performance sampling of camera or video frames without CPU copies, use the native texture extensions.

    1. Feature Detection: Always check for the rnwebgpu/native-texture feature before attempting to import, as some Android drivers/emulators may not support it.
    2. importSharedTextureMemory: Imports a native pixel surface (like CVPixelBuffer on iOS or AHardwareBuffer on Android) as a GPUTexture.
    3. importExternalTexture: A higher-level API that provides a GPUExternalTexture with hardware YUV→RGB conversion and support for rotation and mirrored options.
    if (!device.features.has("rnwebgpu/native-texture" as GPUFeatureName)) {
      return; // rare: some Android drivers/emulators can't import native surfaces
    }
  3. Sample camera frames in WGSL

    main

    When using importExternalTexture for camera frames, bind the texture as a texture_external and use textureSampleBaseClampToEdge in your fragment shader.

    On Android, the texture may arrive in raw [Y, Cb, Cr] format. You must implement a YUV decode function in your WGSL to convert it to RGB. On iOS, hardware handles the NV12 → RGB conversion automatically.

    Android YUV Decode Example

    fn cameraDecode(c: vec4f) -> vec4f {
      let y  = c.r - 0.0627451;
      let cb = c.g - 0.5;
      let cr = c.b - 0.5;
      let r = 1.164384 * y + 1.792741 * cr;
      let g = 1.164384 * y - 0.213249 * cb - 0.532909 * cr;
      let b = 1.164384 * y + 2.112402 * cb;
      return vec4f(clamp(vec3f(r, g, b), vec3f(0.0), vec3f(1.0)), 1.0);
    }
    @group(0) @binding(0) var srcTex: texture_external;
    @group(0) @binding(1) var srcSampler: sampler;
    
    @fragment
    fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
      return cameraDecode(
        textureSampleBaseClampToEdge(srcTex, srcSampler, cameraCoord(uv)),
      );
    }
  4. Core WebGPU concepts

    main

    To develop with React Native WebGPU, you must understand the standard WebGPU object model. The following concepts form the foundation of the API:

    ConceptDescription
    InstanceThe entry point to WebGPU (exposed via navigator.gpu), used to access adapters
    AdapterRepresents a specific physical GPU on the device
    DeviceYour logical connection to the GPU; used to create resources
    QueueThe mechanism used to submit commands to the GPU
    Shader ModuleYour GPU code, written in the WebGPU Shading Language (WGSL)
    PipelinesObjects describing the entire GPU state (shaders, blending) for a specific task
    Bind GroupsObjects that tie data buffers (e.g., textures) to shaders
    Command EncoderAn object used to build a sequence of GPU commands into a command buffer
  5. The WebGPU rendering loop in React Native

    main

    Rendering is performed inside a Canvas view. Because this is running in React Native, you must explicitly call context.present() to display the frame.

    Follow this typical loop:

    1. Get a WebGPU context from the Canvas ref.
    2. Configure the context with your device.
    3. Encode render (or compute) passes.
    4. Call device.queue.submit(...).
    5. Call context.present() (Required).
  6. Integrate React Native Worklets for off-thread WebGPU rendering

    main

    By default, WebGPU rendering runs on the JavaScript thread. To improve performance, you can use React Native Worklets to run rendering on the UI thread or a dedicated worklet runtime.

    WebGPU objects like GPUDevice and GPUCanvasContext are automatically registered for Worklets serialization when the module loads, allowing you to pass them directly from the main thread to a worklet.

    For complex Three.js scenes, you can also explore the experimental Bundle Mode provided by Worklets.

  7. Use TypeGPU Compute Pipelines

    main

    Compute shaders are written as TypeScript functions. You can manage buffers and bind groups using @typegpu/react hooks.

    To handle element-wise dispatching safely, use root.createGuardedComputePipeline(shaderFunction). This method automatically generates bounds checks for the dispatch.

    Key hooks:

    • useBuffer(type, usage): Creates a GPU buffer. Use . $usage("storage") to specify the usage.
    • useBindGroup(layout, options): Creates a bind group based on a defined layout and provided buffers.

    Example of a particle simulation update loop:

    import { useMemo } from "react";
    import tgpu, { d } from "typegpu";
    import { useBindGroup, useBuffer, useRoot } from "@typegpu/react";
    
    const Particle = d.struct({
      position: d.vec2f,
      velocity: d.vec2f,
    });
    
    const ParticleArray = d.arrayOf(Particle);
    
    const layout = tgpu.bindGroupLayout({
      particles: { storage: ParticleArray, access: "mutable" },
    });
    
    function update(idx: number) {
      "use gpu";
      const p = Particle(layout.$.particles[idx]);
      p.position = p.position.add(p.velocity);
      layout.$.particles[idx] = Particle(p);
    }
    
    const count = 1000;
    
    export function useSimulation() {
      const root = useRoot();
    
      const particles = useBuffer(ParticleArray(count)).$usage("storage");
      const group = useBindGroup(layout, { particles });
    
      const pipeline = useMemo(
        () => root.createGuardedComputePipeline(update),
        [root],
      );
    
      return () => {
        pipeline.with(group).dispatchThreads(count);
      };
    }
  8. Resources for learning WebGPU

    main

    Since WebGPU is a low-level API, it is recommended to study the core concepts before diving into specific implementations. The following resources are highly recommended for learning the fundamentals:

    • WebGPU Fundamentals: A comprehensive site that teaches WebGPU concepts from the ground up (webgpufundamentals.org).
    • React Native WebGPU Tutorial: A video tutorial by Daniel Friyia that covers the base concepts of the WebGPU render pipeline specifically within the context of React Native (YouTube link).
  9. Enable transparent compositing over React Native views

    main

    To render a WebGPU scene on top of other React Native components, you must configure transparency in three places to ensure cross-platform compatibility (especially for Android):

    1. Component Prop: Set transparent to true on the <Canvas /> component.
    2. Context Configuration: Set alphaMode: "premultiplied" inside context.configure().
    3. Clear Color: Use a clearValue with an alpha of 0 (e.g., [0, 0, 0, 0]) in your render pass.

    Platform Note: On Android, the alphaMode in configure() is ignored, so the transparent prop is the primary driver for transparency. On iOS, both are required.

    // 1. Set the prop
    <Canvas ref={ref} style={StyleSheet.absoluteFill} transparent />
    
    // 2. Inside the render loop, use an alpha-0 clear color
    const pass = encoder.beginRenderPass({
      colorAttachments: [{
        view: context.getCurrentTexture().createView(),
        clearValue: [0, 0, 0, 0], // 3. Alpha is 0
        loadOp: "clear",
        storeOp: "store",
      }],
    });
    
    // 3. Configure context with premultiplied alpha
    context.configure({ device, format, alphaMode: "premultiplied" });
  10. Implement FiberCanvas to bridge R3F and RN WebGPU

    main

    Since React Three Fiber (R3F) expects a DOM-like canvas and does not natively support the React Native WebGPU present() requirement, you must create a bridge component (e.g., FiberCanvas).

    Key implementation requirements:

    • Register Three.js: Call extend(THREE) to allow using Three.js elements like <mesh /> as JSX.
    • Async Initialization: You must await state.gl.init() inside the onCreated hook to ensure the WebGPURenderer is ready before the first draw.
    • Manual Presentation: R3F does not call present(). You must wrap the gl.render method to call the native context.present() after every frame.
    • Pixel Ratio: Set dpr: 1 in the R3F configuration because physical pixel sizing is handled manually on the native canvas using PixelRatio.get().
    export const FiberCanvas = ({ children, style, scene, camera }) => {
      const root = useRef(null);
      React.useMemo(() => extend(THREE), []);
      const canvasRef = useRef(null);
    
      useEffect(() => {
        const context = canvasRef.current!.getContext("webgpu")!;
        const renderer = makeWebGPURenderer(context);
    
        const canvas = context.canvas as HTMLCanvasElement;
        canvas.width = canvas.clientWidth * PixelRatio.get();
        canvas.height = canvas.clientHeight * PixelRatio.get();
    
        const size = {
          top: 0,
          left: 0,
          width: canvas.clientWidth,
          height: canvas.clientHeight,
        };
    
        if (!root.current) {
          root.current = createRoot(canvas);
        }
        root.current.configure({
          size,
          events,
          scene,
          camera,
          gl: renderer,
          frameloop: "always",
          dpr: 1, // canvas already sized with PixelRatio
          onCreated: async (state) => {
            await state.gl.init();
            const renderFrame = state.gl.render.bind(state.gl);
            state.gl.render = (s, c) => {
              renderFrame(s, c);
              context.present();
            };
          },
        });
        root.current.render(children);
    
        return () => unmountComponentAtNode(canvas);
      });
    
      return <Canvas ref={canvasRef} style={style} />;
    };