Skip to content
Draft
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
2 changes: 1 addition & 1 deletion apps/docs/content/api/canvas.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ On Android, `Canvas` can be backed by one of three native views, with different
| Opaque by default | no | yes | never |
| Status | default | opt-in | experimental, opt-in |

**`TextureView`** is the default: a regular view, so transforms, clipping, alpha and z-order all apply, and the canvas can be composited over other React Native views. The cost is an extra copy, typically a frame of latency, and occasional stalls during some view animations.
**`TextureView`** is the default: a regular view, so transforms, clipping, alpha and z-order all apply, and the canvas can be composited over other React Native views. The cost is an extra copy, typically a frame of latency, and occasional stalls during some view animations. A frame presented on the UI thread, from a worklet, reaches the `TextureView` behind the draw its window has pending, so that the window draws every frame. On Android 11 a `TextureView` takes no new frame before its window has drawn the last one, so a frame that comes sooner renders offscreen and is copied in once the window has drawn.

**`SurfaceView`** is the fastest option. It is not regular view content, though: parent transforms, clipping, rounded corners and z-ordering with sibling views do not apply to it. Use it for a canvas that is a plain rectangle: a full-screen scene or a game. It is opaque unless asked otherwise: a translucent `SurfaceView` blends against the window background (usually black) instead of the React Native views beneath it, which are punched out. Pair it with `zOrderOnTop` to composite over React Native content, and note that `zOrderOnTop` draws above all React Native views in the window, including navigation screens, regardless of `zIndex`.

Expand Down
25 changes: 25 additions & 0 deletions apps/example/src/Tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
PixelRatio,
findNodeHandle,
} from "react-native";
import type { CanvasRef } from "react-native-webgpu";
import {
Canvas,
GPUOffscreenCanvas,
// The flags as the package exports them, not the globals of the same name
// (see the `exportedFlags` spec in Constants.spec.ts).
Expand All @@ -25,6 +27,7 @@ import { redFragWGSL, triangleVertWGSL } from "./Triangle/triangle";
import type { AssetProps } from "./components/useAssets";
import { Texture } from "./components/Texture";
import { workletDeviceStress } from "./components/workletDeviceStress";
import { presentFrames } from "./components/presentFrames";
import { workletSerialization } from "./components/workletSerialization";

export const CI = process.env.CI === "true";
Expand All @@ -33,6 +36,7 @@ const { width } = Dimensions.get("window");
const presentationFormat = navigator.gpu.getPreferredCanvasFormat();
const PROBE_SIZE = 64;
const QUADRANT = PROBE_SIZE / 2;
const TEXTURE_CANVAS_SIZE = 32;

export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => {
const [texture, setTexture] = useState<GPUTexture | null>(null);
Expand All @@ -42,6 +46,10 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => {
// A fixed native view the drawElementImageToTexture spec snapshots: four
// 32x32pt solid quadrants (red, lime, blue, white), 64x64pt in total.
const probeRef = useRef<View>(null);
// A canvas backed by a TextureView on Android, for the TextureView
// present spec, and the view around it, which that spec snapshots.
const textureCanvasRef = useRef<CanvasRef>(null);
const textureCanvasViewRef = useRef<View>(null);
useEffect(() => {
let cancelled = false;
(async () => {
Expand Down Expand Up @@ -129,8 +137,14 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => {
probe: findNodeHandle(probeRef.current),
probeSize: PROBE_SIZE,
pixelRatio: PixelRatio.get(),
textureCanvas: () => textureCanvasRef.current,
textureCanvasView: findNodeHandle(
textureCanvasViewRef.current,
),
textureCanvasSize: TEXTURE_CANVAS_SIZE,
},
workletDeviceStress,
presentFrames,
workletSerialization,
exportedFlags: {
GPUBufferUsage: exportedBufferUsage,
Expand Down Expand Up @@ -229,6 +243,17 @@ export const Tests = ({ assets: { di3D, saturn, moon } }: AssetProps) => {
}}
/>
</View>
<View
ref={textureCanvasViewRef}
collapsable={false}
style={{ width: TEXTURE_CANVAS_SIZE, height: TEXTURE_CANVAS_SIZE }}
>
<Canvas
ref={textureCanvasRef}
style={{ flex: 1 }}
android={{ backingView: "TextureView" }}
/>
</View>
<Texture
texture={texture}
device={device}
Expand Down
81 changes: 81 additions & 0 deletions apps/example/src/components/presentFrames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { RNCanvasContext } from "react-native-webgpu";
import { scheduleOnRN, scheduleOnUI } from "react-native-worklets";

// Test-harness helper (see packages/webgpu TextureViewPresent.spec.ts): clears
// the canvas to each color in turn, the first two in the same animation frame
// and the rest one per frame, and calls done two frames after the last
// present, once the window has drawn it, with the longest time between two of
// those frames. This must live in app code: the "worklet" directive only works
// when the babel worklets plugin compiles it into the app bundle.
const presentColors = (
device: GPUDevice,
context: RNCanvasContext,
colors: GPUColor[],
done: (longestFrameGapMs: number) => void,
) => {
"worklet";
const present = (color: GPUColor) => {
const encoder = device.createCommandEncoder();
encoder
.beginRenderPass({
colorAttachments: [
{
view: context.getCurrentTexture().createView(),
clearValue: color,
loadOp: "clear",
storeOp: "store",
},
],
})
.end();
device.queue.submit([encoder.finish()]);
context.present();
};
let next = 0;
let previousFrameMs = -1;
let longestFrameGapMs = 0;
const onFrame = (callback: () => void) => () => {
const nowMs = performance.now();
if (previousFrameMs >= 0) {
longestFrameGapMs = Math.max(longestFrameGapMs, nowMs - previousFrameMs);
}
previousFrameMs = nowMs;
callback();
};
const step = () => {
if (next === 0) {
present(colors[next++]);
}
if (next < colors.length) {
present(colors[next++]);
requestAnimationFrame(onFrame(step));
return;
}
requestAnimationFrame(
onFrame(() =>
requestAnimationFrame(onFrame(() => done(longestFrameGapMs))),
),
);
};
requestAnimationFrame(onFrame(step));
};

// Presents those frames from Reanimated's UI thread or from the JS thread.
export const presentFrames = (
device: GPUDevice,
context: RNCanvasContext,
colors: GPUColor[],
thread: "UI" | "JS",
): Promise<number> =>
new Promise((resolve) => {
if (thread === "JS") {
presentColors(device, context, colors, resolve);
return;
}
scheduleOnUI(() => {
"worklet";
presentColors(device, context, colors, (longestFrameGapMs) =>
scheduleOnRN(resolve, longestFrameGapMs),
);
});
});
62 changes: 51 additions & 11 deletions packages/webgpu/android/cpp/cpp-adapter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -165,9 +165,11 @@ namespace {
// Weak handle on a Java view and one of its no-argument methods, for native
// threads to call into it: the fence waiter wakes a WebGPUHardwareBufferView
// when a frame is ready, the presenting thread tells a SurfaceView it is
// animating (AnimationReporter). Holding the view weakly means a view that RN
// has dropped is not kept alive by a SurfaceInfo that outlives it (the
// registry keeps the SurfaceInfo until onDropViewInstance).
// animating (AnimationReporter), and a TextureView canvas presenting on the
// main thread posts its present behind the window's pending draw. Holding the
// view weakly means a view that RN has dropped is not kept alive by a
// SurfaceInfo that outlives it (the registry keeps the SurfaceInfo until
// onDropViewInstance).
struct WeakViewCall {
jweak view = nullptr;
jmethodID method = nullptr;
Expand All @@ -188,22 +190,25 @@ struct WeakViewCall {
}
}

// Called on a native thread, outside the SurfaceInfo locks.
void operator()() const {
// Returns whether the view was there to take the call. Called on a native
// thread, outside the SurfaceInfo locks.
bool operator()() const {
facebook::jni::ThreadScope scope;
JNIEnv *env = facebook::jni::Environment::current();
if (env == nullptr || method == nullptr) {
return;
return false;
}
jobject local = env->NewLocalRef(view);
if (local == nullptr) {
return; // the view was garbage collected
return false; // the view was garbage collected
}
env->CallVoidMethod(local, method);
if (env->ExceptionCheck()) {
const bool called = !env->ExceptionCheck();
if (!called) {
env->ExceptionClear();
}
env->DeleteLocalRef(local);
return called;
}
};

Expand Down Expand Up @@ -284,7 +289,8 @@ void setAnimationReporter(int contextId,

extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
JNIEnv *env, jobject thiz, jobject jSurface, jint contextId, jfloat width,
jfloat height, jboolean reportAnimation) {
jfloat height, jboolean presentsBehindPendingDraw,
jboolean acquiresAfterDraw, jboolean reportAnimation) {
if (manager == nullptr) {
return;
}
Expand All @@ -298,13 +304,21 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
auto &runtime = rnwgpu::WebGPURuntime::shared();
auto gpu = runtime.instance();
auto surface = runtime.makeSurface(window);
rnwgpu::NativeSurfacePresentPoster presentPoster;
if (presentsBehindPendingDraw) {
auto postPresent = std::make_shared<WeakViewCall>(
env, thiz, "postPresentBehindPendingDraw");
presentPoster = [postPresent]() { return (*postPresent)(); };
}
// Find-or-create + attach runs atomically under the registry lock so a
// concurrent destroyContext cannot orphan this surface.
auto info = registry.attachSurface(
contextId, gpu, static_cast<int>(width), static_cast<int>(height), window,
surface, [](void *nativeSurface) {
surface,
[](void *nativeSurface) {
ANativeWindow_release(static_cast<ANativeWindow *>(nativeSurface));
});
},
std::move(presentPoster), acquiresAfterDraw);
// A SurfaceView asks to hear when it animates, for its refresh rate vote
// (WebGPUView.onAnimating).
if (reportAnimation) {
Expand All @@ -322,6 +336,32 @@ extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceCreate(
manager->flushPendingSurfaceTransition(info);
}

// The present WebGPUView.postPresentBehindPendingDraw posted (UI thread).
extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_presentPostedFrame(
JNIEnv *env, jobject thiz, jint contextId) {
auto &registry = rnwgpu::SurfaceRegistry::getInstance();
if (auto info = registry.getSurfaceInfo(contextId)) {
info->presentPostedFrame();
}
}

// WebGPUView.surfaceUpdated, while the window draws (UI thread).
extern "C" JNIEXPORT jlong JNICALL Java_com_webgpu_WebGPUView_onSurfaceDrawing(
JNIEnv *env, jobject thiz, jint contextId) {
auto &registry = rnwgpu::SurfaceRegistry::getInstance();
auto info = registry.getSurfaceInfo(contextId);
return info ? static_cast<jlong>(info->presentCount()) : 0;
}

// Posted by WebGPUView.surfaceUpdated behind the window's draw (UI thread).
extern "C" JNIEXPORT void JNICALL Java_com_webgpu_WebGPUView_onSurfaceDrawn(
JNIEnv *env, jobject thiz, jint contextId, jlong presentCount) {
auto &registry = rnwgpu::SurfaceRegistry::getInstance();
if (auto info = registry.getSurfaceInfo(contextId)) {
info->surfaceDrawn(static_cast<uint64_t>(presentCount));
}
}

extern "C" JNIEXPORT void JNICALL
Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz,
jint contextId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,8 @@ void surfaceChanged(

void surfaceOffscreen();

/** A TextureView's window is drawing the newest frame of its surface. */
void surfaceUpdated();

int getContextId();
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,6 @@ public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surfaceTexture)

@Override
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surfaceTexture) {
// No implementation needed
mApi.surfaceUpdated();
}
}
Loading