From f68301dcd84ce2743b16bda016059c74217bbe15 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 678da34fc34f76f9edc4e0967189a74cf9d4318d 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 daedc45ea933ab83310b2a001917e7f3202fc11b 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 ee5d49dbd39f7313f805cd14221d53d80fbd31ea 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 51cd0314d5340540eff060ed57236d3ff4108084 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 d4cd1c33e2b095a3489833cae66d05f65bc07d88 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} }