From 5b277757cfd0fe69a6088534ced81aa845b115d5 Mon Sep 17 00:00:00 2001 From: Viktor Date: Sat, 20 Jun 2020 23:29:48 +0200 Subject: [PATCH] op/clip, gpu: split complex curves in package gpu instead This is a first step towards supporting affine drawing transforms. The rendering algorithm relies on quadratic curves that do not cross x = 0 more than once, thus curves must be split after any rotation/shear transforms. Move this logic and the generation of vertices to package gpu. Also close all curves and draw zero-width edges as preparation for transform since the will no longer implicitly be vertical with no effect. This commit will severely affect performance since vertexes are now transformed also for cached items, using cpu resources. Signed-off-by: Viktor --- gpu/clip.go | 98 +++++++++++++++++++++++++++++ gpu/gpu.go | 67 ++++++++++++++------ gpu/path.go | 51 ++++++++++++--- internal/ops/ops.go | 27 ++++++++ internal/path/path.go | 27 -------- op/clip/clip.go | 143 +++++++----------------------------------- 6 files changed, 240 insertions(+), 173 deletions(-) create mode 100644 gpu/clip.go delete mode 100644 internal/path/path.go diff --git a/gpu/clip.go b/gpu/clip.go new file mode 100644 index 00000000..51022a08 --- /dev/null +++ b/gpu/clip.go @@ -0,0 +1,98 @@ +package gpu + +import ( + "gioui.org/f32" + "gioui.org/internal/ops" +) + +type quadSplitter struct { + verts []byte + bounds f32.Rectangle + contour uint32 + d *drawOps +} + +func encodeQuadTo(data []byte, meta uint32, from, ctrl, to f32.Point) { + // NW. + encodeVertex(data, meta, -1, 1, from, ctrl, to) + // NE. + encodeVertex(data[vertStride:], meta, 1, 1, from, ctrl, to) + // SW. + encodeVertex(data[vertStride*2:], meta, -1, -1, from, ctrl, to) + // SE. + encodeVertex(data[vertStride*3:], meta, 1, -1, from, ctrl, to) +} + +func encodeVertex(data []byte, meta uint32, cornerx, cornery int16, from, ctrl, to f32.Point) { + var corner float32 + if cornerx == 1 { + corner += .5 + } + if cornery == 1 { + corner += .25 + } + v := vertex{ + Corner: corner, + FromX: from.X, + FromY: from.Y, + CtrlX: ctrl.X, + CtrlY: ctrl.Y, + ToX: to.X, + ToY: to.Y, + } + v.encode(data, meta) +} + +func (qs *quadSplitter) encodeQuadTo(from, ctrl, to f32.Point) { + data := qs.d.writeVertCache(vertStride * 4) + encodeQuadTo(data, qs.contour, from, ctrl, to) +} + +func (qs *quadSplitter) splitAndEncode(quad ops.Quad) { + cbnd := f32.Rectangle{ + Min: quad.From, + Max: quad.To, + }.Canon() + from, ctrl, to := quad.From, quad.Ctrl, quad.To + + // If the curve contain areas where a vertical line + // intersects it twice, split the curve in two x monotone + // lower and upper curves. The stencil fragment program + // expects only one intersection per curve. + + // Find the t where the derivative in x is 0. + v0 := ctrl.Sub(from) + v1 := to.Sub(ctrl) + d := v0.X - v1.X + // t = v0 / d. Split if t is in ]0;1[. + if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { + t := v0.X / d + ctrl0 := from.Mul(1 - t).Add(ctrl.Mul(t)) + ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) + mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) + qs.encodeQuadTo(from, ctrl0, mid) + qs.encodeQuadTo(mid, ctrl1, to) + if mid.X > cbnd.Max.X { + cbnd.Max.X = mid.X + } + if mid.X < cbnd.Min.X { + cbnd.Min.X = mid.X + } + } else { + qs.encodeQuadTo(from, ctrl, to) + } + // Find the y extremum, if any. + d = v0.Y - v1.Y + if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { + t := v0.Y / d + y := (1-t)*(1-t)*from.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y + if y > cbnd.Max.Y { + cbnd.Max.Y = y + } + if y < cbnd.Min.Y { + cbnd.Min.Y = y + } + } + + qs.bounds = qs.bounds.Union(cbnd) +} diff --git a/gpu/gpu.go b/gpu/gpu.go index 56d18863..334f03a4 100644 --- a/gpu/gpu.go +++ b/gpu/gpu.go @@ -22,7 +22,6 @@ import ( "gioui.org/internal/f32color" "gioui.org/internal/opconst" "gioui.org/internal/ops" - "gioui.org/internal/path" gunsafe "gioui.org/internal/unsafe" "gioui.org/layout" "gioui.org/op" @@ -55,6 +54,7 @@ type drawOps struct { profile bool reader ops.Reader cache *resourceCache + vertCache []byte viewport image.Point clearColor f32color.RGBA imageOps []imageOp @@ -64,6 +64,7 @@ type drawOps struct { zimageOps []imageOp pathOps []*pathOp pathOpCache []pathOp + qs quadSplitter } type drawState struct { @@ -659,6 +660,7 @@ func (d *drawOps) reset(cache *resourceCache, viewport image.Point) { d.zimageOps = d.zimageOps[:0] d.pathOps = d.pathOps[:0] d.pathOpCache = d.pathOpCache[:0] + d.vertCache = d.vertCache[:0] } func (d *drawOps) collect(cache *resourceCache, root *op.Ops, viewport image.Point) { @@ -693,29 +695,29 @@ loop: state.t = state.t.Multiply(op.TransformOp(dop)) case opconst.TypeAux: aux = encOp.Data[opconst.TypeAuxLen:] - // The first data byte stores whether the MaxY - // fields have been initialized. - maxyFilled := aux[0] == 1 - aux[0] = 1 - aux = aux[1:] - if !maxyFilled { - fillMaxY(aux) - } auxKey = encOp.Key case opconst.TypeClip: var op clipOp op.decode(encOp.Data) off := state.t.Transform(f32.Point{}) - state.clip = state.clip.Intersect(op.bounds.Add(off)) + + bounds := op.bounds + if len(aux) > 0 { + // there is a clipping path, bounds is not filled before for performance + aux, bounds = d.buildVerts(aux) + } + state.clip = state.clip.Intersect(bounds.Add(off)) if state.clip.Empty() { continue } + npath := d.newPathOp() *npath = pathOp{ parent: state.cpath, off: off, } state.cpath = npath + if len(aux) > 0 { state.rect = false state.cpath.pathKey = auxKey @@ -1002,17 +1004,17 @@ func fillMaxY(verts []byte) { for len(verts) > 0 { maxy := float32(math.Inf(-1)) i := 0 - for ; i+path.VertStride*4 <= len(verts); i += path.VertStride * 4 { - vert := verts[i : i+path.VertStride] + for ; i+vertStride*4 <= len(verts); i += vertStride * 4 { + vert := verts[i : i+vertStride] // MaxY contains the integer contour index. - pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)):])) + pathContour := int(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).MaxY)):])) if contour != pathContour { contour = pathContour break } - fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).FromY)):])) - ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).CtrlY)):])) - toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*path.Vertex)(nil)).ToY)):])) + fromy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).FromY)):])) + ctrly := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).CtrlY)):])) + toy := math.Float32frombits(bo.Uint32(vert[int(unsafe.Offsetof(((*vertex)(nil)).ToY)):])) if fromy > maxy { maxy = fromy } @@ -1030,8 +1032,37 @@ func fillMaxY(verts []byte) { func fillContourMaxY(maxy float32, verts []byte) { bo := binary.LittleEndian - for i := 0; i < len(verts); i += path.VertStride { - off := int(unsafe.Offsetof(((*path.Vertex)(nil)).MaxY)) + for i := 0; i < len(verts); i += vertStride { + off := int(unsafe.Offsetof(((*vertex)(nil)).MaxY)) bo.PutUint32(verts[i+off:], math.Float32bits(maxy)) } } + +func (d *drawOps) writeVertCache(n int) []byte { + d.vertCache = append(d.vertCache, make([]byte, n)...) + return d.vertCache[len(d.vertCache)-n:] +} + +func (d *drawOps) buildVerts(aux []byte) (verts []byte, bounds f32.Rectangle) { + // split paths as needed, calculate maxY, bounds and create + // vertices that will be sent to GPU. + inf := float32(math.Inf(+1)) + d.qs.bounds = f32.Rectangle{ + Min: f32.Point{X: inf, Y: inf}, + Max: f32.Point{X: -inf, Y: -inf}, + } + d.qs.d = d + bo := binary.LittleEndian + startLength := len(d.vertCache) + for qi := 0; len(aux) >= (ops.QuadSize + 4); qi++ { + d.qs.contour = bo.Uint32(aux) + quad := ops.DecodeQuad(aux[4:]) + + d.qs.splitAndEncode(quad) + + aux = aux[ops.QuadSize+4:] + } + + fillMaxY(d.vertCache[startLength:]) + return d.vertCache[startLength:], d.qs.bounds +} diff --git a/gpu/path.go b/gpu/path.go index 73e72f50..b182f6ca 100644 --- a/gpu/path.go +++ b/gpu/path.go @@ -6,13 +6,14 @@ package gpu // Pathfinder (https://github.com/servo/pathfinder). import ( + "encoding/binary" "image" + "math" "unsafe" "gioui.org/f32" "gioui.org/gpu/backend" "gioui.org/internal/f32color" - "gioui.org/internal/path" gunsafe "gioui.org/internal/unsafe" ) @@ -104,9 +105,33 @@ type pathData struct { data backend.Buffer } +// vertex data suitable for passing to vertex programs. +type vertex struct { + // Corner encodes the corner: +0.5 for south, +.25 for east. + Corner float32 + MaxY float32 + FromX, FromY float32 + CtrlX, CtrlY float32 + ToX, ToY float32 +} + +func (v vertex) encode(d []byte, maxy uint32) { + bo := binary.LittleEndian + bo.PutUint32(d[0:], math.Float32bits(v.Corner)) + bo.PutUint32(d[4:], maxy) + bo.PutUint32(d[8:], math.Float32bits(v.FromX)) + bo.PutUint32(d[12:], math.Float32bits(v.FromY)) + bo.PutUint32(d[16:], math.Float32bits(v.CtrlX)) + bo.PutUint32(d[20:], math.Float32bits(v.CtrlY)) + bo.PutUint32(d[24:], math.Float32bits(v.ToX)) + bo.PutUint32(d[28:], math.Float32bits(v.ToY)) +} + const ( // Number of path quads per draw batch. pathBatchSize = 10000 + // Size of a vertex as sent to gpu + vertStride = 7*4 + 2*2 ) func newPather(ctx backend.Device) *pather { @@ -152,11 +177,11 @@ func newStenciler(ctx backend.Device) *stenciler { panic(err) } progLayout, err := ctx.NewInputLayout(shader_stencil_vert, []backend.InputDesc{ - {Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).Corner))}, - {Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).MaxY))}, - {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).FromX))}, - {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).CtrlX))}, - {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*path.Vertex)(nil)).ToX))}, + {Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).Corner))}, + {Type: backend.DataTypeFloat, Size: 1, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).MaxY))}, + {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).FromX))}, + {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).CtrlX))}, + {Type: backend.DataTypeFloat, Size: 2, Offset: int(unsafe.Offsetof((*(*vertex)(nil)).ToX))}, }) if err != nil { panic(err) @@ -269,7 +294,7 @@ func buildPath(ctx backend.Device, p []byte) *pathData { panic(err) } return &pathData{ - ncurves: len(p) / path.VertStride, + ncurves: len(p) / vertStride, data: buf, } } @@ -329,8 +354,8 @@ func (s *stenciler) stencilPath(bounds image.Rectangle, offset f32.Point, uv ima if max := pathBatchSize; batch > max { batch = max } - off := path.VertStride * start * 4 - s.ctx.BindVertexBuffer(data.data, path.VertStride, off) + off := vertStride * start * 4 + s.ctx.BindVertexBuffer(data.data, vertStride, off) s.ctx.DrawElements(backend.DrawModeTriangles, 0, batch*6) start += batch } @@ -358,3 +383,11 @@ func (c *coverer) cover(z float32, mat materialType, col f32color.RGBA, scale, o p.UploadUniforms() c.ctx.DrawArrays(backend.DrawModeTriangleStrip, 0, 4) } + +func init() { + // Check that struct vertex has the expected size and + // that it contains no padding. + if unsafe.Sizeof(*(*vertex)(nil)) != vertStride { + panic("unexpected struct size") + } +} diff --git a/internal/ops/ops.go b/internal/ops/ops.go index c8a9f89d..80994e17 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -11,6 +11,33 @@ import ( "gioui.org/op" ) +const QuadSize = 4 * 2 * 3 + +type Quad struct { + From, Ctrl, To f32.Point +} + +func EncodeQuad(d []byte, q Quad) { + bo := binary.LittleEndian + bo.PutUint32(d[0:], math.Float32bits(q.From.X)) + bo.PutUint32(d[4:], math.Float32bits(q.From.Y)) + bo.PutUint32(d[8:], math.Float32bits(q.Ctrl.X)) + bo.PutUint32(d[12:], math.Float32bits(q.Ctrl.Y)) + bo.PutUint32(d[16:], math.Float32bits(q.To.X)) + bo.PutUint32(d[20:], math.Float32bits(q.To.Y)) +} + +func DecodeQuad(d []byte) (q Quad) { + bo := binary.LittleEndian + q.From.X = math.Float32frombits(bo.Uint32(d[0:])) + q.From.Y = math.Float32frombits(bo.Uint32(d[4:])) + q.Ctrl.X = math.Float32frombits(bo.Uint32(d[8:])) + q.Ctrl.Y = math.Float32frombits(bo.Uint32(d[12:])) + q.To.X = math.Float32frombits(bo.Uint32(d[16:])) + q.To.Y = math.Float32frombits(bo.Uint32(d[20:])) + return +} + func DecodeTransformOp(d []byte) op.TransformOp { bo := binary.LittleEndian if opconst.OpType(d[0]) != opconst.TypeTransform { diff --git a/internal/path/path.go b/internal/path/path.go deleted file mode 100644 index 04dfd6d0..00000000 --- a/internal/path/path.go +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: Unlicense OR MIT - -package path - -import ( - "unsafe" -) - -// The vertex data suitable for passing to vertex programs. -type Vertex struct { - // Corner encodes the corner: +0.5 for south, +.25 for east. - Corner float32 - MaxY float32 - FromX, FromY float32 - CtrlX, CtrlY float32 - ToX, ToY float32 -} - -const VertStride = 7*4 + 2*2 - -func init() { - // Check that struct vertex has the expected size and - // that it contains no padding. - if unsafe.Sizeof(*(*Vertex)(nil)) != VertStride { - panic("unexpected struct size") - } -} diff --git a/op/clip/clip.go b/op/clip/clip.go index f77d5a88..59554b53 100644 --- a/op/clip/clip.go +++ b/op/clip/clip.go @@ -9,7 +9,7 @@ import ( "gioui.org/f32" "gioui.org/internal/opconst" - "gioui.org/internal/path" + "gioui.org/internal/ops" "gioui.org/op" ) @@ -21,12 +21,11 @@ import ( // Path generates no garbage and can be used for dynamic paths; path // data is stored directly in the Ops list supplied to Begin. type Path struct { - ops *op.Ops - contour int - pen f32.Point - bounds f32.Rectangle - hasBounds bool - macro op.MacroOp + ops *op.Ops + contour int + pen f32.Point + macro op.MacroOp + start f32.Point } // Op sets the current clip to the intersection of @@ -54,22 +53,24 @@ func (p Op) Add(o *op.Ops) { func (p *Path) Begin(ops *op.Ops) { p.ops = ops p.macro = op.Record(ops) - // Write the TypeAux opcode and a byte for marking whether the - // path has had its MaxY filled out. If not, the gpu will fill it - // before using it. - data := ops.Write(2) + // Write the TypeAux opcode + data := ops.Write(opconst.TypeAuxLen) data[0] = byte(opconst.TypeAux) } // MoveTo moves the pen to the given position. func (p *Path) Move(to f32.Point) { - p.end() to = to.Add(p.pen) + p.end() p.pen = to + p.start = to } // end completes the current contour. func (p *Path) end() { + if p.pen != p.start { + p.lineTo(p.start) + } p.contour++ } @@ -93,56 +94,15 @@ func (p *Path) Quad(ctrl, to f32.Point) { } func (p *Path) quadTo(ctrl, to f32.Point) { - // Zero width curves don't contribute to stenciling. - if p.pen.X == to.X && p.pen.X == ctrl.X { - p.pen = to - return - } - - bounds := f32.Rectangle{ - Min: p.pen, - Max: to, - }.Canon() - - // If the curve contain areas where a vertical line - // intersects it twice, split the curve in two x monotone - // lower and upper curves. The stencil fragment program - // expects only one intersection per curve. - - // Find the t where the derivative in x is 0. - v0 := ctrl.Sub(p.pen) - v1 := to.Sub(ctrl) - d := v0.X - v1.X - // t = v0 / d. Split if t is in ]0;1[. - if v0.X > 0 && d > v0.X || v0.X < 0 && d < v0.X { - t := v0.X / d - ctrl0 := p.pen.Mul(1 - t).Add(ctrl.Mul(t)) - ctrl1 := ctrl.Mul(1 - t).Add(to.Mul(t)) - mid := ctrl0.Mul(1 - t).Add(ctrl1.Mul(t)) - p.simpleQuadTo(ctrl0, mid) - p.simpleQuadTo(ctrl1, to) - if mid.X > bounds.Max.X { - bounds.Max.X = mid.X - } - if mid.X < bounds.Min.X { - bounds.Min.X = mid.X - } - } else { - p.simpleQuadTo(ctrl, to) - } - // Find the y extremum, if any. - d = v0.Y - v1.Y - if v0.Y > 0 && d > v0.Y || v0.Y < 0 && d < v0.Y { - t := v0.Y / d - y := (1-t)*(1-t)*p.pen.Y + 2*(1-t)*t*ctrl.Y + t*t*to.Y - if y > bounds.Max.Y { - bounds.Max.Y = y - } - if y < bounds.Min.Y { - bounds.Min.Y = y - } - } - p.expand(bounds) + data := p.ops.Write(ops.QuadSize + 4) + bo := binary.LittleEndian + bo.PutUint32(data[0:], uint32(p.contour)) + ops.EncodeQuad(data[4:], ops.Quad{ + From: p.pen, + Ctrl: ctrl, + To: to, + }) + p.pen = to } // Cube records a cubic Bézier from the pen through @@ -223,68 +183,12 @@ func (p *Path) approxCubeTo(splits int, maxDist float32, ctrl0, ctrl1, to f32.Po return splits } -func (p *Path) expand(b f32.Rectangle) { - if !p.hasBounds { - p.hasBounds = true - inf := float32(math.Inf(+1)) - p.bounds = f32.Rectangle{ - Min: f32.Point{X: inf, Y: inf}, - Max: f32.Point{X: -inf, Y: -inf}, - } - } - p.bounds = p.bounds.Union(b) -} - -func (p *Path) vertex(cornerx, cornery int16, ctrl, to f32.Point) { - var corner float32 - // Encode corner. - if cornerx == 1 { - corner += .5 - } - if cornery == 1 { - corner += .25 - } - v := path.Vertex{ - Corner: corner, - FromX: p.pen.X, - FromY: p.pen.Y, - CtrlX: ctrl.X, - CtrlY: ctrl.Y, - ToX: to.X, - ToY: to.Y, - } - data := p.ops.Write(path.VertStride) - bo := binary.LittleEndian - bo.PutUint32(data[0:], math.Float32bits(corner)) - // Put the contour index in MaxY. - bo.PutUint32(data[4:], uint32(p.contour)) - bo.PutUint32(data[8:], math.Float32bits(v.FromX)) - bo.PutUint32(data[12:], math.Float32bits(v.FromY)) - bo.PutUint32(data[16:], math.Float32bits(v.CtrlX)) - bo.PutUint32(data[20:], math.Float32bits(v.CtrlY)) - bo.PutUint32(data[24:], math.Float32bits(v.ToX)) - bo.PutUint32(data[28:], math.Float32bits(v.ToY)) -} - -func (p *Path) simpleQuadTo(ctrl, to f32.Point) { - // NW. - p.vertex(-1, 1, ctrl, to) - // NE. - p.vertex(1, 1, ctrl, to) - // SW. - p.vertex(-1, -1, ctrl, to) - // SE. - p.vertex(1, -1, ctrl, to) - p.pen = to -} - // End the path and return a clip operation that represents it. func (p *Path) End() Op { p.end() c := p.macro.Stop() return Op{ - call: c, - bounds: p.bounds, + call: c, } } @@ -332,6 +236,7 @@ func roundRect(ops *op.Ops, r f32.Rectangle, se, sw, nw, ne float32) Op { var p Path p.Begin(ops) p.Move(r.Min) + p.Move(f32.Point{X: w, Y: h - se}) p.Cube(f32.Point{X: 0, Y: se * c}, f32.Point{X: -se + se*c, Y: se}, f32.Point{X: -se, Y: se}) // SE p.Line(f32.Point{X: sw - w + se, Y: 0})