Skip to content
Closed
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
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,7 @@ ctx.canvas.height = ctx.canvas.clientHeight * PixelRatio.get();

### Frame Scheduling

In React Native, we want to keep frame presentation as a manual operation as we plan to provide more advanced rendering options that are React Native specific.
This means that when you are ready to present a frame, you need to call `present` on the context.
In React Native, frame presentation is a manual operation: when you are ready to present a frame, call `present()` on the context after submitting your commands to the queue. This works the same on every runtime: the main JS runtime, the Reanimated UI runtime, and dedicated worklet runtimes (`createWorkletRuntime` / `runOnRuntime`, or a Vision Camera frame processor). `present()` runs synchronously on the calling thread, so the frame is presented from whichever thread did the rendering.

```tsx
// draw
Expand Down Expand Up @@ -293,10 +292,10 @@ const render = () => {

// ... encode a pass that samples `externalTexture`, then:
device.queue.submit([encoder.finish()]);
context.present();

// Release the surface's access window right after the submit that sampled it.
externalTexture.destroy();
context.present();
};
```

Expand Down
10 changes: 5 additions & 5 deletions apps/example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1924,7 +1924,7 @@ PODS:
- ReactCommon/turbomodule/core
- SocketRocket
- Yoga
- react-native-wgpu (0.5.12):
- react-native-webgpu (0.5.15):
- boost
- DoubleConversion
- fast_float
Expand Down Expand Up @@ -2812,7 +2812,7 @@ DEPENDENCIES:
- React-microtasksnativemodule (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`)
- react-native-safe-area-context (from `../../../node_modules/react-native-safe-area-context`)
- "react-native-skia (from `../../../node_modules/@shopify/react-native-skia`)"
- react-native-wgpu (from `../../../node_modules/react-native-wgpu`)
- react-native-webgpu (from `../../../node_modules/react-native-webgpu`)
- React-NativeModulesApple (from `../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`)
- React-oscompat (from `../../../node_modules/react-native/ReactCommon/oscompat`)
- React-perflogger (from `../../../node_modules/react-native/ReactCommon/reactperflogger`)
Expand Down Expand Up @@ -2948,8 +2948,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/react-native-safe-area-context"
react-native-skia:
:path: "../../../node_modules/@shopify/react-native-skia"
react-native-wgpu:
:path: "../../../node_modules/react-native-wgpu"
react-native-webgpu:
:path: "../../../node_modules/react-native-webgpu"
React-NativeModulesApple:
:path: "../../../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios"
React-oscompat:
Expand Down Expand Up @@ -3074,7 +3074,7 @@ SPEC CHECKSUMS:
React-microtasksnativemodule: 75b6604b667d297292345302cc5bfb6b6aeccc1b
react-native-safe-area-context: c00143b4823773bba23f2f19f85663ae89ceb460
react-native-skia: fc73e9bdc46ebb420a98c9c2be29fee80f565e79
react-native-wgpu: 274ffec11ee3a082260d9f3d1fb54030a5ca0873
react-native-webgpu: 02d51c1d86e4d653de06bdc954d2f693dcead7a5
React-NativeModulesApple: 879fbdc5dcff7136abceb7880fe8a2022a1bd7c3
React-oscompat: 93b5535ea7f7dff46aaee4f78309a70979bdde9d
React-perflogger: 5536d2df3d18fe0920263466f7b46a56351c0510
Expand Down
1 change: 0 additions & 1 deletion apps/example/src/CanvasAPI/CanvasAPI.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ export const CanvasAPI = () => {
passEncoder.end();

device.queue.submit([commandEncoder.finish()]);

context.present();
})()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -244,10 +244,10 @@ export const ImportExternalTexture = () => {

pass.end();
device.queue.submit([encoder.finish()]);
context.present();
// Now that the work sampling it has been submitted, end the external
// texture's access window so the frame's surface is released promptly.
externalTex?.destroy();
context.present();
rafRef.current = requestAnimationFrame(render);
};
rafRef.current = requestAnimationFrame(render);
Expand Down
196 changes: 196 additions & 0 deletions apps/example/src/Reanimated/AsyncBuffer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import React, { useEffect, useRef } from "react";
import { StyleSheet, View } from "react-native";
import type { CanvasRef, RNCanvasContext } from "react-native-webgpu";
import { Canvas } from "react-native-webgpu";
import type { SharedValue } from "react-native-reanimated";
import { useSharedValue } from "react-native-reanimated";

import { redFragWGSL, triangleVertWGSL } from "../Triangle/triangle";

// The GPU usage / map-mode constants are plain numbers. We resolve them on the
// JS thread (where the constants are guaranteed to be installed) and pass them
// into the worklet, so the worklet does not depend on those globals being
// present on the UI / dedicated runtime.
interface GPUFlags {
COPY_SRC: number;
COPY_DST: number;
MAP_READ: number;
MAP_WRITE: number;
MAP_MODE_READ: number;
}

// A triangle demo that ALSO performs an async GPU readback (buffer.mapAsync)
// every frame, then presents only after the readback resolves. This makes the
// behaviour of async WebGPU ops on the runtime visible: if the mapAsync Promise
// never settles on this runtime, the animation freezes after the first frame.
export const webGPUAsyncDemo = (
runAnimation: SharedValue<boolean>,
device: GPUDevice,
context: RNCanvasContext,
presentationFormat: GPUTextureFormat,
flags: GPUFlags,
) => {
"worklet";
if (!context) {
throw new Error("No context");
}

context.configure({
device,
format: presentationFormat,
alphaMode: "premultiplied",
});

const pipeline = device.createRenderPipeline({
layout: "auto",
vertex: {
module: device.createShaderModule({ code: triangleVertWGSL }),
entryPoint: "main",
},
fragment: {
module: device.createShaderModule({ code: redFragWGSL }),
entryPoint: "main",
targets: [{ format: presentationFormat }],
},
primitive: { topology: "triangle-list" },
});

const SIZE = 16; // 4 x f32
// Reused across frames: we copy 4 floats into this buffer and read them back.
const readback = device.createBuffer({
size: SIZE,
usage: flags.COPY_DST | flags.MAP_READ,
});

let frameId = 0;

const frame = async () => {
frameId += 1;
const commandEncoder = device.createCommandEncoder();
const textureView = context.getCurrentTexture().createView();

const time = Date.now() / 1000;
const r = (Math.sin(time * 2) + 1) / 2;
const g = (Math.sin(time * 1.5 + Math.PI / 3) + 1) / 2;
const b = (Math.sin(time + Math.PI / 2) + 1) / 2;

const passEncoder = commandEncoder.beginRenderPass({
colorAttachments: [
{
view: textureView,
clearValue: [r, g, b, 1],
loadOp: "clear",
storeOp: "store",
},
],
});
passEncoder.setPipeline(pipeline);
passEncoder.draw(3);
passEncoder.end();

// Put real data on the GPU so the readback below has something to wait on.
const src = device.createBuffer({
size: SIZE,
usage: flags.COPY_SRC | flags.MAP_WRITE,
mappedAtCreation: true,
});
new Float32Array(src.getMappedRange()).set([frameId, r, g, b]);
src.unmap();
commandEncoder.copyBufferToBuffer(src, 0, readback, 0, SIZE);

device.queue.submit([commandEncoder.finish()]);

// THE ASYNC OP. This Promise must settle on the runtime that is running
// this worklet. On the JS thread it does. On the UI / dedicated runtime the
// settlement currently routes through the main JS CallInvoker, so this
// await may resolve on the wrong runtime (or never here), freezing the loop.
console.log(`[asyncBuffer] frame ${frameId}: awaiting mapAsync...`);
await readback.mapAsync(flags.MAP_MODE_READ);
const data = Array.from(new Float32Array(readback.getMappedRange()));
readback.unmap();
src.destroy();
console.log(`[asyncBuffer] frame ${frameId}: resolved ->`, data);

// Present only AFTER the async readback resolves, so a stuck await visibly
// freezes the animation instead of silently dropping the readback.
context.present();

if (runAnimation.value) {
requestAnimationFrame(frame);
}
};
frame();
};

interface AsyncBufferExampleProps {
// Schedules the worklet on a given runtime (e.g. runOnUI for the UI thread,
// or runOnRuntime(runtime, ...) for a dedicated worklet runtime).
run: (
worklet: typeof webGPUAsyncDemo,
) => (
runAnimation: SharedValue<boolean>,
device: GPUDevice,
context: RNCanvasContext,
presentationFormat: GPUTextureFormat,
flags: GPUFlags,
) => void;
}

export function AsyncBufferExample({ run }: AsyncBufferExampleProps) {
const runAnimation = useSharedValue(true);
const ref = useRef<CanvasRef>(null);
useEffect(() => {
const initWebGPU = async () => {
const adapter = await navigator.gpu.requestAdapter();
if (!adapter) {
console.error("Failed to get GPU adapter");
return;
}
const device = await adapter.requestDevice();
if (!device) {
console.error("Failed to get GPU device");
return;
}
const ctx = ref.current!.getContext("webgpu");
if (!ctx) {
console.error("Failed to get GPU canvas context");
return;
}
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
const flags: GPUFlags = {
COPY_SRC: GPUBufferUsage.COPY_SRC,
COPY_DST: GPUBufferUsage.COPY_DST,
MAP_READ: GPUBufferUsage.MAP_READ,
MAP_WRITE: GPUBufferUsage.MAP_WRITE,
MAP_MODE_READ: GPUMapMode.READ,
};
// TODO: stop the animation on unmount
run(webGPUAsyncDemo)(
runAnimation,
device,
ctx,
presentationFormat,
flags,
);
};
initWebGPU();
return () => {
runAnimation.value = false;
};
});
return (
<View style={style.container}>
<Canvas ref={ref} style={style.webgpu} />
</View>
);
}

const style = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "rgb(90, 180, 255)",
},
webgpu: {
flex: 1,
},
});
14 changes: 14 additions & 0 deletions apps/example/src/Reanimated/AsyncBufferDedicatedThread.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React, { useMemo } from "react";
import { createWorkletRuntime, runOnRuntime } from "react-native-worklets";

import { AsyncBufferExample } from "./AsyncBuffer";

export const AsyncBufferDedicatedThread = () => {
const runtime = useMemo(
() => createWorkletRuntime({ name: "WebGPUAsyncBufferRuntime" }),
[],
);
return (
<AsyncBufferExample run={(worklet) => runOnRuntime(runtime, worklet)} />
);
};
8 changes: 8 additions & 0 deletions apps/example/src/Reanimated/AsyncBufferUIThread.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import React from "react";
import { runOnUI } from "react-native-reanimated";

import { AsyncBufferExample } from "./AsyncBuffer";

export const AsyncBufferUIThread = () => {
return <AsyncBufferExample run={runOnUI} />;
};
8 changes: 8 additions & 0 deletions apps/example/src/Reanimated/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ export const examples = [
screen: "FrameProcessor",
title: "📷 Frame Processor",
},
{
screen: "AsyncBufferUIThread",
title: "🧵 Async Buffer (UI)",
},
{
screen: "AsyncBufferDedicatedThread",
title: "🔀 Async Buffer (Dedicated)",
},
] as const;

const styles = StyleSheet.create({
Expand Down
4 changes: 3 additions & 1 deletion apps/example/src/Reanimated/Reanimated.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,10 @@ export const webGPUDemo = (
passEncoder.end();

device.queue.submit([commandEncoder.finish()]);

// Needed on a dedicated worklet runtime (DedicatedThread); a no-op on the
// UI runtime (UIThread), where present is automatic.
context.present();

if (runAnimation.value) {
requestAnimationFrame(frame);
}
Expand Down
2 changes: 2 additions & 0 deletions apps/example/src/Reanimated/Routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ export type Routes = {
UIThread: undefined;
DedicatedThread: undefined;
FrameProcessor: undefined;
AsyncBufferUIThread: undefined;
AsyncBufferDedicatedThread: undefined;
};
16 changes: 16 additions & 0 deletions apps/example/src/Reanimated/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { List } from "./List";
import { UIThread } from "./UIThread";
import { DedicatedThread } from "./DedicatedThread";
import { FrameProcessor } from "./FrameProcessor";
import { AsyncBufferUIThread } from "./AsyncBufferUIThread";
import { AsyncBufferDedicatedThread } from "./AsyncBufferDedicatedThread";

const Stack = createStackNavigator<Routes>();
export const Reanimated = () => {
Expand Down Expand Up @@ -40,6 +42,20 @@ export const Reanimated = () => {
title: "📷 Frame Processor",
}}
/>
<Stack.Screen
name="AsyncBufferUIThread"
component={AsyncBufferUIThread}
options={{
title: "🧵 Async Buffer (UI)",
}}
/>
<Stack.Screen
name="AsyncBufferDedicatedThread"
component={AsyncBufferDedicatedThread}
options={{
title: "🔀 Async Buffer (Dedicated)",
}}
/>
</Stack.Navigator>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,6 @@ export function StorageBufferVertices() {

const commandBuffer = encoder.finish();
device.queue.submit([commandBuffer]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(context as any).present();
});

return (
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/ThreeJS/Backdrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export const Backdrop = () => {
}

renderer.render(scene, camera);
context!.present();
context.present();
}
return () => {
renderer.setAnimationLoop(null);
Expand Down
2 changes: 1 addition & 1 deletion apps/example/src/ThreeJS/Helmet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const Helmet = () => {
function animate() {
animateCamera();
renderer.render(scene, camera);
context!.present();
context.present();
}

return () => {
Expand Down
Loading
Loading