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