diff --git a/apps/docs/content/api/canvas.mdx b/apps/docs/content/api/canvas.mdx
index 156701ac3..dc34e4594 100644
--- a/apps/docs/content/api/canvas.mdx
+++ b/apps/docs/content/api/canvas.mdx
@@ -9,8 +9,7 @@ description: Rendering pixels on-screen
| Prop | Type | Description |
| ---- | ---- | ----------- |
-| `opaque` | `boolean` | Defaults to `true`. `false` alpha-composites the canvas over the views behind it (Android and web; iOS uses `alphaMode` alone) |
-| `android` | `AndroidCanvasProps` | Android-only rendering options: `surfaceType` and `zOrderOnTop`. Ignored on iOS and web |
+| `android` | `AndroidCanvasProps` | Android-only rendering options: the native view behind the canvas (`backingView`) and its options (`opaque`, `zOrderOnTop`, `zOrderMediaOverlay`), see [Android rendering options](#android-rendering-options). Ignored on iOS and web |
| `...ViewProps` | `ViewProps` | All standard React Native View props (`style`, `onLayout`, etc.) |
## Usage
@@ -109,7 +108,7 @@ export function TriangleScene() {
return (
-
+
);
}
@@ -134,52 +133,56 @@ As on the Web, `clientWidth` / `clientHeight` are the layout size in points and
## Transparent compositing
-To show the views behind the canvas, set `opaque={false}` on ``, configure the context with `alphaMode: "premultiplied"`, and clear with an alpha of `0`, as the [Usage](#usage) example does. On Android, `opaque` controls transparency and `alphaMode` is ignored; on iOS it is the other way around. Setting all three gives the same result on both platforms.
+To show the views behind the canvas, configure the context with `alphaMode: "premultiplied"` and clear with an alpha of `0`, as the [Usage](#usage) example does. On iOS and on the web that is all it takes. On Android, `alphaMode` is ignored: the backing view decides, and the default `TextureView` composites with alpha unless `android={{ opaque: true }}` asks it not to (see below). Setting both gives the same result on every platform.
### Android rendering options
-On Android, `Canvas` can be backed by one of three native views, with different trade-offs between speed and how well the canvas composites with the views around it. The defaults are right for most apps; the `android` prop selects one explicitly. It is ignored on iOS and web.
+On Android, `Canvas` can be backed by one of three native views, with different trade-offs between speed and how well the canvas composites with the views around it. The `android` prop names it, along with its options; it is ignored on iOS and web.
| Option | Type | Effect |
| --- | --- | --- |
-| `surfaceType` | `"SurfaceView" \| "TextureView" \| "HardwareBufferView"` | Backing view. Defaults to `SurfaceView` when `opaque` and to `TextureView` otherwise. `HardwareBufferView` is opt-in and needs Android 10 (API 29) |
+| `backingView` | `"TextureView" \| "SurfaceView" \| "HardwareBufferView"` | The native view behind the canvas. Defaults to `TextureView` |
+| `opaque` | `boolean` | Declares that the canvas covers every pixel of its bounds, so it is composited without blending. Defaults to `false` on a `TextureView` and to `true` on a `SurfaceView`; a `HardwareBufferView` always composites with alpha. Applied in place |
| `zOrderOnTop` | `boolean` | `SurfaceView` only: draws the canvas above every React Native view in the window. Defaults to `false` |
+| `zOrderMediaOverlay` | `boolean` | `SurfaceView` only: draws the canvas above the other `SurfaceView`s below the window (video players, other canvases). Defaults to `false`; `zOrderOnTop` takes precedence |
-| | `SurfaceView` | `TextureView` | `HardwareBufferView` |
+| | `TextureView` | `SurfaceView` | `HardwareBufferView` |
| --- | --- | --- | --- |
| Minimum Android | 8 (API 26) | 8 (API 26) | 10 (API 29) |
-| Behaves like a regular view | no | yes | yes |
-| Latency | lowest | highest | in between |
-| Status | default when `opaque` | default when `opaque={false}` | experimental, opt-in |
+| Behaves like a regular view | yes | no | yes |
+| Latency | highest | lowest | in between |
+| Opaque by default | no | yes | never |
+| Status | default | opt-in | experimental, opt-in |
-**`SurfaceView`** is the fastest option, and the default for an opaque canvas. 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 whenever the canvas is a plain opaque rectangle.
+**`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 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.
+**`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`.
-**`HardwareBufferView`** composites like a `TextureView`, with lower latency. It is experimental: if the device cannot allocate the buffers the canvas shows nothing instead of falling back, and some devices may show a stale or torn frame. Test it on your target devices, and drop the `surfaceType` to go back to `TextureView`. Below Android 10 it uses `TextureView`.
+**`HardwareBufferView`** composites like a `TextureView`, with lower latency. It is experimental: if the device cannot allocate the buffers the canvas shows nothing instead of falling back, and some devices may show a stale or torn frame. Test it on your target devices, and drop the `backingView` to go back to `TextureView`. Below Android 10 it uses a `TextureView`, with a warning in development.
-The backing view is chosen when the canvas mounts and kept while it is mounted. Changing `opaque` (and `zOrderOnTop` on Android 11 and later) updates it in place, but a change of `surfaceType` is ignored, with a warning in development. To switch backing views, remount the canvas with a `key`:
+The backing view is created when the canvas mounts and kept while it is mounted. `opaque` applies in place, and so do the z-order options on Android 11 and later; a change of `backingView` is ignored, with a warning in development. To switch backing views, remount the canvas with a `key`. The remount leaves the canvas off screen for a frame or two, showing what is behind it, or black, until the new view draws its first frame: the old view leaves and the new one arrives in different window frames. Keeping a canvas on screen through such a swap is possible, but takes hold-and-retire logic specific to each Android release ([#563](https://github.com/wcandillon/react-native-webgpu/pull/563)) that the library does not carry.
```tsx
// The backing view is chosen at mount: a canvas that switches it remounts
-
+
```
Some combinations for other trade-offs:
```tsx
// Opaque TextureView: stays in stacking order, e.g. inside a ScrollView
-
+
+
+// Full-screen scene on a SurfaceView: no composition pass
+
// Translucent SurfaceView above every React Native view
-
+
-// Translucent HardwareBufferView, opt-in (uses TextureView before Android 10)
-
+// Translucent HardwareBufferView, opt-in (uses a TextureView before Android 10)
+
```
-A translucent `SurfaceView` without `zOrderOnTop` blends against the window background (usually black) instead of the React Native views beneath it. Pair it with `zOrderOnTop`, or use one of the two view-based options. Note that `zOrderOnTop` draws above all React Native views in the window, including navigation screens, regardless of `zIndex`.
-
## See also
- [GPUDeviceProvider](/api/gpu-device-provider) - share one device across components
diff --git a/apps/docs/content/api/draw-element-image-to-texture.mdx b/apps/docs/content/api/draw-element-image-to-texture.mdx
index d1854a93c..9fa5d163b 100644
--- a/apps/docs/content/api/draw-element-image-to-texture.mdx
+++ b/apps/docs/content/api/draw-element-image-to-texture.mdx
@@ -76,7 +76,7 @@ The snapshot is what the platform renders for that view hierarchy:
- iOS renders through `drawViewHierarchyInRect`, which includes Metal layers such as a WebGPU `Canvas`. With `RENDER_ATTACHMENT`, the pixels reach the texture without a copy through the CPU; otherwise they are drawn into a bitmap and uploaded.
- Android 10 (API 29) and above renders the views with the same renderer as the screen, so the result matches it pixel for pixel, including elevation shadows and `TextureView` content. Older versions draw the view into a software canvas, which misses content that is only drawn hardware-accelerated.
- Android 13 and above, with `RENDER_ATTACHMENT`: the pixels reach the texture without a copy through the CPU, and come out the same.
-- On Android, `SurfaceView` content is composed outside the view hierarchy and comes out blank. A WebGPU `Canvas` uses a `SurfaceView` by default when opaque; give it `android={{ surfaceType: "TextureView" }}` if it must appear in a snapshot.
+- On Android, `SurfaceView` content is composed outside the view hierarchy and comes out blank. A WebGPU `Canvas` with `android={{ backingView: "SurfaceView" }}` is not captured: keep the default `TextureView` for a canvas that must appear in a snapshot.
## Threading
diff --git a/apps/docs/content/docs/getting-started/canvas.mdx b/apps/docs/content/docs/getting-started/canvas.mdx
index b2a06c5c5..b9079eede 100644
--- a/apps/docs/content/docs/getting-started/canvas.mdx
+++ b/apps/docs/content/docs/getting-started/canvas.mdx
@@ -7,7 +7,7 @@ The [`Canvas`](/api/canvas) component is where you can access a WebGPU context a
The API is symmetric with its Web counterpart, with a few differences.
For instance on React Native you must call `context.present()` after every `device.queue.submit()` to present the frame to the screen.
-On Android, the canvas picks the right native view for you depending on whether it is opaque. The [Android rendering options](/api/canvas#android-rendering-options) explain the trade-offs and how to pick one explicitly.
+On Android, the canvas is backed by a regular view by default. The [Android rendering options](/api/canvas#android-rendering-options) explain the trade-offs and how to pick a faster one.
| Web concept | React Native WebGPU |
| ----------- | --------------------- |
@@ -83,17 +83,14 @@ Because `clientWidth` / `clientHeight` track layout, recompute the buffer size w
## Transparency
-To render the canvas over the React Native views beneath it, you control transparency from two places, and you normally set both:
-
-- **`opaque={false}`** on `` makes the underlying native surface translucent so the views behind it show through.
-- **`alphaMode: "premultiplied"`** on `context.configure`, together with a clear color whose alpha is `0`, makes WebGPU produce a transparent frame.
+To render the canvas over the React Native views beneath it, configure the context with `alphaMode: "premultiplied"` and clear with a color whose alpha is `0`, so that WebGPU produces a transparent frame:
```tsx
-
+
```
-On Android the `alphaMode` passed to `configure()` is ignored, so `opaque={false}` is what actually gives you a transparent canvas there. On iOS `alphaMode` alone controls it and `opaque` is ignored. Setting `opaque={false}` together with `alphaMode: "premultiplied"` and an alpha-`0` clear color gives you the same result on both platforms. See the [Android rendering options](/api/canvas#android-rendering-options) to pick the backing view explicitly.
+On iOS `alphaMode` alone controls it. On Android the `alphaMode` passed to `configure()` is ignored: the backing view decides, and the default `TextureView` composites with alpha unless `android={{ opaque: true }}` asks it not to, while a `SurfaceView` is opaque unless asked otherwise. See the [Android rendering options](/api/canvas#android-rendering-options).
## See also
diff --git a/apps/docs/content/docs/integrations/typegpu.mdx b/apps/docs/content/docs/integrations/typegpu.mdx
index 313fa0ebd..1e37f3c49 100644
--- a/apps/docs/content/docs/integrations/typegpu.mdx
+++ b/apps/docs/content/docs/integrations/typegpu.mdx
@@ -77,7 +77,7 @@ export function GradientTiles() {
ctx.present?.();
});
- return ;
+ return ;
}
```
diff --git a/apps/docs/src/demos/HelloTriangleDemo.tsx b/apps/docs/src/demos/HelloTriangleDemo.tsx
index 76070735b..58837c809 100644
--- a/apps/docs/src/demos/HelloTriangleDemo.tsx
+++ b/apps/docs/src/demos/HelloTriangleDemo.tsx
@@ -84,7 +84,7 @@ export function HelloTriangleDemo({ transparent = true }: HelloTriangleDemoProps
);
diff --git a/apps/docs/src/demos/TransparencyDemo.tsx b/apps/docs/src/demos/TransparencyDemo.tsx
index cb567ef8c..6614ad852 100644
--- a/apps/docs/src/demos/TransparencyDemo.tsx
+++ b/apps/docs/src/demos/TransparencyDemo.tsx
@@ -146,7 +146,7 @@ export function TransparencyDemo() {
return (
-
+
);
}
diff --git a/apps/example/src/Cube/Cube.tsx b/apps/example/src/Cube/Cube.tsx
index dad05d29e..eccac551d 100644
--- a/apps/example/src/Cube/Cube.tsx
+++ b/apps/example/src/Cube/Cube.tsx
@@ -183,8 +183,7 @@ export function Cube() {
);
diff --git a/apps/example/src/Diagnostics/BackgroundDetach.tsx b/apps/example/src/Diagnostics/BackgroundDetach.tsx
index cc81b5091..beca562ab 100644
--- a/apps/example/src/Diagnostics/BackgroundDetach.tsx
+++ b/apps/example/src/Diagnostics/BackgroundDetach.tsx
@@ -15,12 +15,12 @@ import {
// second, and come back. Primarily an Android repro (iOS keeps the
// CAMetalLayer alive in the background).
//
-// On a broken build the two Android view flavors fail differently:
-// - non-opaque (TextureView): onSurfaceTextureDestroyed removes the surface
+// On a broken build the two Android backing views fail differently:
+// - TextureView (the default): onSurfaceTextureDestroyed removes the surface
// registry entry entirely. The resumed view registers a fresh entry the JS
// context has never seen, so the canvas stays black forever (and the
// context renders into the orphaned surface on a destroyed window).
-// - opaque (SurfaceView): the surface detaches to offscreen and reattaches on
+// - SurfaceView: the surface detaches to offscreen and reattaches on
// resume, but the reattach blit presents a frame the context never
// acquired (watch for a present-without-acquire validation error in the
// log) and permanently widens the configured usage with CopyDst.
@@ -31,7 +31,7 @@ import {
export const BackgroundDetach = () => {
const ref = useRef(null);
const { log, append } = useDiagnosticLog();
- const [opaque, setOpaque] = useState(false);
+ const [surfaceView, setSurfaceView] = useState(false);
useEffect(() => {
const sub = AppState.addEventListener("change", (state) => {
@@ -76,17 +76,21 @@ export const BackgroundDetach = () => {
Background the app and return: the animated gradient must resume. On a
- broken build the non-opaque canvas stays black forever (Android), and
- the opaque one logs a validation error on resume.
+ broken build the TextureView canvas stays black forever (Android), and
+ the SurfaceView one logs a validation error on resume.
- opaque (SurfaceView on Android)
+ SurfaceView (Android; the default is a TextureView)
-
+
-
+
{log.map((line, i) => (
diff --git a/apps/example/src/Diagnostics/DeviceDestroyBeforeDetach.tsx b/apps/example/src/Diagnostics/DeviceDestroyBeforeDetach.tsx
index e8d60eb96..3a1df36ec 100644
--- a/apps/example/src/Diagnostics/DeviceDestroyBeforeDetach.tsx
+++ b/apps/example/src/Diagnostics/DeviceDestroyBeforeDetach.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react";
import { Button, ScrollView, Switch, Text, View } from "react-native";
-import type { CanvasRef } from "react-native-webgpu";
+import type { AndroidCanvasProps, CanvasRef } from "react-native-webgpu";
import { Canvas } from "react-native-webgpu";
import * as THREE from "three";
@@ -58,12 +58,17 @@ type Mode = "raw" | "three";
interface SceneProps {
mode: Mode;
- opaque: boolean;
+ surfaceView: boolean;
unconfigureFirst: boolean;
append: (line: string) => void;
}
-const RawScene = ({ opaque, unconfigureFirst, append }: SceneProps) => {
+// Either kind of Android backing view, to tear both down.
+const backingView = (surfaceView: boolean): AndroidCanvasProps => ({
+ backingView: surfaceView ? "SurfaceView" : "TextureView",
+});
+
+const RawScene = ({ surfaceView, unconfigureFirst, append }: SceneProps) => {
const ref = useRef(null);
useEffect(() => {
@@ -122,10 +127,16 @@ const RawScene = ({ opaque, unconfigureFirst, append }: SceneProps) => {
};
}, [append, unconfigureFirst]);
- return ;
+ return (
+
+ );
};
-const ThreeScene = ({ opaque, append }: SceneProps) => {
+const ThreeScene = ({ surfaceView, append }: SceneProps) => {
const ref = useRef(null);
useEffect(() => {
@@ -181,14 +192,20 @@ const ThreeScene = ({ opaque, append }: SceneProps) => {
};
}, [append]);
- return ;
+ return (
+
+ );
};
export const DeviceDestroyBeforeDetach = () => {
const { log, append } = useDiagnosticLog();
const [mounted, setMounted] = useState(true);
const [mode, setMode] = useState("raw");
- const [opaque, setOpaque] = useState(true);
+ const [surfaceView, setSurfaceView] = useState(true);
const [unconfigureFirst, setUnconfigureFirst] = useState(false);
const Scene = mode === "raw" ? RawScene : ThreeScene;
@@ -216,9 +233,13 @@ export const DeviceDestroyBeforeDetach = () => {
/>
-
+
- opaque (SurfaceView) / non-opaque (TextureView)
+ SurfaceView / TextureView
{mode === "raw" ? (
@@ -237,7 +258,7 @@ export const DeviceDestroyBeforeDetach = () => {
{mounted ? (
diff --git a/apps/example/src/Diagnostics/RenderAfterUnmount.tsx b/apps/example/src/Diagnostics/RenderAfterUnmount.tsx
index f5b36702a..919389029 100644
--- a/apps/example/src/Diagnostics/RenderAfterUnmount.tsx
+++ b/apps/example/src/Diagnostics/RenderAfterUnmount.tsx
@@ -20,14 +20,14 @@ import {
// - iOS: MetalView dealloc removes the registry entry while the stored
// wgpu::Surface still wraps the now-released CAMetalLayer. The next
// getCurrentTexture() asks the dead layer for a drawable: EXC_BAD_ACCESS.
-// - Android (non-opaque canvas / TextureView): onSurfaceTextureDestroyed
+// - Android (TextureView): onSurfaceTextureDestroyed
// removes the registry entry without detaching, so the context keeps a
// wgpu::Surface on a destroyed window: dead-window errors or a crash.
//
-// surfaceType is pinned to TextureView: it is the path that crashes rather
-// than silently going black. A non-opaque canvas already defaults to it, but
-// pinning keeps this test on the SurfaceTexture teardown path if that default
-// ever changes. HardwareBufferView teardown (freeing the AHardwareBuffer pool)
+// backingView is pinned to TextureView: it is the path that crashes rather
+// than silently going black. It is the default, but pinning keeps this test
+// on the SurfaceTexture teardown path if that default ever changes.
+// HardwareBufferView teardown (freeing the AHardwareBuffer pool)
// is covered by TransparencyMode's "hardware buffer" option with hide/show.
export const RenderAfterUnmount = () => {
const ref = useRef(null);
@@ -81,8 +81,7 @@ export const RenderAfterUnmount = () => {
) : (
diff --git a/apps/example/src/Diagnostics/SurfaceChurn.tsx b/apps/example/src/Diagnostics/SurfaceChurn.tsx
index 0c26648ce..4a9744c9c 100644
--- a/apps/example/src/Diagnostics/SurfaceChurn.tsx
+++ b/apps/example/src/Diagnostics/SurfaceChurn.tsx
@@ -12,8 +12,8 @@ import {
// Automated stress for the attach/detach transitions. While a frame loop
// renders on the JS thread, the canvas is fully remounted every 400ms (every
-// third remount also flips the opaque prop, which swaps the Android view
-// flavor). Each epoch tears the previous native view down on the platform
+// third remount also flips the Android backing view between a TextureView
+// and a SurfaceView). Each epoch tears the previous native view down on the platform
// thread while the previous frame may still be in flight, exercising:
// - native teardown (MetalView dealloc / TextureView destroy) racing
// getCurrentTexture()/present() on the JS thread,
@@ -86,8 +86,8 @@ export const SurfaceChurn = () => {
Remounts the canvas every 400ms while rendering every frame, flipping
- the opaque flag every third remount. Expected: flicker, but no crash
- and no validation errors. Leave it running for a minute.
+ the Android backing view every third remount. Expected: flicker, but
+ no crash and no validation errors. Leave it running for a minute.
{
key={epoch}
ref={ref}
style={diagnosticStyles.canvas}
- opaque={Math.floor(epoch / 3) % 2 !== 0}
+ android={{
+ backingView:
+ Math.floor(epoch / 3) % 2 !== 0 ? "SurfaceView" : "TextureView",
+ }}
/>
) : (
diff --git a/apps/example/src/Diagnostics/TransparencyMode.tsx b/apps/example/src/Diagnostics/TransparencyMode.tsx
index ac0c73490..3bef4aa94 100644
--- a/apps/example/src/Diagnostics/TransparencyMode.tsx
+++ b/apps/example/src/Diagnostics/TransparencyMode.tsx
@@ -8,7 +8,11 @@ import {
Text,
View,
} from "react-native";
-import type { AndroidCanvasProps, CanvasRef } from "react-native-webgpu";
+import type {
+ AndroidBackingView,
+ AndroidCanvasProps,
+ CanvasRef,
+} from "react-native-webgpu";
import { Canvas } from "react-native-webgpu";
import {
@@ -18,36 +22,64 @@ import {
useDiagnosticLog,
} from "./surfaceLifecycle";
-// Exercises every Android backing-view combination: opaque toggles in place,
-// while the backing view is chosen when the Canvas mounts, so picking another
-// kind of view remounts it (the kind is its key; zOrderOnTop moves a mounted
-// SurfaceView in place on Android 11 and later, and remounts it before).
-// Half-transparent red is cleared over a blue stage with a yellow RN overlay
-// on top:
+// Exercises every Android backing-view combination: opaque toggles in place
+// (on a SurfaceView or a TextureView; a HardwareBufferView always composites
+// with alpha), while the backing view is created when the Canvas mounts, so
+// picking another kind of view remounts it (the kind is its key; the z-order
+// options move a mounted SurfaceView in place on Android 11 and later, and
+// remount it before). Half-transparent red is cleared over a blue stage with
+// a yellow RN overlay on top:
// - opaque: solid red, overlay visible.
-// - non-opaque TextureView (default): pink, overlay visible.
-// - non-opaque HardwareBufferView (opt-in, Android 10+): pink, overlay
+// - translucent TextureView (the default): pink, overlay visible.
+// - translucent HardwareBufferView (opt-in, Android 10+): pink, overlay
// visible. Requesting it on an older device falls back to TextureView.
-// - non-opaque SurfaceView: blends against the window background (black),
+// - translucent SurfaceView: blends against the window background (black),
// overlay visible only with zOrderOnTop off (the surface sits below it).
-// - non-opaque SurfaceView + zOrderOnTop: pink, overlay hidden underneath.
+// - translucent SurfaceView + zOrderOnTop: pink, overlay hidden underneath.
+// - SurfaceView + zOrderMediaOverlay: above the other SurfaceViews below the
+// window; with a single one, no visible difference.
// The loop draws 5 frames a second by default; "every frame" switches it to
-// requestAnimationFrame, which on an opaque SurfaceView (or "surface") is the
-// case where the canvas votes the display's refresh rate: on an adaptive
-// display, `dumpsys SurfaceFlinger` then lists the SurfaceView layer with a
-// requestedFrameRate while it draws, and without one a second after the loop
-// stops.
-const OPTIONS: { label: string; android?: AndroidCanvasProps }[] = [
- { label: "auto" },
- { label: "hardware buffer", android: { surfaceType: "HardwareBufferView" } },
- { label: "texture", android: { surfaceType: "TextureView" } },
- { label: "surface", android: { surfaceType: "SurfaceView" } },
+// requestAnimationFrame, which on an opaque SurfaceView is the case where the
+// canvas votes the display's refresh rate: on an adaptive display, `dumpsys
+// SurfaceFlinger` then lists the SurfaceView layer with a requestedFrameRate
+// while it draws, and without one a second after the loop stops.
+interface Option {
+ label: string;
+ backingView: AndroidBackingView;
+ zOrderOnTop?: boolean;
+ zOrderMediaOverlay?: boolean;
+}
+
+const OPTIONS: Option[] = [
+ { label: "texture", backingView: "TextureView" },
+ { label: "hardware buffer", backingView: "HardwareBufferView" },
+ { label: "surface", backingView: "SurfaceView" },
+ { label: "surface on top", backingView: "SurfaceView", zOrderOnTop: true },
{
- label: "surface on top",
- android: { surfaceType: "SurfaceView", zOrderOnTop: true },
+ label: "surface media overlay",
+ backingView: "SurfaceView",
+ zOrderMediaOverlay: true,
},
];
+// The `android` prop of an option, with the opacity the switch asks for (a
+// HardwareBufferView takes none).
+const androidProps = (option: Option, opaque: boolean): AndroidCanvasProps => {
+ switch (option.backingView) {
+ case "SurfaceView":
+ return {
+ backingView: "SurfaceView",
+ opaque,
+ zOrderOnTop: option.zOrderOnTop,
+ zOrderMediaOverlay: option.zOrderMediaOverlay,
+ };
+ case "TextureView":
+ return { backingView: "TextureView", opaque };
+ case "HardwareBufferView":
+ return { backingView: "HardwareBufferView" };
+ }
+};
+
const CLEAR_COLOR: GPUColor = [0.5, 0, 0, 0.5];
export const TransparencyMode = () => {
@@ -67,15 +99,15 @@ export const TransparencyMode = () => {
everyFrameRef.current = everyFrame;
}, [everyFrame]);
- // The kind of backing view the option selects, and the key of the Canvas:
- // zOrderOnTop is part of it only where it is read at mount.
- const kind =
- option.android?.surfaceType ?? (opaque ? "SurfaceView" : "TextureView");
- const zOrderOnTopAtMount =
- kind === "SurfaceView" && Number(Platform.Version) < 30;
- const backingKey = `${option.android?.surfaceType ?? "auto"}-${
- option.android?.surfaceType ? "" : opaque
- }-${zOrderOnTopAtMount ? !!option.android?.zOrderOnTop : ""}`;
+ // The key of the Canvas: the kind of backing view, plus its z-order where
+ // that is read at mount.
+ const zOrderAtMount =
+ option.backingView === "SurfaceView" && Number(Platform.Version) < 30;
+ const backingKey = `${option.backingView}-${
+ zOrderAtMount
+ ? `${!!option.zOrderOnTop}-${!!option.zOrderMediaOverlay}`
+ : ""
+ }`;
// Runs again for each mount of the Canvas: hide/show, and the remount that
// picking another kind of view causes.
@@ -120,8 +152,8 @@ export const TransparencyMode = () => {
Half-transparent red over blue. HardwareBufferView and TextureView
keep the yellow RN overlay visible. A SurfaceView on top blends over
it. Picking another kind of view remounts the Canvas; opaque (and, on
- Android 11 and later, zOrderOnTop) updates it in place. Use hide/show
- to test a fresh mount.
+ Android 11 and later, the z-order options) updates it in place. Use
+ hide/show to test a fresh mount.
{OPTIONS.map((value) => (
@@ -160,8 +192,7 @@ export const TransparencyMode = () => {
key={backingKey}
ref={ref}
style={diagnosticStyles.canvas}
- opaque={opaque}
- android={option.android}
+ android={androidProps(option, opaque)}
/>
)}
-
+
span x:
diff --git a/apps/example/src/Resize/Resize.tsx b/apps/example/src/Resize/Resize.tsx
index 9ab442a65..de01ccc0a 100644
--- a/apps/example/src/Resize/Resize.tsx
+++ b/apps/example/src/Resize/Resize.tsx
@@ -133,7 +133,7 @@ export const Resize = () => {
return (
-
+
);
diff --git a/apps/example/src/Triangle/HelloTriangle.tsx b/apps/example/src/Triangle/HelloTriangle.tsx
index 0fb86a5e1..ef859b023 100644
--- a/apps/example/src/Triangle/HelloTriangle.tsx
+++ b/apps/example/src/Triangle/HelloTriangle.tsx
@@ -84,7 +84,7 @@ export function HelloTriangle() {
return (
-
+
);
}
diff --git a/apps/example/src/Triangle/HelloTriangleMSAA.tsx b/apps/example/src/Triangle/HelloTriangleMSAA.tsx
index 222be2a18..43ffa8942 100644
--- a/apps/example/src/Triangle/HelloTriangleMSAA.tsx
+++ b/apps/example/src/Triangle/HelloTriangleMSAA.tsx
@@ -93,7 +93,7 @@ export function HelloTriangleMSAA() {
return (
-
+
);
}
diff --git a/packages/webgpu/android/src/main/java/com/webgpu/BackingViewKind.java b/packages/webgpu/android/src/main/java/com/webgpu/BackingViewKind.java
new file mode 100644
index 000000000..0e441511c
--- /dev/null
+++ b/packages/webgpu/android/src/main/java/com/webgpu/BackingViewKind.java
@@ -0,0 +1,38 @@
+package com.webgpu;
+
+import androidx.annotation.Nullable;
+
+/**
+ * The Android view a WebGPUView draws into, see WebGPUView.updateView(), and
+ * the rules the props resolve by. The JS side (Canvas.tsx) mirrors them to
+ * warn in development.
+ */
+enum BackingViewKind {
+ SURFACE_VIEW,
+ TEXTURE_VIEW,
+ HARDWARE_BUFFER_VIEW;
+
+ /**
+ * The kind the androidBackingView prop names; a TextureView by default. A
+ * HardwareBufferView (a plain View drawing AHardwareBuffers inline) needs
+ * Android 10: below it, the request falls back to a TextureView, which
+ * composites the same way, and the JS side warns about it in development.
+ */
+ static BackingViewKind fromBackingView(@Nullable String backingView, boolean hardwareBufferSupported) {
+ if ("SurfaceView".equals(backingView)) {
+ return SURFACE_VIEW;
+ }
+ if ("HardwareBufferView".equals(backingView) && hardwareBufferSupported) {
+ return HARDWARE_BUFFER_VIEW;
+ }
+ return TEXTURE_VIEW;
+ }
+
+ /**
+ * Whether the canvas composites as opaque: what the prop asks for, except
+ * that a HardwareBufferView always composites with alpha.
+ */
+ static boolean resolveOpaque(BackingViewKind kind, boolean opaque) {
+ return opaque && kind != HARDWARE_BUFFER_VIEW;
+ }
+}
diff --git a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUSurfaceView.java b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUSurfaceView.java
index 1da8bd519..e508483bd 100644
--- a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUSurfaceView.java
+++ b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUSurfaceView.java
@@ -13,10 +13,12 @@ public class WebGPUSurfaceView extends SurfaceView implements SurfaceHolder.Call
WebGPUAPI mApi;
- public WebGPUSurfaceView(Context context, WebGPUAPI api, boolean zOrderOnTop, boolean opaque) {
+ public WebGPUSurfaceView(Context context, WebGPUAPI api, boolean zOrderOnTop,
+ boolean zOrderMediaOverlay, boolean opaque) {
super(context);
mApi = api;
- setZOrderOnTop(zOrderOnTop);
+ // Both are read when the surface is created.
+ setZOrder(zOrderOnTop, zOrderMediaOverlay);
setOpaque(opaque);
getHolder().addCallback(this);
}
@@ -27,6 +29,21 @@ public void setOpaque(boolean opaque) {
getHolder().setFormat(opaque ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT);
}
+ // The sublayer the surface is composited at, relative to the window: above
+ // it (on top), or below it, above the other SurfaceViews there (media
+ // overlay) or among them (the default). setZOrderOnTop(false) resets the
+ // request to the default, so it goes first. Before Android 11 the request
+ // is read when the surface is created; from Android 11 on, SurfaceView
+ // applies a changed request with the window's next frame (its pre-draw
+ // listener runs updateSurface()), which invalidate() schedules.
+ public void setZOrder(boolean onTop, boolean mediaOverlay) {
+ setZOrderOnTop(onTop);
+ if (!onTop) {
+ setZOrderMediaOverlay(mediaOverlay);
+ }
+ invalidate();
+ }
+
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
diff --git a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java
index a6f793758..04663c300 100644
--- a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java
+++ b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUView.java
@@ -6,31 +6,36 @@
import android.view.Surface;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.facebook.proguard.annotations.DoNotStrip;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.views.view.ReactViewGroup;
+/**
+ * The Android view behind . It is backed by a TextureView, a
+ * SurfaceView or a HardwareBufferView, named by the androidBackingView prop
+ * and created once, from the props of the first transaction, for the life of
+ * the view (see updateView()).
+ */
public class WebGPUView extends ReactViewGroup implements WebGPUAPI {
private static final String TAG = "WebGPUView";
- // Backing view kinds, see updateView().
- private static final int KIND_SURFACE_VIEW = 0;
- private static final int KIND_TEXTURE_VIEW = 1;
- private static final int KIND_HARDWARE_BUFFER_VIEW = 2;
-
private int mContextId;
- // Props. opaque is applied in place, and so is zOrderOnTop where Android
- // allows it (see setZOrderOnTop); surfaceType is read once, when the
+ // Props. opaque is applied in place, and so are the z-order props where
+ // Android allows it (see takesZOrder); backingView is read once, when the
// backing view is created (see updateView()).
- private boolean mOpaque = true;
- private String mSurfaceType = "auto";
+ private BackingViewKind mBackingView = BackingViewKind.TEXTURE_VIEW;
+ private boolean mOpaque = false;
private boolean mZOrderOnTop = false;
+ private boolean mZOrderMediaOverlay = false;
private WebGPUModule mModule;
// The backing view, created by the first prop transaction and kept, with
- // the kind it was created with. The kind is -1 until then: it is what
+ // the kind it was created with. The kind is null until then: it is what
// tells a mounted canvas apart.
private View mView = null;
- private int mKind = -1;
+ @Nullable
+ private BackingViewKind mKind = null;
// Only a SurfaceView votes: a TextureView or HardwareBufferView is
// composited into the window, which votes for it.
private final SurfaceFrameRateVote mFrameRateVote = new SurfaceFrameRateVote(
@@ -58,11 +63,7 @@ public void setContextId(int contextId) {
mContextId = contextId;
}
- public void setOpaque(boolean value) {
- mOpaque = value;
- }
-
- // The backing view is chosen from the props of the first transaction and
+ // The backing view is created from the props of the first transaction and
// kept (see updateView()). React calls a setter again only when its prop
// changed, so a call on a mounted canvas is a change: the setters below
// apply it in place where the view can take it, else they ignore it and
@@ -70,98 +71,99 @@ public void setOpaque(boolean value) {
// new key). The JS side warns about the same changes in development (see
// Canvas.tsx), mirroring these rules.
- public void setSurfaceType(String value) {
- if (mKind != -1) {
- warnIgnoredOnceMounted("android.surfaceType");
+ public void setBackingView(@Nullable String value) {
+ if (mKind != null) {
+ warnIgnoredOnceMounted("android.backingView");
return;
}
- mSurfaceType = value;
+ boolean hardwareBufferSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+ mBackingView = BackingViewKind.fromBackingView(value, hardwareBufferSupported);
+ }
+
+ public void setOpaque(boolean value) {
+ mOpaque = value;
}
public void setZOrderOnTop(boolean value) {
- if (mKind == -1) {
+ if (takesZOrder("android.zOrderOnTop")) {
mZOrderOnTop = value;
- return;
- }
- // Only a SurfaceView has a z-order of its own: elsewhere the prop never
- // applied, and the change is moot.
- if (mKind != KIND_SURFACE_VIEW) {
- return;
- }
- if (!zOrderChangesInPlace()) {
- warnIgnoredOnceMounted("android.zOrderOnTop");
- return;
+ applyZOrder();
}
- mZOrderOnTop = value;
- ((WebGPUSurfaceView) mView).setZOrderOnTop(value);
}
- private void warnIgnoredOnceMounted(String prop) {
- Log.w(TAG, "Ignoring a change of " + prop + " on a mounted canvas: the backing view"
- + " is chosen when the canvas mounts. Remount the canvas (give it a new key)"
- + " to change it.");
+ public void setZOrderMediaOverlay(boolean value) {
+ if (takesZOrder("android.zOrderMediaOverlay")) {
+ mZOrderMediaOverlay = value;
+ applyZOrder();
+ }
}
- // SurfaceView.setZOrderOnTop() moves a live surface since Android 11, for
- // an app targeting it; before that, the surface reads it when it is
- // created (see updateView()).
- private boolean zOrderChangesInPlace() {
- return Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
+ // Whether a z-order prop is taken. Before the backing view exists it is
+ // read when the view is created. After that only a SurfaceView has a
+ // z-order of its own (elsewhere the prop never applied, and the change is
+ // moot), and SurfaceView moves a live surface since Android 11, for an app
+ // targeting it; before that, the surface reads it when it is created.
+ private boolean takesZOrder(String prop) {
+ if (mKind == null) {
+ return true;
+ }
+ if (mKind != BackingViewKind.SURFACE_VIEW) {
+ return false;
+ }
+ boolean inPlace = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
&& getContext().getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.R;
+ if (!inPlace) {
+ warnIgnoredOnceMounted(prop);
+ }
+ return inPlace;
}
- // Resolve the backing view from the props. "auto" picks SurfaceView for an
- // opaque canvas and TextureView for a non-opaque one. WebGPUHardwareBufferView
- // (a plain View drawing AHardwareBuffers inline, API 29+) is opt-in via
- // surfaceType until it gets a runtime fallback to TextureView when the
- // AHardwareBuffer import fails on a device; below API 29 the request itself
- // falls back to TextureView.
- private int resolveKind() {
- if ("SurfaceView".equals(mSurfaceType)) {
- return KIND_SURFACE_VIEW;
- }
- if ("TextureView".equals(mSurfaceType)) {
- return KIND_TEXTURE_VIEW;
+ private void applyZOrder() {
+ if (mKind == BackingViewKind.SURFACE_VIEW) {
+ ((WebGPUSurfaceView) mView).setZOrder(mZOrderOnTop, mZOrderMediaOverlay);
}
- if ("HardwareBufferView".equals(mSurfaceType)) {
- boolean hardwareBufferSupported = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
- return hardwareBufferSupported ? KIND_HARDWARE_BUFFER_VIEW : KIND_TEXTURE_VIEW;
- }
- return mOpaque ? KIND_SURFACE_VIEW : KIND_TEXTURE_VIEW;
+ }
+
+ private void warnIgnoredOnceMounted(String prop) {
+ Log.w(TAG, "Ignoring a change of " + prop + " on a mounted canvas: the backing view"
+ + " is chosen when the canvas mounts. Remount the canvas (give it a new key)"
+ + " to change it.");
}
// Apply the complete prop transaction once, after contextId and all rendering
// options have arrived. The first one creates the backing view, which is
// then kept: a SurfaceView must know its z-order before it attaches (before
- // Android 11), and replacing a backing view on screen hands the canvas from
- // one view to another, which no release of Android does without a frame of
- // nothing, or of black, in between (the two views are composited by
- // different frames). A canvas that needs another backing view is remounted
- // instead. Later transactions apply the opacity in place (the setters decide
+ // Android 11), and keeping a canvas on screen while its backing view is
+ // replaced takes hold-and-retire logic specific to each Android release (see
+ // PR #563, and react-native-skia's #4189 and #4192), which this view does
+ // not carry. A canvas that needs another backing view is remounted instead,
+ // and is off screen for a frame or two until the new view draws its first
+ // frame. Later transactions apply the opacity in place (the setters decide
// what else is taken); WebGPUHardwareBufferView always composites with
// alpha, so there it is a no-op.
public void updateView() {
- if (mKind != -1) {
- if (mKind == KIND_TEXTURE_VIEW) {
- ((WebGPUTextureView) mView).setOpaque(mOpaque);
- } else if (mKind == KIND_SURFACE_VIEW) {
- ((WebGPUSurfaceView) mView).setOpaque(mOpaque);
+ if (mKind != null) {
+ boolean opaque = BackingViewKind.resolveOpaque(mKind, mOpaque);
+ if (mKind == BackingViewKind.TEXTURE_VIEW) {
+ ((WebGPUTextureView) mView).setOpaque(opaque);
+ } else if (mKind == BackingViewKind.SURFACE_VIEW) {
+ ((WebGPUSurfaceView) mView).setOpaque(opaque);
}
return;
}
- int kind = resolveKind();
- boolean zOrderOnTop = kind == KIND_SURFACE_VIEW && mZOrderOnTop;
+ BackingViewKind kind = mBackingView;
mKind = kind;
+ boolean opaque = BackingViewKind.resolveOpaque(kind, mOpaque);
Context ctx = getContext();
switch (kind) {
- case KIND_HARDWARE_BUFFER_VIEW:
+ case HARDWARE_BUFFER_VIEW:
mView = new WebGPUHardwareBufferView(ctx, this);
break;
- case KIND_TEXTURE_VIEW:
- mView = new WebGPUTextureView(ctx, this, mOpaque);
+ case TEXTURE_VIEW:
+ mView = new WebGPUTextureView(ctx, this, opaque);
break;
default:
- mView = new WebGPUSurfaceView(ctx, this, zOrderOnTop, mOpaque);
+ mView = new WebGPUSurfaceView(ctx, this, mZOrderOnTop, mZOrderMediaOverlay, opaque);
break;
}
addView(mView);
@@ -188,7 +190,7 @@ public void surfaceCreated(Surface surface) {
float density = getResources().getDisplayMetrics().density;
float width = getWidth() / density;
float height = getHeight() / density;
- boolean votesFrameRate = mKind == KIND_SURFACE_VIEW;
+ boolean votesFrameRate = mKind == BackingViewKind.SURFACE_VIEW;
if (votesFrameRate) {
mFrameRateVote.attach(surface);
}
diff --git a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUViewManager.java b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUViewManager.java
index 27a8fa085..386438b92 100644
--- a/packages/webgpu/android/src/main/java/com/webgpu/WebGPUViewManager.java
+++ b/packages/webgpu/android/src/main/java/com/webgpu/WebGPUViewManager.java
@@ -1,6 +1,7 @@
package com.webgpu;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import com.facebook.react.module.annotations.ReactModule;
import com.facebook.react.uimanager.ThemedReactContext;
@@ -30,15 +31,15 @@ public void onDropViewInstance(@NonNull WebGPUView view) {
}
@Override
- @ReactProp(name = "opaque", defaultBoolean = true)
- public void setOpaque(WebGPUView view, boolean value) {
- view.setOpaque(value);
+ @ReactProp(name = "androidBackingView")
+ public void setAndroidBackingView(WebGPUView view, @Nullable String value) {
+ view.setBackingView(value);
}
@Override
- @ReactProp(name = "androidSurfaceType")
- public void setAndroidSurfaceType(WebGPUView view, String value) {
- view.setSurfaceType(value);
+ @ReactProp(name = "androidOpaque")
+ public void setAndroidOpaque(WebGPUView view, boolean value) {
+ view.setOpaque(value);
}
@Override
@@ -47,6 +48,12 @@ public void setAndroidZOrderOnTop(WebGPUView view, boolean value) {
view.setZOrderOnTop(value);
}
+ @Override
+ @ReactProp(name = "androidZOrderMediaOverlay")
+ public void setAndroidZOrderMediaOverlay(WebGPUView view, boolean value) {
+ view.setZOrderMediaOverlay(value);
+ }
+
@Override
protected void onAfterUpdateTransaction(@NonNull WebGPUView view) {
super.onAfterUpdateTransaction(view);
diff --git a/packages/webgpu/src/Canvas.tsx b/packages/webgpu/src/Canvas.tsx
index a588a20cd..c2c59c191 100644
--- a/packages/webgpu/src/Canvas.tsx
+++ b/packages/webgpu/src/Canvas.tsx
@@ -12,111 +12,194 @@ function generateContextId() {
return CONTEXT_COUNTER++;
}
-export type AndroidSurfaceType =
- "SurfaceView" | "TextureView" | "HardwareBufferView";
+/** The Android view a canvas draws into, see {@link AndroidCanvasProps}. */
+export type AndroidBackingView =
+ "TextureView" | "SurfaceView" | "HardwareBufferView";
-/**
- * Android-only rendering options. They select the backing view, which is
- * chosen when the canvas mounts and kept: a later change is ignored, with a
- * warning in development. Remount the canvas, with a `key`, to change it.
- */
-export interface AndroidCanvasProps {
+/** The options of a canvas backed by a `TextureView`, the default. */
+export interface AndroidTextureViewProps {
/**
- * Backing view. Defaults to `SurfaceView` when the canvas mounts opaque and
- * to `TextureView` otherwise; both composite correctly in React Native
- * stacking order without further flags. `HardwareBufferView` (Android 10+,
- * a plain View that draws each frame's AHardwareBuffer inline with no extra
- * copy) is opt-in and experimental; on older devices it uses `TextureView`.
- * Read once, at mount.
+ * A `TextureView`: a regular view, composited like any other (parent
+ * transforms, clipping, alpha and z-order apply), at the price of an extra
+ * texture copy and typically a frame of latency. The default.
*/
- surfaceType?: AndroidSurfaceType;
+ backingView?: "TextureView";
/**
- * SurfaceView only: composite above every React Native view in the window,
- * ignoring `zIndex`. Ignored for TextureView. Defaults to false. Applied in
- * place on Android 11 and later; read once, at mount, before that.
+ * Declares that the canvas covers every pixel of its bounds, so the UI
+ * toolkit skips blending the view. Defaults to false. Applied in place.
*/
- zOrderOnTop?: boolean;
+ opaque?: boolean;
}
-export interface CanvasProps extends ViewProps {
+/** The options of a canvas backed by a `SurfaceView`. */
+export interface AndroidSurfaceViewProps {
/**
- * Defaults to true. Set to false to alpha-composite the canvas over the
- * views behind it (pair it with `alphaMode: "premultiplied"` and an alpha-0
- * clear color). Android and web only; on iOS `alphaMode` alone controls it.
- * On Android the backing view is chosen from it when the canvas mounts; a
- * later change updates that view in place.
+ * A `SurfaceView`: each frame goes to the system compositor as its own
+ * layer, the fastest path, with no extra copy. It is not view content:
+ * parent transforms, clipping, rounded corners and z-order with sibling
+ * views do not apply to it.
+ */
+ backingView: "SurfaceView";
+ /**
+ * Declares that the canvas covers every pixel of its bounds, so the
+ * compositor skips blending the layer and can show it through a hardware
+ * overlay. Defaults to true: a translucent `SurfaceView` blends against
+ * what is below the window, usually black, rather than the React Native
+ * views beneath it (pair it with `zOrderOnTop`). Applied in place.
*/
opaque?: boolean;
/**
- * Android-only rendering options. They select the backing view, which is
- * chosen when the canvas mounts and kept: a later change is ignored, with a
- * warning in development. Remount the canvas, with a `key`, to change it.
- * Ignored on iOS and web.
+ * Composite above every React Native view in the window, ignoring
+ * `zIndex`. Defaults to false. Applied in place on Android 11 and later;
+ * read once, at mount, before that.
+ */
+ zOrderOnTop?: boolean;
+ /**
+ * Composite above the other `SurfaceView`s below the window (video players,
+ * other canvases), while staying below the window itself. Defaults to
+ * false; `zOrderOnTop` takes precedence. Applied in place on Android 11 and
+ * later; read once, at mount, before that.
+ */
+ zOrderMediaOverlay?: boolean;
+}
+
+/** The options of a canvas backed by a `HardwareBufferView`. */
+export interface AndroidHardwareBufferViewProps {
+ /**
+ * A plain view drawing each frame's `AHardwareBuffer` inline: composited
+ * like a `TextureView`, without its extra copy. Opt-in and experimental,
+ * on Android 10 and later; below that the canvas uses a `TextureView`,
+ * with a warning in development. Always composites with alpha.
+ */
+ backingView: "HardwareBufferView";
+}
+
+/**
+ * Android-only rendering options: the native view behind the canvas and its
+ * options. The view is created when the canvas mounts and kept: a change of
+ * `backingView` is ignored, with a warning in development; remount the canvas
+ * (with a `key`) to change it. `opaque` applies in place, and so do the
+ * z-order options on Android 11 and later. Ignored on iOS and web.
+ */
+export type AndroidCanvasProps =
+ | AndroidTextureViewProps
+ | AndroidSurfaceViewProps
+ | AndroidHardwareBufferViewProps;
+
+export interface CanvasProps extends ViewProps {
+ /**
+ * Android-only rendering options: the native view behind the canvas and its
+ * options, see {@link AndroidCanvasProps}. Ignored on iOS and web.
*/
android?: AndroidCanvasProps;
ref?: React.Ref;
}
+// The union read as one bag of options; which of them apply to the view is
+// decided below, as it is on the native side.
+type AndroidOptions = Partial<{
+ backingView: AndroidBackingView;
+ opaque: boolean;
+ zOrderOnTop: boolean;
+ zOrderMediaOverlay: boolean;
+}>;
+
+const options = (android?: AndroidCanvasProps): AndroidOptions =>
+ (android ?? {}) as AndroidOptions;
+
// Anything else reaching the native component would hit the generated
// string-enum parser, which aborts on unknown values.
-const resolveSurfaceType = (
- surfaceType: AndroidSurfaceType | undefined,
-): "auto" | AndroidSurfaceType =>
- surfaceType === "SurfaceView" ||
- surfaceType === "TextureView" ||
- surfaceType === "HardwareBufferView"
- ? surfaceType
- : "auto";
-
-// The props the Android backing view is chosen from when the canvas mounts.
-interface BackingViewProps {
- opaque: boolean;
- android?: AndroidCanvasProps;
-}
+const resolveBackingView = (
+ android?: AndroidCanvasProps,
+): AndroidBackingView => {
+ const { backingView } = options(android);
+ return backingView === "SurfaceView" || backingView === "HardwareBufferView"
+ ? backingView
+ : "TextureView";
+};
-// The backing view the props select, see WebGPUView.resolveKind().
-const resolveKind = ({ opaque, android }: BackingViewProps) => {
- const surfaceType = resolveSurfaceType(android?.surfaceType);
- if (surfaceType !== "auto") {
- return surfaceType;
+// Whether the canvas composites as opaque, see BackingViewKind.resolveOpaque():
+// a SurfaceView is opaque unless asked otherwise, a TextureView is translucent
+// unless asked otherwise, and a HardwareBufferView always composites with
+// alpha.
+const resolveOpaque = (android?: AndroidCanvasProps): boolean => {
+ const { opaque } = options(android);
+ switch (resolveBackingView(android)) {
+ case "SurfaceView":
+ return opaque !== false;
+ case "TextureView":
+ return opaque === true;
+ case "HardwareBufferView":
+ return false;
}
- return opaque ? "SurfaceView" : "TextureView";
};
-const warnIgnored = (prop: string) =>
+// The native props the `android` prop resolves to.
+const androidNativeProps = (android?: AndroidCanvasProps) => {
+ const backingView = resolveBackingView(android);
+ const { zOrderOnTop, zOrderMediaOverlay } = options(android);
+ const surfaceView = backingView === "SurfaceView";
+ return {
+ androidBackingView: backingView,
+ androidOpaque: resolveOpaque(android),
+ androidZOrderOnTop: surfaceView && !!zOrderOnTop,
+ androidZOrderMediaOverlay: surfaceView && !!zOrderMediaOverlay,
+ };
+};
+
+const warnIgnoredChange = (prop: string) =>
console.warn(
`Ignoring a change of ${prop} on a mounted canvas: the backing view is ` +
"chosen when the canvas mounts. Remount the canvas (give it a new key) " +
"to change it.",
);
-// Warns, in development on Android, about a change of these props that the
-// native view ignores: the backing view is chosen from them when the canvas
-// mounts (see WebGPUView.java, whose setters these rules mirror). The native
-// side only says so in logcat, which is not where a JS developer looks.
-const useBackingViewWarning = (props: BackingViewProps) => {
- const mount = useRef(props);
- const prev = useRef(props);
+// Warns, in development on Android, about an option a mounting canvas asks
+// for that the native view cannot honour, and about a change of these props
+// that the native view ignores: the backing view is created from them when
+// the canvas mounts (see WebGPUView.java, whose setters these rules mirror).
+// The native side only says so in logcat, which is not where a JS developer
+// looks.
+const useBackingViewWarning = (android?: AndroidCanvasProps) => {
+ const mount = useRef(android);
+ const prev = useRef(android);
+ useEffect(() => {
+ if (
+ __DEV__ &&
+ Platform.OS === "android" &&
+ resolveBackingView(mount.current) === "HardwareBufferView" &&
+ Number(Platform.Version) < 29
+ ) {
+ console.warn(
+ "android.backingView: HardwareBufferView needs Android 10, the " +
+ "canvas uses a TextureView.",
+ );
+ }
+ }, []);
useEffect(() => {
if (__DEV__ && Platform.OS === "android") {
const { current: before } = prev;
- if (
- resolveSurfaceType(before.android?.surfaceType) !==
- resolveSurfaceType(props.android?.surfaceType)
- ) {
- warnIgnored("android.surfaceType");
+ if (resolveBackingView(before) !== resolveBackingView(android)) {
+ warnIgnoredChange("android.backingView");
}
// Only a SurfaceView has a z-order of its own, which Android 11 and
// later changes in place.
+ const zOrderAtMount =
+ resolveBackingView(mount.current) === "SurfaceView" &&
+ Number(Platform.Version) < 30;
+ const was = options(before);
+ const is = options(android);
+ if (zOrderAtMount && !!was.zOrderOnTop !== !!is.zOrderOnTop) {
+ warnIgnoredChange("android.zOrderOnTop");
+ }
if (
- !!before.android?.zOrderOnTop !== !!props.android?.zOrderOnTop &&
- resolveKind(mount.current) === "SurfaceView" &&
- Number(Platform.Version) < 30
+ zOrderAtMount &&
+ !!was.zOrderMediaOverlay !== !!is.zOrderMediaOverlay
) {
- warnIgnored("android.zOrderOnTop");
+ warnIgnoredChange("android.zOrderMediaOverlay");
}
}
- prev.current = props;
+ prev.current = android;
});
};
@@ -138,15 +221,10 @@ const measureLayout = (view: View) => {
return size;
};
-export const Canvas = ({
- opaque = true,
- android,
- ref,
- ...props
-}: CanvasProps) => {
+export const Canvas = ({ android, ref, ...props }: CanvasProps) => {
const viewRef = useRef(null);
const [contextId, _] = useState(() => generateContextId());
- useBackingViewWarning({ opaque, android });
+ useBackingViewWarning(android);
// Retire the native registry entry for this contextId on unmount. When a
// native surface is still attached, this is a no-op and the native view's
// own teardown retires the entry instead — which keeps StrictMode's
@@ -190,9 +268,7 @@ export const Canvas = ({
);
diff --git a/packages/webgpu/src/WebGPUViewNativeComponent.ts b/packages/webgpu/src/WebGPUViewNativeComponent.ts
index 46e3473cb..96153bb40 100644
--- a/packages/webgpu/src/WebGPUViewNativeComponent.ts
+++ b/packages/webgpu/src/WebGPUViewNativeComponent.ts
@@ -7,12 +7,14 @@ import type { ViewProps } from "react-native";
export interface NativeProps extends ViewProps {
contextId: Int32;
- opaque?: WithDefault;
- androidSurfaceType?: WithDefault<
- "auto" | "SurfaceView" | "TextureView" | "HardwareBufferView",
- "auto"
+ // The `android` prop, resolved (see Canvas.tsx)
+ androidBackingView?: WithDefault<
+ "TextureView" | "SurfaceView" | "HardwareBufferView",
+ "TextureView"
>;
+ androidOpaque?: WithDefault;
androidZOrderOnTop?: WithDefault;
+ androidZOrderMediaOverlay?: WithDefault;
}
// eslint-disable-next-line import/no-default-export
diff --git a/packages/webgpu/src/WebGPUViewNativeComponent.web.ts b/packages/webgpu/src/WebGPUViewNativeComponent.web.ts
index 529e87bf4..993708941 100644
--- a/packages/webgpu/src/WebGPUViewNativeComponent.web.ts
+++ b/packages/webgpu/src/WebGPUViewNativeComponent.web.ts
@@ -7,9 +7,11 @@ import { contextIdToId } from "./utils";
export interface NativeProps extends ViewProps {
contextId: Int32;
- opaque?: boolean;
- androidSurfaceType?: "auto" | "SurfaceView" | "TextureView";
+ // The `android` prop, resolved: Android-only, never reaches the DOM
+ androidBackingView?: "TextureView" | "SurfaceView" | "HardwareBufferView";
+ androidOpaque?: boolean;
androidZOrderOnTop?: boolean;
+ androidZOrderMediaOverlay?: boolean;
}
// eslint-disable-next-line import/no-default-export
@@ -19,9 +21,10 @@ export default function WebGPUViewNativeComponent(
const {
contextId,
style,
- opaque = true,
- androidSurfaceType: _androidSurfaceType,
+ androidBackingView: _androidBackingView,
+ androidOpaque: _androidOpaque,
androidZOrderOnTop: _androidZOrderOnTop,
+ androidZOrderMediaOverlay: _androidZOrderMediaOverlay,
...rest
} = props;
@@ -34,7 +37,6 @@ export default function WebGPUViewNativeComponent(
style: {
...styles.view,
...styles.flex1,
- ...(opaque ? { backgroundColor: "white" } : {}),
...(typeof style === "object" ? style : {}),
},
});
diff --git a/packages/webgpu/src/index.tsx b/packages/webgpu/src/index.tsx
index 1c1da8c5b..9539af9a1 100644
--- a/packages/webgpu/src/index.tsx
+++ b/packages/webgpu/src/index.tsx
@@ -159,9 +159,9 @@ declare global {
// What is captured is what the platform renders for the view hierarchy
// (iOS: UIView.drawViewHierarchyInRect; Android API 29+: HardwareRenderer
// over the views' display lists, older: View.draw into a software canvas).
- // On Android, SurfaceView content (a WebGPU Canvas with the default
- // "SurfaceView" surfaceType, video players) is not part of the hierarchy and
- // comes out blank; use `android={{ surfaceType: "TextureView" }}` for a
+ // On Android, SurfaceView content (a WebGPU Canvas with
+ // `android={{ backingView: "SurfaceView" }}`, video players) is not part of
+ // the hierarchy and comes out blank; keep the default TextureView for a
// canvas that must be captured.
interface GPUQueue {
drawElementImageToTexture(
diff --git a/packages/webgpu/src/mock.ts b/packages/webgpu/src/mock.ts
index d2bf0148f..6e0b28f6d 100644
--- a/packages/webgpu/src/mock.ts
+++ b/packages/webgpu/src/mock.ts
@@ -82,8 +82,11 @@ const NOT_AVAILABLE_UNDER_JEST =
"is not available under Jest - react-native-webgpu has no native implementation in tests (see react-native-webgpu/jestSetup.js)";
export type {
+ AndroidBackingView,
AndroidCanvasProps,
- AndroidSurfaceType,
+ AndroidHardwareBufferViewProps,
+ AndroidSurfaceViewProps,
+ AndroidTextureViewProps,
CanvasProps,
CanvasRef,
NativeCanvas,
@@ -91,12 +94,7 @@ export type {
} from "./Canvas";
// Strip the same props the real Canvas keeps off its outer View.
-export const Canvas = ({
- opaque: _opaque,
- android: _android,
- ref,
- ...props
-}: CanvasProps) => {
+export const Canvas = ({ android: _android, ref, ...props }: CanvasProps) => {
React.useImperativeHandle(ref, () => ({
getContextId: () => -1,
getContext: (_contextName: "webgpu"): RNCanvasContext | null => null,