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.