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/example/src/Diagnostics/RenderAfterUnmount.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import {
// 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.
// is covered by TransparencyMode's "hardware buffer" option with mounted.
export const RenderAfterUnmount = () => {
const ref = useRef<CanvasRef>(null);
const { log, append } = useDiagnosticLog();
Expand Down
91 changes: 69 additions & 22 deletions apps/example/src/Diagnostics/TransparencyMode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ import {
// 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.
// while it draws, and without one a second after the loop stops. "off"
// presents nothing, so a fresh mount shows the stage until its first frame.
// mounted mounts the Canvas afresh. shown hides it and mounts it again while
// hidden, so it is configured before its surface exists, and reveals it.
interface Option {
label: string;
backingView: AndroidBackingView;
Expand Down Expand Up @@ -82,22 +85,38 @@ const androidProps = (option: Option, opaque: boolean): AndroidCanvasProps => {

const CLEAR_COLOR: GPUColor = [0.5, 0, 0, 0.5];

type Loop = "5 fps" | "every frame" | "off";

const nextLoop = (loop: Loop): Loop => {
switch (loop) {
case "5 fps":
return "every frame";
case "every frame":
return "off";
case "off":
return "5 fps";
}
};

export const TransparencyMode = () => {
const ref = useRef<CanvasRef>(null);
const { log, append } = useDiagnosticLog();
const [option, setOption] = useState(OPTIONS[0]);
const [opaque, setOpaque] = useState(false);
const [mounted, setMounted] = useState(true);
const [everyFrame, setEveryFrame] = useState(false);
const [shown, setShown] = useState(true);
const [hiddenCount, setHiddenCount] = useState(0);
const [loop, setLoop] = useState<Loop>("5 fps");
const [framesPerSecond, setFramesPerSecond] = useState(0);
const [taps, setTaps] = useState(0);
// The loop reads the cadence through a ref so that toggling it does not
// restart the effect: a second initGPU() would configure the context with a
// new device while the first still owns the window, and Vulkan refuses the
// surface (VK_ERROR_NATIVE_WINDOW_IN_USE_KHR, device lost).
const everyFrameRef = useRef(everyFrame);
const loopRef = useRef(loop);
useEffect(() => {
everyFrameRef.current = everyFrame;
}, [everyFrame]);
loopRef.current = loop;
}, [loop]);

// The key of the Canvas: the kind of backing view, plus its z-order where
// that is read at mount.
Expand All @@ -109,14 +128,16 @@ export const TransparencyMode = () => {
: ""
}`;

// Runs again for each mount of the Canvas: hide/show, and the remount that
// picking another kind of view causes.
// Runs again for each mount of the Canvas: mounted, shown, and the remount
// that picking another kind of view causes.
useEffect(() => {
if (!mounted) {
return;
}
let running = true;
let frame = 0;
let readoutStart = performance.now();
let framesSinceReadout = 0;
(async () => {
const { device, format } = await initGPU(append);
const ctx = ref.current!.getContext("webgpu")!;
Expand All @@ -125,14 +146,25 @@ export const TransparencyMode = () => {
if (!running) {
return;
}
try {
drawClearFrame(device, ctx, frame++, CLEAR_COLOR);
} catch (e) {
append(`frame threw: ${(e as Error).message}`);
running = false;
return;
if (loopRef.current !== "off") {
try {
drawClearFrame(device, ctx, frame++, CLEAR_COLOR);
} catch (e) {
append(`frame threw: ${(e as Error).message}`);
running = false;
return;
}
framesSinceReadout++;
}
if (everyFrameRef.current) {
const now = performance.now();
if (now - readoutStart >= 1000) {
setFramesPerSecond(
Math.round((framesSinceReadout * 1000) / (now - readoutStart)),
);
framesSinceReadout = 0;
readoutStart = now;
}
if (loopRef.current === "every frame") {
requestAnimationFrame(tick);
} else {
setTimeout(tick, 200);
Expand All @@ -143,7 +175,7 @@ export const TransparencyMode = () => {
return () => {
running = false;
};
}, [append, mounted, backingKey]);
}, [append, mounted, backingKey, hiddenCount]);

return (
<View style={diagnosticStyles.container}>
Expand All @@ -152,8 +184,9 @@ 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, the z-order options) updates it in place. Use
hide/show to test a fresh mount.
Android 11 and later, the z-order options) updates it in place.
mounted remounts it, shown remounts it hidden and reveals it, and with
the loop off nothing is presented.
</Text>
<View style={styles.buttons}>
{OPTIONS.map((value) => (
Expand All @@ -173,25 +206,38 @@ export const TransparencyMode = () => {
/>
<Button
testID="toggle-canvas"
title={mounted ? "hide canvas" : "show canvas"}
title={`mounted: ${mounted}`}
onPress={() => setMounted((value) => !value)}
/>
<Button
testID="toggle-shown"
title={`shown: ${shown}`}
onPress={() => {
if (shown) {
setHiddenCount((count) => count + 1);
}
setShown(!shown);
}}
/>
<Button
testID="toggle-loop"
title={everyFrame ? "loop: every frame" : "loop: 5 fps"}
onPress={() => setEveryFrame((value) => !value)}
title={`loop: ${loop}`}
onPress={() => setLoop(nextLoop)}
/>
</View>
<Text testID="view-status" style={diagnosticStyles.description}>
view: {option.label} opaque: {String(opaque)} overlay taps: {taps}
</Text>
<Text testID="frame-rate" style={diagnosticStyles.description}>
presented: {framesPerSecond} fps
</Text>
</View>
<View style={styles.stage}>
{mounted && (
<Canvas
key={backingKey}
key={`${backingKey}-${hiddenCount}`}
ref={ref}
style={diagnosticStyles.canvas}
style={[diagnosticStyles.canvas, !shown && styles.hidden]}
android={androidProps(option, opaque)}
/>
)}
Expand All @@ -217,6 +263,7 @@ export const TransparencyMode = () => {
const styles = StyleSheet.create({
buttons: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
stage: { flex: 1, backgroundColor: "blue" },
hidden: { display: "none" },
overlay: {
position: "absolute",
left: 24,
Expand Down