From 1586e600a0e924f26309e6367e4fcc9b38fc3fdb Mon Sep 17 00:00:00 2001 From: Seebs Date: Sun, 4 Jun 2017 12:55:43 -0500 Subject: [PATCH 1/6] The initializer is surprisingly expensive. Removing the call to Alpha(1) and replacing it with an inline definition produces measurable improvements. Replacing each instance of ZV with Vec{} further improves things. We keep an inline RGBA because there are circumstances (mostly when using pictures) where we don't want to have to set colors to get default behavior. For a fairly triangle-heavy thing, this reduces time spent in SetLen from something over 10% of execution time to around 2.5% of execution time. --- data.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data.go b/data.go index 4e6c528..c941241 100644 --- a/data.go +++ b/data.go @@ -45,7 +45,7 @@ func (td *TrianglesData) SetLen(len int) { Color RGBA Picture Vec Intensity float64 - }{ZV, Alpha(1), ZV, 0}) + }{Color: RGBA{1, 1, 1, 1}}) } } if len < td.Len() { From c321515d3cb37157d6587e010b7a97acfe8eeebd Mon Sep 17 00:00:00 2001 From: Seebs Date: Mon, 5 Jun 2017 18:54:53 -0500 Subject: [PATCH 2/6] Simplify Matrix math, use 6-value affine matrixes. It turns out that affine matrices are much simpler than the 3x3 matrices they imply, and we can use this to dramatically streamline some code. For a test program, this was about a 50% gain in frame rate just from the cost of the applyMatrixAndMask calls in imdraw, which were calling matrix.Project() many times. Simplifying matrix.Project, alone, got a nearly 50% frame rate boost! Also modify pixelgl's SetMatrix to copy the six values of a 3x2 Affine into the corresponding locations of a 3x3 matrix. --- geometry.go | 75 ++++++++++++++++++++++++++--------------------- pixelgl/canvas.go | 11 +++++-- 2 files changed, 51 insertions(+), 35 deletions(-) diff --git a/geometry.go b/geometry.go index f934839..928c9c5 100644 --- a/geometry.go +++ b/geometry.go @@ -3,8 +3,6 @@ package pixel import ( "fmt" "math" - - "github.com/go-gl/mathgl/mgl64" ) // Vec is a 2D vector type with X and Y coordinates. @@ -251,7 +249,7 @@ func (r Rect) Union(s Rect) Rect { ) } -// Matrix is a 3x3 transformation matrix that can be used for all kinds of spacial transforms, such +// Matrix is a 3x2 affine matrix that can be used for all kinds of spatial transforms, such // as movement, scaling and rotations. // // Matrix has a handful of useful methods, each of which adds a transformation to the matrix. For @@ -261,38 +259,41 @@ func (r Rect) Union(s Rect) Rect { // // This code creates a Matrix that first moves everything by 100 units horizontally and 200 units // vertically and then rotates everything by 90 degrees around the origin. -type Matrix [9]float64 +// +// Layout is: +// [0] [2] [4] +// [1] [3] [5] +// 0 0 1 [implicit row] +type Matrix [6]float64 // IM stands for identity matrix. Does nothing, no transformation. -var IM = Matrix(mgl64.Ident3()) +var IM = Matrix{1, 0, 0, 1, 0, 0} // String returns a string representation of the Matrix. // // m := pixel.IM -// fmt.Println(m) // Matrix(1 0 0 | 0 1 0 | 0 0 1) +// fmt.Println(m) // Matrix(1 0 0 | 0 1 0) func (m Matrix) String() string { return fmt.Sprintf( - "Matrix(%v %v %v | %v %v %v | %v %v %v)", - m[0], m[3], m[6], - m[1], m[4], m[7], - m[2], m[5], m[8], + "Matrix(%v %v %v | %v %v %v)", + m[0], m[2], m[4], + m[1], m[3], m[5], ) } // Moved moves everything by the delta vector. func (m Matrix) Moved(delta Vec) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Translate2D(delta.XY()).Mul3(m3) - return Matrix(m3) + m[4], m[5] = m[4]+delta.X, m[5]+delta.Y + return m } // ScaledXY scales everything around a given point by the scale factor in each axis respectively. func (m Matrix) ScaledXY(around Vec, scale Vec) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3) - m3 = mgl64.Scale2D(scale.XY()).Mul3(m3) - m3 = mgl64.Translate2D(around.XY()).Mul3(m3) - return Matrix(m3) + m[4], m[5] = m[4]-around.X, m[5]-around.Y + m[0], m[2], m[4] = m[0]*scale.X, m[2]*scale.X, m[4]*scale.X + m[1], m[3], m[5] = m[1]*scale.Y, m[3]*scale.Y, m[5]*scale.Y + m[4], m[5] = m[4]+around.X, m[5]+around.Y + return m } // Scaled scales everything around a given point by the scale factor. @@ -302,36 +303,44 @@ func (m Matrix) Scaled(around Vec, scale float64) Matrix { // Rotated rotates everything around a given point by the given angle in radians. func (m Matrix) Rotated(around Vec, angle float64) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Translate2D(around.Scaled(-1).XY()).Mul3(m3) - m3 = mgl64.Rotate3DZ(angle).Mul3(m3) - m3 = mgl64.Translate2D(around.XY()).Mul3(m3) - return Matrix(m3) + sint, cost := math.Sincos(angle) + m[4], m[5] = m[4]-around.X, m[5]-around.Y + m = m.Chained(Matrix{cost, sint, -sint, cost, 0, 0}) + m[4], m[5] = m[4]+around.X, m[5]+around.Y + return m } // Chained adds another Matrix to this one. All tranformations by the next Matrix will be applied // after the transformations of this Matrix. func (m Matrix) Chained(next Matrix) Matrix { - m3 := mgl64.Mat3(m) - m3 = mgl64.Mat3(next).Mul3(m3) - return Matrix(m3) + return Matrix{ + m[0]*next[0] + m[2]*next[1], + m[1]*next[0] + m[3]*next[1], + m[0]*next[2] + m[2]*next[3], + m[1]*next[2] + m[3]*next[3], + m[0]*next[4] + m[2]*next[5] + m[4], + m[1]*next[4] + m[3]*next[5] + m[5], + } } // Project applies all transformations added to the Matrix to a vector u and returns the result. // // Time complexity is O(1). func (m Matrix) Project(u Vec) Vec { - m3 := mgl64.Mat3(m) - proj := m3.Mul3x1(mgl64.Vec3{u.X, u.Y, 1}) - return V(proj.X(), proj.Y()) + return Vec{X: m[0]*u.X + m[2]*u.Y + m[4], Y: m[1]*u.X + m[3]*u.Y + m[5]} } // Unproject does the inverse operation to Project. // +// It turns out that multiplying a vector by the inverse matrix of m +// can be nearly-accomplished by subtracting the translate part of the +// matrix and multplying by the inverse of the top-left 2x2 matrix, +// and the inverse of a 2x2 matrix is simple enough to just be +// inlined in the computation. +// // Time complexity is O(1). func (m Matrix) Unproject(u Vec) Vec { - m3 := mgl64.Mat3(m) - inv := m3.Inv() - unproj := inv.Mul3x1(mgl64.Vec3{u.X, u.Y, 1}) - return V(unproj.X(), unproj.Y()) + d := (m[0] * m[3]) - (m[1] * m[2]) + u.X, u.Y = (u.X-m[4])/d, (u.Y-m[5])/d + return Vec{u.X*m[3] - u.Y*m[1], u.Y*m[0] - u.X*m[2]} } diff --git a/pixelgl/canvas.go b/pixelgl/canvas.go index a088350..b7b5cfc 100644 --- a/pixelgl/canvas.go +++ b/pixelgl/canvas.go @@ -90,9 +90,16 @@ func (c *Canvas) MakePicture(p pixel.Picture) pixel.TargetPicture { } // SetMatrix sets a Matrix that every point will be projected by. +// pixel.Matrix is 3x2 with an implicit 0, 0, 1 row after it. So +// [0] [2] [4] [0] [3] [6] +// [1] [3] [5] => [1] [4] [7] +// 0 0 1 0 0 1 +// since all matrix ops are affine, the last row never changes, +// and we don't need to copy it +// func (c *Canvas) SetMatrix(m pixel.Matrix) { - for i := range m { - c.mat[i] = float32(m[i]) + for i, j := range [6]int{ 0, 1, 3, 4, 6, 7} { + c.mat[j] = float32(m[i]) } } From 9d1fc5bd1f4fb7b32ed5e9b0a82c2e150ac9e7f2 Mon Sep 17 00:00:00 2001 From: Seebs Date: Mon, 5 Jun 2017 19:37:53 -0500 Subject: [PATCH 3/6] use point pool For internal operations (anything using getAndClearPoints), there's a pretty good chance that the operation will repeatedly invoke something like fillPolygon(), meaning that it needs to push "a few" points and then invoke something that uses those points. So, we add a slice for containing spare slices of points, and on the way out of each such function, shove the current imd.points (as used inside that function) onto a stack, and set imd.points to [0:0] of the thing it was called with. Performance goes from 11-13fps to 17-18fps on my test case. --- imdraw/imdraw.go | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index 626cb2f..2fe1fad 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -52,6 +52,7 @@ type IMDraw struct { EndShape EndShape points []point + pool [][]point matrix pixel.Matrix mask pixel.RGBA @@ -109,7 +110,7 @@ func (imd *IMDraw) Clear() { // // This does not affect matrix and color mask set by SetMatrix and SetColorMask. func (imd *IMDraw) Reset() { - imd.points = nil + imd.points = imd.points[:0] imd.Color = pixel.Alpha(1) imd.Picture = pixel.ZV imd.Intensity = 0 @@ -256,10 +257,22 @@ func (imd *IMDraw) EllipseArc(radius pixel.Vec, low, high, thickness float64) { func (imd *IMDraw) getAndClearPoints() []point { points := imd.points - imd.points = nil + // use one of the existing pools so we don't reallocate as often + if len(imd.pool) > 0 { + pos := len(imd.pool) - 1 + imd.points = imd.pool[pos] + imd.pool = imd.pool[0:pos] + } else { + imd.points = nil + } return points } +func (imd *IMDraw) restorePoints(points []point) { + imd.pool = append(imd.pool, imd.points) + imd.points = points[:0] +} + func (imd *IMDraw) applyMatrixAndMask(off int) { for i := range (*imd.tri)[off:] { (*imd.tri)[off+i].Position = imd.matrix.Project((*imd.tri)[off+i].Position) @@ -271,6 +284,7 @@ func (imd *IMDraw) fillRectangle() { points := imd.getAndClearPoints() if len(points) < 2 { + imd.restorePoints(points) return } @@ -302,12 +316,14 @@ func (imd *IMDraw) fillRectangle() { imd.applyMatrixAndMask(off) imd.batch.Dirty() + imd.restorePoints(points) } func (imd *IMDraw) outlineRectangle(thickness float64) { points := imd.getAndClearPoints() if len(points) < 2 { + imd.restorePoints(points) return } @@ -323,12 +339,14 @@ func (imd *IMDraw) outlineRectangle(thickness float64) { imd.pushPt(pixel.V(b.pos.X, a.pos.Y), mid) imd.polyline(thickness, true) } + imd.restorePoints(points) } func (imd *IMDraw) fillPolygon() { points := imd.getAndClearPoints() if len(points) < 3 { + imd.restorePoints(points) return } @@ -346,6 +364,7 @@ func (imd *IMDraw) fillPolygon() { imd.applyMatrixAndMask(off) imd.batch.Dirty() + imd.restorePoints(points) } func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) { @@ -387,6 +406,7 @@ func (imd *IMDraw) fillEllipseArc(radius pixel.Vec, low, high float64) { imd.applyMatrixAndMask(off) imd.batch.Dirty() } + imd.restorePoints(points) } func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness float64, doEndShape bool) { @@ -485,12 +505,14 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa } } } + imd.restorePoints(points) } func (imd *IMDraw) polyline(thickness float64, closed bool) { points := imd.getAndClearPoints() if len(points) == 0 { + imd.restorePoints(points) return } if len(points) == 1 { @@ -591,4 +613,5 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()-math.Pi) } } + imd.restorePoints(points) } From b6533006e7b3e897f59a3c4c92fe08ebbf389c0a Mon Sep 17 00:00:00 2001 From: Seebs Date: Mon, 5 Jun 2017 19:46:16 -0500 Subject: [PATCH 4/6] smaller imdraw optimizations For polyline, don't compute each normal twice; when we're going through a line, the "next" normal for segment N is always the "previous" normal for segment N+1, and we can compute fewer of them. --- imdraw/imdraw.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index 2fe1fad..0d40452 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -543,6 +543,8 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.pushPt(points[j].pos.Sub(normal), points[j]) // middle points + // compute "previous" normal: + ijNormal := points[1].pos.Sub(points[0].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) for i := 0; i < len(points); i++ { j, k := i+1, i+2 @@ -558,7 +560,6 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { k %= len(points) } - ijNormal := points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) jkNormal := points[k].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) orientation := 1.0 @@ -589,6 +590,8 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.pushPt(points[j].pos.Add(jkNormal), points[j]) imd.pushPt(points[j].pos.Sub(jkNormal), points[j]) } + // "next" normal becomes previous normal + ijNormal = jkNormal } // last point From c6e7a834679b85a464b2f1617280f600d0fdf61c Mon Sep 17 00:00:00 2001 From: Seebs Date: Mon, 5 Jun 2017 20:12:35 -0500 Subject: [PATCH 5/6] Reduce copying in fillPolygon A slice of points means copying every point into the slice, then copying every point's data from the slice to TrianglesData. An array of indicies lets the compiler make better choices. --- imdraw/imdraw.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index 0d40452..e5a8c95 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -354,11 +354,12 @@ func (imd *IMDraw) fillPolygon() { imd.tri.SetLen(imd.tri.Len() + 3*(len(points)-2)) for i, j := 1, off; i+1 < len(points); i, j = i+1, j+3 { - for k, p := range []point{points[0], points[i], points[i+1]} { - (*imd.tri)[j+k].Position = p.pos - (*imd.tri)[j+k].Color = p.col - (*imd.tri)[j+k].Picture = p.pic - (*imd.tri)[j+k].Intensity = p.in + for k, p := range []int{0, i, i + 1} { + tri := &(*imd.tri)[j+k] + tri.Position = points[p].pos + tri.Color = points[p].col + tri.Picture = points[p].pic + tri.Intensity = points[p].in } } From ce8687b80fc0dec46328fcbb4e8436f54d8595b8 Mon Sep 17 00:00:00 2001 From: Seebs Date: Fri, 9 Jun 2017 00:07:08 -0500 Subject: [PATCH 6/6] Don't duplicate computations in gltriangles.go The computation including a call to Stride() can't be optimized away safely because the compiler can't tell that Stride() is effectively constant, but we know it won't change so we can make a slice pointing at that part of the array. CPU time for updateData goes from 26.35% to 18.65% in my test case. --- pixelgl/gltriangles.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/pixelgl/gltriangles.go b/pixelgl/gltriangles.go index 64b7355..bf7a895 100644 --- a/pixelgl/gltriangles.go +++ b/pixelgl/gltriangles.go @@ -103,15 +103,17 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) { tx, ty = (*t)[i].Picture.XY() in = (*t)[i].Intensity ) - gt.data[i*gt.vs.Stride()+0] = float32(px) - gt.data[i*gt.vs.Stride()+1] = float32(py) - gt.data[i*gt.vs.Stride()+2] = float32(col.R) - gt.data[i*gt.vs.Stride()+3] = float32(col.G) - gt.data[i*gt.vs.Stride()+4] = float32(col.B) - gt.data[i*gt.vs.Stride()+5] = float32(col.A) - gt.data[i*gt.vs.Stride()+6] = float32(tx) - gt.data[i*gt.vs.Stride()+7] = float32(ty) - gt.data[i*gt.vs.Stride()+8] = float32(in) + s := gt.vs.Stride() + d := gt.data[i*s : i*s+9] + d[0] = float32(px) + d[1] = float32(py) + d[2] = float32(col.R) + d[3] = float32(col.G) + d[4] = float32(col.B) + d[5] = float32(col.A) + d[6] = float32(tx) + d[7] = float32(ty) + d[8] = float32(in) } return }