Skip to content

feat(svg): real-DOM SVG with SMIL, GPU rendering and shared sources - #153

Merged
triniwiz merged 5 commits into
v3-v8from
feat/svg-dom
Sep 24, 2026
Merged

triniwiz merged 5 commits into
v3-v8from
feat/svg-dom

Conversation

@triniwiz

@triniwiz triniwiz commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Reworks @nativescript/canvas-svg from a render-a-string component into a live SVG DOM: real element objects backed by native nodes, animated with SMIL, and drawn on the GPU.

What's in it

Native (crates/canvas-svg, new crates/canvas-svg-c)

  • SvgDocument: a mutable DOM (nodes, attributes, frame scheduling). Mutations coalesce into one redraw per frame.
  • Animation engine. Skia has none of this, so it lives here:
    • SMIL: <animate>, <animateTransform>, <animateMotion>, <set>
    • CSS: @keyframes from <style> blocks, via the animation shorthand or longhands. These become the same animation records SMIL uses.
    • Frames where nothing moved skip rasterizing.
  • C ABI crate shared by the Android and Apple bindings.
  • Host tests under crates/canvas-svg/tests.

Rendering

  • Android: GL and Vulkan through TextureView (default) or SurfaceView hosts. The bitmap path stays as the fallback.
  • iOS / tvOS / visionOS: Metal through SVGMetalView, with the CPU path as the fallback.
  • threaded (on by default) records on the UI thread and rasterizes on one render thread shared by every threaded view. If the GPU context is lost it gets rebuilt; if rebuilding fails, rendering drops to the CPU path and fires contextLost / contextRestored.
  • Views with the same src share one document, clock and SkPicture.
  • Android: the host view no longer keeps replaying a stale CPU bitmap under the GPU surface (showed as doubled shapes and removed nodes still visible).

JS (packages/canvas-svg, packages/canvas-polyfill)

  • Element classes wrap native nodes (NativeNode.ts). TSpan is new.
  • A JSI module is installed on both platforms.
  • Polyfill: SVGTSpanElement, plus CharacterData / Text fixes so text nodes work inside SVG.

SVG as a canvas image source

  • drawImage, createPattern, drawAtlas, WebGL texImage2D/texSubImage2D/texImage3D, createImageBitmap and WebGPU copyExternalImageToTexture accept an Svg view or polyfill <svg> element. They get the current frame; drawImage rasterizes at the drawn size, and an unchanged frame is reused.
  • ImageAsset.loadSvg / loadSvgSync for markup, paths, URLs and views, with width, height, scale and time options. SVG <img> sources use the same loader.
  • canvas-svg registers itself with canvas on import, so canvas has no dependency on it.
  • ImageAsset.loadFromBytesSync takes a premultiplied flag (JSI change; canvas aar rebuilt, Rust libs unchanged). SVG rasters were premultiplied twice before.

canvas-2d

  • Every draw, and every image upload before a draw, binds the canvas's own GL context. Skia uploads a pattern's image while recording the draw, so with two GL canvases on Android it went into the other context and the pattern came out black or showed another image.
  • A GL resize keeps its Skia context instead of creating a second one on the same EGL context.

canvas-core

  • Vulkan: when a present fails, the error is now reported (PresentStatus) instead of panicking, and the swapchain is rebuilt.
  • Metal: removed the NSAutoreleasePool held on MetalContext / MetalTexture, which was leaking.

Other

  • Bumped the rust-skia fork (b9d2907) and added quick-xml.
  • Packages are now 3.0.0-alpha.12.
  • Rebuilt the prebuilt xcframeworks (iOS, tvOS, xrOS slices) and the Android .aar / .so for all four ABIs. The canvas-svg aar no longer bundles libNativeScript.so.
  • make exports the SDKROOT and deployment targets Xcode uses. Cargo tracks them, so before this every switch between make and the xcframework build recompiled every Apple slice from scratch, std included (about 52 minutes for canvas). A no-change round now takes seconds.
  • Demo: tools/demo/canvas-svg is now a grid of test tiles (shapes, mutation, SMIL, a 200+ element exported animation, a CSS @keyframes export, and shared-src views).

Testing

  • make host tests for canvas-svg
  • Demo app on Android: GL and Vulkan paths checked on device
  • Demo app on iOS: Metal path; tvOS slice checked with simctl spawn
  • SVG image sources: demo tiles 9 and 10 on the iOS simulator (60 FPS, ~0.7 ms/frame for two drawImage(svgView) calls) and a Galaxy A53.
  • Pattern fix on a Galaxy A53: the pattern reads correctly in 8/8 launches plus a 60 s soak; without the fix it is black in 3/3.
  • Shared render thread: one nsc-svg-render thread for the whole demo page on the A53; host tests cover the lifecycle.

Known issue (pre-existing)

On Android GL, in roughly 1 of 5 launches the first image drawImage after clearRect is lost for the whole run. It reproduces on the previous canvas code, without SVG and without render threads, so it is not caused by this PR. Root cause still open.

- canvas-svg crate: live SvgDocument DOM (nodes, attrs, frame scheduling)
  with its own SMIL engine, plus a C ABI crate (canvas-svg-c)
- GPU backends: GL/Vulkan on Android (TextureView/SurfaceView hosts),
  Metal on iOS/tvOS/visionOS, with context-loss recovery and CPU fallback
- Real element classes in @nativescript/canvas-svg backed by native nodes;
  same-src views share one document, clock and SkPicture
- JSI module install on both platforms; tSpan + CharacterData in polyfill
- canvas-core: Vulkan present/rebuild no longer panics on surface loss,
  drop leaked autorelease pools in Metal context
- bump skia fork, packages to 3.0.0-alpha.12
@coderabbitai

coderabbitai Bot commented Sep 23, 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: 81b0d523-6fd1-41f6-a692-63bda6408b23

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

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.

- drawImage/createPattern/drawAtlas, texImage2D/texSubImage2D/texImage3D,
  createImageBitmap and copyExternalImageToTexture accept an Svg view
  (or polyfill <svg> element): the current frame, rasterized at the size
  it lands at for drawImage, reused until the document changes
- ImageAsset.loadSvg/loadSvgSync for markup, paths, URLs and views, with
  width/height/scale/time options; <img> SVG sources use the same loader
- canvas-svg registers the provider on import, so canvas keeps no
  dependency on it
- ImageAsset.loadFromBytesSync takes a premultiplied flag; SVG rasters
  were being premultiplied twice (canvas aar rebuilt for the JSI change)
- drop the dead _svg branches that called the removed getBitmap()
… frames

- the Android aar and jniLibs were built before setText and the current
  frame/SMIL/GPU code, so text mutations threw on Android; rebuilt all
  four ABIs from current source (JSI lib now ships for every ABI) and
  regenerated the cbindgen header
- NSCSVG.onDraw skipped the bitmap only for the single-threaded context,
  and nothing invalidated once a GPU context came up, so Android kept
  replaying an earlier bitmap frame under the GPU one (doubled shapes,
  removed nodes still visible)
- exclude libNativeScript.so from the aar, as canvas does
…ontext use

- one process-wide render thread serves every threaded Svg view (was a
  thread and wakeup per view); threaded is now the default on both
  platforms, as browsers raster off the main thread
- canvas-2d binds its GL context before every draw and before texture
  promotion: Skia uploads a pattern's image while recording the draw, so
  with two GL canvases it landed in the other context (black or wrong
  pattern on Android)
- GL resize keeps its Skia context instead of creating a second one on
  the same EGL context
- SVG snapshots bypass the views' shared frame cache, which only holds
  four sizes
- make exports the SDKROOT and deployment targets Xcode uses: cargo
  tracks them, so every make/Xcode switch rebuilt all Apple slices from
  scratch (~52 min for canvas); a no-change round is now seconds
- canvas and canvas-svg binaries rebuilt for all Android ABIs and Apple
@triniwiz
triniwiz merged commit 9a2832e into v3-v8 Sep 24, 2026
3 of 9 checks passed
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