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 | Call sites | Meaning |
|---|---|---|
| Params::Skip("reason") | 77 | Not run. Feature gap, known bug, reference disagreement, harness mismatch or API policy. The bulk of this doc. |
| Params::UndefinedBehavior("reason") | 51 | Rendered, not compared. Undefined or implementation-defined behavior; see the policy below. |
| Params::DeprecatedInSVG2("reason") | 23 (39 cases) | Rendered, not compared. Feature removed from or deprecated in SVG 2 that Donner does not implement. |
| 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. |
Rendering without a comparison has two named buckets. A case that renders without a pixel comparison hides every regression in its output, so it needs one of two factories:
//tools/resvg_parity:parity_report_tests enforces both rules, and the parity report counts the two buckets separately. Both still render, so they keep no-crash and sanitizer coverage. Donner bugs and missing features, reference disagreements where Donner follows the specification, harness or background mismatches, and API policy differences are Params::Skip with a bug number or reason; skipped cases do not render at all. A UB marker reference does not by itself make a case undefined behavior: a Not impl: or Deferred: feature on a marker reference stays a skip. A case that passes the default comparison compares. The parity report counts 90 render-only cases (51 undefined behavior: 50 on the UB marker reference plus the allowlisted case; 39 deprecated in SVG 2), 77 skipped cases and 1512 compared cases.
| Count | |
|---|---|
| Params::Skip(...) | 77 (grep -o 'Params::Skip(') |
| Params::UndefinedBehavior(...) | 51 (render-must-not-crash, no pixel compare; 51 cases) |
| Params::DeprecatedInSVG2(...) | 23 (render-must-not-crash, no pixel compare; 39 cases including a category default) |
| 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 |
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 coverage | DONE (27/43 files compare) | All cases execute; 16 reference qualifications have independent contract tests. |
| B3 | structure/image golden kernel-era mismatch | DONE | Mitchell cubic for quality <image> rendering and v0.49.0 raster references (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 | DONE | Interaction cases compare; textLength/zero compares with a reference rasterizer generation budget (see F9). |
| F10 | Remaining textPath vertical, kerning, and filter cases | 4 | Feature |
| F11 | BiDi / RTL text shaping | ~8 | Feature (needs text-full) |
| B7 | text reference qualification | 2 | Allowances removed; color-adjust labels compare with generation budgets; font/simple-case and fallback-1 remain skipped (see B7) |
| — | 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 |
All 43 fixtures execute. Twenty-eight retain ordinary comparisons, including blur-function and drop-shadow-function-filter-region; fifteen have explicit reference qualifications and independent rendering assertions. Most use CssFilterContract*; the inherited invalid-URL case uses FilterReferenceStages. No threshold or golden is changed.
| Assigned cases | Disposition |
|---|---|
| Seven color-adjust-functions-* cases | Compare with a 390-pixel reference label glyph budget. Every mismatch is in label glyphs: 354 pixels in default text, 370 in full text, 375 in Geode. Filtered geometry agrees. Independent endpoints, omitted/default values, percentages, clamping and invalid-negative rejection pass. The labels select Noto Sans Regular (see B7). |
| Five remaining drop-shadow-function* cases | Reference disagreement. Each reference shadow is the Donner shadow moved to a truncated offset: rendering Donner at that offset matches within the default allowance (21 to 72 pixels, against 2,513 to 6,582 at Donner's own offset). For the three 4 5 6 cases the device offset is (10, 12.5); the reference truncates it to (10, 12), while Donner rounds to (10, 13) under its half-pixel rounding contract, and an exact half-pixel shadow still differs from the reference by 447 pixels. For em-values and mm-values the reference shadows sit at device offsets (30, 47) and (27, 37) instead of the specified (32, 48) and (28.35, 37.80). Reproducing them needs two truncations: the resolved user lengths to whole units before scaling (12.8 19.2 to 12 19, and 11.34 15.12 to 11 15), a hypothesis the current usvg source does not show, and the scaled offset toward zero, which resvg's apply_drop_shadow does with dx as i32. Color order, currentColor, em/mm resolution and alpha compare independently. |
| two-exact-urls | Valid repeated URL syntax; PNG bytes equal the corpus UB marker. Repeated bounded offsets match their explicit composed offset. Separate blur references retain intermediate clipping and sampling, so they need not equal two blur primitives in one graph. |
| two-urls | Compares by default. The boundary difference was Donner's: the three box passes that approximate the Gaussian discarded what the earlier passes spread past the filter raster edge, which the later passes must read back. TinySkia now runs the passes on zero-padded lines, and Geode evaluates the three passes as one cascade kernel per axis that samples the input directly, so a blur near the raster edge equals the same blur computed with room around it. |
| url-and-grayscale | CSS functions use sRGB. An independent solid-color oracle gives 115 for the sample's encoded-channel luminance; the historical reference center is 122, consistent with linearRGB processing. |
| one-invalid-url-in-list | An invalid URL disables the entire chain rather than preserving the prior stage. FilterReferenceStages.InvalidReferenceDisablesEntireChain independently covers that behavior. |
Filter Effects defines function grammar, sRGB processing and chaining. Drop-shadow now rejects nonzero unitless CSS lengths while preserving SVG presentation-attribute user units and unitless zero. Negative standard deviations reject the whole declaration. Its color alpha is applied once; flood-opacity is a separate factor. Focused regressions first reproduce invalid declarations changing output and translucent shadows receiving alpha twice, then pass on TinySkia, full text and Geode. The full property suite also checks that rejected declarations preserve the prior value at the same cascade priority.
The named gates are properties_tests, renderer_driver_tests, the three renderer_regression_tests configurations and all three resvg corpus targets. A raster sanitizer seed covers valid translucent shadows and invalid declarations. The qualified corpus cases are skips that name their cause: label-glyph differences under #1167, and drop-shadow references whose offsets are truncated, which are reference disagreements. #1169 tracks these classifications. two-urls now compares by default after the box blur edge fix; the remaining filter-region and sampling cases stay with #1170.
Each URL in a CSS filter list receives the previous stage's output as its own SourceGraphic and SourceAlpha. Named results remain local to that filter element; its filter region, primitive units and color space remain intact. Adjacent CSS functions share an sRGB stage. An invalid URL disables the chain, while a valid empty filter produces transparent output; a later flood can still paint over that output. The aggregate 32-node limit and frame budgets apply across the stages, and rejected preparation suppresses the whole filtered subtree.
Independent geometric tests cover repeated references, input and result scope, filter regions, object-bounding-box units, empty stages, expanded captures and original CPU paint inputs. Nested captures preserve their coordinate offsets through groups and use existing renderer surfaces; GPU stages remain on the GPU. These contracts do not replace or widen corpus pixel tolerances. The invalid-FuncIRI and one-invalid-url-in-list PNGs encode incompatible invalid-URL behavior; they are reference-disagreement skips backed by visible-geometry assertions, documented in 0009.
The resvg harness uses a test-only loader that maps the two exact HTTPS identifiers in structure/image/url-to-png.svg and url-to-svg.svg to committed local copies. Other URL schemes are rejected, and ordinary local references still use SandboxedFileResourceLoader. Snapshot reads are bounded to 512 KiB; attribution and source hashes are recorded beside the files. Tests perform no live network requests. Applications independently provide resource bytes through ResourceLoaderInterface; deterministic image-loader tests cover accepted PNG/SVG bytes, denied requests and resource-size limits.
The local structure/use/xlink-to-an-external-file.svg resource resolves and renders its text. Its upstream reference is entirely opaque, whereas the SVG canvas is transparent outside painted content. The comparison remains gated pending background and typography reference qualification. An independently authored inline-versus-external text-fragment comparison covers host style inheritance, fragment selection, the callback identifier and transparent canvas behavior.
clip-path: circle() and ellipse() support length, percentage and closest-side/farthest-side radii, CSS positions, and fill/stroke/view reference boxes. Default SVG border boxes use stroke geometry. Reference geometry includes image occurrences and horizontal text, including hidden glyphs for stroke bounds. Work admission precedes path scans and glyph outline decoding. Vertical text fill/stroke reference boxes are currently refused; viewport-based circles remain available. This does not claim a hard allocation bound inside the existing text shaper.
Both renderers apply radial shapes to entire groups and preserve them during retained masked-child drag. Independent live-bitmap comparisons cover boxes, nested clips, occurrence styles, images, text spans, non-scaling strokes and style mutation; CSS declaration lengths require units while presentation attributes retain user units.
Ellipse radii resolve independently: horizontal percentages use reference width and vertical percentages use reference height. A zero radius on either axis produces an empty clip. The circle_clip_path_tests and properties_tests targets cover ellipse parsing, reference boxes, independent axis resolution, transformed/nested clips and invalid declaration fallback. A single closest-side or farthest-side keyword selects both radii. Corner extent keywords and calc() radii are not supported; explicit radii use the two-value form.
clip-path: polygon() supports comma-separated length/percentage vertices, optional nonzero or evenodd interior rules, and the same SVG reference boxes. Vertices may lie outside the reference box. Valid one-vertex, two-vertex and collinear polygons create empty clips; invalid syntax preserves the earlier declaration. Vertex storage shares the SVG point-list limit and render preparation admits all polygon work before constructing paths. Retained vertex capacity is charged for each computed style, including explicitly inherited copies. Polygon corner rounding and calc() coordinates are unsupported. polygon_clip_path_tests, properties_tests and shape_system_tests cover rendering, nested rules, grammar and resource refusal.
clip-path: inset() supports one to four offsets with margin-style expansion, negative offsets, and the same SVG reference boxes. Accepted units are absolute lengths, em, rem, and percentages. Lengths compute to pixels using the declaring element's computed font size and actual root font size before explicit inheritance. Horizontal percentages use reference width; vertical percentages use reference height. The font-metric units ex/ch and viewport units vw/vh/vmin/vmax are rejected until their computed-value context is available. Unrepresentable computed lengths produce an empty clip. Oversized opposing insets reduce to zero area, which produces an empty clipping region rather than preserving an earlier declaration. Optional round radii accept one to four horizontal values and an optional slash with one to four vertical values, using the same supported units. Radius percentages use the original reference box dimensions; all corners share one overlap reduction constrained by the used inset rectangle. Negative or nonfinite radii invalidate the declaration, and a zero radius on either axis makes a square corner. Fixed inline radius arrays add no dynamic radius storage; rounded path construction admits its maximum command and point work before allocating geometry. calc() remains unsupported. inset_clip_path_tests, properties_tests and shape_system_tests cover exact rendering, nested clips, grammar, inheritance, mutation, numeric limits and work refusal. See the CSS Shapes inset definition.
clip-path: path() supports quoted SVG path data with an optional nonzero or evenodd rule, using the existing SVG path parser and the same reference boxes. Coordinates are pixel offsets from the reference-box origin; selecting a box translates the path without scaling it. Every open contour closes for clipping. Malformed or empty data rejects the entire declaration; valid zero-area paths produce empty clips. The function's interior rule takes precedence over SVG clip-rule. Decoded strings are limited to 64 KiB before parsing, command and point expansion uses the SVG parser's limits, and computed styles charge retained path-vector capacities including inherited copies. Render preparation reserves validation, copy and contour-closing work before allocation and rejects unrepresentable translated coordinates or extents. path_clip_path_tests, properties_tests, style_system_tests and shape_system_tests cover these contracts. Path data accepts SVG numeric coordinates, not CSS units, percentages or calc(). See CSS Shapes path() and CSS Masking reference boxes.
The plain circle-shorthand.svg comparison is restored. The stroke-box and view-box PNGs use an opaque white backdrop, while Donner's RGBA API retains transparency outside painted content. SVG rendering contexts permit different final backdrops. Compositing diagnostic copies of both actual and reference images over white produces zero pixelmatch mismatches on both backends under the unchanged corpus policy. Strict identity still differs at raster edges, so this is a background qualification, not a bit-identity claim. The original PNGs and tolerances remain unchanged; these two fixtures render normally without comparing their opaque references, and authored geometry comparisons continue to use strict identity.
Both renderers distinguish miter-clip from the bevel fallback of ordinary miter. The shared outline stroker clips the exterior join at the configured bisector distance, including exact and near reversals, without constructing an unbounded tip. Selection, marquee, culling, range and thumbnail bounds include the clipped-corner extent. The original miter-clip.svg corpus case uses its ordinary comparison settings; analytic geometry tests and sanitizer corpus seeds cover the bounded construction.
This is support for the existing parsed join value and the geometry described in the SVG Strokes draft, not a claim that all experimental stroke joins are implemented. arcs retains its miter fallback and remains a separate reference/feature qualification case.
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. "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.
Status: the font-family allowances are gone. Every text/font-family/ case except fallback-1 compares at default settings in both CPU text tiers, as do text/text/xml-lang=ja, text/letter-spacing/non-ASCII-character and structure/defs/style-inheritance-on-text. Face selection for these cases is asserted directly in FontSelection_tests.cc.
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.
The vendored references come from more than one resvg rasterizer generation. Newer PNGs cover edges with tiny-skia's 4x4 supersampling, so partial alphas fall on 1/16 steps; older ones match Donner's analytic coverage. The difference is largest on the 1.25-pixel crosshair most text tests draw at x=250: analytic coverage splits it 160/160 across two columns, 4x4 coverage 128/192, so a whole reference can differ by about 400 pixels with identical layout. Donner keeps analytic coverage on both the CPU and GPU backends, so these references cannot match exactly.
A case may carry a pixel budget for this cause, with a reason starting Reference rasterizer generation:, only when all of the following hold:
| Case | Before | Residual (px) | 4x4 (px) | Budget | 1 px text shift |
|---|---|---|---|---|---|
| text/font/font-shorthand | skip | 386 | 0 | 400 | 422 |
| text/text/real-text-height | skip | 397 | 0 | 410 | 441 |
| text/textLength/zero | skip | 396 | 0 | 410 | 433 |
| text/text/ligatures-handling-in-mixed-fonts-1 (full text) | skip | 399 | 0 | 415 | 507 |
| text/text/ligatures-handling-in-mixed-fonts-2 (full text) | skip | 343 | 0 | 355 | 435 |
| text/baseline-shift/nested-with-baseline-1, -2 | 0.1 / 100 | 384 | 0 | 400 | 408 |
| text/dominant-baseline/hanging | 0.1 / 100 | 337 | 0 | 350 | 344 |
| text/tspan/nested-whitespaces | 400 | 387 | 0 | 400 | 416 |
| text/text/zalgo (full text) | 300 | 257 | 1 | 265 | 468 |
The two ligature cases stay gated to full text: the simple tier lacks mixed-font ligature shaping (1555 and 3134 pixels, #1176).
Moving nested-with-baseline and hanging from a 0.1 color threshold with 100 pixels to a 0.02 count is not tighter on both axes: the count is larger and the color threshold smaller. It is justified by what each setting detects:
| Regression | 0.1 / 100 | 0.02 count (budget 350) | 0.02 with anti-aliased pixels |
|---|---|---|---|
| none | 0 | 337 | 940 |
| 1 px text shift | 6 (passes) | 344 (passes) | 1235 |
| 2 px text shift | 225 (fails) | 562 (fails) | 1465 |
| dominant-baseline="alphabetic" | 2714 (fails) | 2997 (fails) | not recorded |
Both settings catch the baseline change the test is about and a 2 px shift; neither catches a 1 px shift. Counting anti-aliased pixels would catch it (940 against 1235), but makes the budget a measure of glyph-edge coverage rather than of the crosshair, so it is not used.
//tools/resvg_parity:parity_report_tests checks the form of this class and the next. Such a compared case:
The parity report counts them as reference_generation_budget_cases and reference_label_glyph_budget_cases.
The seven filters/filter-functions/color-adjust-functions-* cases fail the first condition. They differ from their vendored 500x500 PNGs by 354 simple, 370 full and 375 Geode pixels, all inside the three label bands; the filtered squares compare exactly. Under 4x4 coverage the difference is still 302 and 297 pixels, because those PNGs draw the 8-unit Noto Sans labels about 14% heavier. Against resvg v0.49.0's rendering, 4x4 coverage brings the labels to 5 and 52 pixels.
They compare with a 390-pixel budget whose reason starts Reference label glyphs: and states the analytic and supersampled residuals. Detection was measured on the residual-bearing PNGs:
| Regression | Pixels (simple / full) | Budget |
|---|---|---|
| none | 354 / 370 | 390 |
| 1 px label shift | 1508 / 1524 | fails |
| brightness 50% to 45% on one square | 10354 / 10370 | fails |
| opacity 50% to 45% on one square | 5358 / 5374 | fails |
| sepia 2 to 0.9 on one square | 10354 / 10370 | fails |
| grayscale 2 to 0.9 on one square | 354 / 370 | passes: no pixel moves past the 0.02 threshold |
The grayscale change is invisible to any comparison at the default color threshold, budgeted or not.
Budgets that already passed at default settings were removed from 39 cases, including the structure/transform-origin category default (0.13 threshold, 300 pixels; on-image, on-text-path and on-text still need it and keep it per case), text/text/xml-space (1400 pixels) and painting/fill/pattern-on-text (2100 pixels). Measured at default settings, their largest residuals are:
Geode also passes with hardware Vulkan on Linux.
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).
Resolved (2026-10-26). Placement, intrinsic sizing, MIME sniffing, GIF decode and preserveAspectRatio alignment were already correct; the twelve skipped rows and painting/image-rendering/optimizeSpeed differed only in resampling. A pinned reproduction attributed every snapshot reference to one of four upscalers, each matched to 0 to 55 differing pixels at the default threshold:
At resvg v0.49.0 every raster-image reference is the Mitchell cubic (nearest for optimizeSpeed). Donner now resamples <image> with image-rendering: auto, optimizeQuality and high-quality through the same cubic on TinySkia (tiny-skia FilterQuality::Bicubic) and Geode (a 16-tap cubic image-blit sampling mode), matching the kernel feImage already used; smooth stays bilinear and compositor bitmap blits keep bilinear sampling. resvg renders an feImage raster as an image, so feImage placement shares the same ImageSamplingKernelFor mapping on both backends: smooth is bilinear there too, and a pure translation samples the containing texel, as tiny-skia does for <image>. That also removed a cubic filtering pass that rotated feImage placement and nested-SVG feImage rasters received at one-to-one scale, which re-enabled filters/feImage/with-subregion-5 and refreshed the Donner-generated filters/feImage/svg goldens. The raster-image references were replaced by their v0.49.0 renderings, with provenance in third_party/resvg-test-suite/BUILD.bazel.
The embedded-svg-with-text policy case is independently qualified: the vendored reference omits the embedded SVG's text, while Donner uses the document font catalog. An authored EmbeddedSvgUsesDocumentFontContext case checks embedded-versus-direct identity plus visible text ink in //donner/svg/renderer/tests:renderer_public_api_tests and its //donner/svg/renderer/tests:renderer_public_api_tests_geode variant, so the corpus row is a reference-disagreement skip rather than a missing feature.
B5 (feMorphology degenerate radius) is now fixed — see Recently fixed.
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.
Status: done. Text stretches or compresses to a target length with spacing and spacingAndGlyphs. The arabic/arabic-with-lengthAdjust cases pass on text-full builds and are enabled with .onlyTextFull().
With spacing, the SVG 2 resolve-text-length loop accrues the per-gap adjustment after every character of the owner, its last included, but never applies it to later characters; the specification does not define how following text moves. Chrome adds an explicit shift by the accrued amount, and the resvg references show the same, so the owner's length still ends at its last glyph and the text after it starts one gap later. Donner follows them. A nested owner that ends its parent passes the gap through, and an enclosing owner counts it as fixed advance. Glyph stretching adds no trailing gap. textLength/on-a-single-tspan compares at the default settings, and a renderer regression checks a fitted span against the same span with an equal letter-spacing.
With spacingAndGlyphs, a glyph's outline is stretched before its rotation, so a sideways glyph in vertical text stretches its own x axis and an upright glyph its y axis. lengthAdjust/vertical compares against resvg v0.49.0's 300x300 rendering. The vendored snapshot placed the sideways glyphs 0.28 units across the line from both Donner and resvg v0.49.0, which is consistent with a rendering from before the central baseline. Decorations follow the adjusted glyph extents; lengthAdjust/with-underline and text-decoration/with-textLength-on-a-single-character also compare against resvg v0.49.0, because the snapshot drew their underlines at 0.1em, twice the 0.05em thickness in the vendored Noto Sans post table.
textLength/zero compares with a 410-pixel budget because its reference comes from a different rasterizer generation (see Reference rasterizer generation budgets). Its glyph layout matches the vendored reference, where the last glyph's advance ends at x=20. All 396 differing pixels on both CPU tiers are the x=100 crosshair, which this PNG draws at alpha 128 and 192, exactly what 4x4 supersampled coverage produces for its 1.25-pixel stroke, while the eleven sibling textLength references, lengthAdjust/spacingAndGlyphs and Donner's analytic coverage draw it at 160 and 160. resvg v0.49.0's zero.png is not a replacement: its glyphs sit about 17.3 units right, a 1834-pixel difference.
Inline path, geometry references, side=right, method=stretch, and spacing=auto are implemented. Their focused geometry and renderer contracts are active. The vendored method=stretch image is identical to the align reference, so that corpus row stays skipped while independent curved-outline tests distinguish the methods. A fragment-free relative xlink:href under the secure static resource policy does not resolve to a same-document element or initiate an external fetch; its conflicting corpus image is also skipped. The TextPathInvalidInlineAndFragmentFreeHrefPaintsBlank case enforces that policy in //donner/svg/renderer/tests:renderer_public_api_tests and //donner/svg/renderer/tests:renderer_public_api_tests_geode using a visible control scene and a resource loader that records requests.
Remaining impact: 4 in text/textPath/: vertical/circular layout (complex.svg and writing-mode=tb.svg), kerning across mixed children, and the known filtered-glyph coverage residual. These belong to the vertical, shaping, and effects workstreams respectively.
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 | 0 | structure/svg/funcIRI-with-quotes.svg compares with ordinary budgets after the paint URL parsing fix, and structure/svg/nested-svg-one-with-rect-and-one-with-viewBox.svg compares with ordinary budgets after the viewport sizing fix. |
| structure/style | 1 | CSS @import / external CSS remains unsupported: imports are diagnosed and ignored, with no target requests; UnsupportedCssImportsNeverRequestConfiguredResponses in //donner/svg/renderer/tests:renderer_public_api_tests enforces that. |
| structure/symbol | 1 | transform on <symbol> (SVG2) |
| painting/image-rendering | 1 | Both backends implement the typed values, including two-stage non-integer pixelated scaling. on-feImage uses an independently derived nearest reference; optimizeSpeed compares against resvg v0.49.0 on TinySkia and is disabled on Geode, whose interpolated texture coordinate lands just below texel boundaries on pixel centers. |
| masking/clipPath | 0 | CSS circle shapes support fill, stroke and viewport reference boxes with bounded geometry preparation; two opaque reference images retain explicit background qualifications. The transformed-text case compares against resvg v0.49.0 with zero differing pixels; both use the glyph-cell text box. |
| masking/mask | 0 | Transformed group and rotated mask regions compare with ordinary budgets; mask-type alpha/luminance and mask color-interpolation sRGB/linearRGB are 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. Font fallback is per cluster, so the mixed-languages-with-tb* cases draw their Arabic words; vertical anchoring measures chunks between pen positions. They stay skipped because upright CJK sits 0.026em lower than the reference, and the underline case also lacks bidi runs and its vertical underline. |
An feImage referencing an SVG element uses the resolved primitive subregion origin for content placement, including individually omitted x or y. Placement uses the unclipped user-space origin; clipping the primitive or filter region does not move the referenced artwork. Normal referenced geometry retains its natural dimensions. The CPU and Geode paths share this rule. filters/feImage/with-x-y.svg and both with-x-y-and-protruding-subregion comparisons use ordinary budgets. The authored renderer oracle pins image-rendering="crisp-edges" and compares exact pixels for complete/partial coordinates, protrusion, and translated source content. Arbitrary host scale, rotation, skew, and reflection are not established by this bounded coverage; their existing exceptions remain tracked separately.
The exact placement oracle is FeImageFragmentUsesItsOwnSubregionOrigin in //donner/svg/renderer/tests:renderer_public_api_tests; the configured GPU target is //donner/svg/renderer/tests:renderer_public_api_tests_geode. The ordinary-budget corpus gates are //donner/svg/renderer/tests:resvg_test_suite_default_text and //donner/svg/renderer/tests:resvg_test_suite_geode; full-text uses the API with --config=text-full and //donner/svg/renderer/tests:resvg_test_suite_max. See Filter Effects.
Unknown SVG elements retain their DOM descendants but exclude the subtree from rendering, including after incremental source insertion. The sample viewer uses a real g for its selection chrome. structure/svg/rect-inside-a-non-SVG-element.svg and xmlns-validation.svg use ordinary comparisons. The constructors and namespace-aware source projection live in SVGUnknownElement.cc and SVGDocument.cc; renderer_public_api_tests covers parsed and edited wrappers, foreign namespaces, and promotion of a known SVG container. Namespace declaration edits re-evaluate the affected descendants while retaining their typed state for restoration; foreign shapes, text, stylesheets, paint references, filter children, and clip geometry are excluded from their consumers. This does not purge previously loaded or queued font faces from the existing resource cache.
The named rendering gates are //donner/svg/renderer/tests:renderer_public_api_tests and its configured renderer_public_api_tests_geode counterpart. Corpus enforcement uses //donner/svg/renderer/tests:resvg_test_suite_default_text, resvg_test_suite_geode, and resvg_test_suite_max for the configured text lane.
The XML byte parser accepts UTF-8 input and rejects declarations naming another encoding rather than interpreting those bytes as UTF-8. structure/svg/not-UTF-8-encoding.svg is an explicitly unsupported Windows-1251 input: transcode the bytes and update or remove the declaration first. //donner/base/xml:xml_tests and //donner/svg/parser:parser_tests cover rejection, UTF-8 BOM and case-insensitive declaration acceptance, and unrelated encoding-like content.
These have a question-mark reason in the file and need a root-cause pass to decide bug vs. out-of-scope:
Historical (UB) titles are not a specification oracle. Seven assigned PNGs are byte-for-byte copies of third_party/resvg-test-suite/resources/ub.png, whose SVG source draws a UB label. They remain Params::UndefinedBehavior corpus cases, with independent ReferenceContract* comparisons in RendererRegression_tests.cc. The other assigned fixtures do not use the UB marker, so they are skips with their reason. No reference bitmap or comparison tolerance is changed.
| Assigned fixture | Supported behavior and reference disposition |
|---|---|
| radialGradient/fr=-1 | Negative focal radius is invalid and applies no paint. UB marker reference. |
| radialGradient/fr=0.5 | Coincident start/end circles apply no paint. UB marker reference. |
| radialGradient/fr=0.7 | A start circle containing the end circle applies no paint; the reference instead paints a reversed gradient. Reference-disagreement skip, with an independent transparent-paint oracle. |
| radialGradient/invalid-gradientTransform | The authored all-zero matrix is singular, rather than malformed syntax. No paint; UB marker reference. |
| radialGradient/invalid-gradientUnits | The invalid enumeration is diagnosed and the initial objectBoundingBox value is retained. UB marker reference. |
| radialGradient/negative-r | Negative outer radius is invalid and applies no paint. UB marker reference. |
| fill/rgb-int-int-int | Space-separated RGB is supported CSS Color 4 syntax and matches the equivalent hex color. UB marker reference. |
| fill/valid-FuncIRI-with-a-fallback-ICC-color | The obsolete SVG 1.1 icc-color() paint grammar is unsupported. Invalid CSS declarations are rejected, preserving inherited paint; the historical reference expects a gradient. Params::DeprecatedInSVG2 (removed in SVG 2): rendered, not compared. |
| stroke-linejoin/arcs | The SVG Strokes draft join is not implemented; Donner retains its documented miter fallback. The reference is the UB marker, not an arcs rendering. |
| stroke-linejoin/miter-clip | Separate Geode/editor parity repair and ordinary reference restoration: #2057. |
| svg/no-size | An unsized root uses the caller canvas (512x512 by default) without fitting content bounds. The reference fits the roughly 200-unit artwork to 500 pixels. API-policy skip; caller-canvas geometry has an independent assertion. |
| font/simple-case | The PNG is 400x400, so the harness preserves that explicit canvas. The unitless font presentation shorthand is a Donner extension and matches expanded longhands. The hermetic set has no BoldItalic face: style-first matching selects NotoSans-Italic at weight 400. The vendored PNG synthesizes bold over that face with wider advances, which neither CPU text tier does for document fonts, while resvg v0.49.0 rejects the unitless size and draws 12px default serif text. Reference-disagreement skip (#1167). |
SVG 2 radial-gradient rules and error processing establish the radial error and containing-circle expectations. The independent tests preserve visible sibling artwork and a valid zero-radius last-stop control. Both backends reject negative radii before solid-color shortcuts, including single-stop gradients and an outer radius within the CPU near-zero tolerance. CSS Color 4 defines the RGB syntax; SVG 2 paint grammar does not include the old icc-color() suffix. The arcs, unitless font shorthand, caller canvas and zero-radius controls are explicitly project compatibility contracts, not claims of broader SVG 2 conformance.
The focused regression targets are renderer_regression_tests, renderer_regression_tests_geode and renderer_regression_tests_text_full. The corpus targets are resvg_test_suite_default_text, resvg_test_suite_geode and resvg_test_suite_max. The existing Geode corpus text capability gate still skips the text sample; the independent Geode font contract test executes directly. Render-only corpus execution of the UB-marker cases establishes stability, not pixel agreement with the historical reference.
| Category or case | Tests | State and why |
|---|---|---|
| filters/enable-background | 17 | Category default Params::DeprecatedInSVG2(...): removed in SVG 2 (→ <filter> chains / backdrop-filter). The other four of the 21 cases already match the reference with SVG 2 semantics and compare. See unsupported_svg1_features.md. |
| filters/filter in=Background*-with-enable-background | 2 | Params::DeprecatedInSVG2: the BackgroundImage/BackgroundAlpha snapshot comes from enable-background. |
| text/tref, painting/display/none-on-tref | 9 + 1 | Params::DeprecatedInSVG2: <tref> removed in SVG 2. |
| text/kerning | 2 | Params::DeprecatedInSVG2: kerning property removed in SVG 2. |
| text/glyph-orientation-horizontal | 1 | Params::DeprecatedInSVG2: removed in SVG 2. glyph-orientation-vertical is implemented and keeps a residual skip. |
| painting/fill icc-color, valid-FuncIRI-with-a-fallback-ICC-color | 2 | Params::DeprecatedInSVG2: SVG 1.1 icc-color() paint is not in the SVG 2 paint grammar. |
| structure/style-attribute, structure/style non-presentational-attribute | 2 | Params::DeprecatedInSVG2: <svg version="1.1"> semantics; SVG 2 removed the version attribute. |
| masking/clip | 1 | Params::DeprecatedInSVG2: the CSS clip property is deprecated by CSS Masking in favor of clip-path. |
| text/dominant-baseline reset-size, use-script | 2 | Params::DeprecatedInSVG2: SVG 1.1 values removed in SVG 2; UB marker references. |
| paint-servers/radialGradient | 1 | fr=0.7 is a reference-disagreement skip with the containing-circle oracle above. focal-point-correction compares normally. |
| Other Params::UndefinedBehavior cases | 51 | Undefined or implementation-defined behavior: 50 on the resvg UB marker reference plus feGaussianBlur/huge-stdDeviation; we verify no-crash only. |
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:
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.
text-decoration-thickness accepts auto, from-font, absolute lengths, em, rem, and percentages. The font-metric units ex/ch and viewport units vw/vh/vmin/vmax are rejected until their computed-value context is available in the style pipeline. Explicit percentages use the originating decoration's used font size; font-relative lengths compute before explicit inheritance. The supported text-decoration shorthand resets omitted thickness to auto and preserves independent cascade priorities. Draft line-width keywords, underline offsets, shadows, and emphasis remain unsupported.
Each originating declaration retains its own font, paint and thickness, including repeated underline declarations on nested spans. The software and GPU renderers consume the same bounded decoration geometry. from-font uses the preferred underline width for all line kinds and falls back to automatic metrics when unavailable. Automatic placement retains the existing behavior.
Immutable decoration lists are limited to 64 originating declarations per span and charged to the shared geometry budget. Snapshot copies retain and remap their own font and paint resources; a refused list suppresses those decorations. New live-bitmap comparisons in text_decoration_thickness_tests use synthetic font metrics and the canonical exact comparator.
Underlines and overlines paint beneath a run's glyphs and line-throughs above them, following the Painting Order of Text Decorations in CSS Text Decoration 3, which SVG 2 section 11.11 adopts. Each decoration's own fill and stroke follow the paint-order of the element whose paint it uses. Both renderers make two decoration passes per run around the glyph paint, and text_decoration_paint_order_tests compares overlapping glyph and decoration colors exactly. With per-origin paint and this order, text/text-decoration/indirect-with-multiple-colors compares; its remaining residual is the reference rasterizer generation's coverage on the x=100 crosshair.
text-decoration-line, text-decoration-style and text-decoration-color are CSS properties; the text-decoration shorthand sets all four longhands and resets omitted components. All four are not inherited. SVG 2 section 6.6 lists only text-decoration as a presentation attribute, so the longhands are accepted in style sheets and style attributes rather than as attributes.
text-decoration-style draws solid, double, dotted, dashed and wavy lines from the solid band of thickness t (CSS Text Decoration 3 leaves the proportions to the user agent): double adds a second band after a gap of t, away from the text (a line-through straddles its solid position); dotted draws round dots of diameter t every 2t; dashed draws dashes 3t long every 5t; wavy draws a band of thickness t along quadratic half-waves of amplitude t whose length divides the segment into the whole count nearest 2t. Styled geometry is charged point by point to the per-draw decoration work allowance, which each text draw takes from a per-frame allowance of 4 Mi work units on the text materialization budget. A line that would exceed the per-draw allowance is not drawn and spends the rest of it, so the exhausted allowance also suppresses later decorations of that draw and, once the frame allowance is spent, of the frame. The frame's text itself is not rejected.
SVG 2 section 11.11 paints a decoration with the fill and stroke of the element that declares it and does not define text-decoration-color. Its initial value in CSS Text Decoration 3 is currentcolor, which would replace that fill for every SVG decoration, so Donner treats the unset longhand as "use the declaring fill" and an author-specified color, including currentcolor, as the decoration's fill color. The decoration keeps the declaring element's stroke and fill opacity, and currentcolor resolves against the declaring element's color. text_decoration_style_tests compares each style and color rule exactly against authored scenes.