fix(svg): Android crash, UI stalls, iOS stale frames, hsl() colours and <style> rules - #164
Merged
Merged
Conversation
nativePointer() returned the SvgDocument* as a double. arm64 Android tags heap pointers in the top byte (0xb4...), which is past Long.MAX_VALUE and past a double's 53 bits, so renderDocument(Long) received a clamped 0x7fff... and the first bitmap render segfaulted. Return it as a decimal string, like the canvas __getPointer bindings, and hand it to Kotlin via long(ptr). iOS pointers still fit a double, so iOS converts with Number().
Opening a page of Svg views froze Android's UI thread for ~425ms (A53) before any animation played: - Detaching a threaded view waited for the shared render thread to get to its removal, which was queued behind a whole GPU context build for every view that had just appeared. The view now hands its window to the render thread, which releases it after tearing the surface down (canvas_native_svg_render_thread_release); the blocking destroy stays for iOS. Removals are taken first and between builds, a view detached before its surface was built never gets one, and a removed view is never presented to or rebuilt against its abandoned window. - Each surface is presented as soon as it is built, instead of after every queued build. - Every SvgDocument made a new FontMgr. Since m122 that is a fresh system font scan per call (rust-skia#976); two per view cost ~225ms of UI thread on this page. It is now made once per thread. Longest UI-thread stall opening the SVG demo: 426ms -> ~20ms.
iOS had the same blocking detach as Android: renderThreadDestroy waited for the shared render thread before releasing the host view. It now uses canvas_native_svg_render_thread_release, and the render thread hands the retained view back through a callback that releases it on the main queue, since UIKit views must not be deallocated elsewhere. The Node-API module's nativePointer returns the same decimal string as the V8 bindings. Rebuilds CanvasSVG.xcframework (iOS, visionOS, tvOS).
|
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 |
draw(_:) only skipped the raster image when the single-threaded GPU context was active. With the threaded renderer (the default) the frame rendered before the Metal host had a size stayed in the layer and showed through the transparent Metal layer under every GPU frame, so a changed src looked like its old and new content drawn over each other. It now skips while either GPU path is active, and asks for a redraw when one comes up so the old contents are cleared, as Android does. Rebuilds CanvasSVG.xcframework.
Skia's SVG parser reads hex, named colours, rgb(r,g,b) and rgba(r,g,b,a) only; anything else fails to parse and the paint draws black. Canvas 2D accepts the rest through csscolorparser, so the same markup drew differently in an svg than on a canvas. Those functions are now rewritten to rgb()/rgba() with csscolorparser: in attribute values and <style> contents when a document is parsed (before SMIL/CSS animation extraction, so animated values interpolate), in setAttribute, and in addStylesheet. Text content is left alone. SMIL frames and other internal writes use set_normalized_attribute, so the rewrite stays off the per-frame path. Rebuilds canvassvg-release.aar and CanvasSVG.xcframework.
Skia's SVG module reads presentation attributes and the style attribute but not <style> rules, so class-styled SVGs (what design tools export) drew with default paint: black fills, no strokes. When a document is parsed, each element's matching rule declarations are folded into its style attribute in cascade order: selector rules over presentation attributes, inline style over rules, !important over both, then specificity and source order. The style attribute is written last, since Skia applies attributes in order. Supported selectors: type, *, #id, .class, attribute selectors, :root, :first-child, :last-child, :only-child, and the descendant, >, + and ~ combinators. Rules with anything else are dropped rather than matching too much. Rules apply at parse time only. Rebuilds canvassvg-release.aar and CanvasSVG.xcframework.
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.
Fixes for
@nativescript/canvas-svg: an Android crash, UI stalls on Android and iOS, an iOS redraw bug, colours that draw black on every platform, and<style>rules that Skia ignores.Crash.
SVGDocumentImpl::NativePointerreturned theSvgDocument*as a double. arm64 Android tags heap pointers in the top byte (0xb4…), which is pastLong.MAX_VALUEand past a double's 53 bits.NSCSVG.renderDocument(Long)received a clamped0x7fff…, and the first bitmap render segfaulted (fault address0x800000000000004f).UI stalls. Opening a page of
Svgviews froze the UI thread before any animation played, for about 425 ms on a Galaxy A53. Three causes:SvgDocumentcreated its ownFontMgr. Since Skia m122,FontMgr::new()scans all system fonts on every call (rust-skia#976). Each view makes two documents, so the SVG demo page ran about 14 font scans on the UI thread.iOS stale frame. With the threaded renderer (the default),
NSCSVG.draw(_:)kept drawing the first raster frame, the one rendered before the Metal host had a size, underneath the transparent Metal layer. Whensrcchanged, the old and new content appeared drawn over each other; in the starter's gauge, "64%" stayed visible under the new value. This predates this PR: the alpha.20 framework shows the same thing.Black colours. Skia's SVG parser reads hex, named colours,
rgb(r,g,b)andrgba(r,g,b,a)only.hsl(),hsla(),hwb()andrgb(r g b / a)fail to parse, and the paint draws black. Canvas 2D accepts all of them throughcsscolorparser, so the same colour drew differently in an svg than on a canvas.<style>rules ignored. Skia's SVG module reads presentation attributes and thestyleattribute, but not<style>rules. Class-styled SVGs, which is what design tools export (.cls-1 { fill: … }), drew with default paint: black fills and no strokes.Changes
nativePointer()returns a decimal string, like the__getPointerbindings in canvas. Android passes it on aslong(ptr); iOS converts it withNumber(), since iOS pointers fit in a double. The Node-API module returns the same string.crates/canvas-svg-c/src/gpu/thread.rs): newcanvas_native_svg_render_thread_release(render, release). The view hands its window to the render thread, which tears the surface down and then callsrelease, so the UI thread doesn't wait.ANativeWindow.canvas_native_svg_render_thread_destroykeeps its blocking behaviour.crates/canvas-svg): oneFontMgrper thread, reused by every document. It's thread-local because skia-safe doesn't implementSend/SyncforFontMgr.NSCSVG.swift):draw(_:)skips the raster image while either GPU path is active, not only the single-threaded one, and the view redraws when a GPU context comes up so the old layer contents are cleared. Android already does both.crates/canvas-svg/src/css_color.rs): those functions are rewritten torgb()/rgba()withcsscolorparser, the parser Canvas 2D uses.<style>contents, before SMIL/CSS animation extraction, so animated colours interpolate. Text content is left alone.setAttributeandaddStylesheet.set_normalized_attribute, which skips the rewrite, so the rewrite never runs per frame. On a Mac,normalizecosts 6–57 ns for values it leaves alone and about 300 ns for anhsl()it rewrites.crates/canvas-svg/src/stylesheet.rs): when a document is parsed, each element's matching rule declarations are folded into itsstyleattribute in cascade order.styleover rules,!importantover both, then specificity and source order.styleattribute is written last, because Skia applies attributes in order.*,#id,.class, attribute selectors,:root,:first-child,:last-child,:only-child, and the descendant,>,+and~combinators. A rule using anything else is dropped rather than risk matching too much.smil/css.rs. Rule values go through the colour rewrite too.classoridat runtime, adding elements, or rules passed toaddStylesheet(still@keyframesonly) don't restyle anything.canvassvg-release.aar(4 ABIs) andCanvasSVG.xcframework(iOS, visionOS, tvOS). Windows is not rebuilt.Testing
Android, Galaxy A53 (Android 16). The starter app opens its SVG demo (6 animated tiles and a gauge) from a home page with 8 SVG icons. Profiled with
simpleperf:iOS, iPhone 17 Pro Max simulator:
The starter app ran 15 scripted open/back cycles of the SVG demo with no crash.
Cycle times matched the scheduled waits to within about 13 ms, so the main thread wasn't blocked.
The demo renders and animates afterwards.
The gauge from the starter stepped through 34 → 71 → 12 → 88 → 53 from script. Before: overlapping digits over the initial "64%", same result with the alpha.20 framework. After: a clean "53%" with the matching arc length.
Colours, both platforms: the starter's gauge, whose gradient stops are
hsl(), drew a black arc on Android and iOS. It now draws its gradient, and the hue follows the value (Android A53, iOS simulator).Stylesheets, both platforms: a test SVG with
.bg,.dot,g > .dot:first-childand#ringrules rendered every rule correctly on the A53 and the iOS simulator.tools/demo/canvas/assets/svg/trinidadAndTobagoHigh.svg, whose paths are styled by a.landclass, went from solid black to its intended grey fill with white borders.Unit tests:
cargo test -p canvas-svg-c --features metal gpu::threadpasses 4 tests, including a new one for non-blocking release.cargo test -p canvas-svgpasses 136 tests, includingtests/stylesheets.rs, which renders rule precedence, specificity, inheritance, a CDATA export and keyframes on a styled element, as well as the colour rewrite's unit tests andtests/css_colors.rs, which rendershsl()fills,style, gradient stops, runtimesetAttribute, SMIL interpolation and@keyframes(in-document andaddStylesheet) and checks the pixels. The Node-API test suite passes when run against a local macOS build, but that is only a smoke check, since macOS isn't a supported Node-API target.Not tested: Windows, visionOS and tvOS were not run. visionOS and tvOS were built only.