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})