Skip to content

iOS: threaded WebGL on a CAEAGLLayer, deleteProgram fix - #176

Merged
triniwiz merged 2 commits into
masterfrom
feat/ios-threaded-webgl
Sep 30, 2026
Merged

triniwiz merged 2 commits into
masterfrom
feat/ios-threaded-webgl

Conversation

@triniwiz

@triniwiz triniwiz commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

Summary

Runs iOS WebGL contexts on the shared WebGL thread, as #174 does on Android, by replacing the GLKView with a view backed by a CAEAGLLayer. Also fixes deleteProgram, which deleted a framebuffer instead of the program on every platform.

Threaded WebGL on iOS

  • Why: WebGL drew through a GLKView. Its display, the per-frame present, only runs on the main thread, which is also the JS thread, so a heavy scene held the UI to the GPU's pace.
  • Drawing buffer: the context now owns it. It is a framebuffer whose color buffer is stored in the view's layer (renderbufferStorage:fromDrawable:) and presented with presentRenderbuffer:. Both work from any thread. Depth/stencil is a packed renderbuffer taken from the context attributes. An offscreen context gets plain renderbuffers instead of a GLKView that is never shown.
  • Threading: a context is threaded by default, as on Android; getContext(..., { threaded: false }) opts out. It is created, drawn, presented and dropped on the nsc-webgl thread, and requestAnimationFrame is held back while a canvas is behind (Canvas.threadedWebGL, NSCCanvas.threadedWebGL).
  • Resize: reallocates the drawing buffer, queued behind earlier calls. A GL-backed 2D context (forceGL) reallocates its own as it resizes.
  • On the context's thread: snapshots (toDataURL and friends), the texImage helpers, and video frame uploads. NSCRender gains drawFrame/drawFrameTexImage3D/drawFrameTexSubImage3D overloads that take the context, and makes its GL objects on first use there. @nativescript/canvas-media passes the context.
  • Discard: depth and stencil are discarded after each present unless preserveDrawingBuffer is set, as GLKView did.
  • Renames: CanvasGLKView becomes CanvasGLView, and NSCCanvas.getGlViewPtr() becomes getGlLayerPtr().

deleteProgram fix

  • Both deleteProgram bindings (the slow path and the fast-API path) called canvas_native_webgl_delete_framebuffer with the program's name.
  • So programs were never deleted, and a framebuffer sharing the program's name was deleted in its place.
  • On iOS that can be the canvas's own framebuffer: three.js disposing its PMREM programs left the canvas drawing into nothing, shown as solid magenta.
  • GLKView hid this by rebinding its framebuffer on every present. The C++ is shared, so Android leaked programs and lost framebuffers the same way.

Testing

  • New spec: deleteProgram deletes the program and nothing else (WebGL 1 and 2). It fails without the fix (the program should be gone: expected false, got true).
  • iOS simulator (iPhone 17 Pro): full spec 377/377. While WebGL runs, a sample shows the nsc-webgl thread busy.
  • Starter app on the simulator: the home page, the raw WebGL shaders (WebGL 1, and WebGL 2 by swapping the context), three.js and the mixed dashboard render correctly.
    • three.js runs at 4 fps there, against 5 fps before this change: the simulator's GL is slow for that scene.
  • Android (Galaxy A53), group by group: webgl 83/83, 2d 180/180, offscreen 37/37, imagebitmap 18/18, bitmaprenderer 15/15, canvassource 9/9.
  • Not tested:
    • frame rates on a real iOS device;
    • video uploads into a threaded context;
    • a forceGL 2D canvas on iOS;
    • tvOS.

@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: 2ff46222-aa6d-400b-9a87-af014d44df43

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 a50f6c8 to 0c9b9b3 Compare September 30, 2026 20:58
@triniwiz
triniwiz force-pushed the feat/ios-threaded-webgl branch from fef3426 to 885ffe1 Compare September 30, 2026 21:02
Base automatically changed from feat/offscreen-canvas-threaded to master September 30, 2026 21:13
Both deleteProgram bindings called canvas_native_webgl_delete_framebuffer,
so programs were never deleted, and a framebuffer sharing the program's
name was deleted in its place. On iOS that is the canvas's own
framebuffer when it and the program both got name 1: three.js disposing
its PMREM programs left the canvas drawing into nothing.

The spec deletes a fresh context's first program and checks it is gone
and the canvas still takes a clear.
iOS WebGL drew through a GLKView, whose display (the per-frame present)
only runs on the main thread, which is the JS thread: a heavy scene held
the UI to the GPU's pace.

The GLKView is replaced by a view backed by a CAEAGLLayer. The context
owns its drawing buffer: a framebuffer whose color buffer is stored in the
layer (renderbufferStorage:fromDrawable:) and presented with
presentRenderbuffer:, both of which run on any thread, plus a packed
depth/stencil buffer from the context attributes. An offscreen context
gets plain renderbuffers instead of an unshown GLKView.

That lets an iOS context be threaded as on Android (the default;
getContext(..., { threaded: false }) opts out): it is created, drawn,
presented and dropped on the shared WebGL thread, and requestAnimationFrame
is held back while a canvas is behind.

- Resizing reallocates the drawing buffer, queued behind earlier calls;
  a GL-backed 2D context reallocates its own as it resizes.
- Snapshots read the drawing buffer on the context's thread.
- The texImage helpers and video frame uploads (NSCRender) run on the
  context's thread; NSCRender makes its GL objects on first use there.
- Depth and stencil are discarded after each present unless
  preserveDrawingBuffer is set, as GLKView did.
@triniwiz
triniwiz force-pushed the feat/ios-threaded-webgl branch from 885ffe1 to 88984e6 Compare September 30, 2026 21:15
@triniwiz
triniwiz merged commit c5921ec into master Sep 30, 2026
13 of 20 checks passed
@triniwiz
triniwiz deleted the feat/ios-threaded-webgl branch September 30, 2026 21:17
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