From 9a6e6066bd2fec72ed61bfab2ce35e743a38133a Mon Sep 17 00:00:00 2001 From: Seebs Date: Fri, 9 Jun 2017 19:19:32 -0500 Subject: [PATCH 1/6] don't call Len() when it can't change updateData()'s loops checking gt.Len() turns out to have been costing significant computation, not least because each call then in turn called gt.vs.Stride(). --- pixelgl/gltriangles.go | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/pixelgl/gltriangles.go b/pixelgl/gltriangles.go index 6493270..52ed0c9 100644 --- a/pixelgl/gltriangles.go +++ b/pixelgl/gltriangles.go @@ -101,16 +101,17 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) { } // TrianglesData short path + stride := gt.vs.Stride() + length := gt.Len() if t, ok := t.(*pixel.TrianglesData); ok { - for i := 0; i < gt.Len(); i++ { + for i := 0; i < length; i++ { var ( px, py = (*t)[i].Position.XY() col = (*t)[i].Color tx, ty = (*t)[i].Picture.XY() in = (*t)[i].Intensity ) - s := gt.vs.Stride() - d := gt.data[i*s : i*s+9] + d := gt.data[i*stride : i*stride+9] d[0] = float32(px) d[1] = float32(py) d[2] = float32(col.R) @@ -125,27 +126,27 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) { } if t, ok := t.(pixel.TrianglesPosition); ok { - for i := 0; i < gt.Len(); i++ { + for i := 0; i < length; i++ { px, py := t.Position(i).XY() - gt.data[i*gt.vs.Stride()+0] = float32(px) - gt.data[i*gt.vs.Stride()+1] = float32(py) + gt.data[i*stride+0] = float32(px) + gt.data[i*stride+1] = float32(py) } } if t, ok := t.(pixel.TrianglesColor); ok { - for i := 0; i < gt.Len(); i++ { + for i := 0; i < length; i++ { col := t.Color(i) - 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*stride+2] = float32(col.R) + gt.data[i*stride+3] = float32(col.G) + gt.data[i*stride+4] = float32(col.B) + gt.data[i*stride+5] = float32(col.A) } } if t, ok := t.(pixel.TrianglesPicture); ok { - for i := 0; i < gt.Len(); i++ { + for i := 0; i < length; i++ { pic, intensity := t.Picture(i) - gt.data[i*gt.vs.Stride()+6] = float32(pic.X) - gt.data[i*gt.vs.Stride()+7] = float32(pic.Y) - gt.data[i*gt.vs.Stride()+8] = float32(intensity) + gt.data[i*stride+6] = float32(pic.X) + gt.data[i*stride+7] = float32(pic.Y) + gt.data[i*stride+8] = float32(intensity) } } } From ef6a44fef85629e8c1cc6db1e91b44f95f6c961a Mon Sep 17 00:00:00 2001 From: Seebs Date: Fri, 9 Jun 2017 20:19:17 -0500 Subject: [PATCH 2/6] Slightly clean up normal calculations We never actually need the "normal" value; it's an extra calculation we didn't need, because ijNormal is the same value early on. It's totally possible that we could further simplify this; there's a lot of time going into the normal computations. --- imdraw/imdraw.go | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index f7ce769..ab72ea9 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -528,29 +528,28 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { // first point j, i := 0, 1 - normal := points[i].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + ijNormal := points[1].pos.Sub(points[0].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) if !closed { switch points[j].endshape { case NoEndShape: // nothing case SharpEndShape: - imd.pushPt(points[j].pos.Add(normal), points[j]) - imd.pushPt(points[j].pos.Sub(normal), points[j]) - imd.pushPt(points[j].pos.Add(normal.Rotated(math.Pi/2)), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal), points[j]) + imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal.Rotated(math.Pi/2)), points[j]) imd.fillPolygon() case RoundEndShape: imd.pushPt(points[j].pos, points[j]) - imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()+math.Pi) + imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()+math.Pi) } } - imd.pushPt(points[j].pos.Add(normal), points[j]) - imd.pushPt(points[j].pos.Sub(normal), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal), points[j]) + imd.pushPt(points[j].pos.Sub(ijNormal), 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 @@ -602,10 +601,10 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { // last point i, j = len(points)-2, len(points)-1 - normal = points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + ijNormal = points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) - imd.pushPt(points[j].pos.Sub(normal), points[j]) - imd.pushPt(points[j].pos.Add(normal), points[j]) + imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.fillPolygon() if !closed { @@ -613,13 +612,13 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { case NoEndShape: // nothing case SharpEndShape: - imd.pushPt(points[j].pos.Add(normal), points[j]) - imd.pushPt(points[j].pos.Sub(normal), points[j]) - imd.pushPt(points[j].pos.Add(normal.Rotated(-math.Pi/2)), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal), points[j]) + imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal.Rotated(-math.Pi/2)), points[j]) imd.fillPolygon() case RoundEndShape: imd.pushPt(points[j].pos, points[j]) - imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), normal.Angle(), normal.Angle()-math.Pi) + imd.fillEllipseArc(pixel.V(thickness/2, thickness/2), ijNormal.Angle(), ijNormal.Angle()-math.Pi) } } From f2ef87f1983a3797229d6625cb92f2da7b94d2a4 Mon Sep 17 00:00:00 2001 From: Seebs Date: Fri, 9 Jun 2017 21:42:20 -0500 Subject: [PATCH 3/6] Improve normal calculations Soooo. It turns out that the bunch of smallish (~4-5% of runtime) loads associated with Len(), Unit(), Rotated(), and so on... Were actually more like 15% or more of computational effort. I first figured this out by creating: func (u Vec) Normal(v Vec) Vec which gives you a vector normal to u->v. That consumed a lot of CPU time, and was followed by .Unit().Scaled(imd.thickness / 2), which consumed a bit more CPU time. After some poking, and in the interests of avoiding UI cruft, the final selection is func (u Vec) Normal() Vec This returns the vector rotated 90 degrees, which turns out to be the most common problem. --- geometry.go | 13 +++++++++++++ imdraw/imdraw.go | 15 +++++++-------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/geometry.go b/geometry.go index 13a574d..02a8b51 100644 --- a/geometry.go +++ b/geometry.go @@ -49,6 +49,11 @@ func (u Vec) XY() (x, y float64) { return u.X, u.Y } +// Normal returns a vector normal to u (rotated by math.pi/2) +func (u Vec) Normal() Vec { + return Vec{X: u.Y, Y: -u.X} +} + // Add returns the sum of vectors u and v. func (u Vec) Add(v Vec) Vec { return Vec{ @@ -65,6 +70,14 @@ func (u Vec) Sub(v Vec) Vec { } } +// To returns the vector from vector u to vector v, equivalent to v.Sub(u). +func (u Vec) To(v Vec) Vec { + return Vec{ + v.X - u.X, + v.Y - u.Y, + } +} + // Scaled returns the vector u multiplied by c. func (u Vec) Scaled(c float64) Vec { return Vec{u.X * c, u.Y * c} diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index ab72ea9..7f72a4d 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -495,12 +495,12 @@ func (imd *IMDraw) outlineEllipseArc(radius pixel.Vec, low, high, thickness floa thick := pixel.V(thickness/2, 0).Rotated(normalLow) imd.pushPt(lowCenter.Add(thick), pt) imd.pushPt(lowCenter.Sub(thick), pt) - imd.pushPt(lowCenter.Sub(thick.Rotated(math.Pi/2*orientation)), pt) + imd.pushPt(lowCenter.Sub(thick.Normal().Scaled(orientation)), pt) imd.fillPolygon() thick = pixel.V(thickness/2, 0).Rotated(normalHigh) imd.pushPt(highCenter.Add(thick), pt) imd.pushPt(highCenter.Sub(thick), pt) - imd.pushPt(highCenter.Add(thick.Rotated(math.Pi/2*orientation)), pt) + imd.pushPt(highCenter.Add(thick.Normal().Scaled(orientation)), pt) imd.fillPolygon() case RoundEndShape: imd.pushPt(lowCenter, pt) @@ -528,7 +528,7 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { // first point j, i := 0, 1 - ijNormal := points[1].pos.Sub(points[0].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + ijNormal := points[0].pos.To(points[1].pos).Normal().Unit().Scaled(thickness / 2) if !closed { switch points[j].endshape { @@ -537,7 +537,7 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { case SharpEndShape: imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) - imd.pushPt(points[j].pos.Add(ijNormal.Rotated(math.Pi/2)), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal.Normal()), points[j]) imd.fillPolygon() case RoundEndShape: imd.pushPt(points[j].pos, points[j]) @@ -549,7 +549,6 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) // middle points - // compute "previous" normal: for i := 0; i < len(points); i++ { j, k := i+1, i+2 @@ -565,7 +564,7 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { k %= len(points) } - jkNormal := points[k].pos.Sub(points[j].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + jkNormal := points[j].pos.To(points[k].pos).Normal().Unit().Scaled(thickness / 2) orientation := 1.0 if ijNormal.Cross(jkNormal) > 0 { @@ -601,7 +600,7 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { // last point i, j = len(points)-2, len(points)-1 - ijNormal = points[j].pos.Sub(points[i].pos).Rotated(math.Pi / 2).Unit().Scaled(thickness / 2) + ijNormal = points[i].pos.To(points[j].pos).Normal().Unit().Scaled(thickness / 2) imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) imd.pushPt(points[j].pos.Add(ijNormal), points[j]) @@ -614,7 +613,7 @@ func (imd *IMDraw) polyline(thickness float64, closed bool) { case SharpEndShape: imd.pushPt(points[j].pos.Add(ijNormal), points[j]) imd.pushPt(points[j].pos.Sub(ijNormal), points[j]) - imd.pushPt(points[j].pos.Add(ijNormal.Rotated(-math.Pi/2)), points[j]) + imd.pushPt(points[j].pos.Add(ijNormal.Normal().Scaled(-1)), points[j]) imd.fillPolygon() case RoundEndShape: imd.pushPt(points[j].pos, points[j]) From 0dc27e409b103b3608c08382aa13894ceee17c25 Mon Sep 17 00:00:00 2001 From: Seebs Date: Fri, 9 Jun 2017 22:22:27 -0500 Subject: [PATCH 4/6] Push: Don't convert pixel.RGBA to pixel.RGBA Because that's expensive, even in the case where the conversion is trivial. Use type assertion first. Reduces runtime cost of imdraw.Push from ~15.3% to 8.4%, so not-quite-50% of runtime cost of pushing points. If you were setting imd.Color to Color objects that aren't RGBA every single point, not much help. But if you set it and then draw a bunch of points, this will be a big win. --- imdraw/imdraw.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/imdraw/imdraw.go b/imdraw/imdraw.go index 7f72a4d..e616c39 100644 --- a/imdraw/imdraw.go +++ b/imdraw/imdraw.go @@ -128,7 +128,9 @@ func (imd *IMDraw) Draw(t pixel.Target) { // Push adds some points to the IM queue. All Pushed points will have the same properties except for // the position. func (imd *IMDraw) Push(pts ...pixel.Vec) { - imd.Color = pixel.ToRGBA(imd.Color) + if _, ok := imd.Color.(pixel.RGBA); !ok { + imd.Color = pixel.ToRGBA(imd.Color) + } opts := point{ col: imd.Color.(pixel.RGBA), pic: imd.Picture, From 3fcad7503f72b755efd4d0972389a2cd3808188f Mon Sep 17 00:00:00 2001 From: faiface Date: Sun, 11 Jun 2017 01:17:37 +0200 Subject: [PATCH 5/6] minor doc changes --- geometry.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/geometry.go b/geometry.go index 02a8b51..d1819a8 100644 --- a/geometry.go +++ b/geometry.go @@ -49,11 +49,6 @@ func (u Vec) XY() (x, y float64) { return u.X, u.Y } -// Normal returns a vector normal to u (rotated by math.pi/2) -func (u Vec) Normal() Vec { - return Vec{X: u.Y, Y: -u.X} -} - // Add returns the sum of vectors u and v. func (u Vec) Add(v Vec) Vec { return Vec{ @@ -70,7 +65,7 @@ func (u Vec) Sub(v Vec) Vec { } } -// To returns the vector from vector u to vector v, equivalent to v.Sub(u). +// To returns the vector from u to v. Equivalent to v.Sub(u). func (u Vec) To(v Vec) Vec { return Vec{ v.X - u.X, @@ -115,6 +110,11 @@ func (u Vec) Rotated(angle float64) Vec { } } +// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2). +func (u Vec) Normal() Vec { + return Vec{X: u.Y, Y: -u.X} +} + // Dot returns the dot product of vectors u and v. func (u Vec) Dot(v Vec) float64 { return u.X*v.X + u.Y*v.Y From e6484064aad5b61bd32b7059d05156f20b869765 Mon Sep 17 00:00:00 2001 From: faiface Date: Sun, 11 Jun 2017 01:18:23 +0200 Subject: [PATCH 6/6] one more tiny doc change --- geometry.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry.go b/geometry.go index d1819a8..2b4abab 100644 --- a/geometry.go +++ b/geometry.go @@ -110,7 +110,7 @@ func (u Vec) Rotated(angle float64) Vec { } } -// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2). +// Normal returns a vector normal to u. Equivalent to u.Rotated(math.Pi / 2), but faster. func (u Vec) Normal() Vec { return Vec{X: u.Y, Y: -u.X} }