Repository navigation
fix(🤖): show every frame a worklet presents on a TextureView canvas - #542
YevheniiKotyrlo wants to merge 1 commit into
Conversation
Device measurement — before / after
This PR — every frame, 1202–1213 and 575–586; a
Galaxy S23 (Android 14, API 34, 120 Hz) and Mi MIX 3 (Android 10, API 29, 60 Hz), release builds of the example opening a frame-rate probe screen at launch, the same JavaScript over Before the rebase onto #574 ( Device evidence — before / afterUNFIXED — on FIXED — with this PR, the window shows every frame. before — after — with this PR Galaxy S23, the UI-thread |
bd7e065 to
e57cc26
Compare
e57cc26 to
4844eea
Compare
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.
d60069d to
0f8007b
Compare


Problem
A canvas backed by a
TextureView(the default backing view) and drawn from a worklet on Reanimated's UI thread shows every other frame. The worklet runs in the main thread's Choreographer frame, so each frame is presented before the window draws. TheTextureViewtakes a new buffer only once its frame-available listener has run (TextureView.applyUpdate()), which is after that draw, and the next frame, presented ahead of the next draw, overwrites the buffer it has not taken (BufferQueueProducer::queueBuffer). A frame presented from the JS thread lands between the main thread's frames and is not affected.On a Galaxy S23 (Android 14, 120 Hz), a full-screen
TextureViewcanvas redrawn on the UI thread at 120 fps composites 601–603 frames per window of about 10 s onmainand 1202–1213 with this PR; on a Mi MIX 3 (Android 10, 60 Hz), 299 and 575–586 (measurement and recordings).This is wcandillon/react-native-skia#4129's defect for this repo.
Solution
onSurfaceCreategives aTextureView's surface a present poster. WhenSurfaceInfo::presentFrame()runs on the main thread for such a surface, it keeps the frame and hasWebGPUViewpostpresentPostedFrame()to the main looper: a plain message, which the window's traversal barrier holds until the window has drawn, as Skia'sFrameSchedulerpresents aTextureViewframe. One post is outstanding at a time. The nextgetCurrentTexture(),configure(),reconfigure()orunconfigure()presents a frame still waiting first, so no texture is acquired over one not yet presented, and a detach or a new surface drops it with its surface. Frames presented on another thread, or to aSurfaceVieworHardwareBufferView, 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. #543, stacked on this, holds the present there too, behind its acquire gate.WeakViewCallnow answers whether the view took the call: the posted present goes through it beside the frame-ready wake-up and the animation report, and a frame whose view is gone is presented on the spot.canvas.mdxsays when a frame presented on the UI thread reaches aTextureView.Tests
TextureViewPresent.spec.tspresents five colors on aTextureViewcanvas of the tests screen from a worklet on the UI thread, the first two in the same frame, and checks withdrawElementImageToTexturethat the canvas shows the last one two frames later. With the posted present removed it shows the fourth. The half rate itself shows only in what the window composites, so the numbers above come fromdumpsysandscreenrecord.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 · the example's iOS build for the simulator SDK, ond60069d4, whose iOS-compiled files this head keeps. CI's Android e2e step (debug build, Metro withCI=true) on an Android 15 emulator: 221 of 223,main's 220 and the new spec; the two failures, the YUV cases ofExternalTextureColorSpaceandNativeBufferWrapper, fail onmainthere too.On a Galaxy S23 (Android 14) the whole suite passes, 223 of 223;
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 222 of 222 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. Recordings in this comment.