Skip to content

feat(🤖): render to a native window with MakeWebGPUCanvasContextFromNativeWindow - #588

Merged
wcandillon merged 6 commits into
wcandillon:mainfrom
mlecoq:feat/native-window-canvas-context
Oct 23, 2026
Merged

wcandillon merged 6 commits into
wcandillon:mainfrom
mlecoq:feat/native-window-canvas-context

Conversation

@mlecoq

@mlecoq mlecoq commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

Closes #583.

Adds RNWebGPU.MakeWebGPUCanvasContextFromNativeWindow(window, width, height) (Android): a canvas context that presents to an ANativeWindow that is not a WebGPU view, e.g. the input Surface of a MediaCodec encoder.

const context = RNWebGPU.MakeWebGPUCanvasContextFromNativeWindow(window, width, height);
context.configure({ device, format: "rgba8unorm" });
// per frame: getCurrentTexture(), render, submit, present()
context.unconfigure(); // releases the swapchain right away
  • The window is passed as a BigInt; the context takes its own reference to it (released with the registry entry).
  • The registry entry gets its id from allocateContextId() and is attached like a view's surface, then adopted by configure(). No view retires it, so the context removes it when it is destroyed (GPUCanvasContext::ownRegistryEntry).
  • Other platforms throw: on Apple, CAMetalLayer could come later the same way.
  • Docs: a "Rendering to a native window" section in the native API guide, including how to give an encoder each frame's presentation time (set it on the window right before present(); no Dawn change needed, see Android: render into an arbitrary Surface (e.g. MediaCodec's input) with a per-frame timestamp #583).

Tested in @azzapp/react-native-skia-video, whose Android export now renders straight into MediaCodec's input surface through this context, with no EGL (Qualcomm device, Android 15): correct frame timing and colors, and the export is about 35% faster when the encoder is the bottleneck. There is no e2e test here: the harness can't create an ANativeWindow from JS.

…tiveWindow

A canvas context can now present to an ANativeWindow that is not a WebGPU
view, e.g. the input Surface of a MediaCodec encoder: an app can render
frames straight into a video encoder, with no EGL context and no copy.
The window is passed as a BigInt and the context takes its own reference
to it. Its registry entry gets an id from allocateContextId and is
retired by the context when it is destroyed, since no view does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsWPRiqsBPtiH9G7qvnLrr
@wcandillon
wcandillon self-requested a review October 22, 2026 10:17

@wcandillon wcandillon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it a lot. This doesn't need to be Android only. The use-case on iOS for it is much more narrower but it could still be handy in the future.

You can add it on the PlatformContext API (it actually might already be there I haven't checked).

I see that you use the SurfaceRegistry. Does it make sense? We could argue here it's your responsibility to handle thee native surface and thread-safety associated with it?

…ept a CAMetalLayer

As suggested in review: the registry bridges the lifecycle of the views,
which a window handed in by the caller doesn't have. The context now owns
a SurfaceInfo of its own (makeUnregisteredSurfaceInfo): no context id, no
registry entry to retire. The SurfaceInfo is still tracked for
releaseSurfacesForDevice, so destroying the device first doesn't hit the
Android Dawn crash. The caller keeps the window alive and drives the
context from one thread at a time; the context still holds a reference to
the window.

On Apple platforms, the window is a CAMetalLayer, retained like MetalView's
(released on the main thread). WebGPURuntime::makeSurface already makes
surfaces for both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsWPRiqsBPtiH9G7qvnLrr
mlecoq pushed a commit to AzzappApp/react-native-skia-video that referenced this pull request Oct 22, 2026
The native window context now owns its surface outside of the
SurfaceRegistry, as reworked in wcandillon/react-native-webgpu#588 after
review (and accepts a CAMetalLayer on Apple platforms). No change on this
side: the JS API is the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsWPRiqsBPtiH9G7qvnLrr
@mlecoq

mlecoq commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, done: not Android-only anymore (a CAMetalLayer works too), and the context now owns its surface outside of the SurfaceRegistry, the caller being responsible for the window and its thread.

On Android, a window keeps the buffers it handed to the swapchain until it
is disconnected, which the Vulkan loader only does when the VkSurfaceKHR is
destroyed: unconfigure() destroyed the swapchain but not the surface, so a
MediaCodec input surface kept its buffers until the context was garbage
collected (about 150 MB per 1080x1920 export in our tests). A context made
from a native window now releases its surface and its reference to the
window on unconfigure(); it no longer presents afterwards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsWPRiqsBPtiH9G7qvnLrr
mlecoq pushed a commit to AzzappApp/react-native-skia-video that referenced this pull request Oct 22, 2026
…xport

The export's canvas context now releases its surface on unconfigure()
(React Native WebGPU patch, as in wcandillon/react-native-webgpu#588): on
Android, the encoder's input window keeps the buffers it handed to the
swapchain until the Vulkan surface is destroyed, which otherwise only
happened when the export runtime collected the context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GsWPRiqsBPtiH9G7qvnLrr
@mlecoq
mlecoq requested a review from wcandillon October 22, 2026 12:54

@wcandillon wcandillon left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

look good, added a small change request

Comment thread packages/webgpu/cpp/rnwgpu/api/RNWebGPU.h
MakeWebGPUCanvasContextFromNativeWindow branched on __ANDROID__ in the
shared RNWebGPU.h to retain the window and build its releaser. Move that
into PlatformContext::retainNativeWindow, next to wrapNativeBuffer, so the
platform specifics live in the Android and Apple platform contexts.
unconfigure() on a context made from a native window detached its surface
for good: a later configure() succeeded but rendered offscreen, with no
error, which breaks the web's unconfigure/configure pattern. Handing the
window its buffers back only takes destroying the Dawn surface, not
dropping the window. SurfaceInfo now has a releaseSurfaceOnUnconfigure
mode in which unconfigure() destroys the surface and configure() makes a
new one for the same window (recreateSurfaceLocked, now on both platforms
through a static WebGPURuntime::makeSurface). The window is retained until
the SurfaceInfo is destroyed, like a view's, and GPUCanvasContext loses
its _ownsSurface flag.

MakeWebGPUCanvasContextFromNativeWindow rejects a non-finite or
non-positive size and a window Dawn cannot make a surface for (there is
no view to fall back to), and retains the window last, so nothing can
throw between the retain and the SurfaceInfo adopting the reference. The
registry's two SurfaceInfo creation paths share makeSurfaceInfoLocked.

Docs: the example uses getPreferredCanvasFormat() (a CAMetalLayer wants
bgra8unorm) and says what the context keeps alive (the window) and what
the caller does (whatever it belongs to).
@wcandillon
wcandillon merged commit 1661f37 into wcandillon:main Oct 23, 2026
5 checks passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.19.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android: render into an arbitrary Surface (e.g. MediaCodec's input) with a per-frame timestamp

3 participants