|
|
Donner SVG Editor & Engine
SVG-native editor and embeddable SVG2 + CSS3 engine in C++20, with GPU (WebGPU) and compact CPU renderers, built for correctness, security, and performance.
|
Status: Living catalog. The CPU-backend (RendererTinySkia) feature gaps and bugs are the active front, and lead this backlog. Geode runs the same resvg Params and thresholds as the CPU variants; backend-specific override tables are not part of the suite policy.
The triage backlog for 0022's Milestone 2 — working through the tests the suite upgrade pulled in and either fixing the underlying gap or recording why a Params::Skip(...) is the correct state. Each entry corresponds to one or more skips/threshold-overrides in resvg_test_suite.cc.
When a gap is fixed, delete its entry here and un-skip the tests in the same PR. Golden overrides (where Donner is right and resvg's golden is wrong) live in 0009, not here.
Conventions:
There are five supported ways the suite records a known gap. All of them must be expressed through the normal Params path close to the affected tests:
| State | Count | Meaning |
|---|---|---|
| Params::Skip("reason") | 126 | Not run. Feature gap or known bug. The bulk of this doc. |
| Params::RenderOnly("reason") | 58 | Rendered, not compared. Used for UB/deprecated cases where no-crash coverage is still useful. |
| Commented-out INSTANTIATE_TEST_SUITE_P | 1 block | filters/filter-functions — whole category dark on CI. See B2. |
| Params::WithThreshold(…, maxPx) / local max-pixel budget | 90 | Passes with an explicit threshold or pixel budget. Large non-text budgets remain suspect; see Masked bugs behind inflated CPU thresholds. |
| Geode-disabled local Params entries | 0 | Geode now runs every active resvg case. Verified analytic edge residuals use exact per-backend goldens instead of disabling the backend or inflating thresholds. |
| Count | |
|---|---|
| Params::Skip(...) | 126 (grep -o 'Params::Skip(', 2026-07-12) |
| Params::RenderOnly(...) | 58 (render-must-not-crash, no pixel compare) |
| WithThreshold / max-pixel overrides | 90 (grep -oE 'WithThreshold\(|WithMaxPixels\(', 2026-07-12; large non-text budgets remain masked-bug candidates) | | Geode-disabled local Params entries | 0 (all active cases now run on Geode) | | Commented-out category blocks | 1 (filters/filter-functions) |
Landed 2026-05-25 from a parallel CPU-backend debugging sweep. IDs are burned (not reused).
Highest-value first. "Out of scope" rows are correct-as-skipped and listed at the bottom for completeness.
Recently fixed (PRs #608–#611, in review) — see Recently fixed. F2 (transform-origin regression), B1 (intrinsic sizing), B5 (feMorphology), B6 (feImage resampling) are resolved; their IDs are burned. The rows below are what's left.
| ID | Gap | Impact | Kind |
|---|---|---|---|
| B2 | filters/filter-functions disabled (CI "Data corrupted") | ~30 | CI gap — whole category dark |
| B3 | structure/image golden kernel-era mismatch | 13 | Golden refresh + <image> upscale-kernel decision (see B3) |
| F12 | transform-origin on <textPath> baseline | DONE | Resolved by #868; all category cases are active. |
| F7 | paint-order rendering | DONE (8/8) | Shapes and text run on both backends; on-tspan uses a project-owned oracle because the vendored PNG breaks cross-span kerning. |
| F9 | textLength + lengthAdjust stretch/compress | 8 | Feature |
| F10 | textPath SVG2 attributes (path/side/method/spacing) | 8 | Feature |
| F11 | BiDi / RTL text shaping | ~8 | Feature (needs text-full) |
| B7 | font substitution — missing bundled families (masked by fat thresholds) | ~9 | Triage: bundle fonts vs. document as known gap |
| — | masking edge cases (mask 8, clipPath 5) | ~13 | Mixed; clipPath/with-use-child is now active on both backends. |
| — | uncertain Bug? entries (need triage) | ~12 | Needs investigation |
| F1 | enable-background + in=Background* | 23 | Out of scope (deprecated) |
| — | other deprecated/UB skips | ~30 | Out of scope |
Impact: ~30 tests — the entire filters/filter-functions/ block, commented out at resvg_test_suite.cc:1410.
Symptom: The INSTANTIATE_TEST_SUITE_P(FiltersFilterFunctions, …) block is commented out. The category produces "Data corrupted" parse errors on CI x86_64 runners but passes locally on aarch64. (Note: the harmless per-test "Data corrupted" log lines from UrlLoader font fallback are unrelated — this is a parse failure that fails the comparison.)
Root cause: unknown. Candidates: a resvg-test-suite data-integrity issue on CI, an x86_64-specific parser bug, or a runfiles/encoding difference between the runners. This is exactly the CI-vs-local gap the project's always-green-main policy calls out — the fix is to close the gap, not route around it.
Next step: reproduce on an x86_64 runner (or container). Capture the exact SVG that triggers "Data corrupted" and minimize it. These tests were enabled once in #515 before being disabled, so the rendering path works — this is an input/parse problem on one arch. Two custom goldens (drop-shadow-function-{mm,em}-values) are parked for re-enable; see 0009.
These tests pass, but only because maxMismatchedPixels was raised far above the suite default (100). pixelmatch already excludes anti-aliased pixels, so a multi-thousand-px diff on the CPU backend is a real rendering difference. Per CLAUDE.md §"Anti-Aliasing Is Never the Root Cause", "AA drift" is not a valid reason for these magnitudes. The full audit list lives in the test file. B6 (feImage resampling) is now fixed — see Recently fixed; the real cause was a bilinear-vs-bicubic kernel, not the suspected transform bug, and the 3 "transform" tests were never broken (their fat thresholds were over-inflation, now removed). The remaining structural cluster is below.
Impact: ~9 text/font-family/ tests at maxPx 600–5200 (serif 4200, sans-serif 1900, monospace 600, cursive 5000, fantasy 5200, bold-sans-serif 5200, source-sans-pro 1300, font-list 1300, fallback-2 1000), plus text/text/xml-lang=ja (19100, CJK) and structure/defs/ style-inheritance-on-text (6500).
Symptom: the diffs are whole-glyph — Donner substitutes a different font than the golden was rendered with (the suite's cursive/fantasy/CJK families aren't bundled), so every glyph outline differs. This is not a renderer bug; it's a font-availability gap currently silently absorbed by a fat threshold.
Next step (triage decision): either (a) bundle the missing families and tighten the thresholds to default, or (b) reclassify these as explicit Skip("font not bundled: <family>") so the gap is visible instead of hidden. Do not leave them as unexplained fat thresholds. Decide per-family; serif/sans-serif/monospace likely map to already-bundled Noto faces (real diff to chase), while cursive/fantasy are genuinely missing.
The remaining sub-1000-px CPU thresholds (feColorMatrix matrix/saturate variants, feDropShadow, text-decoration rotate-lists, pattern AA) are small enough to be plausible coverage-geometry differences; audit opportunistically but they are not promoted bugs.
B1 (intrinsic sizing + percent on non-square viewBox) is now fixed — see Recently fixed. It was three coupled causes, not just the suspected transformPosition→transformVector (also per-axis percent extent + <marker> % parsing).
B4 (<use> referencing inline <svg> elements) is now fixed. Two coupled causes: (1) the <use> width/height override + viewport machinery (LayoutSystem::createShadowSizedElementComponent, ComputedShadowSizedElementComponent) only accepted <symbol> targets, never <svg>, so no instance viewport (clip) was created and the shadow content transform dropped the referenced svg's x/y when it had no viewBox; (2) a CSS shadow-tree bug — ShadowedElementAdapter::parentElement() looked up ElementTypeComponent on the raw tree entity, so a shadow entity whose parent was also a shadow entity appeared parentless, matched :root, and the UA rule svg:not(:root) { overflow: hidden } never clipped nested <use> → <use> → <svg> chains (descendant combinators through shadow parents were broken generally). 5 tests un-skipped (70k–130k px → pass).
Re-triaged (2026-07-03): the old "embedded data URLs render at wrong size" description was wrong. Donner's <image> placement, intrinsic sizing (no-width/no-height/auto), MIME sniffing, GIF decode, and preserveAspectRatio alignment are all correct (verified per-test against the goldens: alignment diffs are zero-displacement, residuals hug resampled edges only). The real gap: the vendored 2023 resvg-test-suite goldens were generated across several resvg eras with different <image> upscale kernels:
Plus one policy case: embedded-svg-with-text — resvg parses <image>-embedded SVGs with an empty fontdb, so its golden renders no text; Donner renders the text (browser-consistent).
Next step (needs a maintainer decision): refresh the whole structure/image golden set from current resvg and adopt Mitchell-bicubic in RendererTinySkia::drawImage/drawBitmap (+ the Geode sampler) to match current resvg, or keep bilinear and leave the 13 mismatched-era tests skipped. Per-test threshold inflation is not an option.
B5 (feMorphology degenerate radius) is now fixed — see Recently fixed.
Resolved. All 8 tests in painting/paint-order/ are active. Shape marker/fill/stroke ordering and text whole-run fill/stroke ordering run on both backends. The vendored on-tspan PNG loses kerning across a paint-only span boundary, so Donner uses explicit CPU and Geode goldens that preserve continuous shaping while still gating paint order.
Impact: ~6 (text/textLength 2 + text/lengthAdjust 3 + text/text-decoration interaction). Text stretching/compressing to a target length (spacing and spacingAndGlyphs). The arabic/arabic-with-lengthAdjust cases pass on text-full builds and are enabled with .onlyTextFull().
Impact: 8 in text/textPath/: path attribute, side=right, method=stretch, spacing=auto, path+xlink:href combinations, filter on textPath, plus the deferred vertical/writing-mode=tb cases.
Impact: ~8 across text/direction (2), text/unicode-bidi (1), text/text/bidi-reordering, text/tspan/bidi-reordering, text/letter-spacing/mixed-scripts, text/textLength Arabic. Needs the BiDi algorithm + RTL shaping (text-full). Group as one workstream.
Resolved by #868. All structure/transform-origin/ cases, including on-text-path, are active.
Gradient/pattern paint-servers and <image>/text apply the transform-origin pivot as Translate(-origin)·M·Translate(origin) (matching the shape path; Donner's operator* is left-first). For paint-servers the pivot is recomputed in the renderers from each entity's ComputedLocalTransformComponent — RendererTinySkia::resolveGradientTransform, RendererGeode::resolveGradientTransform, and the shared pattern transform in RendererDriver — not via the getRawEntityFromParentTransform accessor, which is unrelated. For <image>/text the layout composes the resolved origin with the content-placement transform.
on-text-path still renders the baseline path without the pivot, so a rotated <textPath> samples its glyphs off-screen → #624.
| Category | Tests | Gap |
|---|---|---|
| structure/svg | 2 | nested-svg overflow |
| structure/style | 1 | CSS @import / external CSS |
| structure/symbol | 1 | transform on <symbol> (SVG2) |
| painting/image-rendering | 2 | image-rendering (pixelated/crisp-edges) |
| masking/clipPath | 6 | clipPath with <text> children, <use> child, shorthand edge cases |
| masking/mask | 8 | mask-type, mask-units, color-interpolation, mask-on-self |
| text/font | 2 | font shorthand; canvas-size mismatch (test harness) |
| text/tspan | 3 | tspan interaction with clip-path/filter/mask |
| painting/stroke-dasharray | 4 | 0 n dash patterns with caps; 40 0 closed-rect dash-seam (see note) |
| painting/marker | 3 | multiple closepaths, recursive-5 (rounded-rect corner fixed, #623) |
painting/stroke-dasharray/n-0 (40 0) — root-caused under #623 and intentionally left skipped: an SVG <rect> is a closed contour, so tiny-skia (the faithful Rust-tiny-skia port) seam-joins the first and last 40-unit dash across the start vertex into one continuous dash, making the start corner an interior MITER. resvg's golden butt-caps that corner because usvg flattens the rect to a non-closed path before dashing. Donner's mitered closed-contour seam is the spec-conformant behavior (matches Skia/Chrome/Firefox); the diff is a resvg-pipeline difference, not a Donner/tiny-skia bug. Pinned by RendererTests.DashSeamClosedContourMitersStartCorner.
painting/marker/marker-on-rounded-rect — fixed under #623: Path::vertices() now emits the arrival marker-mid at a rounded rect's zero-length-close start corner (stacking start + mid + end, matching resvg), while still excluding smooth all-curve loops (circle/ellipse). | text/writing-mode | ~7 | writing-mode=tb with dx/dy, vertical-lr/rl edge cases, mixed-script (upright CJK + rotated Latin) column geometry (also skips text/alignment-baseline/hanging-on-vertical) |
These have a question-mark reason in the file and need a root-cause pass to decide bug vs. out-of-scope:
| Category | Tests | Why |
|---|---|---|
| filters/enable-background | 21 | Category default Params::RenderOnly(...): deprecated in SVG 2 (→ <filter> chains / backdrop-filter). See unsupported_svg1_features.md. |
| filters/filter in=Background* | 2 | Same deprecation (BackgroundImage/BackgroundAlpha inputs). |
| text/tref | 9 (+1 display) | <tref> removed in SVG 2. |
| text/kerning | 2 | kerning attribute deprecated SVG 1.1. |
| text/glyph-orientation-* | 2 | deprecated SVG 1.1. |
| paint-servers/radialGradient | 1 | test-suite bug (fr> default — SVG2 behavior changed). focal-point-correction now passes and is enabled. |
| structure/style-attribute | 1 | <svg version="1.1"> disables geometry-in-style (SVG 1.1 behavior). |
| Other RenderOnly UB cases | 51 | Implementation-defined output; we verify no-crash only (per project policy, kept RenderOnly not Skip). |
Geode is part of the same resvg test matrix as the CPU variants. It should use the same ImageComparisonParams thresholds, render-only state, skips, and golden overrides as the other renderers. Backend support is recorded through normal Params feature requirements or local backend disables, never through side-table gates.
Policy:
The practical goal is fewer overrides over time. A large override map is a signal to either fix the feature, classify it as a clear unsupported/deprecated case, or write a focused non-resvg regression that exercises the root cause directly.
RESOLVED. Geode now uses official Slug analytic dual-ray coverage at 1 sample/pixel on every adapter (4× MSAA and the Intel-Arc alpha-coverage fallback deleted; Mac/Linux unified; GeodeTinyParity retired). See 0041 (as-built).
The earlier theory in this section — that ~16 Geode gates shared one "slug_fill edge-coverage quantization" root cause — was wrong, and is preserved here only as a caution: the analytic rewrite left those tests byte-identical, proving coverage was never the cause. They were three real, separate bugs plus two legitimate per-backend goldens, all now fixed/closed:
Lesson: a large diff amplified by a filter/matrix is not evidence of a coverage problem — inspect whether a coverage change actually moves it before attributing it. No active resvg case is backend-disabled. Verified analytic-vs-finite-sample edge residuals, including feGaussianBlur/complex-transform, paint-order text, and the rotated pattern case, use exact Geode goldens so the backend remains fully gated.