Repository navigation
fix(🤖): keep a canvas on screen while it swaps its backing view in place - #563
YevheniiKotyrlo wants to merge 10 commits into
Conversation
A canvas drawn from Reanimated's UI thread presents inside the main thread's Choreographer frame, ahead of the window's draw. A TextureView takes its next buffer only when the window draws, and its buffer queue replaces a buffer it has not taken yet, so the next present replaced every other frame and the window showed half of them. A frame presented on the main thread to a TextureView surface now waits for a present the view posts to the main looper, behind the draw the window has pending, as react-native-skia's FrameScheduler does. The next frame boundary presents a frame still waiting first, a detach drops it with its surface, and frames presented on other threads, or to a SurfaceView or HardwareBufferView, are presented as before. Android 11 keeps presenting at once: its Vulkan loader gives a TextureView's buffer queue a 4.3 s dequeue timeout, and a present held behind the draw stretched those waits into 14 s freezes and an ANR on the Android 11 emulator. The weak handle the hardware buffer view's waker kept on its view is shared by both calls from native, the canvas docs say when a UI-thread frame reaches a TextureView, and the e2e suite presents frames from a worklet on a TextureView canvas on the tests screen.
…oid 11 Android 11's Vulkan loader gives a TextureView's queue a 4.29 s dequeue timeout where it means none, and a canvas presenting on the UI thread then waits there for a buffer only that thread's next draw gives back. On Android 11 a TextureView canvas acquires only once its window has drawn the frame presented last, rendering into the offscreen texture meanwhile, so a frame presented on the UI thread waits behind the window's pending draw there too. The TextureViewPresent spec runs on the UI thread as well.
Device evidence — before / afterUNFIXED — without this PR, the canvas region shows the white stage under the canvas for two frames when it swaps to a FIXED — with this PR the canvas shows a frame in every one. before after — with this PR Galaxy S23 (Android 14, 120 Hz): a full-screen canvas over a white stage that a Device measurement — before / afterBefore — on the S23, 16 of 16 swaps show the stage on This PR — the canvas shows a frame through every swap on both.
Release builds of the example opening the probe screen, this PR's JavaScript over the native code each column names; |
An opaque canvas is a SurfaceView behind its window, and the window opens its hole once the SurfaceView reports its draw finished. WebGPUSurfaceView reported it as soon as surfaceChanged returned, so the hole opened on a surface with no frame and showed black until the first one landed. The view now holds the report until a frame is presented into the surface, as GLSurfaceView holds it until its next frame. attachSurface takes a callback for the surface's first present, and each surface carries a token, so a report for a replaced surface is ignored. Before Android 14 the wait is bounded at a second; from Android 14 the platform bounds it. A report that goes out without a frame, at that second or for a later surface of the view, which SurfaceView counts as reported already, no longer opens the hole: the view punches it, and adds it to the window's transparent region, only while its surface has a frame. A context configured before its surface exists, or left by a lost one, no longer presents an empty frame when the next surface attaches, and detach is one operation for a lost surface and a destroyed view. The canvas docs say when the hole opens, the example gains a SurfaceView First Frame diagnostic that can also draw its first frame late, FirstFrameDrawGate is unit tested, and CI runs the Android library's unit tests.
Before Android 12L a SurfaceView removes its layer as it leaves its window, ahead of the window frame that stops punching its hole, so an opaque canvas leaving the screen can show a black frame where it was. An opaque canvas with no surfaceType now gets a TextureView on those releases, unless it sets zOrderOnTop, which only a SurfaceView offers; surfaceType: "SurfaceView" still selects one. BackingViewKind decides the backing view, as in react-native-skia, and is unit tested. The canvas docs and the surfaceType TSDoc state the default, and the example's opaque canvas diagnostic gains a mounted switch.
…ture-view-before-12l
…eated A SurfaceView's surface goes with its buffers when the app goes to the background or the view leaves its window, and the canvas presented nothing into the next one, so a canvas that draws once, like ImageBlur, came back black. surfaceDestroyed is the last moment that frame can be read: the view reads it back there with PixelCopy and writes it into the canvas's offscreen texture, which the next surface's attach presents. A TextureView replaced by another backing view hands its frame over the same way, through getBitmap. Nothing is copied while the canvas draws. The canvas docs say a canvas that stops drawing keeps its last frame.
# Conflicts: # packages/webgpu/cpp/rnwgpu/SurfaceRegistry.h
ef6ebac to
78beb81
Compare
|
I would like to prevent the backing view from being changed once mounted but let's keep discussing it if needed. |
Same model as react-native-skia: the `opaque` prop was a shortcut to get a SurfaceView on Android, consumed nowhere else on native (the web view only painted a white background from it). It moves under `android` as a compositing hint, applied in place on whichever view backs the canvas, and the view itself is named by `android.backingView` (replacing `surfaceType`), with `TextureView` as the default whatever the opacity. - `android` is a discriminated union: a TextureView takes `opaque` (translucent by default), a SurfaceView takes `opaque` (opaque by default), `zOrderOnTop` and the new `zOrderMediaOverlay`, which composites it above the other SurfaceViews below the window; both z-order options apply in place on Android 11 and later. A HardwareBufferView takes no option: it always composites with alpha, and falls back to a TextureView below Android 10, with a warning in development. - Nothing derives the backing view from another prop any more, so the JS and Java rules collapse to reading `backingView`; the pure rules live in BackingViewKind. - On the web, `alphaMode` alone decides how the canvas composites, as in a browser: the white background `opaque` used to paint is gone. - The docs describe the model, including what a remount through a key costs (a frame or two off screen) and that keeping the canvas on screen through a swap is possible but needs per-release logic (#563). Verified on the Pixel 8 (Android 17) with the Android View Props diagnostic: opaque applies in place on both views, the on-top and media overlay z-orders move a mounted SurfaceView without a remount, and the HardwareBufferView composites with alpha; no warning, error or crash in logcat.


Problem
Changing a mounted canvas's
surfaceType,zOrderOnTopor, with the defaultsurfaceType,opaquereplaces its Android backing view, and the canvas region shows what lies under the canvas for a few frames:TextureViewmakes itsSurfaceTexturein its first draw and takes a frame presented then only in its next draw, so it draws empty once or twice;SurfaceViewshows nothing until its first frame, and fix(🤖): open a SurfaceView canvas's hole with its first frame #561 opens its hole only then, while the outgoingTextureViewis gone with the window frame that removed it;SurfaceView's layer leaves before the window frame that stops punching its hole, so that frame shows black.A full-screen canvas over a white stage, cleared every frame by a
requestAnimationFrameloop, itssurfaceTypeswitched betweenSurfaceViewandTextureVieweight times per round: on a Galaxy S23 (Android 14), 16 of 16 swaps show the stage for a frame or more onmainand 14 of 16 on this PR's base, and on a Mi MIX 3 (Android 10) 14 of 16 show black or a frame of neither the canvas nor the stage on both; with this change none does on either (measurement).These are wcandillon/react-native-skia#4189 and #4192 for this repo.
Solution
The same design as those two PRs:
TextureViewgets its texture before its first draw when it is shown and laid out:WebGPUTextureView.supplySurfaceTexturemakes theSurfaceTexture, andonSurfaceCreateadopts the surface on the main thread right away, which blits the latest frame (fix(🤖): keep the last frame of a canvas whose Android surface is recreated #544 keeps it as the outgoing view goes) and presents it, so the first draw shows it.TextureViewreports noonSurfaceTextureAvailablefor a texture it was given.SurfaceViewkeeps the outgoingTextureViewabove it (OutgoingViewHold): the outgoing view stops reporting to the canvas, keeps the frames presented into it until the new surface is adopted, and leaves once theSurfaceViewhas presented its first frame (fix(🤖): open a SurfaceView canvas's hole with its first frame #561's report) and a window frame drawn after it has committed. The new surface starts from the frame the held view shows as it is created. On Android 13 the hold also waits for the hole, which opens a message after the report there. A new view that shows nothing within fix(🤖): open a SurfaceView canvas's hole with its first frame #561's one-second wait is not waited for, and another swap or the canvas leaving its window removes the held view at once.SurfaceViewis retired rather than removed: it reads its frame back for the canvas and releases its surface before the new view attaches, stays attached, andWebGPUViewstops drawing it, so its layer is hidden with the window frame that draws the new view. It is removed at the next swap, when the canvas leaves its window, or when the window hides.BackingViewKindholds the three decisions. With #543 in the stack,keepFrameleaves a frame deferred until the window draws, on Android 11, where it waits in the offscreen texture: it is newer than the frame the surface shows.canvas.mdxsays the canvas stays on screen through a replacement.Tests
BackingViewKindTestcovers when a new view gets its surface before its first draw, keeps the outgoing one, or retires it, over every kind and release;OutgoingViewHoldTestdrives the hold through the first frame, the hole, the window frame and the one-second wait with a recording host;FirstFrameDrawGateTestcovershasSurface. The gap itself shows only in what the window composites, so the counts come fromscreenrecord.Verification
yarn lint·yarn tsc·yarn turbo run build --filter='./packages/*'· the Expo template's web build ·yarn test:plugin·yarn check-dawn·yarn build:docs·cpplintwith the package's flags ·clang-formaton the changed lines ·./gradlew :react-native-webgpu:testDebugUnitTest(47 tests) · the example's iOS build for the simulator SDK. CI's Android e2e step (debug build, Metro withCI=true) on an Android 15 emulator: 221 of 223,main8f664fca's 219 and the two cases #542 and #543 add toTextureViewPresent; the two failures, the YUV cases ofExternalTextureColorSpaceandNativeBufferWrapper, fail onmainthere too. Not run here: the iOS e2e job, which needs a simulator, the SwiftPM example's build, and the semantic-release dry run, which needs the repository's token.Known limits
On Android 10 a retired
SurfaceViewkeeps its last buffer in the compositor, one canvas-sized buffer, until the canvas swaps again, leaves its window or its window hides: removing it any earlier is what shows black. Android 8 and 9 still remove the outgoingSurfaceViewat once, since they hide nothing for a view the window stops drawing. A swap made before the canvas is laid out keeps the lazy path: there is no size to give a texture.This builds on #544 and #562, which carries #561, #542 and #543, and shows their commits until they merge.