dobrovolskiy.com
RU
← Notes2026-08-31 · by Dmitriy Dobrovolskiy

A dependency-free SVG renderer in C11: in-place XML, exact-area coverage, and a thread pool

How a complete 2D vector stack fits in 4 280 lines of C with nothing beyond the Win32 API: an in-place XML parser, an SVG compiler with CSS selectors and GDI glyph outlines, and an analytically exact anti-aliased rasterizer that draws 1.9 M edges at 1600x1600 in about 60 ms.

SVGViewer is a Windows SVG viewer written in pure C11 with no third-party code at all: 16 files, 4 280 lines, linking only gdi32, comdlg32, shell32 and user32. It parses XML in place into an arena, compiles the DOM to a flat shape list, and rasterises with a font-rs style exact-area signed-coverage accumulator across a persistent thread pool. A 20 000-path, 1.9 M-edge stress file renders at 1600x1600 in about 60 ms on an 8-thread laptop; ordinary icons take 1 to 3 ms.

Why write one at all

Every SVG viewer I had on Windows was either a browser or a 40 MB Electron app that took two seconds to show a 12 KB icon. The pieces of a vector graphics stack are all well understood - XML in, shapes in the middle, coverage out - so I wrote the whole thing: 16 files, 4 280 lines of C11, linking nothing but gdi32, comdlg32, shell32 and user32. Built with one gcc line under MinGW-w64.

The interesting part is not that it works. It is which four decisions make it fast.

1. Parse in place, allocate in an arena

xml.c never copies a string. The file is read once into a buffer, and the DOM holds pointers and lengths into that buffer, with entity handling done by rewriting in place. Nodes and attributes come from a bump allocator (arena.h), so parsing allocates in a handful of large blocks and frees in one instruction. Element id lookups - which use, href gradients and clip-path need constantly - go through a hash map, not a tree walk. There is also a custom float parser, because strtod is locale-aware and surprisingly slow when you call it a few million times on path data.

2. Compile the DOM into a flat shape list

svg.c turns the tree into a flat array of shapes: geometry, paint, transform, clip. All path commands, absolute and relative, compact arc flags; linear and radial gradients with href inheritance, gradientUnits, gradientTransform and spreadMethod; clipPath and mask (as a geometric clip); use/symbol expansion; <style> CSS with tag, .class, #id, tag.class and .a.b selectors; 148 named colours; viewBox with preserveAspectRatio; and lengths in px, pt, pc, mm, cm, in, em, ex and %.

Text is the part people expect to be hard. It isn't, if you refuse to write a font engine: glyph outlines come from the installed system fonts through GDI's GetGlyphOutline, and then they are ordinary paths. Which means text is filled, stroked, clipped, transformed and anti-aliased by exactly the same code as a rectangle, and supports tspan nesting, text-anchor, letter-spacing and font substitution for free.

3. The rasterizer: exact area, no supersampling

This is the heart of it. raster.c uses a font-rs style signed-coverage accumulator: for each edge you add its exact area contribution to a per-pixel accumulation buffer, then a prefix sum along each row turns accumulated deltas into coverage. Two consequences matter:

Fills use the accumulated winding number for nonzero and even-odd. Strokes are built as unions of consistently oriented quads plus joins and caps, then filled nonzero - so stroking needs no separate rasterisation path at all. Gradients are evaluated through 256-entry premultiplied lookup tables.

4. Parallelise both halves, keep the pool alive

Two phases, both across all cores on a persistent thread pool that is never torn down between frames:

On top of that, only shapes that intersect the viewport are prepared at all, and the view is a plain affine matrix, so pan and zoom are just a re-render with different numbers.

The numbers

On an 8-thread laptop, a stress file of 20 000 paths and 1.9 M edges renders at 1600x1600 in about 60 ms. Ordinary icons and illustrations: 1 to 3 ms. The title bar shows parse time, render time, shape and edge counts and the thread count live, and T toggles single-threaded mode, so every claim here is one keypress from being checked. There is also a headless mode - svgviewer.exe file.svg -o out.png -n 10 - that writes the PNG plus a timings file, which is what I actually benchmark with.

What is deliberately missing

No image, no filters, no patterns, no textPath curvature, and masks are approximated by their geometry rather than by luminance. Filters in particular would double the size of the project and are not what a viewer is for. The app icon, incidentally, is generated by the viewer itself from icon.svg.

What I'd do differently

I would write the headless PNG path and the timing report on day one instead of near the end. Every performance decision above was settled by measurement, and for the first stretch I was measuring by watching a title bar - which is fine for eyeballing, useless for comparing two builds. The second thing: the persistent thread pool should have existed from the first parallel commit. Spawning threads per frame hid the real cost of the prepare phase behind scheduler noise for longer than I would like to admit.

FAQ

How large is it?

16 files and about 4 280 lines of C11: an in-place XML parser, the SVG compiler, the rasterizer, GDI glyph outlines, the Win32 window with its DIB back buffer and PNG export, and a bump allocator. No third-party code at all - it links gdi32, comdlg32, shell32 and user32.

Why is exact-area coverage faster than supersampling?

Because the cost of an edge is proportional to the number of rows it spans rather than to a sample count per pixel, and because the anti-aliasing it produces is analytically exact - so there is no quality reason to raise a sampling rate that does not exist. Fills read the accumulated winding number directly for nonzero or even-odd.

How is text rendered without a font engine?

Glyph outlines are pulled from the installed system fonts through GDI's GetGlyphOutline and then treated as ordinary paths. Text is therefore filled, stroked, transformed, clipped and anti-aliased by the same rasterizer as every other shape, with tspan nesting, text-anchor, letter-spacing and font substitution supported.

Want something like this?

I build it live on a Zoom call, you watch the screen, the timer stops when you say stop. First 15 minutes are free.

$50 / hourBook an hour

More projects

SVGViewer: C11 rendererA dependency-free SVG renderer in C11 with its own XML parser and multithreaded analytic rasterizer.