A GPU vector graphics renderer for Apple platforms, 2–5× faster than Vello and Skia on real-world documents.
Rasterizer turns vector paths into reference-quality anti-aliased pixels with Metal. It draws maps, engineering drawings and illustrations with hundreds of thousands of paths at interactive frame rates, and loads SVG and PDF files directly.
Wall time per frame in milliseconds, fitting each file to a 2048×2048 view on a MacBook Air with an M4. Lower is better.
| File | Draws | Rasterizer | Vello | Skia Graphite | Skia Ganesh |
|---|---|---|---|---|---|
| tiger | 301 | 1.35 | 1.43 | 0.86 | 3.56 |
| london-rail | 10,643 | 1.76 | 3.90 | 7.83 | 8.36 |
| Paris 30K | 50,791 | 4.94 | 28.09 | 34.58 | 66.08 |
| spoorkaart | 84,287 | 9.87 | 44.34 | 66.27 | 327.85 |
| 6000-DRG | 414,657 | 20.04 | 129.98 | 224.56 | 233.40 |
| pathological | 580,473 | 18.14 | 106.74 | 267.01 | 324.32 |
Across all 20 test files, Vello takes 2.2× as long as Rasterizer, Skia Graphite 2.4× and Skia Ganesh 5.0× (geometric means). The advantage grows with the number of paths: on the 7 documents with more than 40,000 draws, Vello takes 2–6.5× as long and Skia 6–33×. Small scenes, under about 1,000 draws, take under 2 ms in Rasterizer, Vello and Skia Graphite, and Graphite is fastest on 5 of the 6. On an M4 Max, with about four times as many GPU cores, Rasterizer's lead is wider.
The benchmark is a single self-contained script that builds every library and replays the same geometry in each. See Benchmarks for the method, all the results and the caveats.
- Fills with even-odd and non-zero fill rules, following the PostScript model
- Strokes with butt, square or round caps, miter or round joins, and widths in user or device space
- Dashes, by making a dashed copy of a path
- Linear and radial gradients, and images
- Clipping to rectangles, or to anti-aliased clip paths with either fill rule
- Blend modes: all of Core Graphics' separable and non-separable modes
- Text laid out by Core Text
- SVG import via NanoSVG
- PDF import, with a parallel content-stream parser that spreads each page across all CPU cores
- Editable scenes: change a draw's transform, color or width in place, without rebuilding the scene
- Up to 120Hz on ProMotion displays
- A CPU reference renderer using Core Graphics, for comparison
Rasterizer is a Swift package, with its sources in Package/Sources, and three libraries:
| Library | What it is |
|---|---|
RasterizerCpp |
The C++17 renderer core |
RasterizerObjC |
The API used from Swift and Objective-C, and RasterizerView, a Metal-backed UIView or NSView |
RasterizerSwift |
Swift helpers, such as fitting a scene to a view |
In Xcode, choose File ▸ Add Package Dependencies…, enter https://github.com/mindbrix/Rasterizer, choose Up to Next Minor Version from 0.1.0, and link RasterizerObjC, plus RasterizerSwift if you want the helpers. Or, in a Package.swift:
dependencies: [
.package(url: "https://github.com/mindbrix/Rasterizer", .upToNextMinor(from: "0.1.0")),
],
targets: [
.target(name: "MyApp", dependencies: [
.product(name: "RasterizerObjC", package: "Rasterizer"),
.product(name: "RasterizerSwift", package: "Rasterizer"),
]),
]Until 1.0.0, minor versions may make breaking changes, so this takes fixes (0.1.x) but not the next minor version. See the changelog for what's in each release. To work on Rasterizer alongside your app, add your clone with Add Local… instead.
A RasterizerView asks its delegate each frame whether to redraw, and for the list of scenes to draw:
import UIKit
import RasterizerObjC
class ViewController: UIViewController, RASceneListDelegate {
let list = RASceneList()
var redraw = true
override func loadView() {
let view = RasterizerView()
view.listDelegate = self
self.view = view
}
override func viewDidLoad() {
super.viewDidLoad()
let scene = RAScene()
let orange = RAPaint(red: 1, green: 0.45, blue: 0.1, alpha: 1)
scene.addFill(RAPath(ellipse: CGRect(x: 40, y: 40, width: 240, height: 240)), ctm: .identity, color: orange, evenOdd: false)
scene.addStroke(RAPath(rect: CGRect(x: 20, y: 20, width: 280, height: 280)), ctm: .identity,
color: RAPaint(gray: 0.1, alpha: 1), width: 8, capStyle: .capRound, joinStyle: .joinRound)
list.add(scene, ctm: .identity, clip: .zero)
list.clearColor = RAPaint(gray: 0.95, alpha: 1)
}
// RasterizerView asks every frame, & draws when this returns true
func shouldRedraw(atTime time: Double, scale: Double, width: Double, height: Double) -> Bool {
redraw
}
func getListAtTime(_ time: Double, scale: Double, width: Double, height: Double) -> RASceneList {
redraw = false
return list
}
}On macOS, it's the same code with NSViewController.
Load an SVG or a PDF page, and fit it to the view:
import RasterizerSwift
let scene = RAScene()
let ctm = scene.addSvg(from: svg) // Or scene.addPdf(from: pdf, pageIndex: 0)
let list = RASceneList()
list.add(scene, ctm: ctm, clip: .zero)
list.ctm = view.bounds.fitTransform(b: list.bounds) // Fit it to the viewEdit draws in place, for example to animate them:
scene.updateDraws(in: NSRange(location: 0, length: scene.count)) { index, draw in
draw.color = RAPaint(hue: Double(index) / Double(scene.count), saturation: 0.8, value: 0.9, alpha: 1)
return true // Changed, so it's redrawn
}Changing a draw's transform, color or stroke width is cheap. Changing its path, its visibility, or turning a stroke into a fill means the scene is prepared again.
Both build out of the box, as all dependencies are included.
Open RasterizeriOS/RasterizeriOS.xcodeproj.
- Tap to step through the bundled SVGs and your own files.
- Drag and pinch to pan and zoom.
- The folder button imports SVG and PDF files, which are kept between launches. The trash button removes the one showing.
- PDFs with more than one page get a page stepper.
- The grid button animates every draw into a grid of cells, and back.
- Files can also be sent to the app with Share in the Files app.
Open Rasterizer.xcworkspace and run the Run Rasterizer scheme for the best performance, or Debug Rasterizer for debugging.
- Open SVG and PDF files, and use the
-and+keys to change PDF page. - Drag and zoom or rotate with the trackpad or mouse. Hold
Shiftto zoom or rotate around the pointer. - The HUD, top left, lists the keys.
0switches between Rasterizer and Core Graphics,Oshows path outlines, andTshows an animated clock built from text. - Set the font with the fonts panel (
⌘T).
Path objects follow the PostScript model. Scene objects group paths with their draw parameters: color, transform, stroke width, flags, and an optional clip. SceneList objects group scenes, each with its own transform and clip, for rendering.
- Filled paths are rasterized in two stages: first to a float mask buffer, then to the color buffer.
- Small screen-space fills, such as glyphs, are the fastest case, as their path geometry can be copied straight into GPU memory.
- Larger fills use a fat scanlines algorithm on clipped, device-space geometry.
- Pixel coverage is calculated with a novel windowed inverse-lerp algorithm.
- Strokes are rasterized straight to the color buffer, using GPU triangulation.
- Quadratic Bézier curves are solved on the GPU, so curve geometry can stay coarse.
- Clip paths are rendered to an anti-aliased coverage mask within the same render pass.
- The CPU stages use simple batch parallelism.
The GPU holds no state. Each frame is written afresh to double-buffered shared memory. In practice, copying is cheaper than managing GPU-resident paths in a parallel context.
Inspired by my love of Adobe Flash, I started work on a GPU-accelerated 2D vector graphics engine for the original iPhone, and then the Mac. Three iterations and many years later, Rasterizer is the result.
The long gestation came from endlessly iterating over one core problem: turning vector paths into reference-quality anti-aliased pixels efficiently. Some of the solutions may extend the state of the art.
Many thanks to the creators of:
Rasterizer is licensed under the MIT license. NanoSVG and xxHash keep their own licenses.
You can show appreciation for my work here.
