Repository navigation
fix(🤖): keep the last frame of a canvas whose Android surface is recreated - #544
YevheniiKotyrlo wants to merge 2 commits into
Conversation
Device measurement — before / afterThis PR's base, #561 — a This PR — it comes back showing its frame.
The colour at the canvas's centre, read in a The read-back runs once, as the surface goes, and nothing runs per frame: Device evidence — before / after, before the rebase onto #574
Galaxy S23 (Android 14), ImageBlur opened and left to draw, then Home, then back to the app; each screenshot is 4 s after the return. Both builds run the same JavaScript; only the native code differs. On that head ( |
b9735a8 to
587c530
Compare
587c530 to
c346cfa
Compare
c346cfa to
9dd79b8
Compare
A SurfaceView canvas sits 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.
…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 SurfaceView canvas that draws once 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. Nothing is copied while the canvas draws. The canvas docs say a canvas that stops drawing keeps its last frame.
9dd79b8 to
1876c01
Compare


Problem
A canvas that draws once and stops, like the example's ImageBlur or any scene rendered on demand, comes back black once Android destroys its
SurfaceView's surface, which takes the frame's buffers with it:SurfaceViewreleases its surface whenever its window's own surface is gone, before it reads its surface lifecycle, soSURFACE_LIFECYCLE_FOLLOWS_ATTACHMENTkeeps nothing (SurfaceView.updateSurface, measured on Android 14);removeClippedSubviews(the default forFlatListon Android).A
TextureViewkeeps its frame in the background.The example's SurfaceView First Frame diagnostic draws one frame on a
SurfaceViewcanvas and stops. After the app goes home and comes back through its recents card, the canvas shows the stage behind it on #561: at 3 of 3 returns on a Galaxy S23 (Android 14) and on a Mi MIX 3 (Android 10), and 2 of 2 on an Android 13 emulator. With this change it shows its frame at every return on all three (measurement).Solution
WebGPUSurfaceView.surfaceDestroyedreads the surface's frame back withPixelCopy(SurfaceFrameCapture) before the canvas lets the surface go: Android keeps the surface valid until the callback returns, and it is the last moment the frame exists. The canvas keeps the copy as its offscreen frame (SurfaceInfo::keepFrame), which the next attached surface presents first, unless the canvas draws a newer frame before.keepFramewrites RGBA8 and BGRA8 drawing buffers; a canvas configured with another format comes back with its next frame, as before. The offscreen texture gainsCopyDstfor it.drawElementImageToTexture's bitmap reader and the kept frame share one locked-bitmap class incpp-adapter.cpp.canvas.mdxsays which canvases keep their frame.Tests
No automated test: the e2e harness evaluates JavaScript and reads textures, and the defect is in what a recreated native surface shows. The SurfaceView First Frame diagnostic reproduces it, and so does TransparencyMode's loop-off state (#545): stop the canvas drawing, then send the app to the background.
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(16 tests) · the example's iOS build for the simulator SDK, on9dd79b8d, whose iOS-compiled files this head keeps. CI's Android e2e step (debug build, Metro withCI=true) on an Android 15 emulator: 220 of 222, as onmainc080934e; the two failures, the YUV cases ofExternalTextureColorSpaceandNativeBufferWrapper, fail onmainthere too. On a Galaxy S23 (Android 14) the whole suite passes, 222 of 222;mainc080934epasses 222 of 222. 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), it passed 221 of 221 before the rebase onto #574 (main8f664fca: 221 of 221). Not run here: the SwiftPM example's build and the semantic-release dry run, which needs the repository's token.Base
Stacked on #561, whose single
detachno longer presents an empty texture on the next surface; this shows its commit until that merges.