Rune text vs native macOS text,
pixel by pixel
The same paragraph, in the same font, size, colour and position, drawn by our Rune app and by macOS itself (CoreText). The layout matches exactly; every pixel that differs comes from how glyphs are rasterised and blended. Matching exactly would need CoreGraphics to draw the glyphs.
See it
Drag across each image to swipe between the two renders, or switch to Flicker to alternate them in place (the easiest way to see the weight difference). Zoom shows the pixels at 3×.
Where the pixels differ
Heatmaps over the text band. Values are amplified 4× so small differences show.




Setup
- Machine: MacBook, macOS 15.3, LG UltraFine 2× display.
- Text style (the app's body style from
theme.rs): Inter 4.1 Regular fromcrates/app/assets/fonts, 16 pt, line height 26 pt, ink#1d1e22on paper#fcfcfb. One plain paragraph, no Markdown. - Rune app: headless
--snapat 2× and 1×, 640 pt window (sidebar hidden), caret moved below the text. Text box at (49, 40) pt, wrap width 567 pt; both renderers break lines in the same three places. - CPU copy of Rune: the same parley 0.12 / swash 0.2.10 code as Rune's
text.rs, with the shader's compositing redone on the CPU. It matches the app's GPU output except for ±1 rounding in 0.3% of pixels, so each factor below was switched on or off inside the real pipeline. - macOS reference (Swift): the same TTF, 16 pt at 2× backing, into an sRGB premultiplied RGBA8
CGBitmapContext. Modes:CTLineDraw,CTFontDrawGlyphsat parley's positions,NSAttributedString.draw,NSTextView.cacheDisplay, alpha-only coverage; switches for font smoothing, subpixel positioning/quantisation and colour space. - Measured over the text band only: 1280×180 px at 2×, 640×90 px at 1×. "meanabs" = mean absolute difference per channel on 0–255; "ink" = summed darkness relative to macOS.
- Not done: a real window capture (no Screen Recording permission over ssh; no settings were changed).
Headline numbers
| differing px | off by >8 | max | meanabs | Rune ink ÷ macOS ink | |
|---|---|---|---|---|---|
| 2×: app vs CoreText (defaults) | 8.4% | 7.2% | 200 | 4.92 | 0.784 |
| 1×: app vs CoreText (defaults) | 15.1% | 12.8% | 172 | 6.60 | 0.727 |
macOS text is about 27% darker at 2× and 37% darker at 1×. The glyphs sit in exactly the same places.
The difference, factor by factor (2×)
Each row changes one more thing in Rune's pipeline (via the CPU copy) and compares against macOS defaults.
| step (Rune setting changed) | off by >8 | max | meanabs | ink ratio | meanabs |
|---|---|---|---|---|---|
| 0. app as shipped (coverage^0.806, linear blending, ¼-px round) | 7.22% | 200 | 4.92 | 0.784 | |
| 1. no coverage gamma | 7.31% | 202 | 5.44 | 0.756 | |
| 2. + blending in sRGB-encoded space, like macOS | 6.71% | 200 | 3.78 | 0.856 | |
| 3. + macOS x quantisation (floor to ¼ pt = ½ px at 2×) | 6.49% | 137 | 3.12 | 0.856 | |
| 4. + macOS-style smoothing dilation (+0.375 px in x split evenly, +0.375 px upward) | 2.43% | 66 | 0.55 | 0.982 |
Against macOS with smoothing off, steps 2 and 3 alone get to 1.50% of pixels off by more than 8, max 61, meanabs 0.33, ink 1.000. That last gap is the two rasterisers.
At 1× the same chain goes 12.8% → 12.2% (hinting off) → 11.5% → 10.9% → 8.6% (meanabs 6.60 → 2.40). The 1× leftover is bigger because of factor 8.
Each source in detail
Layout: no difference
Glyph IDs, kerning (GPOS: AV −2.19 px, LT −3.08 px, To −2.50 px), x positions (parley vs
CTRunGetPositions, max |Δx| = 0.0000 px at both scales) and line breaks are identical. Neither applies an fi/fl ligature with this font.CTLineDrawandCTFontDrawGlyphsat parley's positions give byte-identical images, and the baseline matches (117 px).One exception:
NSAttributedString/NSTextViewwith a fixed 26 pt line height puts all the extra leading above the line, so the baseline lands 3.5 pt (7 px) lower. Shifted by 7 px, it's byte-identical toCTFontDrawGlyphs.Font smoothing ("stem darkening"): the biggest factor, +17% ink
Smoothing on vs off in macOS changes 6.5% of pixels by more than 8 (max 139). Measured on straight stems:
- It dilates the outline, it isn't an alpha curve: with our ink at 32 px it adds 0.1875 px to each side in x and 0.375 px to the top edge only.
- Pixels with zero coverage pick up ink.
- The amount grows with size (≈3/256 px per px of em: 16 px +0.19, 24 px +0.28, 32 px +0.375 total), up to a cap.
- It depends on the text colour's luminance: dark text +0.375, mid-grey and coloured text +0.53, light-on-dark +0.60.
Copying it in swash (
embolden(0.1875)plus a −0.1875 px x shift) takes meanabs from 3.12 to 0.55. What's left is in counters and curves, where macOS offsets the outline differently.
macOS with smoothing off (top) vs on (middle), and the difference: smoothing alone thickens every stroke. Blending space
macOS blends coverage in the context's encoded values: with an sRGB context, the result is simply
ink·a + paper·(1−a)on sRGB bytes (reproduced within 2 levels). Rune renders into an…UnormSrgbtarget, so blending happens in linear light, which thins dark-on-light text. Rune'sglyph_gamma(0.806 for this ink) gives some weight back, but not all: ink ratio 0.916 vs 1.000 with sRGB blending.- Display P3 is within 1 level of sRGB; DeviceRGB is identical to sRGB here.
- Generic RGB (gamma 1.8) is lighter (max 15, ink 0.972) — and
NSView.cacheDisplayuses it. - An extended-linear sRGB context (linear blending, like Rune) is lighter still: ink 0.895.
Subpixel x positioning
Rune rounds glyph positions to ¼ device pixel. macOS (quantisation on, the default) floors to ¼ point: ½ px at 2×, ¼ px at 1×. Matching macOS's grid cuts the 2× coverage difference from max 169 / meanabs 2.66 to max 71 / 0.38.
Hinting (1× only)
Rune turns swash hinting on below scale 1.5; CoreText never hints. Hinted vs unhinted Rune at 1× differs in 7.5% of pixels by more than 8 (max 202). It moves stems and heights rather than changing the amount of ink. At 2× there's no hinting.
Rasteriser differences on curves
Both compute analytic area coverage. On straight edges they agree within 1 level at every subpixel phase. On curves swash has more coverage on outer edges and macOS more on inner (counter) edges, which looks like different curve flattening. With everything else matched: 1.5–1.6% of pixels off by more than 8, max ≈61–71, meanabs 0.33. Total coverage agrees to 0.2%.

Raw coverage, swash vs CoreGraphics, everything else matched. Atlas, sampling and output: nothing
Quads sit on whole pixels,
textureLoadreads texels unfiltered, the baseline rounds to a whole pixel, and the CPU copy proves none of this adds anything beyond ±1 rounding. Rune'sCAMetalLayerhas nocolorspaceset; if the compositor treats the content as display-native it would shift these greys by at most 1 level (unconfirmed).An unexplained macOS quirk at integer baselines (small sizes)
At 10, 12 and 16 px, a glyph drawn at an exactly integer baseline comes out of CoreGraphics differently from the same glyph 1/16 px off (the "T" bar's top rows read 255/180 in macOS vs 164/219 in swash). At every other phase they agree within 1, and it doesn't happen at 14 or 18–32 px. Moving both baselines 1/64 px off halves the 1× leftover (meanabs 1.69 → 0.79). It's undocumented and no public switch turns it off.
Zoomed glyphs
8–14× nearest-neighbour. Rows: Rune (or the first image), macOS, then the difference (red = Rune lighter, blue = Rune darker).






What it would take to match each factor
| source | effect (2×) | to match | exact? |
|---|---|---|---|
| Layout | none | already identical | yes, already |
| Font smoothing dilation | ink +17%; >8 px share 6.5% → 2.4% once copied | embolden outlines in the rasteriser (x split evenly, y upward only) by the measured size- and luminance-dependent amounts | no — Apple's algorithm (luminance buckets, cap, curves) is unpublished |
| Blending space | ink 0.916 → 1.000 | blend text in sRGB-encoded space (non-sRGB target or manual blending) and drop glyph_gamma | yes (±1) |
| x quantisation | meanabs 3.78 → 3.12, max 200 → 137 | floor to ¼ pt (½ px at 2×) instead of rounding to ¼ px | yes |
| Hinting (1×) | large at 1×, none at 2× | turn swash hinting off at 1× | yes |
| Rasteriser on curves | >8 px share 1.5%, max ~61 | rasterise glyphs with CoreGraphics into the atlas | only by using CG |
| Integer-baseline quirk | 1× only, ~half the 1× leftover | only by using CG | no, with our own rasteriser |
| Surface colour management | ≤1 level for these greys | set CAMetalLayer.colorspace to sRGB | yes |
Files
- Renders: app 2×, app 1×, CPU copy of Rune 2×, CoreText 2×, CoreText 1×, CoreText 2× no smoothing, CTLineDraw 2×, AppKit 2×, Rune emulating CG 2×.
- Experiment code (Swift reference renderer, edge probe, Rust CPU copy of Rune's text path) lives on the Mac in
~/tmp/pixel-compare/. No project code was changed.