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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2021-03-23 17:17:06 +01:00
parent 06c53c3777
commit bc2c3db43e
5 changed files with 99 additions and 100 deletions
+4 -29
View File
@@ -18,10 +18,8 @@ import (
"gioui.org/internal/f32color" "gioui.org/internal/f32color"
"gioui.org/internal/ops" "gioui.org/internal/ops"
"gioui.org/internal/scene" "gioui.org/internal/scene"
"gioui.org/internal/stroke"
"gioui.org/layout" "gioui.org/layout"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip"
) )
type compute struct { 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 += g.encodeClipStack(clip, bounds, p.parent, true)
nclips += 1 nclips += 1
} }
s := isStroke(p) isStroke := p.stroke.Width > 0
if p != nil && p.path { if p != nil && p.path {
if s { if isStroke {
g.enc.fillMode(scene.FillModeStroke) g.enc.fillMode(scene.FillModeStroke)
g.enc.lineWidth(p.stroke.Width) g.enc.lineWidth(p.stroke.Width)
} }
@@ -685,7 +683,7 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b
} }
if begin { if begin {
g.enc.beginClip(clip) g.enc.beginClip(clip)
if s { if isStroke {
g.enc.fillMode(scene.FillModeNonzero) g.enc.fillMode(scene.FillModeNonzero)
} }
if p != nil && p.path { if p != nil && p.path {
@@ -695,31 +693,8 @@ func (g *compute) encodeClipStack(clip, bounds f32.Rectangle, p *pathOp, begin b
return nclips return nclips
} }
func supportsStroke(p *pathOp) bool { func encodePath(verts []byte) encoder {
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 {
var enc 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 { for len(verts) >= scene.CommandSize+4 {
cmd := ops.DecodeCommand(verts[4:]) cmd := ops.DecodeCommand(verts[4:])
enc.scene = append(enc.scene, cmd) enc.scene = append(enc.scene, cmd)
+11 -46
View File
@@ -131,7 +131,6 @@ type pathOp struct {
// For compute // For compute
trans f32.Affine2D trans f32.Affine2D
stroke clip.StrokeStyle stroke clip.StrokeStyle
dashes stroke.DashOp
} }
type imageOp struct { type imageOp struct {
@@ -143,29 +142,14 @@ type imageOp struct {
place placement 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 { func decodeStrokeOp(data []byte) clip.StrokeStyle {
_ = data[10] _ = data[4]
if opconst.OpType(data[0]) != opconst.TypeStroke { if opconst.OpType(data[0]) != opconst.TypeStroke {
panic("invalid op") panic("invalid op")
} }
bo := binary.LittleEndian bo := binary.LittleEndian
return clip.StrokeStyle{ return clip.StrokeStyle{
Width: math.Float32frombits(bo.Uint32(data[1:])), 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) data := buildPath(ctx, p.pathVerts)
var computePath encoder var computePath encoder
if d.compute { if d.compute {
computePath = encodePath(p) computePath = encodePath(p.pathVerts)
} }
d.pathCache.put(p.pathKey, opCacheValue{ d.pathCache.put(p.pathKey, opCacheValue{
data: data, data: data,
@@ -844,7 +828,7 @@ func (d *drawOps) newPathOp() *pathOp {
return &d.pathOpCache[len(d.pathOpCache)-1] 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 := d.newPathOp()
*npath = pathOp{ *npath = pathOp{
parent: state.cpath, parent: state.cpath,
@@ -852,7 +836,6 @@ func (d *drawOps) addClipPath(state *drawState, aux []byte, auxKey ops.Key, boun
off: off, off: off,
trans: tr, trans: tr,
stroke: stroke, stroke: stroke,
dashes: dashes,
} }
state.cpath = npath state.cpath = npath
if len(aux) > 0 { 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) { func (d *drawOps) collectOps(r *ops.Reader, state drawState) {
var ( var (
quads quadsOp quads quadsOp
str clip.StrokeStyle str clip.StrokeStyle
dashes stroke.DashOp z int
z int
) )
d.save(opconst.InitialStateID, state) d.save(opconst.InitialStateID, state)
loop: loop:
@@ -897,22 +879,6 @@ loop:
dop := ops.DecodeTransform(encOp.Data) dop := ops.DecodeTransform(encOp.Data)
state.t = state.t.Mul(dop) 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: case opconst.TypeStroke:
str = decodeStrokeOp(encOp.Data) str = decodeStrokeOp(encOp.Data)
@@ -940,7 +906,7 @@ loop:
op.bounds = v.bounds op.bounds = v.bounds
} else { } else {
pathData, bounds := d.buildVerts( pathData, bounds := d.buildVerts(
quads.aux, trans, op.outline, str, dashes, quads.aux, trans, op.outline, str,
) )
op.bounds = bounds op.bounds = bounds
if !d.compute { if !d.compute {
@@ -956,10 +922,9 @@ loop:
quads.key.SetTransform(trans) quads.key.SetTransform(trans)
} }
state.clip = state.clip.Intersect(op.bounds.Add(off)) 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{} quads = quadsOp{}
str = clip.StrokeStyle{} str = clip.StrokeStyle{}
dashes = stroke.DashOp{}
case opconst.TypeColor: case opconst.TypeColor:
state.matType = materialColor state.matType = materialColor
@@ -997,7 +962,7 @@ loop:
// The paint operation is sheared or rotated, add a clip path representing // The paint operation is sheared or rotated, add a clip path representing
// this transformed rectangle. // this transformed rectangle.
encOp.Key.SetTransform(trans) 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) 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. // 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)) inf := float32(math.Inf(+1))
d.qs.bounds = f32.Rectangle{ d.qs.bounds = f32.Rectangle{
Min: f32.Point{X: inf, Y: inf}, 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), Cap: stroke.StrokeCap(str.Cap),
Join: stroke.StrokeJoin(str.Join), Join: stroke.StrokeJoin(str.Join),
} }
quads := stroke.StrokePathCommands(ss, dashes, pathData) quads := stroke.StrokePathCommands(ss, stroke.DashOp{}, pathData)
for _, quad := range quads { for _, quad := range quads {
d.qs.contour = quad.Contour d.qs.contour = quad.Contour
quad.Quad = quad.Quad.Transform(tr) quad.Quad = quad.Quad.Transform(tr)
+1 -4
View File
@@ -33,7 +33,6 @@ const (
TypeCursor TypeCursor
TypePath TypePath
TypeStroke TypeStroke
TypeDash
) )
const ( const (
@@ -61,8 +60,7 @@ const (
TypeProfileLen = 1 TypeProfileLen = 1
TypeCursorLen = 1 + 1 TypeCursorLen = 1 + 1
TypePathLen = 1 TypePathLen = 1
TypeStrokeLen = 1 + 4 + 4 + 1 + 1 TypeStrokeLen = 1 + 4
TypeDashLen = 1 + 4 + 1
) )
// StateMask is a bitmask of state types a load operation // StateMask is a bitmask of state types a load operation
@@ -106,7 +104,6 @@ func (t OpType) Size() int {
TypeCursorLen, TypeCursorLen,
TypePathLen, TypePathLen,
TypeStrokeLen, TypeStrokeLen,
TypeDashLen,
}[t-firstOpIndex] }[t-firstOpIndex]
} }
+82 -20
View File
@@ -27,29 +27,31 @@ type Op struct {
} }
func (p Op) Add(o *op.Ops) { func (p Op) Add(o *op.Ops) {
if p.path.hasSegments { str := p.stroke
data := o.Write(opconst.TypePathLen) dashes := p.dashes
data[0] = byte(opconst.TypePath) path := p.path
p.path.spec.Add(o) 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 := o.Write(opconst.TypeStrokeLen)
data[0] = byte(opconst.TypeStroke) data[0] = byte(opconst.TypeStroke)
bo := binary.LittleEndian bo := binary.LittleEndian
bo.PutUint32(data[1:], math.Float32bits(p.stroke.Width)) bo.PutUint32(data[1:], math.Float32bits(str.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)
} }
data := o.Write(opconst.TypeClipLen) 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[5:], uint32(p.bounds.Min.Y))
bo.PutUint32(data[9:], uint32(p.bounds.Max.X)) bo.PutUint32(data[9:], uint32(p.bounds.Max.X))
bo.PutUint32(data[13:], uint32(p.bounds.Max.Y)) bo.PutUint32(data[13:], uint32(p.bounds.Max.Y))
if p.outline { if outline {
data[17] = byte(1) 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 { type PathSpec struct {
spec op.CallOp spec op.CallOp
// open is true if any path contour is not closed. A closed contour starts // open is true if any path contour is not closed. A closed contour starts
// and ends in the same point. // and ends in the same point.
open bool 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 hasSegments bool
} }
+1 -1
View File
@@ -93,7 +93,7 @@ func (d *Dash) Phase(v float32) {
func (d *Dash) Dash(length float32) { func (d *Dash) Dash(length float32) {
if d.size == math.MaxUint8 { if d.size == math.MaxUint8 {
panic("clip: too large dash pattern") panic("clip: dash pattern too large")
} }
data := d.ops.Write(4) data := d.ops.Write(4)
bo := binary.LittleEndian bo := binary.LittleEndian