diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index 359d3600d..dc7c63064 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -18,8 +18,8 @@ runs: run: | HASH_FILE="/tmp/.yarn-lock-hash" CURRENT_HASH=$(shasum yarn.lock | cut -d' ' -f1) - if [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE")" = "$CURRENT_HASH" ] && [ -d "node_modules" ]; then - echo "yarn.lock unchanged and node_modules exists — skipping install" + if [ -f "$HASH_FILE" ] && [ "$(cat "$HASH_FILE")" = "$CURRENT_HASH" ] && [ -d "node_modules" ] && [ -d "apps/example/node_modules" ] && [ -d "packages/webgpu/node_modules" ]; then + echo "yarn.lock unchanged and workspace node_modules present, skipping install" else yarn install --immutable echo "$CURRENT_HASH" > "$HASH_FILE" diff --git a/README.md b/README.md index 0ae1359b4..75f105480 100644 --- a/README.md +++ b/README.md @@ -128,8 +128,6 @@ export function HelloTriangle() { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - - context.present(); }; helloTriangle(); }, [ref]); @@ -174,16 +172,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. - -```tsx -// draw -// submit to the queue -device.queue.submit([commandEncoder.finish()]); -// This method is React Native only -context.present(); -``` +Frame presentation is automatic, matching the behavior of `GPUCanvasContext` on the Web. A native display link (CADisplayLink on iOS, Choreographer on Android) ticks once per vsync and presents any surface whose texture was acquired during a previous vsync interval, which gives your render code the full frame between two vsyncs to encode and submit. ### Canvas Transparency @@ -244,7 +233,6 @@ const renderFrame = (device: GPUDevice, context: GPUCanvasContext) => { const commandEncoder = device.createCommandEncoder(); // ... render ... device.queue.submit([commandEncoder.finish()]); - context.present(); }; // Initialize WebGPU on main thread, then run on UI thread diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index 667f87164..20ae81722 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -35,6 +35,8 @@ import { ComputeToys } from "./ComputeToys"; import { Reanimated } from "./Reanimated"; import { AsyncStarvation } from "./Diagnostics/AsyncStarvation"; import { DeviceLostHang } from "./Diagnostics/DeviceLostHang"; +import { PresentRace } from "./Diagnostics/PresentRace"; +import { MultiCanvasSubmit } from "./Diagnostics/MultiCanvasSubmit"; import { StorageBufferVertices } from "./StorageBufferVertices"; // The two lines below are needed by three.js @@ -93,6 +95,11 @@ function App() { + + { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - - context.present(); })() } title="check surface" diff --git a/apps/example/src/ComputeToys/engine/index.ts b/apps/example/src/ComputeToys/engine/index.ts index f0fa08f07..354bb5009 100644 --- a/apps/example/src/ComputeToys/engine/index.ts +++ b/apps/example/src/ComputeToys/engine/index.ts @@ -4,7 +4,7 @@ */ import { Mutex } from "async-mutex"; -import type { CanvasRef, RNCanvasContext } from "react-native-wgpu"; +import type { CanvasRef } from "react-native-wgpu"; import { Bindings } from "./bind"; import { Blitter, ColorSpace } from "./blit"; @@ -37,7 +37,7 @@ export class ComputeEngine { private device: GPUDevice; - private surface: RNCanvasContext | null = null; + private surface: GPUCanvasContext | null = null; private screenWidth = -1; private screenHeight = -1; @@ -110,7 +110,7 @@ export class ComputeEngine { } public setSurface(canvas: CanvasRef) { - const context = canvas.getContext("webgpu") as RNCanvasContext; + const context = canvas.getContext("webgpu"); if (!context) { throw new Error("WebGPU not supported"); } @@ -398,7 +398,6 @@ fn passSampleLevelBilinearRepeat(pass_index: int, uv: float2, lod: float) -> flo // Submit command buffer this.device.queue.submit([encoder.finish()]); - this.surface!.present(); // Update frame counter this.bindings!.time.host.frame += 1; diff --git a/apps/example/src/Diagnostics/MultiCanvasSubmit.tsx b/apps/example/src/Diagnostics/MultiCanvasSubmit.tsx new file mode 100644 index 000000000..b02a37d14 --- /dev/null +++ b/apps/example/src/Diagnostics/MultiCanvasSubmit.tsx @@ -0,0 +1,158 @@ +import React, { useEffect, useRef } from "react"; +import { StyleSheet, Text, View } from "react-native"; +import type { CanvasRef } from "react-native-wgpu"; +import { Canvas } from "react-native-wgpu"; + +type Mode = "combined" | "split"; + +const runPair = ( + device: GPUDevice, + contextA: GPUCanvasContext, + contextB: GPUCanvasContext, + format: GPUTextureFormat, + mode: Mode, + shouldStop: () => boolean, +) => { + contextA.configure({ device, format, alphaMode: "premultiplied" }); + contextB.configure({ device, format, alphaMode: "premultiplied" }); + + const frame = () => { + if (shouldStop()) { + return; + } + + const textureA = contextA.getCurrentTexture(); + const textureB = contextB.getCurrentTexture(); + + const time = Date.now() / 1000; + const r = (Math.sin(time * 2.0) + 1) / 2; + const g = (Math.sin(time * 1.5 + Math.PI / 3) + 1) / 2; + const b = (Math.sin(time * 1.0 + Math.PI / 2) + 1) / 2; + + const drawClear = ( + encoder: GPUCommandEncoder, + view: GPUTextureView, + color: GPUColor, + ) => { + const pass = encoder.beginRenderPass({ + colorAttachments: [ + { + view, + clearValue: color, + loadOp: "clear", + storeOp: "store", + }, + ], + }); + pass.end(); + }; + + if (mode === "combined") { + // One encoder, two passes targeting two different surfaces, one + // command buffer, one submit. Tracks that beginRenderPass accumulates + // every color-attachment surface into the encoder's presentable set. + const encoder = device.createCommandEncoder(); + drawClear(encoder, textureA.createView(), [r, g, b, 1]); + drawClear(encoder, textureB.createView(), [1 - r, 1 - g, 1 - b, 1]); + device.queue.submit([encoder.finish()]); + } else { + // Two encoders, two command buffers, one submit. Tracks that + // queue.submit aggregates presentable surfaces across every command + // buffer in the array. + const encoderA = device.createCommandEncoder(); + drawClear(encoderA, textureA.createView(), [r, g, b, 1]); + const encoderB = device.createCommandEncoder(); + drawClear(encoderB, textureB.createView(), [1 - r, 1 - g, 1 - b, 1]); + device.queue.submit([encoderA.finish(), encoderB.finish()]); + } + + requestAnimationFrame(frame); + }; + + frame(); +}; + +const Pair = ({ mode, label }: { mode: Mode; label: string }) => { + const refA = useRef(null); + const refB = useRef(null); + useEffect(() => { + let stopped = false; + (async () => { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + return; + } + const device = await adapter.requestDevice(); + const contextA = refA.current?.getContext("webgpu"); + const contextB = refB.current?.getContext("webgpu"); + if (!contextA || !contextB) { + return; + } + const format = navigator.gpu.getPreferredCanvasFormat(); + runPair(device, contextA, contextB, format, mode, () => stopped); + })(); + return () => { + stopped = true; + }; + }, [mode]); + return ( + + {label} + + + + + + ); +}; + +export const MultiCanvasSubmit = () => { + return ( + + + Each row drives two canvases that render inverted hues from a single + submit. If the presentable-surface tracking is broken, one of the two + canvases will stop updating (no display-link tick will present it). + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#111", + padding: 12, + }, + intro: { + color: "#f5f5f5", + fontSize: 13, + lineHeight: 18, + marginBottom: 12, + }, + pair: { + flex: 1, + marginBottom: 12, + }, + label: { + color: "#f5f5f5", + fontSize: 13, + marginBottom: 6, + }, + row: { + flex: 1, + flexDirection: "row", + }, + canvas: { + flex: 1, + marginRight: 6, + }, +}); diff --git a/apps/example/src/Diagnostics/PresentRace.tsx b/apps/example/src/Diagnostics/PresentRace.tsx new file mode 100644 index 000000000..0e0e3a60f --- /dev/null +++ b/apps/example/src/Diagnostics/PresentRace.tsx @@ -0,0 +1,139 @@ +import React, { useEffect, useRef } from "react"; +import { StyleSheet, Text, View } from "react-native"; +import type { CanvasRef } from "react-native-wgpu"; +import { Canvas } from "react-native-wgpu"; + +type Mode = "sync" | "microtask" | "longAwait"; + +const runAnimatedClear = ( + device: GPUDevice, + context: GPUCanvasContext, + format: GPUTextureFormat, + mode: Mode, + shouldStop: () => boolean, +) => { + context.configure({ + device, + format, + alphaMode: "premultiplied", + }); + + const frame = async () => { + if (shouldStop()) { + return; + } + + const texture = context.getCurrentTexture(); + + if (mode === "microtask") { + await Promise.resolve(); + } else if (mode === "longAwait") { + // Sleeps past one vsync interval, so the display-link tick presents + // the surface before our submit lands. + await new Promise((resolve) => setTimeout(resolve, 30)); + } + + const time = Date.now() / 1000; + const r = (Math.sin(time * 2.0) + 1) / 2; + const g = (Math.sin(time * 1.5 + Math.PI / 3) + 1) / 2; + const b = (Math.sin(time * 1.0 + Math.PI / 2) + 1) / 2; + + const commandEncoder = device.createCommandEncoder(); + const passEncoder = commandEncoder.beginRenderPass({ + colorAttachments: [ + { + view: texture.createView(), + clearValue: [r, g, b, 1], + loadOp: "clear", + storeOp: "store", + }, + ], + }); + passEncoder.end(); + device.queue.submit([commandEncoder.finish()]); + + requestAnimationFrame(() => { + frame(); + }); + }; + + frame(); +}; + +const Panel = ({ mode, label }: { mode: Mode; label: string }) => { + const ref = useRef(null); + useEffect(() => { + let stopped = false; + (async () => { + const adapter = await navigator.gpu.requestAdapter(); + if (!adapter) { + return; + } + const device = await adapter.requestDevice(); + const context = ref.current?.getContext("webgpu"); + if (!context) { + return; + } + const format = navigator.gpu.getPreferredCanvasFormat(); + runAnimatedClear(device, context, format, mode, () => stopped); + })(); + return () => { + stopped = true; + }; + }, [mode]); + return ( + + {label} + + + ); +}; + +export const PresentRace = () => { + return ( + + + All three panels animate a clear color via requestAnimationFrame. The + present is driven by a native display link, so a short microtask await + between acquire and submit is safe. A long await (greater than one + vsync interval) still races: the display link presents the surface + before submit lands, producing a stale frame. + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#111", + padding: 12, + }, + intro: { + color: "#f5f5f5", + fontSize: 13, + lineHeight: 18, + marginBottom: 12, + }, + panel: { + flex: 1, + marginBottom: 12, + }, + label: { + color: "#f5f5f5", + fontSize: 13, + marginBottom: 6, + }, + canvas: { + flex: 1, + }, +}); diff --git a/apps/example/src/GradientTiles/GradientTiles.tsx b/apps/example/src/GradientTiles/GradientTiles.tsx index 3268cb7f4..fc5875b05 100644 --- a/apps/example/src/GradientTiles/GradientTiles.tsx +++ b/apps/example/src/GradientTiles/GradientTiles.tsx @@ -125,7 +125,6 @@ export function GradientTiles() { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - context.present(); }, [ref, device, root, spanX, spanY, state]); return ( diff --git a/apps/example/src/Home.tsx b/apps/example/src/Home.tsx index 9272dfec9..6658b2a3b 100644 --- a/apps/example/src/Home.tsx +++ b/apps/example/src/Home.tsx @@ -123,6 +123,14 @@ export const examples = [ screen: "DeviceLostHang", title: "⚠️ Device Lost Hang", }, + { + screen: "PresentRace", + title: "⚠️ Present Race (await before submit)", + }, + { + screen: "MultiCanvasSubmit", + title: "🖼️ Multi-Canvas Submit Tracking", + }, { screen: "StorageBufferVertices", title: "💾 Storage Buffer Vertices", diff --git a/apps/example/src/Reanimated/Reanimated.tsx b/apps/example/src/Reanimated/Reanimated.tsx index fa1fa36b6..e7ce983a4 100644 --- a/apps/example/src/Reanimated/Reanimated.tsx +++ b/apps/example/src/Reanimated/Reanimated.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from "react"; import { StyleSheet, View } from "react-native"; -import type { CanvasRef, RNCanvasContext } from "react-native-wgpu"; +import type { CanvasRef } from "react-native-wgpu"; import { Canvas } from "react-native-wgpu"; import type { SharedValue } from "react-native-reanimated"; import { runOnUI, useSharedValue } from "react-native-reanimated"; @@ -10,7 +10,7 @@ import { redFragWGSL, triangleVertWGSL } from "../Triangle/triangle"; const webGPUDemo = ( runAnimation: SharedValue, device: GPUDevice, - context: RNCanvasContext, + context: GPUCanvasContext, presentationFormat: GPUTextureFormat, ) => { "worklet"; @@ -48,18 +48,15 @@ const webGPUDemo = ( }, }); const frame = () => { - console.log(Date.now()); const commandEncoder = device.createCommandEncoder(); const textureView = context.getCurrentTexture().createView(); - // Animate the clearValue color based on Date.now() - const time = Date.now() / 1000; // Convert to seconds for smoother animation + const time = Date.now() / 1000; - // Create animated RGB values using sine waves with different frequencies - const r = (Math.sin(time * 2) + 1) / 2; // Red channel oscillates faster - const g = (Math.sin(time * 1.5 + Math.PI / 3) + 1) / 2; // Green with phase offset - const b = (Math.sin(time * 1 + Math.PI / 2) + 1) / 2; // Blue with different phase + 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 * 1 + Math.PI / 2) + 1) / 2; const renderPassDescriptor: GPURenderPassDescriptor = { colorAttachments: [ @@ -79,7 +76,6 @@ const webGPUDemo = ( device.queue.submit([commandEncoder.finish()]); - context.present(); if (runAnimation.value) { requestAnimationFrame(frame); } diff --git a/apps/example/src/Route.ts b/apps/example/src/Route.ts index 152923e1e..5721093c0 100644 --- a/apps/example/src/Route.ts +++ b/apps/example/src/Route.ts @@ -28,5 +28,7 @@ export type Routes = { Reanimated: undefined; AsyncStarvation: undefined; DeviceLostHang: undefined; + PresentRace: undefined; + MultiCanvasSubmit: undefined; StorageBufferVertices: undefined; }; diff --git a/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx b/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx index 907264638..b1906cf74 100644 --- a/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx +++ b/apps/example/src/StorageBufferVertices/StorageBufferVertices.tsx @@ -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 ( diff --git a/apps/example/src/ThreeJS/Backdrop.tsx b/apps/example/src/ThreeJS/Backdrop.tsx index 8ed2a8c91..113325b9d 100644 --- a/apps/example/src/ThreeJS/Backdrop.tsx +++ b/apps/example/src/ThreeJS/Backdrop.tsx @@ -150,7 +150,6 @@ export const Backdrop = () => { } renderer.render(scene, camera); - context!.present(); } return () => { renderer.setAnimationLoop(null); diff --git a/apps/example/src/ThreeJS/Cube.tsx b/apps/example/src/ThreeJS/Cube.tsx index d3e9707b5..ea3fe0f23 100644 --- a/apps/example/src/ThreeJS/Cube.tsx +++ b/apps/example/src/ThreeJS/Cube.tsx @@ -31,7 +31,6 @@ export const Cube = () => { mesh.rotation.y = time / 1000; renderer.render(scene, camera); - context.present(); } renderer.setAnimationLoop(animate); return () => { diff --git a/apps/example/src/ThreeJS/Helmet.tsx b/apps/example/src/ThreeJS/Helmet.tsx index be7cb626f..70720d360 100644 --- a/apps/example/src/ThreeJS/Helmet.tsx +++ b/apps/example/src/ThreeJS/Helmet.tsx @@ -49,7 +49,6 @@ export const Helmet = () => { function animate() { animateCamera(); renderer.render(scene, camera); - context!.present(); } return () => { diff --git a/apps/example/src/ThreeJS/InstancedMesh.tsx b/apps/example/src/ThreeJS/InstancedMesh.tsx index 06e95245e..efbc3649a 100644 --- a/apps/example/src/ThreeJS/InstancedMesh.tsx +++ b/apps/example/src/ThreeJS/InstancedMesh.tsx @@ -61,7 +61,6 @@ export const InstancedMesh = () => { function animate() { render(); - context!.present(); } function render() { diff --git a/apps/example/src/ThreeJS/PostProcessing.tsx b/apps/example/src/ThreeJS/PostProcessing.tsx index d94ef1728..0c2980501 100644 --- a/apps/example/src/ThreeJS/PostProcessing.tsx +++ b/apps/example/src/ThreeJS/PostProcessing.tsx @@ -72,7 +72,6 @@ export const PostProcessing = () => { mixer.update(delta); } postProcessing.render(); - context!.present(); } return () => { renderer.setAnimationLoop(null); diff --git a/apps/example/src/ThreeJS/components/FiberCanvas.tsx b/apps/example/src/ThreeJS/components/FiberCanvas.tsx index 91b699553..92b928987 100644 --- a/apps/example/src/ThreeJS/components/FiberCanvas.tsx +++ b/apps/example/src/ThreeJS/components/FiberCanvas.tsx @@ -66,7 +66,6 @@ export const FiberCanvas = ({ const renderFrame = state.gl.render.bind(state.gl); state.gl.render = (s: THREE.Scene, c: THREE.Camera) => { renderFrame(s, c); - context?.present(); }; }, }); diff --git a/apps/example/src/Triangle/HelloTriangle.tsx b/apps/example/src/Triangle/HelloTriangle.tsx index 3e28d6c12..caeb560b3 100644 --- a/apps/example/src/Triangle/HelloTriangle.tsx +++ b/apps/example/src/Triangle/HelloTriangle.tsx @@ -77,8 +77,6 @@ export function HelloTriangle() { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - - context.present(); })(); }, [ref]); diff --git a/apps/example/src/Triangle/HelloTriangleMSAA.tsx b/apps/example/src/Triangle/HelloTriangleMSAA.tsx index 5d66983d5..b9518fbe9 100644 --- a/apps/example/src/Triangle/HelloTriangleMSAA.tsx +++ b/apps/example/src/Triangle/HelloTriangleMSAA.tsx @@ -87,7 +87,6 @@ export function HelloTriangleMSAA() { } frame(); - context.present(); })(); }, [ref]); diff --git a/apps/example/src/components/Texture.tsx b/apps/example/src/components/Texture.tsx index d9e689b41..5bd82a911 100644 --- a/apps/example/src/components/Texture.tsx +++ b/apps/example/src/components/Texture.tsx @@ -145,7 +145,6 @@ export const Texture = ({ texture, style, device }: GPUTextureProps) => { renderPass.end(); device.queue.submit([commandEncoder.finish()]); - context.present(); }, [device, state, texture, ref]); return ; }; diff --git a/apps/example/src/components/useWebGPU.ts b/apps/example/src/components/useWebGPU.ts index ac8a631ac..1a399aafe 100644 --- a/apps/example/src/components/useWebGPU.ts +++ b/apps/example/src/components/useWebGPU.ts @@ -57,7 +57,6 @@ export const useWebGPU = (scene: Scene) => { const render = () => { const timestamp = Date.now(); renderScene(timestamp); - context.present(); animationFrameId.current = requestAnimationFrame(render); }; diff --git a/packages/webgpu/README.md b/packages/webgpu/README.md index 0ae1359b4..75f105480 100644 --- a/packages/webgpu/README.md +++ b/packages/webgpu/README.md @@ -128,8 +128,6 @@ export function HelloTriangle() { passEncoder.end(); device.queue.submit([commandEncoder.finish()]); - - context.present(); }; helloTriangle(); }, [ref]); @@ -174,16 +172,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. - -```tsx -// draw -// submit to the queue -device.queue.submit([commandEncoder.finish()]); -// This method is React Native only -context.present(); -``` +Frame presentation is automatic, matching the behavior of `GPUCanvasContext` on the Web. A native display link (CADisplayLink on iOS, Choreographer on Android) ticks once per vsync and presents any surface whose texture was acquired during a previous vsync interval, which gives your render code the full frame between two vsyncs to encode and submit. ### Canvas Transparency @@ -244,7 +233,6 @@ const renderFrame = (device: GPUDevice, context: GPUCanvasContext) => { const commandEncoder = device.createCommandEncoder(); // ... render ... device.queue.submit([commandEncoder.finish()]); - context.present(); }; // Initialize WebGPU on main thread, then run on UI thread diff --git a/packages/webgpu/android/cpp/cpp-adapter.cpp b/packages/webgpu/android/cpp/cpp-adapter.cpp index 2a441c218..3fbec8c0a 100644 --- a/packages/webgpu/android/cpp/cpp-adapter.cpp +++ b/packages/webgpu/android/cpp/cpp-adapter.cpp @@ -12,6 +12,7 @@ #include "AndroidPlatformContext.h" #include "GPUCanvasContext.h" #include "RNWebGPUManager.h" +#include "SurfaceRegistry.h" #define LOG_TAG "WebGPUModule" @@ -68,4 +69,9 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceDestroy( JNIEnv *env, jobject thiz, jint contextId) { auto ®istry = rnwgpu::SurfaceRegistry::getInstance(); registry.removeSurfaceInfo(contextId); +} + +extern "C" JNIEXPORT void JNICALL +Java_com_webgpu_WebGPUModule_nativeTick(JNIEnv * /*env*/, jobject /*thiz*/) { + rnwgpu::SurfaceRegistry::getInstance().tickAll(); } \ No newline at end of file diff --git a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUModule.java b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUModule.java index 75375700e..c0a91b29a 100644 --- a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUModule.java +++ b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUModule.java @@ -1,6 +1,7 @@ package com.webgpu; import android.util.Log; +import android.view.Choreographer; import androidx.annotation.OptIn; @@ -24,6 +25,19 @@ public class WebGPUModule extends NativeWebGPUModuleSpec { System.loadLibrary("react-native-wgpu"); // Load the C++ library } + private volatile boolean mTickActive = false; + + private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() { + @Override + public void doFrame(long frameTimeNanos) { + if (!mTickActive) { + return; + } + nativeTick(); + Choreographer.getInstance().postFrameCallback(this); + } + }; + public WebGPUModule(ReactApplicationContext reactContext) { super(reactContext); // Initialize the C++ module @@ -41,10 +55,46 @@ public boolean install() { throw new RuntimeException("React Native's BlobModule was not found!"); } initializeNative(jsContext.get(), (CallInvokerHolderImpl) callInvokerHolder, blobModule); + startVsyncTicks(); return true; } + @Override + public void invalidate() { + stopVsyncTicks(); + super.invalidate(); + } + + private void startVsyncTicks() { + if (mTickActive) { + return; + } + mTickActive = true; + // Choreographer instances are per-thread; the FrameCallback fires on the + // thread that posted it. Posting from the UI thread keeps the callback + // there, which is required for Vulkan/Surface ops on Android. + getReactApplicationContext().runOnUiQueueThread(new Runnable() { + @Override + public void run() { + Choreographer.getInstance().postFrameCallback(mFrameCallback); + } + }); + } + + private void stopVsyncTicks() { + mTickActive = false; + getReactApplicationContext().runOnUiQueueThread(new Runnable() { + @Override + public void run() { + Choreographer.getInstance().removeFrameCallback(mFrameCallback); + } + }); + } + @OptIn(markerClass = FrameworkAPI.class) @DoNotStrip private native void initializeNative(long jsRuntime, CallInvokerHolderImpl jsInvoker, BlobModule blobModule); + + @DoNotStrip + private native void nativeTick(); } diff --git a/packages/webgpu/apple/WebGPUModule.mm b/packages/webgpu/apple/WebGPUModule.mm index 99580aa14..2f081de55 100644 --- a/packages/webgpu/apple/WebGPUModule.mm +++ b/packages/webgpu/apple/WebGPUModule.mm @@ -1,7 +1,9 @@ #import "WebGPUModule.h" #include "ApplePlatformContext.h" #import "GPUCanvasContext.h" +#include "SurfaceRegistry.h" +#import #import #import #import @@ -20,7 +22,10 @@ @interface RCTBridge (JSIRuntime) - (void *)runtime; @end -@implementation WebGPUModule +@implementation WebGPUModule { + CADisplayLink *_displayLink; + BOOL _displayLinkActive; +} RCT_EXPORT_MODULE(WebGPUModule) @@ -42,7 +47,47 @@ + (BOOL)requiresMainQueueSetup { } - (void)invalidate { + [self stopDisplayLink]; webgpuManager = nil; + [super invalidate]; +} + +- (void)startDisplayLink { + _displayLinkActive = YES; + if (_displayLink != nil) { + return; + } + // CADisplayLink callbacks must be scheduled on a run loop. The main run + // loop is the safest choice: CAMetalLayer ops are main-thread-only, and + // SurfaceInfo's mutex serialises access with the JS thread. + dispatch_async(dispatch_get_main_queue(), ^{ + if (!self->_displayLinkActive) { + return; + } + if (self->_displayLink != nil) { + return; + } + self->_displayLink = + [CADisplayLink displayLinkWithTarget:self selector:@selector(onVsync:)]; + [self->_displayLink addToRunLoop:[NSRunLoop mainRunLoop] + forMode:NSRunLoopCommonModes]; + }); +} + +- (void)stopDisplayLink { + _displayLinkActive = NO; + CADisplayLink *link = _displayLink; + _displayLink = nil; + if (link == nil) { + return; + } + dispatch_async(dispatch_get_main_queue(), ^{ + [link invalidate]; + }); +} + +- (void)onVsync:(CADisplayLink *)__unused link { + rnwgpu::SurfaceRegistry::getInstance().tickAll(); } - (std::shared_ptr)getManager { @@ -78,6 +123,7 @@ - (void)invalidate { std::make_shared(); webgpuManager = std::make_shared(runtime, jsInvoker, platformContext); + [self startDisplayLink]; return @true; } diff --git a/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h b/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h index 110a45d44..7f8f710ca 100644 --- a/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h +++ b/packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h @@ -1,12 +1,24 @@ #pragma once +#include +#include #include +#include #include #include #include +#include #include "webgpu/webgpu_cpp.h" +#ifdef __APPLE__ +namespace dawn::native::metal { + +void WaitForCommandsToBeScheduled(WGPUDevice device); + +} +#endif + namespace rnwgpu { struct NativeInfo { @@ -29,6 +41,7 @@ class SurfaceInfo { void reconfigure(int newWidth, int newHeight) { std::unique_lock lock(_mutex); + _resetPresentationStateLocked(); config.width = newWidth; config.height = newHeight; _configure(); @@ -36,6 +49,7 @@ class SurfaceInfo { void configure(wgpu::SurfaceConfiguration &newConfig) { std::unique_lock lock(_mutex); + _resetPresentationStateLocked(); config = newConfig; config.width = width; config.height = height; @@ -45,6 +59,7 @@ class SurfaceInfo { void unconfigure() { std::unique_lock lock(_mutex); + _resetPresentationStateLocked(); if (surface) { surface.Unconfigure(); } else { @@ -54,6 +69,7 @@ class SurfaceInfo { void *switchToOffscreen() { std::unique_lock lock(_mutex); + _resetPresentationStateLocked(); // We only do this if the onscreen surface is configured. auto isConfigured = config.device != nullptr; if (isConfigured) { @@ -72,6 +88,7 @@ class SurfaceInfo { void switchToOnscreen(void *newNativeSurface, wgpu::Surface newSurface) { std::unique_lock lock(_mutex); + _resetPresentationStateLocked(); nativeSurface = newNativeSurface; surface = std::move(newSurface); // If we are comming from an offscreen context, we need to configure the new @@ -103,6 +120,7 @@ class SurfaceInfo { wgpu::Queue queue = device.GetQueue(); queue.Submit(1, &commands); surface.Present(); + _resetPresentationStateLocked(); texture = nullptr; } } @@ -115,16 +133,35 @@ class SurfaceInfo { void present() { std::unique_lock lock(_mutex); - if (surface) { - surface.Present(); + _presentLocked(); + } + + // Called by the display-link tick. Presents only after the app has submitted + // work for an acquired texture and at least one tick has passed since + // acquire. + void maybePresentForFrame(uint64_t currentFrame) { + std::unique_lock lock(_mutex); + if (_readyToPresent && _acquiredAtFrame && + *_acquiredAtFrame < currentFrame) { + _presentLocked(); } } - wgpu::Texture getCurrentTexture() { - std::shared_lock lock(_mutex); + void markSubmittedForPresentation() { + std::unique_lock lock(_mutex); + if (_textureAcquired) { + _readyToPresent = true; + } + } + + wgpu::Texture getCurrentTexture(uint64_t currentFrame) { + std::unique_lock lock(_mutex); if (surface) { wgpu::SurfaceTexture surfaceTexture; surface.GetCurrentTexture(&surfaceTexture); + _textureAcquired = true; + _readyToPresent = false; + _acquiredAtFrame = currentFrame; return surfaceTexture.texture; } else { return texture; @@ -167,6 +204,28 @@ class SurfaceInfo { } } + // Caller must hold _mutex as unique_lock. + void _presentLocked() { + if (surface && _textureAcquired) { +#ifdef __APPLE__ + if (config.device) { + dawn::native::metal::WaitForCommandsToBeScheduled(config.device.Get()); + } +#endif + surface.Present(); + _textureAcquired = false; + _readyToPresent = false; + _acquiredAtFrame.reset(); + } + } + + // Caller must hold _mutex as unique_lock. + void _resetPresentationStateLocked() { + _textureAcquired = false; + _readyToPresent = false; + _acquiredAtFrame.reset(); + } + mutable std::shared_mutex _mutex; void *nativeSurface = nullptr; wgpu::Surface surface = nullptr; @@ -175,6 +234,9 @@ class SurfaceInfo { wgpu::SurfaceConfiguration config; int width; int height; + bool _textureAcquired = false; + bool _readyToPresent = false; + std::optional _acquiredAtFrame; }; class SurfaceRegistry { @@ -221,10 +283,46 @@ class SurfaceRegistry { return info; } + // Monotonically increasing tick counter. getCurrentTexture stamps the + // surface with the value seen at acquisition time; tickAll() presents + // surfaces whose stamp is strictly less than the current counter, which + // guarantees the JS render code between two vsyncs has finished. + uint64_t getCurrentFrame() const { return _frameCounter.load(); } + + void tickAll() { + auto current = _frameCounter.fetch_add(1, std::memory_order_acq_rel) + 1; + std::vector> snapshot; + { + std::shared_lock lock(_mutex); + snapshot.reserve(_registry.size()); + for (auto &entry : _registry) { + snapshot.push_back(entry.second); + } + } + for (auto &info : snapshot) { + info->maybePresentForFrame(current); + } + } + + void markSubmittedSurfacesForPresentation() { + std::vector> snapshot; + { + std::shared_lock lock(_mutex); + snapshot.reserve(_registry.size()); + for (auto &entry : _registry) { + snapshot.push_back(entry.second); + } + } + for (auto &info : snapshot) { + info->markSubmittedForPresentation(); + } + } + private: SurfaceRegistry() = default; mutable std::shared_mutex _mutex; std::unordered_map> _registry; + std::atomic _frameCounter{0}; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp index d75eb7b0f..e852d973c 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.cpp @@ -3,14 +3,6 @@ #include "RNWebGPUManager.h" #include -#ifdef __APPLE__ -namespace dawn::native::metal { - -void WaitForCommandsToBeScheduled(WGPUDevice device); - -} -#endif - namespace rnwgpu { void GPUCanvasContext::configure( @@ -37,7 +29,7 @@ void GPUCanvasContext::configure( _surfaceInfo->configure(surfaceConfiguration); } -void GPUCanvasContext::unconfigure() {} +void GPUCanvasContext::unconfigure() { _surfaceInfo->unconfigure(); } std::shared_ptr GPUCanvasContext::getCurrentTexture() { auto prevSize = _surfaceInfo->getConfig(); @@ -47,21 +39,14 @@ std::shared_ptr GPUCanvasContext::getCurrentTexture() { if (sizeHasChanged) { _surfaceInfo->reconfigure(width, height); } - auto texture = _surfaceInfo->getCurrentTexture(); - // Pass reportsMemoryPressure=false to avoid triggering spurious Hermes GC - // cycles every frame since the canvas texture doesn't own the buffer. - return std::make_shared(texture, "", false); -} - -void GPUCanvasContext::present() { -#ifdef __APPLE__ - dawn::native::metal::WaitForCommandsToBeScheduled( - _surfaceInfo->getDevice().Get()); -#endif auto size = _surfaceInfo->getSize(); _canvas->setClientWidth(size.width); _canvas->setClientHeight(size.height); - _surfaceInfo->present(); + auto currentFrame = SurfaceRegistry::getInstance().getCurrentFrame(); + auto texture = _surfaceInfo->getCurrentTexture(currentFrame); + // Pass reportsMemoryPressure=false to avoid triggering spurious Hermes GC + // cycles every frame since the canvas texture doesn't own the buffer. + return std::make_shared(texture, "", false, _surfaceInfo); } } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.h b/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.h index 4b97a7887..12b0b4475 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUCanvasContext.h @@ -47,7 +47,6 @@ class GPUCanvasContext : public NativeObject { &GPUCanvasContext::unconfigure); installMethod(runtime, prototype, "getCurrentTexture", &GPUCanvasContext::getCurrentTexture); - installMethod(runtime, prototype, "present", &GPUCanvasContext::present); } // TODO: is this ok? @@ -55,7 +54,6 @@ class GPUCanvasContext : public NativeObject { void configure(std::shared_ptr configuration); void unconfigure(); std::shared_ptr getCurrentTexture(); - void present(); private: std::shared_ptr _canvas; diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUCommandBuffer.h b/packages/webgpu/cpp/rnwgpu/api/GPUCommandBuffer.h index 4f537d084..c656b7d0f 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUCommandBuffer.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUCommandBuffer.h @@ -1,6 +1,9 @@ #pragma once +#include #include +#include +#include #include "Unions.h" @@ -12,12 +15,17 @@ namespace rnwgpu { namespace jsi = facebook::jsi; +class SurfaceInfo; + class GPUCommandBuffer : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPUCommandBuffer"; - explicit GPUCommandBuffer(wgpu::CommandBuffer instance, std::string label) - : NativeObject(CLASS_NAME), _instance(instance), _label(label) {} + explicit GPUCommandBuffer( + wgpu::CommandBuffer instance, std::string label, + std::vector> presentableSurfaces = {}) + : NativeObject(CLASS_NAME), _instance(instance), _label(label), + _presentableSurfaces(std::move(presentableSurfaces)) {} public: std::string getBrand() { return CLASS_NAME; } @@ -36,10 +44,14 @@ class GPUCommandBuffer : public NativeObject { } inline const wgpu::CommandBuffer get() { return _instance; } + const std::vector> &getPresentableSurfaces() { + return _presentableSurfaces; + } private: wgpu::CommandBuffer _instance; std::string _label; + std::vector> _presentableSurfaces; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.cpp index 7ef1ce064..a75327716 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.cpp @@ -34,7 +34,8 @@ std::shared_ptr GPUCommandEncoder::finish( auto commandBuffer = _instance.Finish(&desc); return std::make_shared( commandBuffer, - descriptor.has_value() ? descriptor.value()->label.value_or("") : ""); + descriptor.has_value() ? descriptor.value()->label.value_or("") : "", + _presentableSurfaces); } std::shared_ptr GPUCommandEncoder::beginRenderPass( @@ -56,6 +57,23 @@ std::shared_ptr GPUCommandEncoder::beginRenderPass( throw std::runtime_error("PUCommandEncoder::beginRenderPass(): couldn't " "get GPURenderPassDescriptor"); } + for (const auto &attachment : descriptor->colorAttachments) { + if (std::holds_alternative>( + attachment)) { + auto colorAttachment = + std::get>(attachment); + if (colorAttachment) { + if (colorAttachment->view) { + addPresentableSurface(colorAttachment->view->getSurfaceInfo()); + } + if (colorAttachment->resolveTarget.has_value() && + colorAttachment->resolveTarget.value()) { + addPresentableSurface( + colorAttachment->resolveTarget.value()->getSurfaceInfo()); + } + } + } + } auto renderPass = _instance.BeginRenderPass(&desc); return std::make_shared(renderPass, descriptor->label.value_or("")); @@ -91,6 +109,7 @@ void GPUCommandEncoder::copyTextureToTexture( !conv(size, copySize)) { return; } + addPresentableSurface(destination->texture->getSurfaceInfo()); _instance.CopyTextureToTexture(&src, &dst, &size); } @@ -148,6 +167,7 @@ void GPUCommandEncoder::copyBufferToTexture( return; } + addPresentableSurface(destination->texture->getSurfaceInfo()); _instance.CopyBufferToTexture(&src, &dst, &size); } @@ -176,4 +196,18 @@ void GPUCommandEncoder::insertDebugMarker(std::string markerLabel) { _instance.InsertDebugMarker(markerLabel.c_str()); } +void GPUCommandEncoder::addPresentableSurface( + std::weak_ptr surfaceInfo) { + auto surface = surfaceInfo.lock(); + if (!surface) { + return; + } + for (const auto &existing : _presentableSurfaces) { + if (existing.lock() == surface) { + return; + } + } + _presentableSurfaces.push_back(surface); +} + } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.h b/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.h index 153426785..8fc807455 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUCommandEncoder.h @@ -2,6 +2,7 @@ #include #include +#include #include "Unions.h" @@ -25,6 +26,8 @@ namespace rnwgpu { namespace jsi = facebook::jsi; +class SurfaceInfo; + class GPUCommandEncoder : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPUCommandEncoder"; @@ -104,8 +107,14 @@ class GPUCommandEncoder : public NativeObject { inline const wgpu::CommandEncoder get() { return _instance; } private: + void addPresentableSurface(std::weak_ptr surfaceInfo); + wgpu::CommandEncoder _instance; std::string _label; + // Any encoder operation that can write to a canvas texture must register the + // texture's SurfaceInfo here so queue.submit() can make only those surfaces + // eligible for display-link presentation. + std::vector> _presentableSurfaces; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp index d3c0d65af..539cb5288 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUQueue.cpp @@ -2,9 +2,12 @@ #include #include +#include +#include #include #include "Convertors.h" +#include "SurfaceRegistry.h" namespace rnwgpu { @@ -26,6 +29,28 @@ void GPUQueue::submit( return; } _instance.Submit(bufs_size, bufs.data()); + std::vector> presentableSurfaces; + for (const auto &commandBuffer : commandBuffers) { + for (const auto &weakSurface : commandBuffer->getPresentableSurfaces()) { + auto surface = weakSurface.lock(); + if (!surface) { + continue; + } + bool alreadyTracked = false; + for (const auto &presentableSurface : presentableSurfaces) { + if (presentableSurface == surface) { + alreadyTracked = true; + break; + } + } + if (!alreadyTracked) { + presentableSurfaces.push_back(surface); + } + } + } + for (const auto &surface : presentableSurfaces) { + surface->markSubmittedForPresentation(); + } } void GPUQueue::writeBuffer(std::shared_ptr buffer, diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUTexture.cpp b/packages/webgpu/cpp/rnwgpu/api/GPUTexture.cpp index f1d84b99c..859262dfb 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUTexture.cpp +++ b/packages/webgpu/cpp/rnwgpu/api/GPUTexture.cpp @@ -19,7 +19,8 @@ std::shared_ptr GPUTexture::createView( auto view = _instance.CreateView(&desc); return std::make_shared( view, - descriptor.has_value() ? descriptor.value()->label.value_or("") : ""); + descriptor.has_value() ? descriptor.value()->label.value_or("") : "", + _surfaceInfo); } uint32_t GPUTexture::getWidth() { return _instance.GetWidth(); } diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h b/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h index 772cf3788..2a5b83240 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUTexture.h @@ -3,6 +3,7 @@ #include #include #include +#include #include "Unions.h" @@ -17,14 +18,18 @@ namespace rnwgpu { namespace jsi = facebook::jsi; +class SurfaceInfo; + class GPUTexture : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPUTexture"; explicit GPUTexture(wgpu::Texture instance, std::string label, - bool reportsMemoryPressure = true) + bool reportsMemoryPressure = true, + std::weak_ptr surfaceInfo = {}) : NativeObject(CLASS_NAME), _instance(instance), _label(label), - _reportsMemoryPressure(reportsMemoryPressure) {} + _reportsMemoryPressure(reportsMemoryPressure), + _surfaceInfo(std::move(surfaceInfo)) {} public: std::string getBrand() { return CLASS_NAME; } @@ -68,6 +73,7 @@ class GPUTexture : public NativeObject { } inline const wgpu::Texture get() { return _instance; } + std::weak_ptr getSurfaceInfo() { return _surfaceInfo; } size_t getMemoryPressure() override { if (!_reportsMemoryPressure) { @@ -157,6 +163,7 @@ class GPUTexture : public NativeObject { wgpu::Texture _instance; std::string _label; bool _reportsMemoryPressure = true; + std::weak_ptr _surfaceInfo; }; } // namespace rnwgpu diff --git a/packages/webgpu/cpp/rnwgpu/api/GPUTextureView.h b/packages/webgpu/cpp/rnwgpu/api/GPUTextureView.h index c37058517..cbb384171 100644 --- a/packages/webgpu/cpp/rnwgpu/api/GPUTextureView.h +++ b/packages/webgpu/cpp/rnwgpu/api/GPUTextureView.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include #include "Unions.h" @@ -12,12 +14,16 @@ namespace rnwgpu { namespace jsi = facebook::jsi; +class SurfaceInfo; + class GPUTextureView : public NativeObject { public: static constexpr const char *CLASS_NAME = "GPUTextureView"; - explicit GPUTextureView(wgpu::TextureView instance, std::string label) - : NativeObject(CLASS_NAME), _instance(instance), _label(label) {} + explicit GPUTextureView(wgpu::TextureView instance, std::string label, + std::weak_ptr surfaceInfo = {}) + : NativeObject(CLASS_NAME), _instance(instance), _label(label), + _surfaceInfo(std::move(surfaceInfo)) {} public: std::string getBrand() { return CLASS_NAME; } @@ -35,10 +41,12 @@ class GPUTextureView : public NativeObject { } inline const wgpu::TextureView get() { return _instance; } + std::weak_ptr getSurfaceInfo() { return _surfaceInfo; } private: wgpu::TextureView _instance; std::string _label; + std::weak_ptr _surfaceInfo; }; } // namespace rnwgpu diff --git a/packages/webgpu/package.json b/packages/webgpu/package.json index bda3bc9a3..cf3c7fb55 100644 --- a/packages/webgpu/package.json +++ b/packages/webgpu/package.json @@ -1,6 +1,6 @@ { "name": "react-native-wgpu", - "version": "0.5.11", + "version": "1.0.0", "description": "React Native WebGPU", "main": "lib/commonjs/index", "module": "lib/module/index", diff --git a/packages/webgpu/src/Canvas.tsx b/packages/webgpu/src/Canvas.tsx index 142e5de2c..dca590ee4 100644 --- a/packages/webgpu/src/Canvas.tsx +++ b/packages/webgpu/src/Canvas.tsx @@ -18,7 +18,7 @@ declare global { contextId: number, width: number, height: number, - ) => RNCanvasContext; + ) => GPUCanvasContext; DecodeToUTF8: (buffer: NodeJS.ArrayBufferView | ArrayBuffer) => string; createImageBitmap: typeof createImageBitmap; }; @@ -34,13 +34,9 @@ export interface NativeCanvas { clientHeight: number; } -export type RNCanvasContext = GPUCanvasContext & { - present: () => void; -}; - export interface CanvasRef { getContextId: () => number; - getContext(contextName: "webgpu"): RNCanvasContext | null; + getContext(contextName: "webgpu"): GPUCanvasContext | null; getNativeSurface: () => NativeCanvas; } @@ -57,7 +53,7 @@ export const Canvas = ({ transparent, ref, ...props }: CanvasProps) => { getNativeSurface: () => { return RNWebGPU.getNativeSurface(contextId); }, - getContext(contextName: "webgpu"): RNCanvasContext | null { + getContext(contextName: "webgpu"): GPUCanvasContext | null { if (contextName !== "webgpu") { throw new Error(`[WebGPU] Unsupported context: ${contextName}`); } diff --git a/packages/webgpu/src/Offscreen.ts b/packages/webgpu/src/Offscreen.ts index c4e460bb2..6ce2f589c 100644 --- a/packages/webgpu/src/Offscreen.ts +++ b/packages/webgpu/src/Offscreen.ts @@ -64,10 +64,6 @@ class GPUOffscreenCanvasContext implements GPUCanvasContext { throw new Error("Method not implemented."); } - present() { - // Do nothing - } - getDevice() { if (!this.device) { throw new Error("Device is not configured."); diff --git a/packages/webgpu/src/WebPolyfillGPUModule.ts b/packages/webgpu/src/WebPolyfillGPUModule.ts index 9dcc1f1c5..04229cd05 100644 --- a/packages/webgpu/src/WebPolyfillGPUModule.ts +++ b/packages/webgpu/src/WebPolyfillGPUModule.ts @@ -39,10 +39,7 @@ function makeWebGPUCanvasContext( canvas.setAttribute("height", pixelHeight); } - const context = canvas.getContext("webgpu")!; - return Object.assign(context, { - present: () => {}, - }); + return canvas.getContext("webgpu")!; } // @ts-expect-error - polyfill for RNWebGPU native module diff --git a/packages/webgpu/src/index.tsx b/packages/webgpu/src/index.tsx index a497a9bf0..6de9b0a47 100644 --- a/packages/webgpu/src/index.tsx +++ b/packages/webgpu/src/index.tsx @@ -1,6 +1,6 @@ /// -import type { NativeCanvas, RNCanvasContext } from "./types"; +import type { NativeCanvas } from "./types"; export * from "./main"; @@ -19,7 +19,7 @@ declare global { contextId: number, width: number, height: number, - ) => RNCanvasContext; + ) => GPUCanvasContext; DecodeToUTF8: (buffer: NodeJS.ArrayBufferView | ArrayBuffer) => string; createImageBitmap: typeof createImageBitmap; }; diff --git a/packages/webgpu/src/types.ts b/packages/webgpu/src/types.ts index af4684cfa..6dba47c42 100644 --- a/packages/webgpu/src/types.ts +++ b/packages/webgpu/src/types.ts @@ -8,13 +8,9 @@ export interface NativeCanvas { clientHeight: number; } -export type RNCanvasContext = GPUCanvasContext & { - present: () => void; -}; - export interface CanvasRef { getContextId: () => number; - getContext(contextName: "webgpu"): RNCanvasContext | null; + getContext(contextName: "webgpu"): GPUCanvasContext | null; getNativeSurface: () => NativeCanvas; whenReady: (callback: () => void) => void; }