mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-26 16:10:21 +00:00
64 lines
3.7 KiB
Markdown
64 lines
3.7 KiB
Markdown
# Text rasterization: what a type render costs
|
||
|
||
Measured 2026-10-17 on Windows 11, `i5-10300H` (4 cores / 8 threads, 16 GB), release build,
|
||
`rustc 1.98.1`, with a probe that renders one paragraph layer through
|
||
`photocraft_text::render::rasterize_warped` and times each stage of it.
|
||
|
||
Document: 300 dpi, a 2400 × 1600 px paragraph box holding 4000 characters at 14 pt — the worst
|
||
case a type edit has (a whole page of text at print resolution; 1851 laid-out glyphs, 2329 × 1610 px
|
||
of ink = 3.75 MP).
|
||
|
||
| Stage | ms | What it does |
|
||
|---|---:|---|
|
||
| ink rectangle (`ink_rect_warped`) | 4.1 | its own pass over every glyph, to size the surface |
|
||
| draw (`draw_pass` into `Coverage`) | 18.6–22.9 | glyph outlines, curve flattening (0.03 px tolerance), winding scanlines |
|
||
| prefix sum (`Coverage::finish`) | 7.0–8.1 | resolves the winding accumulator to coverage 0…1, one running sum over 3.75 M pixels |
|
||
| composite | 22.6–33.4 | each colour's coverage over the premultiplied accumulator |
|
||
| unpremultiply | 11.2–14.0 | divides the accumulator back to straight colour |
|
||
| `Surface::write_region` | 23.8 | converts to the document format and fills the tiles |
|
||
| `Surface::prune` | 3.1 | drops tiles that came back the layer's default |
|
||
| **total** | **86–104** | |
|
||
|
||
So the glyph geometry — what one would expect to dominate — is under a quarter of it. The rest is
|
||
scalar work over the pixel buffers, and every one of those passes is per pixel: a pixel's maths
|
||
reads and writes only its own slots.
|
||
|
||
## What runs in parallel now
|
||
|
||
`composite`, `unpremultiply` and the tile conversion run on all cores (rayon, native only; the web
|
||
build keeps the serial loops, since wasm has no threads). The rasterizer writes its tiles with
|
||
`Surface::write_region_par`; `Surface::write_region` stays serial, because mask builds call it inside
|
||
a `OnceLock` initializer, which must not enter rayon. Nothing about them
|
||
is order-dependent, so each pixel still sees the same operations on the same inputs — the pixels
|
||
are identical, not merely close.
|
||
|
||
| | before | after |
|
||
|---|---:|---:|
|
||
| the 3.75 MP case above | 98.3–103.9 ms | 60.3–61.8 ms |
|
||
|
||
`crates/text/src/tests.rs::parallel_raster_matches_a_single_thread` renders the shapes the pipeline
|
||
can take (plain, faux bold italic, no antialias, wide gamut, a wrapping paragraph box; 8/16/32-bit)
|
||
on a one-thread pool and on the default pool and compares the surfaces; `crates/raster`'s existing
|
||
`region_writes_match_scalar_bytes_for_every_format` compares `write_region` and `write_region_par`
|
||
against a scalar reference implementation, tile for tile.
|
||
|
||
## What stays on one thread, and why
|
||
|
||
- **draw** (19–23 ms): the winding accumulator carries across scanlines and rows — a pixel's value
|
||
depends on the rows above it, and one scanline can spill into the next row's first pixel. A band
|
||
split can be exact (bands keep absolute coordinates, carry the spill in band order, and are
|
||
assembled before the prefix sum), but each band re-traverses every glyph — the traversal is not
|
||
free, so the win is bounded by the band count. Worth doing when the draw matters more than it does
|
||
now.
|
||
- **prefix sum** (7–8 ms): a running f32 sum over the whole image. Splitting it per row needs the
|
||
row's starting sum, which is that same sequential chain — the only exactly-known starting sums are
|
||
the ones the chain computes.
|
||
- **ink rectangle** (4.1 ms): a second full traversal into a bounds sink. Per-glyph bounds reduce by
|
||
min/max, which is associative and exact, so this is a candidate for the same treatment as the
|
||
stages above (a glyph-range split, not a band split).
|
||
|
||
## Related
|
||
|
||
Byte-identical output matters here beyond taste: text pixels reach a PSD's saved cache and the
|
||
round-trip corpora. `cargo xtask test-corpus` and the Photoshop oracle tests are the gate.
|