Repository navigation
fix(🤖): back an opaque canvas with a TextureView before Android 12L - #562
YevheniiKotyrlo wants to merge 7 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 — on FIXED — with this PR the canvas is a
Mi MIX 3 (Android 10), debug build of the example app, the opaque canvas diagnostic: three consecutive Device measurement — before / afterBefore — on the Mi MIX 3 (Android 10), 8 of 28 unmounts leave a black frame where the canvas was on This PR — no departure leaves a black frame on either.
Release builds of the example opening the diagnostic, this PR's JavaScript over the native code each row names and over this PR's; two 58 s Android 12 and 12L come from a build of the same change on an earlier Cost under load
Release builds of the example opening Particles, on |
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
f1c1733 to
ca5beae
Compare
|
Closing: #574 makes the default a |


Problem
An opaque canvas with no
android.surfaceTypeis aSurfaceViewbehind the window. Before Android 12L aSurfaceViewremoves its layer as it leaves its window, ahead of the window frame that stops punching its hole, so an opaque canvas leaving the screen — a conditional render, a navigation — can show a black frame where it was. Android 8 to 10 remove the layer inSurfaceView.onDetachedFromWindow(android-10.0.0_r47); Android 11 and 12 hand the removal to the render thread'spositionLost, which runs after the detach has cleared the view's root and so applies it at once too; Android 12L applies it with the window's next draw (android-12.1.0_r27).The example's opaque canvas diagnostic draws a red canvas over a blue stage. On a Mi MIX 3 (Android 10), 8 of 28 unmounts leave a black frame on
mainand none of 27 with this change; on an Android 11 emulator, 29 of 43 remounts do with #561 alone and none of 43 with this change (measurement).Solution
BackingViewKinddecides the backing view, as in react-native-skia:fromSurfaceTypefor a namedsurfaceType(HardwareBufferViewstill needs Android 10) andforAutoSurfaceTypefor"auto". Before Android 12L an opaque canvas gets aTextureView, unless it setszOrderOnTop, which only aSurfaceViewoffers; from 12L it keeps theSurfaceView.surfaceType: "SurfaceView"still selects one on every release.WebGPUViewkeeps the kind as the enum rather than three integers, and the two decisions fix(🤖): show every frame a worklet presents on a TextureView canvas #542 and fix(🤖): stop a TextureView canvas from freezing the UI thread on Android 11 #543 make for aTextureViewcanvas, presenting behind the window's pending draw and, on Android 11, acquiring a frame only after the window draws, areBackingViewKind's too.surfaceTypeTSDoc state the default, and the example's diagnostic gains amountedswitch.This is wcandillon/react-native-skia#4194 for this repo.
A
TextureViewcomposites through the window with one more copy. On the Mi MIX 3 (Android 10) it is still the faster path under load: with 50,000 particles on screen it showed 1179–1191 frames in each of four 20 s windows, where aSurfaceViewon the same native code showed 777–815, and the window's render thread spent 19.2–20.6% of a core compositing it. On a Galaxy S23 (Android 14) both show 120 frames a second and theTextureViewcosts the render thread 22.8–23.6% of a core, which is why 12L and later keep theSurfaceView(measurement).Tests
BackingViewKindTestcovers the decision across Android 8 to 14: a translucent canvas is aTextureView, an opaque one aTextureViewbefore 12L and aSurfaceViewfrom it,zOrderOnTopkeeps theSurfaceView, a namedsurfaceTypeis honoured on every release,HardwareBufferViewfalls back below Android 10, and only aTextureViewpresents behind the window's pending draw, and on Android 11 alone acquires after the window draws.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(25 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. On devices the whole suite passes, 223 of 223: on a Galaxy S23 (Android 14), and on an iPhone 12 Pro Max (iOS 27.0.1) built and run as CI's iOS job does on a simulator (Debug build, Metro withCI=true).mainpasses 221 of 221 on both. Not run here: the SwiftPM example's build and the semantic-release dry run, which needs the repository's token.Known limits
A canvas that names a
SurfaceViewstill leaves black on Android 8 to 12; one that setszOrderOnTopis above the window and punches no hole to leave. On Android 11 and 12 an opaque canvas no longer casts the frame-rate vote, which only aSurfaceViewcasts;surfaceType: "SurfaceView"keeps it.This builds on #561, whose JVM test tier and diagnostic it extends, and on #543, which includes #542: before 12L an opaque canvas becomes a
TextureView, which onmainshows every other frame a worklet presents (#542) and on Android 11 freezes the UI thread for 4.3 s at a time (#543). It shows their commits until they merge.