From bc2c3db43ea5bf335fa01b5e7949b6622e5b4e91 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Tue, 23 Mar 2021 17:17:06 +0100 Subject: [PATCH] op/clip,gpu: move approximation of complex strokes to op/clip.Op.Add Before this change, the two renderers both had special case code for approximating strokes they don't support natively. This change moves that conversion to clip.Op.Add, for several reasons: - The compute renderer no longer need fallback logic and caches for strokes it doesn't support. - The approximation logic is slow. Moving it to clip.Op.Add will not speed it up, but will make the cost easier to spot in profiles. Until all strokes are supported natively, users can use macros to cache expensive strokes. - Reduced garbage: Op.Add takes an op.Ops anyway, and can use that for storing the approximated stroke outline. Signed-off-by: Elias Naur --- gpu/compute.go | 33 ++----------- gpu/gpu.go | 57 +++++----------------- internal/opconst/ops.go | 5 +- op/clip/clip.go | 102 ++++++++++++++++++++++++++++++++-------- op/clip/stroke.go | 2 +- 5 files changed, 99 insertions(+), 100 deletions(-) diff --git a/gpu/compute.go b/gpu/compute.go index fee0a1e9..11991b10 100644 --- a/gpu/compute.go +++ b/gpu/compute.go @@ -18,10 +18,8 @@ import ( "gioui.org/internal/f32color" "gioui.org/internal/ops" "gioui.org/internal/scene" - "gioui.org/internal/stroke" "gioui.org/layout" "gioui.org/op" - "gioui.org/op/clip" ) type compute struct { @@ -671,9 +669,9 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b nclips += g.encodeClipStack(clip, bounds, p.parent, true) nclips += 1 } - s := isStroke(p) + isStroke := p.stroke.Width > 0 if p != nil && p.path { - if s { + if isStroke { g.enc.fillMode(scene.FillModeStroke) g.enc.lineWidth(p.stroke.Width) } @@ -685,7 +683,7 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b } if begin { g.enc.beginClip(clip) - if s { + if isStroke { g.enc.fillMode(scene.FillModeNonzero) } if p != nil && p.path { @@ -695,31 +693,8 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b return nclips } -func supportsStroke(p *pathOp) bool { - return stroke.IsSolidLine(p.dashes) && p.stroke.Miter == 0 && p.stroke.Join == clip.RoundJoin && p.stroke.Cap == clip.RoundCap -} - -func isStroke(p *pathOp) bool { - return p.stroke.Width > 0 && supportsStroke(p) -} - -func encodePath(p *pathOp) encoder { +func encodePath(verts []byte) encoder { var enc encoder - verts := p.pathVerts - if p.stroke.Width > 0 && !supportsStroke(p) { - ss := stroke.StrokeStyle{ - Width: p.stroke.Width, - Miter: p.stroke.Miter, - Cap: stroke.StrokeCap(p.stroke.Cap), - Join: stroke.StrokeJoin(p.stroke.Join), - } - quads := stroke.StrokePathCommands(ss, p.dashes, verts) - for _, quad := range quads { - q := quad.Quad - enc.quad(q.From, q.Ctrl, q.To) - } - return enc - } for len(verts) >= scene.CommandSize+4 { cmd := ops.DecodeCommand(verts[4:]) enc.scene = append(enc.scene, cmd) diff --git a/gpu/gpu.go b/gpu/gpu.go index 2ae2993d..2ada935a 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -131,7 +131,6 @@ type pathOp struct { // For compute trans f32.Affine2D stroke clip.StrokeStyle - dashes stroke.DashOp } type imageOp struct { @@ -143,29 +142,14 @@ type imageOp struct { place placement } -func decodeDashOp(data []byte) stroke.DashOp { - _ = data[5] - if opconst.OpType(data[0]) != opconst.TypeDash { - panic("invalid op") - } - bo := binary.LittleEndian - return stroke.DashOp{ - Phase: math.Float32frombits(bo.Uint32(data[1:])), - Dashes: make([]float32, data[5]), - } -} - func decodeStrokeOp(data []byte) clip.StrokeStyle { - _ = data[10] + _ = data[4] if opconst.OpType(data[0]) != opconst.TypeStroke { panic("invalid op") } bo := binary.LittleEndian return clip.StrokeStyle{ Width: math.Float32frombits(bo.Uint32(data[1:])), - Miter: math.Float32frombits(bo.Uint32(data[5:])), - Cap: clip.StrokeCap(data[9]), - Join: clip.StrokeJoin(data[10]), } } @@ -827,7 +811,7 @@ func (d *drawOps) collect(ctx driver.Device, cache *resourceCache, root *op.Ops, data := buildPath(ctx, p.pathVerts) var computePath encoder if d.compute { - computePath = encodePath(p) + computePath = encodePath(p.pathVerts) } d.pathCache.put(p.pathKey, opCacheValue{ data: data, @@ -844,7 +828,7 @@ func (d *drawOps) newPathOp() *pathOp { return &d.pathOpCache[len(d.pathOpCache)-1] } -func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle, dashes stroke.DashOp) { +func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, bounds f32.Rectangle, off f32.Point, tr f32.Affine2D, stroke clip.StrokeStyle) { npath := d.newPathOp() *npath = pathOp{ parent: state.cpath, @@ -852,7 +836,6 @@ func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, boun off: off, trans: tr, stroke: stroke, - dashes: dashes, } state.cpath = npath if len(aux) > 0 { @@ -882,10 +865,9 @@ func (d *drawOps) save(id int, state drawState) { func (d *drawOps) collectOps(r *ops.Reader, state drawState) { var ( - quads quadsOp - str clip.StrokeStyle - dashes stroke.DashOp - z int + quads quadsOp + str clip.StrokeStyle + z int ) d.save(opconst.InitialStateID, state) loop: @@ -897,22 +879,6 @@ loop: dop := ops.DecodeTransform(encOp.Data) state.t = state.t.Mul(dop) - case opconst.TypeDash: - dashes = decodeDashOp(encOp.Data) - if len(dashes.Dashes) > 0 { - encOp, ok = r.Decode() - if !ok { - panic("gpu: could not decode dashes pattern") - } - data := encOp.Data[1:] - bo := binary.LittleEndian - for i := range dashes.Dashes { - dashes.Dashes[i] = math.Float32frombits(bo.Uint32( - data[i*4:], - )) - } - } - case opconst.TypeStroke: str = decodeStrokeOp(encOp.Data) @@ -940,7 +906,7 @@ loop: op.bounds = v.bounds } else { pathData, bounds := d.buildVerts( - quads.aux, trans, op.outline, str, dashes, + quads.aux, trans, op.outline, str, ) op.bounds = bounds if !d.compute { @@ -956,10 +922,9 @@ loop: quads.key.SetTransform(trans) } state.clip = state.clip.Intersect(op.bounds.Add(off)) - d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, str, dashes) + d.addClipPath(&state, quads.aux, quads.key, op.bounds, off, state.t, str) quads = quadsOp{} str = clip.StrokeStyle{} - dashes = stroke.DashOp{} case opconst.TypeColor: state.matType = materialColor @@ -997,7 +962,7 @@ loop: // The paint operation is sheared or rotated, add a clip path representing // this transformed rectangle. encOp.Key.SetTransform(trans) - d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}, stroke.DashOp{}) + d.addClipPath(&state, clipData, encOp.Key, bnd, off, state.t, clip.StrokeStyle{}) } bounds := boundRectF(cl) @@ -1349,7 +1314,7 @@ func (d *drawOps) writeVertCache(n int) []byte { } // transform, split paths as needed, calculate maxY, bounds and create GPU vertices. -func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str clip.StrokeStyle, dashes stroke.DashOp) (verts []byte, bounds f32.Rectangle) { +func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str clip.StrokeStyle) (verts []byte, bounds f32.Rectangle) { inf := float32(math.Inf(+1)) d.qs.bounds = f32.Rectangle{ Min: f32.Point{X: inf, Y: inf}, @@ -1367,7 +1332,7 @@ func (d *drawOps) buildVerts(pathData []byte, tr f32.Affine2D, outline bool, str Cap: stroke.StrokeCap(str.Cap), Join: stroke.StrokeJoin(str.Join), } - quads := stroke.StrokePathCommands(ss, dashes, pathData) + quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData) for _, quad := range quads { d.qs.contour = quad.Contour quad.Quad = quad.Quad.Transform(tr) diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 002bf31f..3c2ab62c 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -33,7 +33,6 @@ const ( TypeCursor TypePath TypeStroke - TypeDash ) const ( @@ -61,8 +60,7 @@ const ( TypeProfileLen = 1 TypeCursorLen = 1 + 1 TypePathLen = 1 - TypeStrokeLen = 1 + 4 + 4 + 1 + 1 - TypeDashLen = 1 + 4 + 1 + TypeStrokeLen = 1 + 4 ) // StateMask is a bitmask of state types a load operation @@ -106,7 +104,6 @@ func (t OpType) Size() int { TypeCursorLen, TypePathLen, TypeStrokeLen, - TypeDashLen, }[t-firstOpIndex] } diff --git a/op/clip/clip.go b/op/clip/clip.go index f0400f97..49570672 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -27,29 +27,31 @@ type Op struct { } func (p Op) Add(o *op.Ops) { - if p.path.hasSegments { - data := o.Write(opconst.TypePathLen) - data[0] = byte(opconst.TypePath) - p.path.spec.Add(o) + str := p.stroke + dashes := p.dashes + path := p.path + outline := p.outline + approx := str.Width > 0 && !(dashes == DashSpec{} && str.Miter == 0 && str.Join == RoundJoin && str.Cap == RoundCap) + if approx { + // If the stroke is not natively supported by the compute renderer, construct a filled path + // that approximates it. + path = p.approximateStroke(o) + dashes = DashSpec{} + str = StrokeStyle{} + outline = true } - if p.stroke.Width > 0 { + if path.hasSegments { + data := o.Write(opconst.TypePathLen) + data[0] = byte(opconst.TypePath) + path.spec.Add(o) + } + + if str.Width > 0 { data := o.Write(opconst.TypeStrokeLen) data[0] = byte(opconst.TypeStroke) bo := binary.LittleEndian - bo.PutUint32(data[1:], math.Float32bits(p.stroke.Width)) - bo.PutUint32(data[5:], math.Float32bits(p.stroke.Miter)) - data[9] = uint8(p.stroke.Cap) - data[10] = uint8(p.stroke.Join) - } - - if p.dashes.phase != 0 || p.dashes.size > 0 { - data := o.Write(opconst.TypeDashLen) - data[0] = byte(opconst.TypeDash) - bo := binary.LittleEndian - bo.PutUint32(data[1:], math.Float32bits(p.dashes.phase)) - data[5] = p.dashes.size // FIXME(sbinet) uint16? uint32? - p.dashes.spec.Add(o) + bo.PutUint32(data[1:], math.Float32bits(str.Width)) } data := o.Write(opconst.TypeClipLen) @@ -59,17 +61,77 @@ func (p Op) Add(o *op.Ops) { bo.PutUint32(data[5:], uint32(p.bounds.Min.Y)) bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) - if p.outline { + if outline { data[17] = byte(1) } } +func (p Op) approximateStroke(o *op.Ops) PathSpec { + if !p.path.hasSegments { + return PathSpec{} + } + + var r ops.Reader + // Add path op for us to decode. Use a macro to omit it from later decodes. + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.path.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt path data") + } + pathData := encOp.Data[opconst.TypeAuxLen:] + + // Decode dashes in a similar way. + var dashes stroke.DashOp + if p.dashes.phase != 0 || p.dashes.size > 0 { + ignore := op.Record(o) + r.ResetAt(o, ops.NewPC(o)) + p.dashes.spec.Add(o) + ignore.Stop() + encOp, ok := r.Decode() + if !ok || opconst.OpType(encOp.Data[0]) != opconst.TypeAux { + panic("corrupt dash data") + } + dashes.Dashes = make([]float32, p.dashes.size) + dashData := encOp.Data[opconst.TypeAuxLen:] + bo := binary.LittleEndian + for i := range dashes.Dashes { + dashes.Dashes[i] = math.Float32frombits(bo.Uint32(dashData[i*4:])) + } + dashes.Phase = p.dashes.phase + } + + // Approximate and output path data. + var outline Path + outline.Begin(o) + ss := stroke.StrokeStyle{ + Width: p.stroke.Width, + Miter: p.stroke.Miter, + Cap: stroke.StrokeCap(p.stroke.Cap), + Join: stroke.StrokeJoin(p.stroke.Join), + } + quads := stroke.StrokePathCommands(ss, dashes, pathData) + pen := f32.Pt(0, 0) + for _, quad := range quads { + q := quad.Quad + if q.From != pen { + pen = q.From + outline.MoveTo(pen) + } + outline.contour = int(quad.Contour) + outline.QuadTo(q.Ctrl, q.To) + } + return outline.End() +} + type PathSpec struct { spec op.CallOp // open is true if any path contour is not closed. A closed contour starts // and ends in the same point. open bool - // hasSegments tracks whether there is more than one path segment in the path. + // hasSegments tracks whether there are any segments in the path. hasSegments bool } diff --git a/op/clip/stroke.go b/op/clip/stroke.go index 54e63723..17b661c9 100644 --- a/op/clip/stroke.go +++ b/op/clip/stroke.go @@ -93,7 +93,7 @@ func (d *Dash) Phase(v float32) { func (d *Dash) Dash(length float32) { if d.size == math.MaxUint8 { - panic("clip: too large dash pattern") + panic("clip: dash pattern too large") } data := d.ops.Write(4) bo := binary.LittleEndian