Donner SVG 0.8.0-pre
SVG editor and embeddable C⁠+⁠+⁠20 engine.
Loading...
Searching...
No Matches
resvg-test-suite: Feature Gaps & Open Bugs

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:

  • Impact = number of currently-skipped (or fat-thresholded) tests the entry covers.
  • Root cause = best-known localized explanation, or "needs investigation".
  • Next step = what a fix PR touches first.
  • Prefix B = bug (Donner is wrong), F = feature gap (standard feature not implemented). Numbers are stable IDs other docs/PRs can reference; retired entries leave their number burned.

How a test can be "not passing"

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") 133 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 0 Every vendored category is instantiated.
Params::WithThreshold(…, maxPx) / local max-pixel budget 103 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.

Current totals

Count
Params::Skip(...) 133 (grep -o 'Params::Skip(')
Params::RenderOnly(...) 58 (render-must-not-crash, no pixel compare)
WithThreshold / max-pixel overrides 103 call sites: 79 WithThreshold, 11 WithMaxPixels, and 13 direct withMaxPixelsDifferent calls. 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 0

Recently fixed (PRs #608–#611)

Landed 2026-05-25 from a parallel CPU-backend debugging sweep. IDs are burned (not reused).

  • F2 — transform-origin regression (#514)#609. The pivot sandwich was written Translate(O) * raw * Translate(-O), but Transform2d::operator* is left-first, so the pivot-out translate applied last — the pivot wasn't a fixed point. Swapped to Translate(-O) * raw * Translate(O). 13 tests un-skipped (9.7k–151k px → pass). The 7 paint-server/<image>/text cases were a separate never-implemented gap → re-filed as F12 below.
  • B1 — intrinsic sizing + percent on non-square viewBox#611. Three coupled causes: calculateRawDocumentSize used transformPosition (folded the letterbox translation into the size); percent resolution used the viewBox diagonal extent instead of per-axis X/Y; <marker> length attrs were parsed with a no-suffix parser that rejected %. 10 tests un-skipped.
  • B5 — feMorphology degenerate radius#608. Negative/zero/empty/absent radius blanked the shape to transparent black; per Filter Effects §15.4 a disabled morphology passes the input through. 5 tests un-skipped.
  • B6 — feImage resampling#610. The suspected fragment-ref-transform bug was a red herring — those 3 tests were never broken (their 22k–34k px thresholds were pure over-inflation, now removed). The real bug: tiny-skia upscaled feImage with bilinear; resvg uses Mitchell-Netravali bicubic. 4 subregion tests 2.6k–8.7k px → 0. svg.svg's custom golden refreshed to bicubic. Geode now has the matching WGSL bicubic sampler (filter_image.wgsl, edge-clamped, RGB≤A) plus a per-attribute placement-rect fix in GeodeFilterEngine::applyImage (each of x/y/width/height resolved independently, percent/OBB-aware, defaulting to the filter region): All 7 Geode feImage cases are active. svg.svg uses an exact Geode golden for its verified analytic edge-coverage residual, rather than disabling the backend.

Priority 0: CPU-backend backlog (the active front)

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 coverage DONE (26/43 files compare) Category active; 17 mismatches are explicit triage skips.
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 (14/14) 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 3, clipPath 5) 8 mask-type and vector text clip children are active on both backends; remaining cases are units, transforms, color interpolation, bitmap text clips, nested intersections, and shorthand geometry.
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

Tracked regressions & disabled blocks

B2: filters/filter-functions category coverage

Resolved as a coverage gap. All 43 files are instantiated. A combined TinySkia and Geode run executes 86 comparisons: 53 passed before exception classification, representing 26 files on both backends plus one TinySkia-only pass. Seventeen unique mismatching files now carry explicit normal-path Triage: skips for blur, color-adjust, drop-shadow, or URL-chain cases. No category remains dark, but these skips are not yet classified as Donner defects.

Next step: classify the 17 mismatches against the normative grammar and independent browser pixels, then fix only confirmed Donner defects through their owning blur, color-adjust, drop-shadow, and URL-chain paths. Remove each local skip only after that classification, a focused regression, and both comparison modes pass; do not restore the category-level disable.


Masked bugs behind inflated CPU thresholds

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.

B7: font substitution — missing bundled families

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.


High-leverage bugs (one root cause, many tests)

B1 (intrinsic sizing + percent on non-square viewBox) is now fixed — see Recently fixed. It was three coupled causes, not just the suspected transformPositiontransformVector (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).

B3: structure/image golden kernel-era mismatch

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:

  • Bilinear-era goldens (external-jpeg/png, embedded-png, embedded-jpeg-as-*, slices, with-transform, odd-numbers, ...): match tiny-skia Bilinear — these pass today (the nine former 300-px "slice/on-svg" thresholds now pass at the default 100 and were removed).
  • Mitchell-era goldens (no-height-non-square, width-and-height-set-to-auto): match current resvg, which upscales <image> with Mitchell-bicubic (tiny_skia::FilterQuality::Bicubic) — verified: both pass when Donner's drawImage is switched to Bicubic, and current upstream resvg goldens reproduce with a Mitchell kernel to mean |Δ| ≈ 0.2.
  • Intermediate-era goldens (no-width/no-height/no-width-and-height, embedded-gif, external-gif, embedded-jpeg-without-mime, preserveAspectRatio=none + the three *-meet variants): match neither kernel (12.5k px vs Bilinear, 21.8k px vs Bicubic for the no-* group; the deterministic-decode GIF golden rules out decoder differences).

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.


Unimplemented features (clean single-feature scope)

F7: paint-order rendering

Resolved. All 14 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.

F9: textLength + lengthAdjust

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().

Zero target length is applied to spacing in the text engine and has focused unit coverage. The resvg zero.svg case remains disabled with a 397-pixel residual concentrated on the overlapping glyph raster; its layout is no longer ignored, but the remaining oracle difference still needs independent browser triage before the exception can be removed.

Per-span adjustment now updates the current text position inherited by following runs and stops at the first explicit inline-axis position, including a reset later inside a span. The on-a-single-tspan.svg layout is active in focused helper and DOM geometry tests; its resvg entry remains classified for a 499-pixel small-text raster residual. Nested-container aggregation, decoration extents, vertical layout, and textPath ordering remain implementation work.

F10: textPath SVG2 attributes

Impact: 8 in text/textPath/: path attribute, side=right, method=stretch, spacing=auto, path+xlink:href combinations, filter on textPath, plus modern vertical writing-mode cases. Obsolete SVG 1.1 writing-mode aliases are classification work, not new feature work.

F11: BiDi / RTL text shaping

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.

F12: transform-origin on paint-servers / <image> / text

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 ComputedLocalTransformComponentRendererTinySkia::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.

Smaller feature gaps

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 Both backends implement the complete typed value set, including two-stage non-integer pixelated scaling. The remaining skipped legacy optimizeSpeed cases use an allowed nearest algorithm but disagree with the vendored golden's pixel-center grid, so they remain oracle triage rather than missing value semantics.
masking/clipPath 5 Nested clip intersections and shorthand edge cases remain. Vector text children use shared placed glyph outlines. The transformed-text vendored golden uses ink bounds, unlike SVG 2 full-glyph-cell bounds and Firefox, so it remains classified pending broader browser review.
masking/mask 3 mask-units, transformed regions, and color-interpolation; mask-type alpha/luminance is active.
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 | vertical-lr / vertical-rl edge cases and mixed-script (upright CJK plus rotated Latin) column geometry. Obsolete tb* SVG 1.1 values should be classified or mapped to their required compatibility aliases, not implemented as distinct modes. |


Needs triage (uncertain Bug? entries)

These have a question-mark reason in the file and need a root-cause pass to decide bug vs. out-of-scope:

  • structure/svg: non-UTF-8 encoding, rect-inside-non-SVG-element, xmlns validation (XML entity references ×3 and mixed-namespaces now pass and are enabled)
  • paint-servers/stop: stop-color inherit edge case
  • text/letter-spacing/non-ASCII-character: different CJK glyph (wrong font? → overlaps B7)
  • text/font-family/fallback-1: fallback from invalid family
  • masking/clip/simple-case: CSS2 clip property (rect() clipping on viewport elements, deprecated) — not implemented, 76k px diff
  • filters/feImage/empty.svg: std::bad_alloc crash on Linux CI runners (passes on macOS; enabled briefly on 2026-07-03 and reverted after the Linux lane crashed in all variants). Likely shares a resource-loading root cause with #576 — a failed/corrupt load yielding garbage dimensions would explain the giant allocation. Crash = "never crash on untrusted input" violation; root-cause on a Linux x86_64 env.

Out of scope (correctly skipped — do not "fix")

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 / Resvg Override Policy

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. Build capability is recorded through normal Params feature requirements, never through parity-only backend disables or side-table gates.

Policy:

  • Do not add geodeCategoryGate, geodeFilenameGate, or backend-specific threshold side tables.
  • Do not maintain symptom-ledger sets such as kEdgeFloor or kGenuineG2 in the resvg file. If an independently verified analytic Geode result differs from the shared reference, attach an exact local withGeodeGoldenOverride(...) and current reason to that case. Never disable the backend or widen a Geode-only threshold to absorb the difference.
  • Category-wide defaults are acceptable only when every file in the category has the same reason. filters/enable-background is the model: one category default Params::RenderOnly(...), not a per-file list. Category feature requirements such as text/* requiring text support are additive, so per-test overrides do not accidentally opt out of backend capability checks.
  • Non-resvg regression tests belong in focused renderer test files, not in resvg_test_suite.cc. Use the resvg suite file only for resvg-test-suite data.
  • Test comments should state the current expected behavior and why an override exists. Avoid PR history, audit logs, and long failure narratives in the test file; put durable analysis here instead.

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.

Geode coverage: analytic Slug dual-ray (resolved) + the misdiagnosis correction

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:

  • filters/feConvolveMatrix/* (10) + filters/feMorphology/source-with-opacity — a pattern-tile filter-region-scissor leak (beginPatternTile didn't clear the outer clip stack, shifting tiled cells ~1px) + a missing feMorphology linearRGB round-trip. Both fixed → 0 px.
  • structure/svg/preserveAspectRatio=xMinYMin + proportional-viewBox — were parity-only; pass once GeodeTinyParity is retired.
  • painting/marker/orient=auto-on-M-L-Z — degenerate zero-area closed stroke decomposed into overlapping triangles; fixed by de-closing collinear closed subpaths before strokeToFill → 0 px.
  • filters/feColorMatrix/type=matrix-with-non-normalized-values + filters/feImage/svg — Geode verified-correct, differs from resvg's finite-sample reference; per-backend Geode goldens (withGeodeGoldenOverride).

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.


Template for new entries

### Bn or Fn: Short title
**Impact:** N tests.
**Symptom:** (What does the diff look like?)
**Root cause:** (file:line if known; "needs investigation" otherwise.)
**Next step:** (Concrete action for a fix PR.)
**Affected tests:**
- path/to/first-test.svg