|
|
Donner 0.5.1
Embeddable browser-grade SVG2 engine
|
Status: Draft Author: Claude Opus 4.6 (with SpecBot, PerfBot, BazelBot) Created: 2026-04-12
Comprehensive audit of the resvg test suite, rendering pipeline, and build infrastructure to identify actionable GitHub issues. This doc catalogs **~307 skipped tests** (plus ~51 render-only) across the resvg suite, distills them into 24 SVG feature/bug issues, and proposes 14 CI/build optimization issues for reducing CI runtime and binary size.
Key findings:
Source data: donner/svg/renderer/tests/resvg_test_suite.cc (1,539 lines), 307 Params::Skip() entries, 51 Params::RenderOnly() entries, 9 elevated-threshold entries.
These are deprecated SVG 1.1 features. They should remain Params::Skip() permanently.
| Feature | Tests | Spec Status |
|---|---|---|
| enable-background | 17 | Removed in SVG2 §11.7.7 |
| <tref> | 9 | Removed in SVG2 §11.5 |
| kerning attribute | 2 | Deprecated; replaced by font-kerning |
| glyph-orientation-* | 2 | Deprecated; replaced by text-orientation |
These features are already parsed — the rendering path just needs to be connected.
| Field | Value |
|---|---|
| Tests unblocked | 30 (filters/filter-functions/*) |
| Complexity | Small — this is a bug, not a new feature |
| Spec | Filter Effects §8 — Filter Functions |
| Already tracked | B2 in docs/design_docs/0021-resvg_feature_gaps.md |
Current state: PropertyRegistry::ParseFilterFunction() correctly parses all 10 CSS filter functions (blur(), brightness(), contrast(), drop-shadow(), grayscale(), hue-rotate(), invert(), opacity(), saturate(), sepia()). The rendering path only recognizes ElementReference variants (SVG url(#id) form) and ignores the CSS function vector.
Key files: RenderingContext.cc:244, RendererDriver.cc Dependencies: None
| Field | Value |
|---|---|
| Tests unblocked | 20 (structure/transform-origin/*) |
| Complexity | Small–Medium |
| Spec | CSS Transforms 2 §6, SVG2 §5.1 |
| Already tracked | F2 in docs/design_docs/0021-resvg_feature_gaps.md |
Current state: Works in style="" attribute. LayoutSystem.cc computes and applies it correctly. The presentation-attribute parsing path (<rect transform-origin="50% 50%">) does not recognize it. Single-keyword syntax (center) doesn't parse either.
Key files: PropertyRegistry.cc, TransformOrigin.h Dependencies: None
| Field | Value |
|---|---|
| Tests unblocked | 18 (structure/image/*) |
| Complexity | Medium — multiple sub-bugs |
| Spec | SVG2 §5.8, CSS Images 3 §5 |
Embedded data-URL images render but at wrong size. preserveAspectRatio modes need investigation. 5 of 18 tests involve <use> referencing inline <svg> elements with width/height/viewBox combinations.
Partially blocked by: B1 (non-square viewBox + percent resolution) Key files: LayoutSystem.cc, SizedElementComponent.h
| Field | Value |
|---|---|
| Tests unblocked | 15 (structure/switch/* + structure/systemLanguage/*) |
| Complexity | Medium |
| Spec | SVG2 §5.9 — Conditional Processing |
Not implemented — no SVGSwitchElement in ElementType.h. For a standalone renderer, the language should be configurable (default: en). systemLanguage uses BCP 47 prefix matching.
Dependencies: None Key files: New SVGSwitchElement.h/.cc, ElementType.h
| Field | Value |
|---|---|
| Tests unblocked | 14 (text/dominant-baseline/*) |
| Complexity | Small — adding 4 mapped keywords |
| Spec | CSS Inline 3 §4.2 |
9 of 13 keywords work. Missing: before-edge → text-top, after-edge → text-bottom, text-before-edge → text-top, text-after-edge → text-bottom (SVG 1.1 legacy mappings).
Key files: PropertyRegistry.cc:198-215, TextEngine.cc:311-325
| Field | Value |
|---|---|
| Tests unblocked | 12 (painting/context/*) |
| Complexity | Medium–Large |
| Spec | SVG2 §13.3 — Context paint |
Parsed (PaintServer::ContextFill/ContextStroke variants exist). The marker rendering path does not propagate context paint. Heavily used by icon libraries.
Key files: RenderingContext.cc, PaintServer.h
| Field | Value |
|---|---|
| Tests unblocked | 10 (text/alignment-baseline/*) |
| Complexity | Small–Medium |
| Spec | CSS Inline 3 §4.4 |
Missing baseline (the initial value per spec), no-change, reset-size, use-script.
Dependencies: Issue S5 (shared enum expansion) Key files: PropertyRegistry.cc:1522
| Field | Value |
|---|---|
| Tests unblocked | 8 (painting/paint-order/*) |
| Complexity | Medium |
| Spec | SVG2 §13.11 |
Parsed as an unparsed CSS string, not applied at render time. Controls fill/stroke/markers paint order. Commonly used for "stroke behind fill" text effects.
Key files: All three renderers, PropertyRegistry.cc
| Field | Value |
|---|---|
| Tests unblocked | 6 (masking/mask/*) |
| Complexity | Medium |
| Spec | CSS Masking 1 §6.1 |
Property not defined in PropertyRegistry. Sub-issues: mask-type=alpha, mask-type in style, mask color-interpolation in linearRGB, mask-on-self, mask-units edge cases.
Key files: RendererDriver.cc (mask compositing), PropertyRegistry.cc
| Field | Value |
|---|---|
| Tests unblocked | 5 (filters/feMorphology/*) |
| Complexity | Small |
| Spec | Filter Effects 1 §15 |
Core erode/dilate works. Failures on boundary conditions: empty radius, non-numeric radius, zero radius (should be passthrough), negative radius.
Key files: FilterSystem.cc, FilterGraphExecutor.cc:483-497
| Field | Value |
|---|---|
| Tests unblocked | 5 (structure/use/*) |
| Complexity | Medium |
| Spec | SVG2 §5.6 |
Shadow tree instantiation edge cases with <use> referencing inline <svg> elements with various width/height/viewBox combinations.
Dependencies: Issue S3 (shared viewport resolution logic) Key files: SVGUseElement.cc, ShadowTreeSystem.cc
| Field | Value |
|---|---|
| Tests unblocked | 5 (across 3 categories) |
| Complexity | Medium |
For SVGs with non-square viewBox and no explicit width/height, Donner's intrinsic document size comes out wrong. Already tracked as B1 in 0021-resvg_feature_gaps.md.
Key files: LayoutSystem.cc, SizedElementComponent.h
| Field | Value |
|---|---|
| Tests unblocked | 4 (painting/stroke-dasharray/*) |
| Complexity | Small |
| Spec | SVG2 §13.5.6 |
Edge cases: odd-length arrays (must be doubled per spec), zero-length dashes, all-zero sums (treat as solid line).
| Field | Value |
|---|---|
| Tests unblocked | 4 (filters/filter/*) |
| Complexity | Medium |
Skipped tests: content-outside-the-canvas-2, in=BackgroundAlpha, with-mask-on-parent, with-transform-outside-of-canvas.
| Field | Value |
|---|---|
| Tests unblocked | 4 (text/text/*) |
| Complexity | Medium |
Filter bbox, ligatures in mixed fonts, percent values on dx/dy, real text height.
| Field | Value |
|---|---|
| Tests unblocked | 4–7 (masking/clipPath/*) |
| Complexity | Medium–Large |
| Spec | CSS Masking 1 §7.2 |
ClipPath works for shapes but text children are not supported.
Dependencies: Text rendering pipeline
| Field | Value |
|---|---|
| Tests unblocked | 3 (filters/feImage/*) |
| Complexity | Small–Medium |
Subregion with rotation, x/y with protruding subregions. Also feImage with external href (separate from ResourceLoader story).
| Field | Value |
|---|---|
| Tests unblocked | 3 (structure/a/*) |
| Complexity | Small — treat as <g> for rendering |
| Spec | SVG2 §5.4 |
Not implemented. For rendering purposes <a> just needs to be a grouping element.
| Field | Value |
|---|---|
| Tests unblocked | 4 (text/textLength/*) + 3 (text/lengthAdjust/*) = 7 |
| Spec | SVG2 §11.6.2 |
| Field | Value |
|---|---|
| Tests unblocked | 5+ (text/textPath/*) |
| Complexity | Medium–Large per sub-feature |
| Spec | SVG2 §11.7 |
Sub-features: method="stretch", spacing="auto", side="right", path attribute (inline path data), referencing <rect>/<circle>.
| Field | Value |
|---|---|
| Tests unblocked | 5+ (across text/direction, text/unicode-bidi, RTL tests) |
| Complexity | Large — requires Unicode Bidirectional Algorithm |
| Spec | CSS Writing Modes 4 §2, UAX #9 |
Properties registered but ignored. Consider scoping as "basic LTR/RTL" first.
| Field | Value |
|---|---|
| Tests unblocked | 2 (text/text-decoration/*) |
| Spec | CSS Text Decoration 3 |
Basic underline/overline/line-through works. Independent color/style not supported.
| Field | Value |
|---|---|
| Tests unblocked | 2 (text/font-kerning/*) |
| Complexity | Small (for --config=text-full) |
Would toggle HarfBuzz's kern feature.
| Field | Value |
|---|---|
| Tests unblocked | 2 (painting/image-rendering/*) |
| Complexity | Small |
| Spec | CSS Images 3 §5.3 |
Only pixelated → nearest-neighbor is implemented. Missing crisp-edges, smooth/auto.
| Priority | Issues | Tests Unblocked | Cumulative |
|---|---|---|---|
| P0 | S1, S2 | 50 | 50 |
| P1 | S3–S7 | 69 | 119 |
| P2 | S8–S18 | ~51 | ~170 |
| P3 | S19–S24 | ~23 | ~193 |
| Intentional skips | — | 30 | — |
The dependency graph suggests this phasing:
Source data: CI workflow YAMLs, .bazelrc, build_defs/rules.bzl, MODULE.bazel, measured CI run times from run 24295860066.
| Job | Platform | Queue Wait | Actual Work | Total |
|---|---|---|---|---|
| Linux | ubuntu-24.04 | ~0s | ~12 min | ~12 min |
| Linux-Geode | ubuntu-24.04 | ~0s | ~2 min | ~2 min |
| macOS | macos-15 | 55–130 min | ~9 min | 65–140 min |
| CMake (Linux) | ubuntu-24.04 | ~0s | ~10 min | ~10 min |
| CMake (macOS) | macos-15 | varies | ~10 min | varies |
| Coverage | ubuntu-24.04 | ~0s | ~30–45 min | ~30–45 min |
| CodeQL | ubuntu-latest | ~0s | ~10–60 min | varies |
End-to-end CI: 65–191 min, dominated by macOS runner queue wait.
| Field | Value |
|---|---|
| Impact | −60 to −120 min per CI run |
| Effort | 1–2 hours |
The single largest CI bottleneck is macOS runner queue wait (55–130 min), not build/test time. Actual macOS work is only ~9 min.
Options:
Key files: .github/workflows/main.yml
| Field | Value |
|---|---|
| Impact | −30–50% build time on cache-hit runs |
| Effort | 2–4 hours |
No remote cache is configured. Every CI run rebuilds from scratch on cache miss.
Options: GitHub Actions Cache via bazel-contrib/setup-bazel, self-hosted bazel-remote, or Google's free RBE tier for open-source.
Key flags: --remote_cache=<url> --remote_upload_local_results --remote_timeout=300 Key files: .bazelrc, .github/workflows/main.yml
| Field | Value |
|---|---|
| Impact | −20–40% test execution time on Linux |
| Effort | 5 min (one-line change) |
Linux CI artificially limits test parallelism to 2 despite 4-core runners. The test step is 7.8 min — the dominant cost of the Linux job. No tests use local = True or tags = ["exclusive"].
Key files: .github/workflows/main.yml:75,125
| Field | Value |
|---|---|
| Impact | −30–50% compile time for ECS-touching TUs |
| Effort | 2–3 days |
EcsRegistry.h includes the monolithic 95,903-line single-include header. 48 non-test files transitively include it. Donner only uses entt::entity, entt::basic_registry, entt::basic_handle. The modular headers (73 files) are already vendored.
Replace with:
Also create EcsRegistry_fwd.h for headers that only need type names.
Key files: donner/base/EcsRegistry.h, donner/svg/AllSVGElements.h
| Field | Value |
|---|---|
| Impact | Maintainability + tuning |
| Effort | 30 min |
CI-specific flags are scattered across workflow YAML. Centralizing in .bazelrc as --config=ci makes them testable locally.
| Field | Value |
|---|---|
| Impact | 2–3× test speedup for largest suites |
| Effort | 30 min (one-line change per target) |
Only resvg_test_suite_impl has shard_count = 16. All other tests run single-threaded.
| Target | Test Cases | Recommended Shards |
|---|---|---|
| base_tests | ~445 | 10 |
| svg_tests | ~417 | 10 |
| parser_tests | ~176 | 5 |
| core_tests | ~140 | 4 |
| css_tests | ~104 | 3 |
GTest supports sharding natively via GTEST_SHARD_INDEX/GTEST_TOTAL_SHARDS which Bazel sets automatically.
| Field | Value |
|---|---|
| Impact | Meaningful coverage per variant + text flag bug fix |
| Effort | 3–4 hours |
Bug found: The _multi_transition in rules.bzl:201 sets text_full but never sets //donner/svg/renderer:text. Since text defaults to false, all 6 transitioned variants run without text. The *_text and *_text_full suffixes produce identical binaries. The "6 variants" are really 3 (one per backend, all without text).
Also: The transitioned variants don't propagate shard_count, so each runs as a single enormous test process instead of 16 shards.
Proposed fix: Replace the 3×2 combinatorial matrix with 4 named product tiers that map to actual Donner build configurations:
| Tier | Backend | Filters | Text | Bazel config | CI trigger |
|---|---|---|---|---|---|
| donner-tiny | tiny-skia | no | no | --config=tiny | All PRs |
| donner (default) | tiny-skia | yes | simple (stb) | --config=text | All PRs |
| donner-max | tiny-skia | yes | full (FT+HB+WOFF2) | --config=text-full | All PRs |
| legacy-skia-ref | skia | yes | full | removed full-Skia config + --config=text-full | main only |
Changes needed:
Key files: build_defs/rules.bzl, donner/svg/renderer/tests/BUILD.bazel, .bazelrc, .github/workflows/main.yml
| Field | Value |
|---|---|
| Impact | Eliminates ~30 min job from every PR |
| Effort | 10 min |
Coverage runs on every push to every branch. Uses --nocache_test_results, full LLVM instrumentation (2× overhead), and requires JDK 11. Change trigger to push: branches: [main] + daily cron.
| Field | Value |
|---|---|
| Impact | −10–20% binary size |
| Effort | 15 min |
No LTO configured anywhere. No -ffunction-sections -fdata-sections + -Wl,--gc-sections for Donner's own code (only for Skia's internal build).
Add to .bazelrc:
| Field | Value |
|---|---|
| Impact | −10–20% incremental compile time |
| Effort | 3–5 days |
Donner has zero *_fwd.h files. Every header that references Entity, Registry, or component types pulls the full definition chain. ~20 of 48 ECS includers only pass handles by value/reference and could use a lightweight forward declaration.
| Field | Value |
|---|---|
| Impact | ~30% faster incremental builds |
| Effort | 1–2 weeks |
svg_core has 60 .cc files and 60 headers — shapes, filters, text, gradients, resources all in one target. Every consumer recompiles and links everything.
Proposed split:
| Target | Contents (~files) | Rationale |
|---|---|---|
| svg_core_elements | Foundation types (~10) | Everything needs these |
| svg_core_shapes | Shape elements (~16) | Self-contained |
| svg_core_filters | FE* elements (~27) | Behind feature flag |
| svg_core_resources | Paint/resource elements (~15) | Paint servers |
| Field | Value |
|---|---|
| Impact | Avoid redundant coverage rebuilds |
| Effort | 5 min |
Coverage uses bazelcov- prefix and hashes non-existent WORKSPACE* files. Never hits the main build cache.
| Field | Value |
|---|---|
| Impact | Higher cache hit rate |
| Effort | 15 min |
Current key includes .bazelrc — any comment change invalidates the entire cache. Split: use MODULE.bazel + .bazelversion for primary key, .bazelrc only in restore-key prefix.
| Field | Value |
|---|---|
| Impact | Better incremental compile times |
| Effort | 3–5 days |
The four largest .cc files are all renderers: the archived full-Skia renderer source (4,245 lines), RendererTinySkia.cc (2,254), RendererDriver.cc (2,072), RendererGeode.cc (1,896).
Split by concern: FullSkia_Gradient.cc, FullSkia_Filter.cc, FullSkia_Text.cc, FullSkia_Shape.cc. Changes to gradient code no longer recompile text code.
| Priority | Issues | CI Time Savings | Binary Size |
|---|---|---|---|
| P0 | B1 | −60–120 min/run | — |
| P1 | B2–B6 | −30–50% build+test | — |
| P2 | B7–B14 | −30 min/PR + incremental | −10–20% |
| Area | Spec | Key Sections |
|---|---|---|
| Filter functions | Filter Effects 1 | §8 (functions), §15 (feMorphology) |
| Transform origin | CSS Transforms 2 | §6 |
| Image sizing | SVG2 + CSS Images 3 | SVG2 §5.8, CSS §5.2 |
| Text baselines | CSS Inline 3 | §4.2, §4.4 |
| Paint order | SVG2 | §13.11 |
| Context paint | SVG2 | §13.3 |
| Switch | SVG2 | §5.9 |
| Masking | CSS Masking 1 | §6.1, §7.2 |
| BiDi | CSS Writing Modes 4 + UAX #9 | §2 |
| Text on path | SVG2 | §11.7 |
This doc was synthesized from analysis by:
Each bot independently identified the same top priorities (CSS filter functions, transform-origin, EnTT modular headers, test sharding), giving high confidence in the recommendations.