Skip to content

Commit

Permalink
Optimize for performance
Browse files Browse the repository at this point in the history
  • Loading branch information
tdewolff committed Nov 11, 2024
1 parent c61aca1 commit e3219a7
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 14 deletions.
1 change: 1 addition & 0 deletions font.go
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,7 @@ func (m FontMetrics) String() string {

// Metrics returns the font metrics. See https://developer.apple.com/library/archive/documentation/TextFonts/Conceptual/CocoaTextArchitecture/Art/glyph_metrics_2x.png for an explanation of the different metrics.
func (face *FontFace) Metrics() FontMetrics {
// TODO: use resolution
sfnt := face.Font.SFNT
ascender, descender, lineGap := sfnt.VerticalMetrics()
return FontMetrics{
Expand Down
19 changes: 17 additions & 2 deletions path.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (fillRule FillRule) String() string {

// Command values as powers of 2 so that the float64 representation is exact
// TODO: make CloseCmd a LineTo + CloseCmd, where CloseCmd is only a command value, no coordinates
// TODO: optimize command memory layout in paths: we need three bits to represent each command, thus 6 to specify command going forward and going backward. The remaining 58 bits, or 28 per direction, should specify the index into Path.d of the end of the sequence of coordinates. MoveTo should have an index to the end of the subpath. Use math.Float64bits. For long flat paths this will reduce memory usage in half since besides all coordinates, the only overhead is: 64 bits first MoveTo, 64 bits end of MoveTo and start of LineTo, 64 bits end of LineTo and start of Close, 64 bits end of Close.
const (
MoveToCmd = 1.0 << iota // 1.0
LineToCmd // 2.0
Expand Down Expand Up @@ -207,8 +208,19 @@ func (p *Path) HasSubpaths() bool {

// Copy returns a copy of p.
func (p *Path) Copy() *Path {
q := &Path{}
q.d = append(q.d, p.d...)
q := &Path{d: make([]float64, len(p.d))}
copy(q.d, p.d)
return q
}

// CopyTo returns a copy of p, using the memory of path q.
func (p *Path) CopyTo(q *Path) *Path {
if q == nil || len(q.d) < len(p.d) {
q.d = make([]float64, len(p.d))
} else {
q.d = q.d[:len(p.d)]
}
copy(q.d, p.d)
return q
}

Expand Down Expand Up @@ -1331,6 +1343,9 @@ func (p *Path) Markers(first, mid, last *Path, align bool) []*Path {

// Split splits the path into its independent subpaths. The path is split before each MoveTo command.
func (p *Path) Split() []*Path {
if p == nil {
return nil
}
var i, j int
ps := []*Path{}
for j < len(p.d) {
Expand Down
27 changes: 18 additions & 9 deletions path_intersection.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,14 @@ func (q SweepEvents) Swap(i, j int) {
}

func (q *SweepEvents) AddPathEndpoints(p *Path, seg int, clipping bool) int {
//open := !pi.Closed()
// TODO: change this if we allow non-flat paths
// allocate all memory at once to prevent multiple allocations/memmoves below
n := len(p.d) / 4
if cap(*q) < len(*q)+n {
q2 := make(SweepEvents, len(*q), len(*q)+n)
copy(q2, *q)
*q = q2
}
for i := 4; i < len(p.d); {
if p.d[i] != LineToCmd && p.d[i] != CloseCmd {
panic("non-flat paths not supported")
Expand Down Expand Up @@ -891,9 +898,11 @@ func compareIntersections(a, b Intersection) int {
}
}

func addIntersections(queue *SweepEvents, handled map[SweepPointPair]bool, zs Intersections, a, b *SweepPoint) bool {
func addIntersections(queue *SweepEvents, handled map[SweepPointPair]struct{}, zs Intersections, a, b *SweepPoint) bool {
// a and b are always left-endpoints and a is below b
if handled[SweepPointPair{a, b}] || handled[SweepPointPair{b, a}] {
if _, ok := handled[SweepPointPair{a, b}]; ok {
return false
} else if _, ok := handled[SweepPointPair{b, a}]; ok {
return false
}

Expand All @@ -914,7 +923,7 @@ func addIntersections(queue *SweepEvents, handled map[SweepPointPair]bool, zs In

// no (valid) intersections
if len(zs) == 0 {
handled[SweepPointPair{a, b}] = true
handled[SweepPointPair{a, b}] = struct{}{}
return false
}

Expand Down Expand Up @@ -1002,7 +1011,7 @@ func addIntersections(queue *SweepEvents, handled map[SweepPointPair]bool, zs In

for _, a := range aLefts {
for _, b := range bLefts {
handled[SweepPointPair{a, b}] = true
handled[SweepPointPair{a, b}] = struct{}{}
}
}
return 0 < len(aLefts) || 0 < len(bLefts)
Expand Down Expand Up @@ -1198,10 +1207,10 @@ func bentleyOttmann(ps, qs Paths, op pathOp, fillRule FillRule) *Path {

// construct sweep line status structure
zs_ := [2]Intersection{}
zs := zs_[:] // reusable buffer
var preResult []*SweepPoint // TODO: remove in favor of keeping points in queue, implement queue.Clear() to put back all points in pool
status := &SweepStatus{} // contains only left events
handled := map[SweepPointPair]bool{} // prevent testing for intersections more than once
zs := zs_[:] // reusable buffer
var preResult []*SweepPoint // TODO: remove in favor of keeping points in queue, implement queue.Clear() to put back all points in pool
status := &SweepStatus{} // contains only left events
handled := make(map[SweepPointPair]struct{}, len(*queue)*2) // prevent testing for intersections more than once, allocation length is an approximation
for 0 < len(*queue) {
// TODO: skip or stop depending on operation if we're to the left/right of subject/clipping polygon

Expand Down
13 changes: 10 additions & 3 deletions renderers/rasterizer/rasterizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"golang.org/x/image/vector"
)

// TODO: add ASM optimized version for NRGBA images, since those are much faster to write as PNG

// Draw draws the canvas on a new image with given resolution (in dots-per-millimeter). Higher resolution will result in larger images.
func Draw(c *canvas.Canvas, resolution canvas.Resolution, colorSpace canvas.ColorSpace) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, int(c.W*resolution.DPMM()+0.5), int(c.H*resolution.DPMM()+0.5)))
Expand All @@ -32,7 +34,7 @@ func New(width, height float64, resolution canvas.Resolution, colorSpace canvas.
return FromImage(img, resolution, colorSpace)
}

// FromImage returns a renderer that draws to an existing image. A resolution of 1.0 means that canvas coordinates in millimeters are a 1-to-1 relation to pixels. A higher resolution means that a smaller rectangle in the canvas space corresponds to the final rasterized image (eg. a resolution of 2.0 means that 10 mm from the origin becomes the 20th pixel).
// FromImage returns a renderer that draws to an existing image. Resolution is in pixels per unit of canvas coordinates (millimeters). A higher resolution will give a larger and more detailed image.
func FromImage(img draw.Image, resolution canvas.Resolution, colorSpace canvas.ColorSpace) *Rasterizer {
bounds := img.Bounds()
if bounds.Dx() == 0 || bounds.Dy() == 0 {
Expand Down Expand Up @@ -126,15 +128,18 @@ func (r *Rasterizer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.
}
}

// TODO: reuse rasterizer and call Reset? It would require it to be the size of image
ras := vector.NewRasterizer(w, h)
fill = fill.Translate(-float64(x)/dpmm, -float64(size.Y-y-h)/dpmm)
fill.ToRasterizer(ras, r.resolution)
var src image.Image
if style.Fill.IsColor() {
src = image.NewUniform(r.colorSpace.ToLinear(style.Fill.Color))
c := r.colorSpace.ToLinear(style.Fill.Color)
src = image.NewUniform(r.Image.ColorModel().Convert(c))
} else if style.Fill.IsGradient() {
gradient := style.Fill.Gradient.SetColorSpace(r.colorSpace)
src = NewGradientImage(gradient, zp, size, r.resolution)
// TODO: convert to dst color model
} else if style.Fill.IsPattern() {
pattern := style.Fill.Pattern.SetColorSpace(r.colorSpace)
pattern.ClipTo(r, fill)
Expand All @@ -156,10 +161,12 @@ func (r *Rasterizer) RenderPath(path *canvas.Path, style canvas.Style, m canvas.
stroke.ToRasterizer(ras, r.resolution)
var src image.Image
if style.Stroke.IsColor() {
src = image.NewUniform(r.colorSpace.ToLinear(style.Stroke.Color))
c := r.colorSpace.ToLinear(style.Stroke.Color)
src = image.NewUniform(r.Image.ColorModel().Convert(c))
} else if style.Stroke.IsGradient() {
gradient := style.Stroke.Gradient.SetColorSpace(r.colorSpace)
src = NewGradientImage(gradient, zp, size, r.resolution)
// TODO: convert to dst color model
} else if style.Stroke.IsPattern() {
pattern := style.Stroke.Pattern.SetColorSpace(r.colorSpace)
pattern.ClipTo(r, stroke)
Expand Down

0 comments on commit e3219a7

Please sign in to comment.