perf(pathpaint): amortize rasterizer allocations + tighten inner loops - #8
Merged
Merged
Conversation
Measure-first audit of the AA vector rasterizer (FillPath/StrokePath,
v0.3.0), the least-benched render path. Committed Benchmark* with
-benchmem established the baseline; cpu+alloc profiles pinned three
proven costs, each fixed with byte-identical output (the exact-coverage
and winding pixel tests pass unchanged) and 100% coverage held.
Proven hotspots and fixes:
1. coverGrid's per-call `make([]float64, w*h)` was 93% of alloc SPACE
(the fill grid + one full-size grid PER stroke segment). Now a
reusable painter scratch (pathCov/pathTmp), grown once and re-zeroed
per use, so a stream of vector draws amortizes to ~0 grid allocs.
2. A `sort.Slice` per sub-scanline was 59% of alloc OBJECTS (its
reflection swapper + escaping closure). Replaced with an allocation-
free in-place insertion sort over the short per-scanline crossing
list (ties bound only zero-width spans, so output is unchanged).
3. StrokePath rasterised every segment over the WHOLE stroke box and
merged full grids; now each segment rasterises over its own clamped
sub-box (coverInto) and unions via maxSub, and diskMax scans only
the disk's bounding box. Both are byte-identical (skipped pixels had
zero coverage).
4. composite hoists the clip/surface intersection out of the per-pixel
loop and inlines the write (shared blendInto), dropping the
per-pixel PutPixel call + redundant bounds/clip branches.
Representative before -> after (512x512, Apple M4 Max):
FillPath circle 1.46 ms / 2.03 MB / 5779 allocs
-> 1.22 ms / 18 KB / 17 allocs
FillPath clipped 1.40 ms / 2.03 MB / 5779 allocs
-> 0.89 ms / 18 KB / 17 allocs
StrokePath line 13.65 ms / 38 MB / 97352 allocs
-> 0.78 ms / 16 KB / 78 allocs (17x)
StrokePath circ 50.66 ms / 170 MB / 13916 allocs
-> 0.23 ms / 35 KB / 146 allocs (220x)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Measure-first performance audit of the anti-aliased vector rasterizer (
FillPath/StrokePath, shipped in v0.3.0) — the least-benched render path. CommittedBenchmark*(with-benchmem) set the baseline; cpu + alloc profiles pinned the real costs. Every fix keeps output byte-identical (the existing exact-coverage + winding pixel tests pass unchanged) at 100% coverage.Proven hotspots -> fixes
coverGridper-callmake([]float64, w*h)— 93% of alloc space (the fill grid, plus one full-size grid per stroke segment). Now a reusable painter scratch (pathCov/pathTmp), grown once and re-zeroed per use.sort.Sliceper sub-scanline — 59% of alloc objects (reflection swapper + escaping closure). Replaced with an allocation-free in-place insertion sort over the short crossing list (ties bound only zero-width spans, so output is unchanged).StrokePathfull-box-per-segment — each segment now rasterises over its own clamped sub-box and unions viamaxSub;diskMaxscans only the disk's bbox. Byte-identical (skipped pixels had zero coverage).composite— hoists the clip/surface intersection out of the per-pixel loop and inlines the write (sharedblendInto), dropping the per-pixelPutPixelcall + redundant bounds/clip branches.Before -> after (512x512, Apple M4 Max)
Verified:
go vet, 100% coverage gate,-race(CGO=1), and cross-compile for all 6 64-bit arches + wasm + darwin — all green locally.🤖 Generated with Claude Code