Repository navigation
Conversation
…the Choreographer frame A render loop driven from the main thread (Reanimated frame callbacks) presents in the Choreographer frame's animation phase, before its traversal. When a traversal is already scheduled, HWUI latches the new buffer in that frame and the SurfaceTexture frame-available callback does not invalidate the TextureView, so the next vsync has no traversal and its buffer is replaced before it is drawn: a loop presenting every vsync was shown at every other one (60 fps on a 120 Hz screen). For a TextureView's surface, presentFrame() on the main thread now marks the present pending and an eventfd on the main ALooper runs it right after the current frame. Anything that touches the surface first (getCurrentTexture, configure, unconfigure, attach, detach) flushes it, and recreating the surface for a destroyed device drops it. WebGPUView passes the backing kind explicitly; SurfaceViews and presents from other threads are unchanged.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
On Android with the TextureView backing, a canvas rendered from a main-thread loop (Reanimated
useFrameCallback/requestAnimationFrameon the UI runtime) presents during the Choreographer frame's animation phase, before that frame's traversal. If a traversal is already scheduled, HWUI latches the new buffer in that same frame, and the SurfaceTexture frame-available callback (which sets TextureView'smUpdateLayerand invalidates) does not fire for it. So the next vsync has no traversal, and that frame's buffer is replaced by the following one before it is drawn. A loop presenting every vsync is shown at every other vsync: 60 fps on a 120 Hz screen. This PR presents those frames right after the Choreographer frame instead.The rest was written with a coding agent.
Evidence
Perfetto trace on a Solana Seeker (Android 16, Mali, 120 Hz, Release build of our app): the app's
QueuePresentKHRran 120 times a second, but RenderThreadDrawingran 60 times a second.SurfaceTexture$1(frame available) appeared only after presents that were not consumed in the same frame. react-native-skia's TextureView canvases, which present from a separate PresentThread (usually after the traversal), did not show the halving.Fix
SurfaceRegistry.h:MainLooperPresentsregisters an eventfd on the mainALooper(ALooper_addFd), so deferred presents run right after the current Choreographer frame. For a surface that defers,SurfaceInfo::presentFrame()on the main thread marks_presentPendinginstead of calling_surface.Present()and wakes the looper.presentDeferredLocked()flushes the pending present before anything else touches the surface: the nextgetCurrentTexture(),configureSurfaceLocked(),unconfigure(),detach(), and the surface swap inapplyPendingAttach(). TheapplyPendingAttach()flush sits after its early return, becausepresentFrame()itself callsapplyPendingAttach()and a flush at its top would undo the deferral.recreateSurfaceLocked()(fromreleaseSurfaceForDevice(), theDAWN_WORKAROUND_DEVICE_DESTROY_BEFORE_SURFACE_RELEASEpath) drops a pending present, like it drops_acquiredFromSurface: the replacement surface is unconfigured.~SurfaceInfocancels its pending entry. The cancel takes the same lock the looper callback holds while it presents, so a surface can't be freed in the middle of its deferred present.WebGPUView.surfaceCreatedpassesdeferPresent = mKind == BackingViewKind.TEXTURE_VIEWthroughonSurfaceCreate, andcpp-adapter.cppcallsinfo->setDeferPresent(deferPresent).MakeWebGPUCanvasContextFromNativeWindowsurfaces, iOS, and presents from any thread other than the main one (the JS thread, dedicated worklet runtimes).Trade-off
A deferred frame is shown one vsync later. That is the latency any TextureView producer that presents after the traversal already pays, Skia's PresentThread included. A SurfaceView, which composites outside the view hierarchy, keeps presenting immediately.
Measurements
These numbers come from our app (fiddl) on react-native-webgpu 0.21.0 with the same change applied as a patch, not from this branch. Seeker, Release build, TextureView, the same flick script before and after, from
dumpsys SurfaceFlinger --latency:A canvas paced to 60 fps on the 120 Hz screen went from a p90 of 24.9 ms to 16.7 ms.
What I ran on this branch
yarn build:android(example app, every ABI) and an arm64assembleRelease. Both pass, with no warnings from the changed files.yarn cpplintflags (as in CI) and clang-format on the changed C++ files are clean.Reproducing
The example's Reanimated → UI Thread screen is a reproducer: it renders with
requestAnimationFrameon the UI runtime into a default (TextureView)Canvas.app.jsonlists onlydist/resfor Android, so bundle todist/main.android.jsbundleand add it toresources.android. The demo alsoconsole.logs every frame.adb shell dumpsys SurfaceFlinger --list | grep com.reactnativewebgpu.example, then runadb shell dumpsys SurfaceFlinger --latency '<full layer name>'(quote the full name, hash prefix included).main, consecutive present timestamps sit about 16.7 ms apart. With this branch they sit about 8.3 ms apart. SurfaceFlinger keeps only the last ~127 frames, so sample while the demo is running.