Skip to content
Open
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, except on Android 11.

**`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 { workletPresentFrames } from "./components/workletPresentFrames";
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,
workletPresentFrames,
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
51 changes: 51 additions & 0 deletions apps/example/src/components/workletPresentFrames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
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 from Reanimated's UI thread, the first two
// in the same animation frame and the rest one per frame, and resolves two
// frames after the last present, once the window has drawn it. This must live
// in app code: the "worklet" directive only works when the babel worklets
// plugin compiles it into the app bundle.
export const workletPresentFrames = (
device: GPUDevice,
context: RNCanvasContext,
colors: GPUColor[],
): Promise<void> =>
new Promise((resolve) => {
scheduleOnUI(() => {
"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;
const step = () => {
if (next === 0) {
present(colors[next++]);
}
if (next < colors.length) {
present(colors[next++]);
requestAnimationFrame(step);
return;
}
requestAnimationFrame(() =>
requestAnimationFrame(() => scheduleOnRN(resolve)),
);
};
requestAnimationFrame(step);
});
});
45 changes: 34 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 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));
// A SurfaceView asks to hear when it animates, for its refresh rate vote
// (WebGPUView.onAnimating).
if (reportAnimation) {
Expand All @@ -322,6 +336,15 @@ 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();
}
}

extern "C" JNIEXPORT void JNICALL
Java_com_webgpu_WebGPUView_switchToOffscreenSurface(JNIEnv *env, jobject thiz,
jint contextId) {
Expand Down
32 changes: 29 additions & 3 deletions packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.Surface;
import android.view.View;
Expand Down Expand Up @@ -36,6 +38,10 @@ public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
private View mView = null;
@Nullable
private BackingViewKind mKind = null;
// Native posts a frame presented on the main thread here, to present it
// behind the draw the window has pending (see SurfaceRegistry.h).
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
private final Runnable mPostedPresent = () -> presentPostedFrame(mContextId);
// Only a SurfaceView votes: a TextureView or HardwareBufferView is
// composited into the window, which votes for it.
private final SurfaceFrameRateVote mFrameRateVote = new SurfaceFrameRateVote(
Expand Down Expand Up @@ -194,7 +200,15 @@ public void surfaceCreated(Surface surface) {
if (votesFrameRate) {
mFrameRateVote.attach(surface);
}
onSurfaceCreate(surface, mContextId, width, height, votesFrameRate);
onSurfaceCreate(surface, mContextId, width, height, presentsBehindPendingDraw(), votesFrameRate);
}

// A TextureView shows a new frame only when its window draws. Android 11's
// Vulkan loader gives a TextureView's buffer queue a 4.3 s dequeue timeout,
// and a present held behind the draw there stretches those waits into
// freezes long enough for an ANR.
private boolean presentsBehindPendingDraw() {
return mKind == BackingViewKind.TEXTURE_VIEW && Build.VERSION.SDK_INT != Build.VERSION_CODES.R;
}

@Override
Expand Down Expand Up @@ -226,6 +240,12 @@ public int getContextId() {
return mContextId;
}

/** Called by native on the main thread, see mPostedPresent. */
@DoNotStrip
private void postPresentBehindPendingDraw() {
mMainHandler.post(mPostedPresent);
}

/**
* Called from WebGPUViewManager.onDropViewInstance when React removes this
* view: the view dies with its Canvas, so it retires the registry entry.
Expand All @@ -235,14 +255,17 @@ public void destroy() {
onViewDestroyed(mContextId);
}

// reportAnimation asks native to call onAnimating() when frames start being
// presented to this surface back to back.
// presentsBehindPendingDraw has native post a present made on the main
// thread through postPresentBehindPendingDraw(); reportAnimation asks native
// to call onAnimating() when frames start being presented to this surface
// back to back.
@DoNotStrip
private native void onSurfaceCreate(
Surface surface,
int contextId,
float width,
float height,
boolean presentsBehindPendingDraw,
boolean reportAnimation
);

Expand All @@ -254,6 +277,9 @@ private native void onSurfaceChanged(
float height
);

@DoNotStrip
private native void presentPostedFrame(int contextId);

@DoNotStrip
private native void switchToOffscreenSurface(int contextId);

Expand Down
Loading