Grimoire native · rendering research · Oct 27 2026

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.

0.0000 pxmax difference in glyph positions
0.784×Rune's ink vs macOS at 2×
4.92 → 0.55mean diff after copying macOS (0–255)
2 of 8factors that can't be matched without CG

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.

identicalfirst image lighter than macOSfirst image darker than macOS
Rune app vs macOS, 2×: red outlines around almost every stroke, because macOS text is heavier.
Rune with macOS's blending, positioning and smoothing copied vs macOS, 2×: most of the red is gone.
Raw coverage only, every other factor matched: what's left is the two rasterisers disagreeing on curves.
Rune app vs macOS, 1× (scaled up): hinting and an integer-baseline quirk make 1× worse.

Setup

Headline numbers

differing pxoff by >8maxmeanabsRune ink ÷ macOS ink
2×: app vs CoreText (defaults)8.4%7.2%2004.920.784
1×: app vs CoreText (defaults)15.1%12.8%1726.600.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 >8maxmeanabsink ratiomeanabs
0. app as shipped (coverage^0.806, linear blending, ¼-px round)7.22%2004.920.784
1. no coverage gamma7.31%2025.440.756
2. + blending in sRGB-encoded space, like macOS6.71%2003.780.856
3. + macOS x quantisation (floor to ¼ pt = ½ px at 2×)6.49%1373.120.856
4. + macOS-style smoothing dilation (+0.375 px in x split evenly, +0.375 px upward)2.43%660.550.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

  1. 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. CTLineDraw and CTFontDrawGlyphs at parley's positions give byte-identical images, and the baseline matches (117 px).

    One exception: NSAttributedString/NSTextView with 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 to CTFontDrawGlyphs.

  2. 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.
  3. 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 …UnormSrgb target, so blending happens in linear light, which thins dark-on-light text. Rune's glyph_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.cacheDisplay uses it.
    • An extended-linear sRGB context (linear blending, like Rune) is lighter still: ink 0.895.
  4. 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.

  5. 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.

  6. 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.
  7. Atlas, sampling and output: nothing

    Quads sit on whole pixels, textureLoad reads texels unfiltered, the baseline rounds to a whole pixel, and the CPU copy proves none of this adds anything beyond ±1 rounding. Rune's CAMetalLayer has no colorspace set; if the compositor treats the content as display-native it would shift these greys by at most 1 level (unconfirmed).

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

"The qui…", Rune app vs macOS, 2×.
Same, with macOS behaviour copied into Rune's pipeline.
Kerning pairs "AV To", 2×: same positions, different weight.
"fi fl", 2×: no ligatures in either.
"The qui…", 1×: hinting moves stems in Rune.
"AV To", 1×.

What it would take to match each factor

sourceeffect (2×)to matchexact?
Layoutnonealready identicalyes, already
Font smoothing dilationink +17%; >8 px share 6.5% → 2.4% once copiedembolden outlines in the rasteriser (x split evenly, y upward only) by the measured size- and luminance-dependent amountsno — Apple's algorithm (luminance buckets, cap, curves) is unpublished
Blending spaceink 0.916 → 1.000blend text in sRGB-encoded space (non-sRGB target or manual blending) and drop glyph_gammayes (±1)
x quantisationmeanabs 3.78 → 3.12, max 200 → 137floor to ¼ pt (½ px at 2×) instead of rounding to ¼ pxyes
Hinting (1×)large at 1×, none at 2×turn swash hinting off at 1×yes
Rasteriser on curves>8 px share 1.5%, max ~61rasterise glyphs with CoreGraphics into the atlasonly by using CG
Integer-baseline quirk1× only, ~half the 1× leftoveronly by using CGno, with our own rasteriser
Surface colour management≤1 level for these greysset CAMetalLayer.colorspace to sRGByes
The only route to identical pixels: have CoreText/CoreGraphics rasterise each glyph and subpixel phase into the atlas (with smoothing, keyed by text-colour luminance bucket, since macOS's dilation depends on it), blend in sRGB-encoded space, and quantise to ¼ pt. Anything short of using CoreGraphics gets to "looks the same" (meanabs 0.55, max 66) but not byte-identical.

Files