Skip to content

OffscreenCanvas - #175

Merged
triniwiz merged 7 commits into
masterfrom
feat/offscreen-canvas-threaded
Sep 30, 2026
Merged

triniwiz merged 7 commits into
masterfrom
feat/offscreen-canvas-threaded

Conversation

@triniwiz

@triniwiz triniwiz commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds OffscreenCanvas, plus fixes found while testing it on iOS and Android.

OffscreenCanvas

  • A canvas with no view. It draws through the same native contexts as Canvas: on a detached canvas made at the first getContext(), or, after canvas.transferControlToOffscreen(), on that canvas's own surface so its frames show in the view.
  • Contexts: 2d, bitmaprenderer, webgl, webgl2, webgpu.
  • transferToImageBitmap() hands over the frame and clears the canvas, as a present would. convertToBlob() encodes it. contextlost/contextrestored are forwarded from the host canvas.
  • Canvas: getContext() moves to CanvasBase, which throws InvalidStateError once the canvas is transferred and otherwise calls the host's _getContext(). transferControlToOffscreen() refuses a canvas that has a context or was already transferred. A transferred canvas's width/height setters throw, and its OffscreenCanvas resizes it through _resizeBitmap().
  • Image sources: drawImage, createPattern, createImageBitmap, texImage2D/texSubImage2D and copyExternalImageToTexture take an OffscreenCanvas where they take a Canvas. isOffscreenCanvas checks the tag, so these modules don't need to import it.
  • Exports: OffscreenCanvas and OffscreenCanvasRenderingContext2D are exported, set on global, and on the polyfill's window. The Canvas typings gain transferControlToOffscreen() and threadedWebGL.
  • It still can't move to a Worker: the contexts belong to the UI thread.

Fixes

  • V8 string helpers read freed memory. ConvertFromV8StringView/ConvertFromV8StringViewValue returned a std::string_view into a v8::String::Utf8Value destroyed on return. On iOS, __base64Decode/__base64Encode returned '' for any input, and a WebGPU featureLevel option was ignored. They now return std::string.
  • getParameter returns what was bound. FRAMEBUFFER_BINDING (and READ_FRAMEBUFFER_BINDING, PIXEL_PACK_BUFFER_BINDING) came back as a wrapper around the bare GL name, which binds nothing when passed back, and on iOS named the view's framebuffer when none was bound. So the usual save → bind → restore pattern left the drawing buffer bound. The context now keeps the objects it bound, clears them when deleted, and answers without a native round trip.
  • iOS WebGL canvas as an image source. canvas_native_webgl_read_drawing_buffer bound framebuffer 0, but on iOS the drawing buffer is the GLKView's framebuffer. texImage2D/copyExternalImageToTexture from a WebGL canvas read back transparent there.
  • canvas_native.h regenerated.

Testing

  • New spec group offscreen in tools/demo/canvas/spec/offscreen.ts (37 tests: API rules, each context, image-source use, transfer), run with --demo=canvas-spec --suite=offscreen. Also 4 WebGL tests for the binding fix.
  • iOS simulator (iPhone 17 Pro): full spec 375/375.
  • Android (Galaxy A53), group by group: offscreen 37/37, 2d 180/180, webgl 81/81, imagebitmap 18/18, bitmaprenderer 15/15, canvassource 9/9.
    • webgpu.video (2), scaling and lifecycle fail on origin/master too; scaling and lifecycle also vary from run to run.
    • Running every group in one Android process gets killed by lmkd on origin/master too. Native heap grows about 7 MB per WebGL test until GC runs.
  • Not tested: a transferred canvas on screen (the tests use detached canvases), and Windows.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 63632dec-ca0d-4119-8bcc-46c0154f68d2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@triniwiz
triniwiz force-pushed the feat/offscreen-canvas-threaded branch from 94d2e37 to b3ca855 Compare September 30, 2026 14:37
Base automatically changed from fix/android-layout-and-gl-thread to master September 30, 2026 20:56
An OffscreenCanvas draws through the same native contexts as Canvas: on a
detached canvas made at its first getContext(), or, after
canvas.transferControlToOffscreen(), on that canvas's own surface so its
frames show in the view. 2d, bitmaprenderer, webgl, webgl2 and webgpu
contexts; transferToImageBitmap() (clearing the frame, as a present would);
convertToBlob(); contextlost/restored events forwarded from the host.

- Canvas: getContext() moves to CanvasBase, which throws InvalidStateError
  once the canvas is transferred and otherwise calls the host's _getContext().
  transferControlToOffscreen() refuses a canvas with a context or one already
  transferred; a transferred canvas's width/height setters throw, and its
  OffscreenCanvas resizes it through _resizeBitmap().
- drawImage, createPattern, createImageBitmap, texImage2D/texSubImage2D and
  copyExternalImageToTexture take an OffscreenCanvas where they take a Canvas
  (isOffscreenCanvas, checked by tag to keep them free of the module).
- OffscreenCanvas and OffscreenCanvasRenderingContext2D are exported, set on
  global, and on the polyfill's window.
- Canvas typings gain transferControlToOffscreen() and threadedWebGL.

Contexts still belong to the UI thread, so an OffscreenCanvas can't move to a
Worker yet.
ConvertFromV8StringView and ConvertFromV8StringViewValue returned a
std::string_view into a v8::String::Utf8Value that is destroyed when they
return, so callers read freed memory. On iOS it read as empty:
__base64Decode and __base64Encode returned '' for any input, and a WebGPU
featureLevel option was never seen. They now return a std::string.
canvas_native_webgl_read_drawing_buffer bound framebuffer 0, but on iOS the
drawing buffer is the GLKView's framebuffer (binding null binds it), so
texImage2D and copyExternalImageToTexture from a WebGL canvas read back
transparent. It now binds the drawing buffer the way bindFramebuffer(null) does.
… were bound

FRAMEBUFFER_BINDING (and READ_FRAMEBUFFER_BINDING, PIXEL_PACK_BUFFER_BINDING)
came back as a wrapper around the bare GL name, which binds nothing when
passed back, and on iOS named the view's framebuffer when none was bound. So
the usual save / bind / restore pattern silently left the drawing buffer
bound. The context now keeps what it bound, clears it when that object is
deleted, and returns it, without a round trip to the native side.
OffscreenCanvas saves and restores through it.
offscreen.api, .2d, .source, .webgl, .webgpu and .transfer: the constructor and
getContext rules, each context, transferToImageBitmap and convertToBlob,
use as an image source by 2d, WebGL and createImageBitmap, and
transferControlToOffscreen. Run with --suite offscreen.
A WebGPU canvas's toDataURL (and so OffscreenCanvas.transferToImageBitmap
and convertToBlob) encoded the texture's bytes as BGRA on Apple and RGBA
everywhere else, whatever the texture held. Windows canvases are
bgra8unorm, so red and blue came back swapped there, as they would for an
rgba8unorm canvas on iOS. The order now comes from the configured format.
@triniwiz
triniwiz force-pushed the feat/offscreen-canvas-threaded branch from a50f6c8 to 0c9b9b3 Compare September 30, 2026 20:58
@triniwiz
triniwiz merged commit 610bb09 into master Sep 30, 2026
14 of 21 checks passed
@triniwiz
triniwiz deleted the feat/offscreen-canvas-threaded branch September 30, 2026 21:13
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant