Android: threaded WebGL, 2D render-thread backpressure, % sizes under MasonKit - #174
Merged
Merged
Conversation
AshGraphics::drop destroyed the surface, device and instance but left the
swapchain, its image views and the semaphores. A leaked swapchain keeps the
ANativeWindow connected to Vulkan, so a GL fallback on the same window
failed with EGL_BAD_ALLOC ("already connected to another API"), and a new
Vulkan surface on it failed too. device_wait_idle no longer panics in drop
after a device loss.
A canvas's default width/height is 100%, which its layout params can't express, so under a MasonKit parent the canvas was measured to its surface while the surface followed the layout: three.js canvases stuck at 1px and shader canvases grew to the page's height. The Windows host already handed % sizes to the parent's _setChildPercentSize; Android and iOS now do the same, through a shared setParentPercentSize that also clears a % the size no longer has.
With EGL_IMG_context_priority, the UI's own rendering is scheduled ahead of a WebGL scene that saturates the GPU. 2D canvas contexts keep the default.
WebGL calls ran on the JS thread, which on Android is the UI thread, and the
per-frame present (eglSwapBuffers) blocked it whenever the GPU fell behind:
with a heavy scene on screen the UI dropped to a few frames a second.
A threaded context (the default, getContext(..., { threaded: false }) opts
out) is created, used, presented and dropped on one shared "nsc-webgl"
thread, like threaded 2D:
- The canvas-c FFI reaches the context through WebGLState::post/sync. Calls
that return nothing are queued, copying any data they borrow; calls that
return a value, read into the caller's memory or read another canvas run on
the thread while the caller waits. An unthreaded context (iOS, offscreen)
runs both inline, as before, so the V8 bindings are unchanged.
- The present is queued. A context with a frame queued behind an unpresented
one counts as behind, and requestAnimationFrame is held back a frame while
any is, rather than queueing more work; a context also waits once it has
three presents queued.
- Surface updates from the view's callbacks are queued (blocking them can
deadlock the buffer queue); detaching from a window waits, as the window
goes away on return.
- Extensions, Bitmap uploads, snapshots, 2D drawImage/createPattern and
WebGPU copies from a WebGL canvas go through the context's thread, and the
Java GL helpers for video run there via Utils.nativeRunWithWebGL.
- Context creation waits for the thread, so the init natives are no longer
@fastNative.
A threaded 2D canvas's commits merge into the frame the render thread hasn't picked up yet, and a frame only replaces the pending one if it clears the whole canvas. A canvas that fades rather than clears (trails) therefore grew its pending frame every vsync once the render thread fell behind; each replay got slower, so it fell further behind, until the GPU memory it used got the app killed. A canvas with a frame merged is now behind, which holds requestAnimationFrame back (the counter WebGL uses, now canvas_native_canvases_behind), and commit waits for the render thread once four frames have merged.
… a channel Without MSAA, Skia rasterized every antialiased path's coverage on the render thread and uploaded it each frame (glTexSubImage2D), which kept a busy canvas from reaching the display rate. The offscreen is now 4x multisampled where the GPU supports it. Opaque canvases used RGB565, which the web never does: a translucent fill can't converge on its colour at 5-6 bits, so faded trails left a permanent tint. Opaque surfaces are now RGBA8888 marked opaque.
Letting go of the window waits for the WebGL thread; presents queued for a window that is going away no longer each wait on the GPU first.
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
RGBA8888 marked opaque still stored the alpha drawn into it, so an alpha:false canvas read back transparent where nothing opaque had been drawn (a bitmaprenderer compositing onto black), where RGB565 had read opaque. RGB888x keeps 8 bits a colour and reads alpha as 255; a wrapped opaque framebuffer is declared RGB8, which is what its config has and what Skia pairs with RGB888x.
Merged
The requestAnimationFrame hold-back checked the count through a JNI call every frame. The count is now exposed to JS as an ArrayBuffer over the native counter, so the check is a memory read. It no longer depends on Android, so the iOS host holds back threaded 2D frames too. Regenerates canvas_native.h for the new export.
This was referenced Sep 30, 2026
triniwiz
added a commit
that referenced
this pull request
Sep 30, 2026
* fix(canvas-core): require glutin 0.32.3 The Android GL context asks for a low priority with ContextAttributesBuilder::with_priority, which glutin 0.32.0 does not have. A lockfile still on 0.32.0 fails to build canvas-core. * chore: rebuild Android and Apple native libraries canvas-release.aar and CanvasNative.xcframework, canvassvg-release.aar and CanvasSVG.xcframework, rebuilt from f5424ba with the steps of build-native.yml, for #174, #175, #176 and #177. canvas_native.h catches up with declarations already in the source. * chore: 3.0.0-beta.0
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.
Summary
Fixes a set of Android problems found profiling the canvas starter on a Galaxy A53 (Mali-G68): pages that stalled the UI, a three.js canvas that rendered into 1 px, and a 2D canvas that lagged and could leak several GB of GPU memory until the app was killed.
Layout
%sizes go to MasonKit on Android and iOS (setParentPercentSizeinCanvas/utils.ts), as the Windows host already did. A canvas defaults to100%, which its layout params can't express, so under a MasonKit parent it was measured at its surface size while the surface followed its layout. The result: three.js canvases stuck at 1 px, and shader canvases that grew to the page's height and pushed their controls out of tap range. Needs MasonKit1.0.0-beta.106(Mason.setPercentWidth/Height).WebGL on its own thread (Android, default on)
eglSwapBuffersran on the JS thread, which on Android is the UI thread. It blocked whenever the GPU fell behind: the dashboard dropped to about 5 UI fps.nsc-webglthread. Thecanvas-cFFI reaches it throughWebGLState::post(queued; borrowed data copied) andWebGLState::sync(anything that returns a value or reads JS memory or another canvas). Unthreaded contexts (iOS, offscreen) still run inline, so the V8 bindings are unchanged.getContext(..., { threaded: false })opts out.drawImage/createPatternand WebGPU copies from a WebGL canvas go through the context's thread. The Java video helpers run there viaUtils.nativeRunWithWebGL.EGL_IMG_context_priority), so UI rendering is scheduled first.Pacing
requestAnimationFrameholds its callbacks back a frame, as a browser does when its compositor falls behind. It readsUtils.canvasesBehind(), and the hold-back is installed with the first threaded context.lmkdkilled the app. It now counts as behind, andcommitwaits once 4 frames have merged.2D rendering (Android)
Vulkan
AshGraphics::dropnow destroys the swapchain, image views and semaphores. A leaked swapchain kept the window connected, so canvas-svg's fallback to GL failed withEGL_BAD_ALLOC.Results (Galaxy A53, canvas starter)
Testing
-PcanvasAbis=arm64-v8a). Other ABIs not built.make aarch64-apple-iosbuilds; not run on a device. iOS keeps the unthreaded path.tsc --noEmit -p packages/canvasclean.