From a43502b598cc88598e8ad2d7b9a7586561bb5a2e Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Sun, 18 Oct 2026 19:40:35 +0300 Subject: [PATCH] =?UTF-8?q?chore(=F0=9F=A4=96):=20add=20shown,=20a=20loop-?= =?UTF-8?q?off=20state=20and=20a=20frame-rate=20readout=20to=20the=20Trans?= =?UTF-8?q?parencyMode=20diagnostic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The loop's off state presents nothing, so a remount shows what is on screen before a first frame. shown hides the canvas, mounts it again while hidden, so it is configured before its surface exists, and reveals it. The hide/show button reads mounted, with its test id unchanged, and the screen reads out the frames presented per second. --- .../src/Diagnostics/RenderAfterUnmount.tsx | 2 +- .../src/Diagnostics/TransparencyMode.tsx | 91 ++++++++++++++----- 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/apps/example/src/Diagnostics/RenderAfterUnmount.tsx b/apps/example/src/Diagnostics/RenderAfterUnmount.tsx index 919389029..d6b61dacb 100644 --- a/apps/example/src/Diagnostics/RenderAfterUnmount.tsx +++ b/apps/example/src/Diagnostics/RenderAfterUnmount.tsx @@ -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(null); const { log, append } = useDiagnosticLog(); diff --git a/apps/example/src/Diagnostics/TransparencyMode.tsx b/apps/example/src/Diagnostics/TransparencyMode.tsx index 3bef4aa94..b81000191 100644 --- a/apps/example/src/Diagnostics/TransparencyMode.tsx +++ b/apps/example/src/Diagnostics/TransparencyMode.tsx @@ -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; @@ -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(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("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. @@ -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")!; @@ -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); @@ -143,7 +175,7 @@ export const TransparencyMode = () => { return () => { running = false; }; - }, [append, mounted, backingKey]); + }, [append, mounted, backingKey, hiddenCount]); return ( @@ -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. {OPTIONS.map((value) => ( @@ -173,25 +206,38 @@ export const TransparencyMode = () => { />