Skip to content

fix(raster): bound native curve subdivision - #541

Open
timzifer wants to merge 1 commit into
gogpu:mainfrom
timzifer:fix/raster-bound-curve-subdivision
Open

timzifer wants to merge 1 commit into
gogpu:mainfrom
timzifer:fix/raster-bound-curve-subdivision

Conversation

@timzifer

Copy link
Copy Markdown

Problem

Large finite coordinates can crash the analytic rasterizer with an unrecoverable stack overflow. EdgeBuilder.addCubic and addQuad subdivide until an absolute 0.1 px deviation test passes, but neither bounds recursion. At float32 precision, a midpoint can round back to an input point and leave a child identical to its parent. The deviation then never decreases.

For example, the cubic with x coordinates {2289292.75, 2289293, 2289293, 2289293} and all y coordinates equal to math.Float32frombits(0x43817f0a) repeats indefinitely. Its endpoints are one float32 ULP apart. Quadratics have the same failure with x coordinates {2289293, 2289292.75, 2289292.75}.

Setting a clip rectangle does not prevent this: the out-of-bounds curve guard runs after the recursive deviation branch.

Fix

Thread a depth through the native quadratic and cubic subdivision helpers. At depth > 10, emit the remaining chord through addLine, matching the existing flattening routines. This keeps line clipping and winding handling on the fallback path and bounds subdivision even when float32 rounding prevents progress.

The existing tolerance, subdivision arithmetic and clip-check ordering stay in place. There is no public API change.

Minimal example

This only needs gg; no GPU or window is involved:

c := gg.NewContext(100, 100)
c.SetRasterizerMode(gg.RasterizerAnalytic)
c.SetRGB(0, 0, 1)
c.MoveTo(10, 10)
c.LineTo(2289292.75, 10)
c.CubicTo(2289293, 35, 2289293, 65, 2289293, 90)
c.LineTo(10, 90)
c.ClosePath()
err := c.Fill()

On v0.52.5 this ends in fatal error: stack overflow. With the fix, Fill returns nil and the clipped blue region covers x=10..99, y=10..89. The standalone reproducer checks all 10,000 pixels: 7,200 filled pixels, with the rest transparent.

Tests

  • TestEdgeBuilderCurveSubdivisionTerminates: 16 subprocess cases cover cubic and quadratic fixed points, both coordinate axes, both signs, and with/without clipping. All 16 fail with stack overflow on unmodified main (bd563f4); all pass with the fix. Each child has a 1 MiB stack limit and a timeout so a regression cannot exhaust the test runner's stack or hang the suite.
  • TestEdgeBuilderLargeCurvesPreserveClippedFill: cubic and quadratic contours extending far beyond the right clip boundary retain the expected visible fill, checked pixel by pixel. These also run in bounded subprocesses.
  • go test -race -tags nogpu ./... passes with CGO_ENABLED=1, following the repository's CI configuration. The raster package was rerun after adding the clipped-fill assertions.
  • go build ./... and golangci-lint run --timeout=5m pass with CGO_ENABLED=0; the final linter run reports zero issues.

Validated on Windows amd64 with Go 1.25.3, based on upstream main at bd563f4c293fd1f70afe56276c444a38302bac15.

@timzifer
timzifer marked this pull request as ready for review September 17, 2026 12:24
@timzifer
timzifer requested a review from kolkov as a code owner September 17, 2026 12:24
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