feat(🎨): add Canvas frame presentation callback#3935
Conversation
Expose the native presentation boundary so applications can avoid racing UI handoffs against frames that have only been recorded or submitted.
|
I have signed the CLA! |
|
Thank You for this. This looks intriguing. Can you give me a sense of the paint points/use-case this solves? This never came up while working with graphic APIs. The only thing I can think of is that in RN Skia Ganesh due to the current architecture, it would be nice indeed to know if the job is "done" on the UI thread. But this will be nicely solved in Graphite. I would really love to understand the use-case better. |
|
Hey William, thanks for taking a look. The concrete use case is coordinating the handoff between a placeholder and Skia-rendered content. For example, we keep a cached low-resolution image visible while a high-resolution image is decoded or composited in the background. Decode completion or even the React state update is too early to hide the placeholder: the updated Canvas still needs to render and present its next frame. Hiding it immediately can expose a blank or stale Canvas for one frame, causing a visible flash. The callback lets us keep the placeholder visible until Skia reaches the presentation point for the updated frame, and only then complete the transition. Let me know if there is a better way of doing this, but this has definitely helped us in our app. |
|
@PradyumnaShome Thank You for the use-case description, do you have a reproducible example that I can test? |
Summary
onFramePresented?: () => voidto<Canvas>and marshal it safely back to the JavaScript threadWhy
React commit and Skia picture recording only show that a frame was prepared. They do not show that the platform presentation path has accepted—or, where the platform exposes it, actually displayed—the frame. Applications that remove placeholders, take measurements, or start captures at the earlier boundary can race the pixels they are waiting for and observe blank or misleading intermediate state.
This callback exposes the strongest presentation boundary available on each backend:
MTLDrawable.addPresentedHandler, and only whenpresentedTime > 0confirms on-screen displayeglSwapBuffers(handed to the compositor, not guaranteed physical scanout)Surface::Present()returns (presentation submission)Callbacks are delivered asynchronously on the JS thread. Failed renders do not emit. Replacing or clearing the prop invalidates native notifications that have not started, while work already queued to JS may still run once.
Attribution
The original idea was authored by Szymon Kapała (@Szymon20000).
Validation
yarn workspace @shopify/react-native-skia test CanvasPresentation --runInBandyarn workspace @shopify/react-native-skia lintyarn workspace @shopify/react-native-skia tscyarn workspace @shopify/react-native-skia buildclang-formatNo React Native codegen output changes are required because the callback uses the existing JSI view-property path rather than adding a codegen native prop.
The docs workspace's standalone
tscwas also attempted, but it currently fails in existingExpoSnackApi.tscode because its TypeScript configuration does not include DOM globals (document/window); this change does not touch that file.