diff --git a/apps/example/src/App.tsx b/apps/example/src/App.tsx index 887eb0d66..cc85c03b5 100644 --- a/apps/example/src/App.tsx +++ b/apps/example/src/App.tsx @@ -35,7 +35,13 @@ import { ComputeToys } from "./ComputeToys"; import { Reanimated } from "./Reanimated"; import { AsyncStarvation } from "./Diagnostics/AsyncStarvation"; import { DeviceLostHang } from "./Diagnostics/DeviceLostHang"; +import { DiagnosticsList } from "./Diagnostics/DiagnosticsList"; import { WorkletRequestAdapter } from "./Diagnostics/WorkletRequestAdapter"; +import { ContextEdgeCases } from "./Diagnostics/ContextEdgeCases"; +import { ViewFormatsUseAfterFree } from "./Diagnostics/ViewFormatsUseAfterFree"; +import { RenderAfterUnmount } from "./Diagnostics/RenderAfterUnmount"; +import { BackgroundDetach } from "./Diagnostics/BackgroundDetach"; +import { SurfaceChurn } from "./Diagnostics/SurfaceChurn"; import { StorageBufferVertices } from "./StorageBufferVertices"; import { SharedTextureMemory } from "./SharedTextureMemory"; import { ImportExternalTexture } from "./ImportExternalTexture"; @@ -96,12 +102,28 @@ function App() { + + + + + + { + const ref = useRef(null); + const { log, append } = useDiagnosticLog(); + const [transparent, setTransparent] = useState(true); + + useEffect(() => { + const sub = AppState.addEventListener("change", (state) => { + append(`AppState: ${state}`); + }); + return () => sub.remove(); + }, [append]); + + useEffect(() => { + let running = true; + let frame = 0; + (async () => { + const { device, format } = await initGPU(append); + const ctx = ref.current!.getContext("webgpu")!; + ctx.configure({ device, format, alphaMode: "premultiplied" }); + append( + "rendering, background the app and come back (or flip the switch)", + ); + const loop = () => { + if (!running) { + return; + } + try { + drawClearFrame(device, ctx, frame++); + if (frame % 120 === 0) { + append(`frame ${frame} ok`); + } + } catch (e) { + append(`frame ${frame}: ${e}`); + } + requestAnimationFrame(loop); + }; + loop(); + })(); + return () => { + running = false; + }; + }, [append]); + + return ( + + + + Background the app and return: the animated gradient must resume. On a + broken build the transparent canvas stays black forever (Android), and + the opaque one logs a validation error on resume. + + + + transparent (TextureView on Android) + + + + + + + {log.map((line, i) => ( + + {line} + + ))} + + + ); +}; + +const styles = { + row: { + flexDirection: "row" as const, + alignItems: "center" as const, + justifyContent: "space-between" as const, + }, +}; diff --git a/apps/example/src/Diagnostics/ContextEdgeCases.tsx b/apps/example/src/Diagnostics/ContextEdgeCases.tsx new file mode 100644 index 000000000..01ef7f808 --- /dev/null +++ b/apps/example/src/Diagnostics/ContextEdgeCases.tsx @@ -0,0 +1,113 @@ +import React, { useRef } from "react"; +import { Button, ScrollView, Text, View } from "react-native"; +import type { CanvasRef } from "react-native-webgpu"; +import { Canvas } from "react-native-webgpu"; + +import { + diagnosticStyles, + drawClearFrame, + initGPU, + useDiagnosticLog, +} from "./surfaceLifecycle"; + +// Repros for GPUCanvasContext entry points the WebGPU spec expects to fail +// gracefully but that crash or silently misbehave in the native +// implementation: +// +// 1. getCurrentTexture() before configure(): SurfaceInfo has no device yet. +// The native size-changed path reconfigures with a null wgpu::Device +// (CreateTexture on the offscreen path, Surface::Configure on-screen) and +// crashes the app instead of throwing a catchable JS error. +// +// 2. configure() on a 0x0 canvas: Dawn refuses zero-sized textures, so +// getCurrentTexture() wraps a null texture and createView() dereferences +// it. A canvas can legitimately be measured at 0x0 for a frame (collapsed +// layout, display: none equivalents), so this is reachable from ordinary +// app code. +// +// 3. unconfigure(): the native method is an empty stub. After unconfigure() +// the context keeps handing out textures as if still configured, where the +// spec says the canvas should behave as if it was never configured. +// +// Each button is an independent repro; on a broken build the first two +// terminate the app, so relaunch between attempts. +export const ContextEdgeCases = () => { + const ref = useRef(null); + const { log, append } = useDiagnosticLog(); + + const getCurrentTextureUnconfigured = () => { + try { + const ctx = ref.current!.getContext("webgpu")!; + append("getCurrentTexture() before configure()..."); + const texture = ctx.getCurrentTexture(); + append( + `returned a ${texture.width}x${texture.height} texture (spec: should throw)`, + ); + } catch (e) { + append(`threw (correct per spec): ${e}`); + } + }; + + const zeroSizedCanvas = async () => { + try { + const { device, format } = await initGPU(append); + const ctx = ref.current!.getContext("webgpu")!; + const canvas = ctx.canvas as HTMLCanvasElement; + canvas.width = 0; + canvas.height = 0; + append("configure() with canvas.width = canvas.height = 0..."); + ctx.configure({ device, format, alphaMode: "opaque" }); + const texture = ctx.getCurrentTexture(); + append(`getCurrentTexture() -> ${texture.width}x${texture.height}`); + texture.createView(); + append("createView() survived"); + } catch (e) { + append(`threw: ${e}`); + } + }; + + const unconfigureStub = async () => { + try { + const { device, format } = await initGPU(append); + const ctx = ref.current!.getContext("webgpu")!; + ctx.configure({ device, format, alphaMode: "opaque" }); + drawClearFrame(device, ctx, 0); + append("rendered one frame, calling unconfigure()..."); + ctx.unconfigure(); + const texture = ctx.getCurrentTexture(); + append( + `getCurrentTexture() after unconfigure() -> ${texture.width}x${texture.height} texture (spec: context should be unconfigured)`, + ); + } catch (e) { + append(`threw (correct per spec): ${e}`); + } + }; + + return ( + + + + Each button exercises a context entry point that must fail gracefully + per the WebGPU spec. On a broken build the first two crash the app. + +