software.go

Functions Structs

Functions

func Fill

Fill implements Renderer.Fill using analytic anti-aliasing.

For complex paths, it auto-selects the registered CoverageFiller (tile-based

rasterizer) when available, using an adaptive threshold based on both path

element count and bounding box area.

 

When rasterizerMode is set (via Context.SetRasterizerMode), the forced

algorithm is used instead of auto-selection.

func (r *SoftwareRenderer) Fill(pixmap *Pixmap, p *Path, paint *Paint) error {
	// Non-AA path: completely separate code path (Skia/tiny-skia pattern).
	// Integer scanline, binary coverage, no CoverageFiller/AnalyticFiller.
	if !r.antiAlias {
		return r.fillNoAA(pixmap, p, paint)
	}

	// Force mode: specific algorithm without auto-selection.
	switch r.rasterizerMode {
	case RasterizerAnalytic:
		// Skip CoverageFiller entirely → always use AnalyticFiller below.

	case RasterizerSparseStrips:
		if filler := r.forcedFiller(RasterizerSparseStrips); filler != nil {
			r.fillWithCoverageFiller(pixmap, p, paint, filler)
			return nil
		}

	case RasterizerTileCompute:
		if filler := r.forcedFiller(RasterizerTileCompute); filler != nil {
			r.fillWithCoverageFiller(pixmap, p, paint, filler)
			return nil
		}

	default:	// RasterizerAuto
		// Auto-selection: use CoverageFiller for complex paths.
		if filler := GetCoverageFiller(); filler != nil && shouldUseTileRasterizer(p) {
			r.fillWithCoverageFiller(pixmap, p, paint, filler)
			return nil
		}
	}

	// AnalyticFiller path (scanline) — simple paths, forced analytic, or no filler
	r.edgeBuilder.Reset()
	r.analyticFiller.Reset()

	// Clip paths to canvas bounds to prevent FDot6→FDot16 integer overflow.
	// At aaShift=4, coordinates > 2048px overflow int32 in FDot16, causing
	// silent wrap-around that places edges at wrong positions (RAST-010).
	// Small margin for AA bleed — coordinates at canvas+2px are still well
	// within safe range.
	clipMargin := float32(2)
	clipRect := raster.Rect{
		MinX:	-clipMargin,
		MinY:	-clipMargin,
		MaxX:	float32(pixmap.Width()) + clipMargin,
		MaxY:	float32(pixmap.Height()) + clipMargin,
	}
	r.edgeBuilder.SetClipRect(&clipRect)

	// Flatten curves to line segments for the AnalyticFiller.
	// Forward differencing (QuadraticEdge/CubicEdge) can produce zero-height
	// segments after FDot6 rounding, silently losing winding contribution.
	// Pre-flattening with adaptive subdivision (0.1px tolerance) eliminates
	// this class of errors. This is the standard approach in tiny-skia and
	// Skia's analytic AA scanline rasterizer.
	r.edgeBuilder.SetFlattenCurves(true)
	defer r.edgeBuilder.SetFlattenCurves(false)

	// Build edges from the path directly from float64 coords (zero-alloc).
	// PathVerb values match between gg and raster packages (both 0-4 iota).
	// gg.PathVerb is byte, so []PathVerb has identical memory layout to []byte.
	verbs := p.Verbs()
	if len(verbs) > 0 {
		verbBytes := unsafe.Slice((*byte)(unsafe.Pointer(unsafe.SliceData(verbs))), len(verbs))
		r.edgeBuilder.BuildFromPathF64(verbBytes, p.Coords())
	}

	// If no edges, nothing to fill
	if r.edgeBuilder.IsEmpty() {
		return nil
	}

	// Convert fill rule
	coreFillRule := raster.FillRuleNonZero
	if paint.FillRule == FillRuleEvenOdd {
		coreFillRule = raster.FillRuleEvenOdd
	}

	if color, ok := solidColorFromPaint(paint); ok {
		// Fast path: solid color
		clipFn := paint.ClipCoverage
		maskFn := paint.MaskCoverage
		r.analyticFiller.Fill(r.edgeBuilder, coreFillRule, func(y int, runs *raster.AlphaRuns) {
			r.blendAlphaRunsFromCoreRuns(pixmap, y, runs, color, clipFn, maskFn)
		})
	} else {
		// Pattern/gradient path: per-pixel color sampling
		clipFn := paint.ClipCoverage
		maskFn := paint.MaskCoverage
		r.analyticFiller.Fill(r.edgeBuilder, coreFillRule, func(y int, runs *raster.AlphaRuns) {
			r.blendAlphaRunsFromCoreRunsPaint(pixmap, y, runs, paint, clipFn, maskFn)
		})
	}

	return nil
}

func NewSoftwareRenderer

NewSoftwareRenderer creates a new software renderer with analytic anti-aliasing.

func NewSoftwareRenderer(width, height int) *SoftwareRenderer {
	eb := raster.NewEdgeBuilder(2)	// 4x AA (Skia default), max coord 8191px
	return &SoftwareRenderer{
		edgeBuilder:	eb,
		analyticFiller:	raster.NewAnalyticFiller(width, height),
		width:		width,
		height:		height,
		deviceScale:	1.0,
		antiAlias:	true,
	}
}

func Resize

Resize updates the renderer dimensions (physical pixels).

This should be called when the context is resized.

func (r *SoftwareRenderer) Resize(width, height int) {
	r.width = width
	r.height = height
	eb := raster.NewEdgeBuilder(2)	// 4x AA (Skia default), max coord 8191px
	if r.deviceScale > 1.0 {
		eb.SetFlattenTolerance(0.1 / r.deviceScale)
	}
	r.edgeBuilder = eb
	r.analyticFiller = raster.NewAnalyticFiller(width, height)
	// Reset lazy no-AA resources so they pick up new dimensions.
	r.noAAFiller = nil
	r.noAAEdgeBuilder = nil
}

func SetAntiAlias

SetAntiAlias enables or disables anti-aliasing for subsequent Fill/Stroke calls.

When disabled, the NoAAFiller (integer scanline, binary coverage) is used

instead of the AnalyticFiller or CoverageFiller.

 

This method is intended for use by the scene renderer which needs to

propagate per-draw AA state decoded from TagSetAntiAlias commands.

func (r *SoftwareRenderer) SetAntiAlias(enabled bool) {
	r.antiAlias = enabled
}

func SetDeviceScale

SetDeviceScale sets the HiDPI device scale factor for the renderer.

When scale > 1.0, curve flattening tolerance is reduced for finer

subdivision on HiDPI displays (femtovg pattern: tol = baseTol / scale).

This produces smoother curves at physical pixel resolution.

func (r *SoftwareRenderer) SetDeviceScale(scale float32) {
	if scale <= 0 {
		scale = 1.0
	}
	r.deviceScale = scale
	if scale > 1.0 {
		r.edgeBuilder.SetFlattenTolerance(0.1 / scale)
	}
}

func Stroke

Stroke implements Renderer.Stroke with anti-aliasing support.

Strokes are expanded to fill paths and rendered with the Fill method,

which provides analytic anti-aliased results.

func (r *SoftwareRenderer) Stroke(pixmap *Pixmap, p *Path, paint *Paint) error {
	// Get effective line width
	width := paint.EffectiveLineWidth()

	// Get transform scale for dash pattern scaling
	transformScale := paint.TransformScale
	if transformScale <= 0 {
		transformScale = 1.0
	}

	// Apply dash pattern if set
	// Scale dash pattern by transform scale (Cairo/Skia convention)
	pathToDraw := p
	if paint.IsDashed() {
		dash := paint.EffectiveDash()
		if transformScale > 1.0 {
			dash = dash.Scale(transformScale)
		}
		pathToDraw = dashPath(p, dash)
	}

	// Convert gg.PathVerb to stroke.PathVerb (same layout, just cast)
	strokeVerbs := convertVerbsToStroke(pathToDraw.Verbs())

	// Create stroke style from paint
	// Scale line width by transform scale (path coordinates are already transformed)
	effectiveWidth := width * transformScale
	if effectiveWidth < 1.0 {
		effectiveWidth = 1.0	// Minimum 1px stroke for visibility
	}
	strokeStyle := stroke.Stroke{
		Width:		effectiveWidth,
		Cap:		convertLineCap(paint.EffectiveLineCap()),
		Join:		convertLineJoin(paint.EffectiveLineJoin()),
		MiterLimit:	paint.EffectiveMiterLimit(),
	}
	if strokeStyle.MiterLimit <= 0 {
		strokeStyle.MiterLimit = 4.0	// Default
	}

	// Create stroke expander with tight tolerance for smooth curves.
	// 0.1 px base tolerance; on HiDPI, divide by deviceScale for finer curves.
	expander := stroke.NewStrokeExpander(strokeStyle)
	strokeTol := float64(0.1)
	if r.deviceScale > 1.0 {
		strokeTol = 0.1 / float64(r.deviceScale)
	}
	expander.SetTolerance(strokeTol)

	// Expand stroke to fill path (SOA: verb+coords in, verb+coords out)
	outVerbs, outCoords := expander.Expand(strokeVerbs, pathToDraw.Coords())

	// Convert back to gg.Path (reuse scratch to avoid per-stroke allocation).
	if r.scratchStrokePath == nil {
		r.scratchStrokePath = NewPath()
	}
	strokeResultToPath(r.scratchStrokePath, outVerbs, outCoords)

	// Route stroke fills through AnalyticFiller (Skia AAA scanline).
	// Stroke-expanded multi-contour outlines (e.g., closed path → 4 contours)
	// require per-scanline winding tracking that the tile-based SparseStripsFiller
	// does not support (Vello's strip pipeline uses per-strip fill_gap flags).
	// This matches Skia Ganesh which routes strokes through scanline renderers,
	// not tile rasterizers. Single-contour strokes work with either filler after
	// the expander.go fix (#347), but multi-contour needs scanline.
	prevMode := r.rasterizerMode
	r.rasterizerMode = RasterizerAnalytic
	err := r.Fill(pixmap, r.scratchStrokePath, paint)
	r.rasterizerMode = prevMode
	return err
}

Structs

type SoftwareRenderer struct

SoftwareRenderer is a CPU-based scanline rasterizer using analytic anti-aliasing.

 

Analytic AA computes the exact area of the shape within each pixel using

trapezoidal integration. This provides higher quality anti-aliasing than

supersampling approaches, with no extra memory overhead.

type SoftwareRenderer struct {
	// Analytic AA components
	edgeBuilder	*raster.EdgeBuilder
	analyticFiller	*raster.AnalyticFiller

	// Dimensions (physical pixels)
	width, height	int

	// HiDPI device scale factor (1.0 = no scaling).
	// Used to adjust curve flattening tolerance for sharper rendering on Retina.
	deviceScale	float32

	// rasterizerMode is set by Context before calling Fill/Stroke
	// to support forced algorithm selection (RasterizerSparseStrips, etc.).
	// Reset to RasterizerAuto after each call.
	rasterizerMode	RasterizerMode

	// antiAlias is set by Context before calling Fill/Stroke.
	// When false, the NoAAFiller (integer scanline, binary coverage) is used
	// instead of AnalyticFiller/CoverageFiller. Reset to true after each call.
	antiAlias	bool

	// noAAFiller is the non-anti-aliased filler (lazy-initialized).
	noAAFiller	*raster.NoAAFiller

	// noAAEdgeBuilder is a separate EdgeBuilder with aaShift=0 for non-AA.
	// Non-AA does not need sub-pixel edge coordinate shifting.
	noAAEdgeBuilder	*raster.EdgeBuilder

	// scratchStrokePath reuses path allocation across Stroke calls.
	// Matches Skia fOuter.reset() pattern — zero per-stroke allocation.
	scratchStrokePath	*Path
}