Skip to content

computeCoverage only: single-pass gather + delta prefix-sum coverage#589

Open
treeform wants to merge 1 commit into
masterfrom
claude-compute-coverage
Open

computeCoverage only: single-pass gather + delta prefix-sum coverage#589
treeform wants to merge 1 commit into
masterfrom
claude-compute-coverage

Conversation

@treeform

Copy link
Copy Markdown
Owner

Summary

Minimal variant of #586 per guzba's request: everything reverted except the computeCoverage changes (plus the 1/m inverse in PartitionEntry and the small bits that necessarily reach outside — fillCoverage gaining an offset/len window, the extra scratch seqs in fillShapes, a sortHits offset overload, and walk taking openArray).

Not included (vs #586): thread-local scratch reuse, the partitionSegments rewrite (numEntries, slope-based clipping, alloc reuse), shapesToSegments into a reused buffer, newSeqUninit, blender hoisting / inlined blendNormal in the trapezoid path.

Timings

Ghostscript Tiger, nim c -d:danger tests/bench_svg.nim, M-series Mac. All three binaries run interleaved in the same session to control for machine load (the box was moderately loaded; ratios were stable across rounds even as absolute numbers drifted):

build min time (typical range) vs master
master (9b6b49d) 6.96 – 7.03 ms
this PR (computeCoverage only) 6.48 – 6.56 ms ~ -7%
full #586 6.04 – 6.20 ms ~ -11%

So the computeCoverage rework is the meat (~60% of the total win); the scratch/allocation/partitionSegments changes in #586 account for the remaining ~4-5%.

What the computeCoverage change does

  1. Single-pass hit gathering. Master rescans all active entries once per subsample line (5x for AA). Now each entry is loaded once, its segment y-range/slope kept in registers, and hits are written into 5 per-line buckets. Hit order within each bucket is unchanged, so sorting and walking produce bit-identical results. On the tiger this is 467k entry visits per render down to 93k.
  2. Delta + prefix-sum coverage. Instead of adding sample coverage to every covered pixel per subsample (up to 5 passes over each span), each fill span writes +cov/-cov into an int16 delta buffer. One prefix-sum pass per scanline (NEON-vectorized on arm64, scalar elsewhere) produces the exact same coverage bytes. The deltas buffer is provably re-zeroed by the pass itself.
  3. Coverage regions. While walking hits the covered spans are recorded (merged, small gaps fused), so fillCoverage and clearing only touch real coverage instead of the full path-bounds width for Overwrite/Normal blends. On the tiger that's 1.69M blended pixels per render down to 1.08M against an 810k-pixel image.
  4. PartitionEntry.invM replaces the per-hit divide with a multiply (also used by solveX).

Correctness

Every test_svg and test_paths xray score matches master to 4 decimal places (remaining last-bit differences come from (y-b)*invM vs (y-b)/m). test_images_draw, test_paints, test_contexts, test_fonts pass.

🤖 Generated with Claude Code

Minimal variant of #586 keeping only the computeCoverage changes:

- Gather subsample hits in one pass over the entries (each entry
  loaded once instead of once per subsample line), bucketed per line
- Accumulate coverage as int16 deltas and convert with one prefix-sum
  pass per scanline (NEON on arm64) instead of adding sample coverage
  to every covered pixel per subsample
- Track covered regions so blending only touches real coverage,
  removing the per-scanline zeroMem for Overwrite/Normal blends
- Store 1/m in PartitionEntry, replacing the per-hit divide

Ghostscript Tiger: master 6.96-7.03 ms, this branch 6.48-6.56 ms
(interleaved runs; full #586 measures 6.04-6.20 ms in the same runs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant