From 1d1875901580fc468884765ee9ebab6a32ee1d7d Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Sat, 17 Aug 2024 14:28:50 -0500 Subject: [PATCH 01/11] Initialize pixel tools subpackage --- tools/cmd/root.go | 23 +++++++++++++++++++++++ tools/go.mod | 22 ++++++++++++++++++++++ tools/go.sum | 26 ++++++++++++++++++++++++++ tools/main.go | 9 +++++++++ 4 files changed, 80 insertions(+) create mode 100644 tools/cmd/root.go create mode 100644 tools/go.mod create mode 100644 tools/go.sum create mode 100644 tools/main.go diff --git a/tools/cmd/root.go b/tools/cmd/root.go new file mode 100644 index 0000000..77d161c --- /dev/null +++ b/tools/cmd/root.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "pixeltools", + Short: "Pixel tools provide benchmarking and validation tools for developing the pixel library", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, +} + +func Execute() { + if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} diff --git a/tools/go.mod b/tools/go.mod new file mode 100644 index 0000000..e146c3d --- /dev/null +++ b/tools/go.mod @@ -0,0 +1,22 @@ +module github.com/gopxl/pixel/tools + +go 1.21 + +require ( + github.com/gopxl/pixel/v2 v2.2.0-local + github.com/spf13/cobra v1.8.1 +) + +replace github.com/gopxl/pixel/v2 v2.2.0-local => ../ + +require ( + github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect + github.com/go-gl/mathgl v1.1.0 // indirect + github.com/gopxl/glhf/v2 v2.0.0 // indirect + github.com/gopxl/mainthread/v2 v2.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/image v0.18.0 // indirect +) diff --git a/tools/go.sum b/tools/go.sum new file mode 100644 index 0000000..ad238f5 --- /dev/null +++ b/tools/go.sum @@ -0,0 +1,26 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= +github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/mathgl v1.1.0 h1:0lzZ+rntPX3/oGrDzYGdowSLC2ky8Osirvf5uAwfIEA= +github.com/go-gl/mathgl v1.1.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= +github.com/gopxl/glhf/v2 v2.0.0 h1:SJtNy+TXuTBRjMersNx722VDJ0XHIooMH2+7+99LPIc= +github.com/gopxl/glhf/v2 v2.0.0/go.mod h1:InKwj5OoVdOAkpzsS0ILwpB+RrWBLw1i7aFefiGmrp8= +github.com/gopxl/mainthread/v2 v2.0.0 h1:jRbeWFzX6/UyhRab00xS3xIVYywBgc0DgwPgwS6EVYw= +github.com/gopxl/mainthread/v2 v2.0.0/go.mod h1:/uFQhUiSP53SSU/RQ5w0FFkljRArJlaQkDPza3zE2V8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/main.go b/tools/main.go new file mode 100644 index 0000000..3694170 --- /dev/null +++ b/tools/main.go @@ -0,0 +1,9 @@ +package main + +import ( + "github.com/gopxl/pixel/tools/cmd" +) + +func main() { + cmd.Execute() +} From ba5eb8272e758dd58535593e0a5b24c3f0803b15 Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:48:40 -0500 Subject: [PATCH 02/11] Benchmark interface --- tools/benchmark/benchmark.go | 138 +++++++++++++++++++++++ tools/benchmark/stats.go | 211 +++++++++++++++++++++++++++++++++++ 2 files changed, 349 insertions(+) create mode 100644 tools/benchmark/benchmark.go create mode 100644 tools/benchmark/stats.go diff --git a/tools/benchmark/benchmark.go b/tools/benchmark/benchmark.go new file mode 100644 index 0000000..9f562f4 --- /dev/null +++ b/tools/benchmark/benchmark.go @@ -0,0 +1,138 @@ +package benchmark + +import ( + "fmt" + "slices" + "time" + + "github.com/gopxl/pixel/v2" + "github.com/gopxl/pixel/v2/backends/opengl" +) + +var Benchmarks = &Registry{benchmarks: map[string]Config{}} + +// Config defines how to run a given benchmark, along with metadata describing it +type Config struct { + Name string + Description string + + // New returns the benchmark to be executed + New func(win *opengl.Window) (Benchmark, error) + // Duration sets the maximum duration to run the benchmark + Duration time.Duration + // WindowConfig defines the input parameters to the benchmark's window + WindowConfig opengl.WindowConfig +} + +// Run executes the benchmark and calculates statistics about its performance +func (c Config) Run() (*Stats, error) { + fmt.Printf("Running benchmark %s\n", c.Name) + + windowConfig := c.WindowConfig + title := windowConfig.Title + if title == "" { + title = c.Name + } + windowConfig.Title = fmt.Sprintf("%s | FPS -", title) + + if windowConfig.Bounds.Empty() { + windowConfig.Bounds = pixel.R(0, 0, 1024, 1024) + } + if windowConfig.Position.Eq(pixel.ZV) { + windowConfig.Position = pixel.V(50, 50) + } + + duration := c.Duration + if duration == 0 { + duration = 10 * time.Second + } + + win, err := opengl.NewWindow(windowConfig) + if err != nil { + return nil, err + } + defer win.Destroy() + + benchmark, err := c.New(win) + if err != nil { + return nil, err + } + + frame := 0 + frameSeconds := make([]int, 0) + prevFrameCount := 0 + second := time.NewTicker(time.Second) + done := time.NewTicker(duration) + start := time.Now() +loop: + for frame = 0; !win.Closed(); frame++ { + benchmark.Step(win) + win.Update() + + select { + case <-second.C: + frameSeconds = append(frameSeconds, frame) + win.SetTitle(fmt.Sprintf("%s | FPS %v", title, frame-prevFrameCount)) + prevFrameCount = frame + case <-done.C: + break loop + default: + } + } + stats := NewStats(c.Name, time.Since(start), frame, frameSeconds) + + if win.Closed() { + return nil, fmt.Errorf("window closed early") + } + + return stats, err +} + +// Benchmark provides hooks into the stages of a window's lifecycle +type Benchmark interface { + Step(win *opengl.Window) +} + +// Registry is a collection of benchmark configs +type Registry struct { + benchmarks map[string]Config +} + +// List returns a copy of all registered benchmark configs +func (r *Registry) List() []Config { + configs := make([]Config, len(r.benchmarks)) + for i, name := range r.ListNames() { + configs[i] = r.benchmarks[name] + i++ + } + return configs +} + +// ListNames returns a sorted list of all registered benchmark names +func (r *Registry) ListNames() []string { + names := make([]string, len(r.benchmarks)) + i := 0 + for name := range r.benchmarks { + names[i] = name + i++ + } + slices.Sort(names) + return names +} + +// Add a benchmark config to the registry +func (r *Registry) Add(configs ...Config) { + for _, config := range configs { + r.benchmarks[config.Name] = config + } +} + +// Get a benchmark config by name +func (r *Registry) Get(name string) (Config, error) { + config, ok := r.benchmarks[name] + if !ok { + return config, fmt.Errorf("unknown benchmark %s", name) + } + + return config, nil +} diff --git a/tools/benchmark/stats.go b/tools/benchmark/stats.go new file mode 100644 index 0000000..8817af5 --- /dev/null +++ b/tools/benchmark/stats.go @@ -0,0 +1,211 @@ +package benchmark + +import ( + "encoding/json" + "fmt" + "math" + "os" + "os/user" + "runtime/debug" + "slices" + "strings" + "time" + + "github.com/olekukonko/tablewriter" +) + +var ( + machineName, pixelVersion string +) + +func init() { + machineName = getMachineName() + pixelVersion = getPixelVersion() +} + +// NewStats calculates statistics about a benchmark run +func NewStats(name string, duration time.Duration, frames int, frameSeconds []int) *Stats { + stats := &Stats{ + Name: name, + Frames: frames, + Duration: duration, + Machine: machineName, + PixelVersion: pixelVersion, + } + + milliseconds := stats.Duration.Milliseconds() + if milliseconds > 0 { + stats.AvgFPS = roundFloat(1000*float64(frames)/float64(milliseconds), 2) + } + + fps := make([]float64, 0, len(frameSeconds)) + for i, frame := range frameSeconds { + if i == 0 { + fps = append(fps, float64(frame)) + } else { + fps = append(fps, float64(frame-frameSeconds[i-1])) + } + } + if len(fps) > 0 { + stats.MinFPS = slices.Min(fps) + stats.MaxFPS = slices.Max(fps) + stats.StdevFPS = standardDeviation(fps) + } + + return stats +} + +// Stats stores data about the performance of a benchmark run +type Stats struct { + Name string `json:"name"` + AvgFPS float64 `json:"avgFPS"` + MinFPS float64 `json:"minFPS"` + MaxFPS float64 `json:"maxFPS"` + StdevFPS float64 `json:"stdevFPS"` + + Frames int `json:"frames"` + Duration time.Duration `json:"duration"` + + Machine string `json:"machine"` + PixelVersion string `json:"pixelVersion"` +} + +// Print stats to stdout in a human-readable format +func (s *Stats) Print() { + StatsCollection{s}.Print() +} + +// StatsCollection holds stats from multiple benchmark runs +type StatsCollection []*Stats + +func (sc StatsCollection) Print() { + data := make([][]string, len(sc)) + for i, stats := range sc { + data[i] = []string{ + stats.Machine, + stats.PixelVersion, + stats.Name, + roundDuration(stats.Duration, 2).String(), + toString(stats.Frames), + toString(stats.AvgFPS), + toString(stats.MinFPS), + toString(stats.MaxFPS), + toString(stats.StdevFPS), + } + } + + table := tablewriter.NewWriter(os.Stdout) + headers := []string{"Machine", "Pixel", "Benchmark", "Duration", "Frames", "FPS Avg", "FPS Min", "FPS Max", "FPS Stdev"} + widths := map[string]int{ + "Machine": 18, + "Pixel": 6, + "Benchmark": 28, + } + for i, header := range headers { + minWidth := widths[header] + if minWidth == 0 { + minWidth = 6 + } + table.SetColMinWidth(i, minWidth) + } + table.SetHeader(headers) + table.SetAutoFormatHeaders(false) + table.SetAlignment(tablewriter.ALIGN_LEFT) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.AppendBulk(data) + table.Render() +} + +// Dump writes a JSON file of all stored statistics to the given path +func (sc StatsCollection) Dump(path string) error { + bytes, err := json.Marshal(sc) + if err != nil { + return err + } + if err := os.WriteFile(path, bytes, 0666); err != nil { + return err + } + return nil +} + +// roundFloat rounds the value to the given number of decimal places +func roundFloat(val float64, precision uint) float64 { + ratio := math.Pow(10, float64(precision)) + return math.Round(val*ratio) / ratio +} + +// roundDuration rounds the duration to the given number of decimal places based on the unit +func roundDuration(duration time.Duration, precision uint) time.Duration { + durationRounding := time.Duration(math.Pow(10, float64(precision))) + switch { + case duration > time.Second: + return duration.Round(time.Second / durationRounding) + case duration > time.Millisecond: + return duration.Round(time.Millisecond / durationRounding) + case duration > time.Microsecond: + return duration.Round(time.Microsecond / durationRounding) + default: + return duration + } +} + +func toString(val any) string { + switch v := val.(type) { + case float64: + return fmt.Sprintf("%v", roundFloat(v, 2)) + case float32: + return fmt.Sprintf("%v", roundFloat(float64(v), 2)) + default: + return fmt.Sprintf("%v", v) + } +} + +// standardDeviation calulates the variation of the given values relative to the average +func standardDeviation(values []float64) float64 { + var sum, avg, stdev float64 + for _, val := range values { + sum += val + } + count := float64(len(values)) + avg = sum / count + + for _, val := range values { + stdev += math.Pow(val-avg, 2) + } + stdev = math.Sqrt(stdev / count) + return stdev +} + +func getMachineName() string { + envs := []string{"MACHINE_NAME", "USER", "USERNAME"} + var name string + for _, env := range envs { + name = os.Getenv(env) + if name != "" { + return name + } + } + if u, err := user.Current(); err == nil { + return u.Username + } + return "unknown" +} + +func getPixelVersion() string { + ver := os.Getenv("PIXEL_VERSION") + if ver != "" { + return ver + } + + bi, ok := debug.ReadBuildInfo() + if ok { + for _, dep := range bi.Deps { + if dep.Path == "github.com/gopxl/pixel/v2" { + return strings.Split(dep.Version, "-")[0] + } + } + } + return "x.y.z" +} From 557f55b360bf661d28ca77c887073754a88e19ac Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:49:26 -0500 Subject: [PATCH 03/11] Benchmark command --- tools/cmd/bench.go | 166 +++++++++++++++++++++++++++++++++++++++++++++ tools/cmd/root.go | 18 +++-- 2 files changed, 177 insertions(+), 7 deletions(-) create mode 100644 tools/cmd/bench.go diff --git a/tools/cmd/bench.go b/tools/cmd/bench.go new file mode 100644 index 0000000..2d50e6d --- /dev/null +++ b/tools/cmd/bench.go @@ -0,0 +1,166 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + "runtime" + "runtime/pprof" + "text/tabwriter" + "time" + + "github.com/gopxl/pixel/tools/benchmark" + "github.com/gopxl/pixel/v2/backends/opengl" + "github.com/spf13/cobra" +) + +var ( + benchRunAll bool + benchRunOutput, + benchRunCpuprofile, + benchRunMemprofile string + benchRunDuration time.Duration + + benchStatsInput string +) + +func NewBenchCmd() *cobra.Command { + bench := &cobra.Command{ + Use: "bench", + Short: "Benchmark the pixel library", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + bench.AddCommand(NewBenchLsCmd(), NewBenchRunCmd(), NewBenchStatsCmd()) + return bench +} + +func NewBenchRunCmd() *cobra.Command { + run := &cobra.Command{ + Use: "run [names...] [opts]", + Short: "Run one or more benchmark tests", + RunE: func(cmd *cobra.Command, args []string) error { + if benchRunAll { + args = benchmark.Benchmarks.ListNames() + } else if len(args) == 0 { + return fmt.Errorf("requires at least one benchmark") + } + cmd.SilenceUsage = true + + // Start CPU profile + if benchRunCpuprofile != "" { + f, err := os.Create(benchRunCpuprofile) + if err != nil { + return fmt.Errorf("could not create CPU profile: %v", err) + } + defer f.Close() + if err := pprof.StartCPUProfile(f); err != nil { + return fmt.Errorf("could not start CPU profile: %v", err) + } + defer pprof.StopCPUProfile() + } + + // Run benchmark(s) + benchStats := make(benchmark.StatsCollection, len(args)) + var err error + run := func() { + var config benchmark.Config + for i, name := range args { + config, err = benchmark.Benchmarks.Get(name) + if err != nil { + return + } + + if benchRunDuration != 0 { + config.Duration = benchRunDuration + } + + var stats *benchmark.Stats + stats, err = config.Run() + if err != nil { + return + } + benchStats[i] = stats + } + } + + opengl.Run(run) + if err != nil { + return err + } + fmt.Println() + benchStats.Print() + + // Dump memory profile + if benchRunMemprofile != "" { + f, err := os.Create(benchRunMemprofile) + if err != nil { + return fmt.Errorf("could not create memory profile: %v", err) + } + defer f.Close() + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + return fmt.Errorf("could not write memory profile: %v", err) + } + } + + // Dump stats + if benchRunOutput != "" { + err := benchStats.Dump(benchRunOutput) + if err != nil { + return err + } + } + return nil + }, + } + + run.Flags().BoolVarP(&benchRunAll, "all", "a", false, "Run all registered benchmarks") + run.Flags().StringVarP(&benchRunOutput, "output", "o", "", "Output path for statistics file") + run.Flags().DurationVarP(&benchRunDuration, "duration", "d", 0, "Override duration for benchmark runs") + run.Flags().StringVarP(&benchRunCpuprofile, "cpuprofile", "c", "", "CPU profiling file") + run.Flags().StringVarP(&benchRunMemprofile, "memprofile", "m", "", "Memory profiling file") + return run +} + +func NewBenchLsCmd() *cobra.Command { + return &cobra.Command{ + Use: "ls", + Short: "List available benchmarks", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + w := tabwriter.NewWriter(os.Stdout, 1, 4, 8, ' ', 0) + for _, config := range benchmark.Benchmarks.List() { + fmt.Fprintf(w, "%s\t%s\n", config.Name, config.Description) + } + w.Flush() + }, + } +} + +func NewBenchStatsCmd() *cobra.Command { + stats := &cobra.Command{ + Use: "stats -i [path/to/stats.json]", + Short: "Pretty print the contents of a stats file", + SilenceUsage: true, + RunE: func(cmd *cobra.Command, args []string) error { + bytes, err := os.ReadFile(benchStatsInput) + if err != nil { + return err + } + + var benchStats benchmark.StatsCollection + if err := json.Unmarshal(bytes, &benchStats); err != nil { + return err + } + benchStats.Print() + + return nil + }, + } + + stats.Flags().StringVarP(&benchStatsInput, "input", "i", "", "Input path for statistics file") + stats.MarkFlagRequired("input") + return stats +} diff --git a/tools/cmd/root.go b/tools/cmd/root.go index 77d161c..a536cd4 100644 --- a/tools/cmd/root.go +++ b/tools/cmd/root.go @@ -7,16 +7,20 @@ import ( "github.com/spf13/cobra" ) -var rootCmd = &cobra.Command{ - Use: "pixeltools", - Short: "Pixel tools provide benchmarking and validation tools for developing the pixel library", - Run: func(cmd *cobra.Command, args []string) { - cmd.Help() - }, +func NewRootCmd() *cobra.Command { + root := &cobra.Command{ + Use: "pixeltools", + Short: "Pixel tools provide benchmarking and validation tools for developing the pixel library", + Run: func(cmd *cobra.Command, args []string) { + cmd.Help() + }, + } + root.AddCommand(NewBenchCmd()) + return root } func Execute() { - if err := rootCmd.Execute(); err != nil { + if err := NewRootCmd().Execute(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } From 8cb7c1a58a3d2d1fe0659127f458d6bbd24f8b1a Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:49:43 -0500 Subject: [PATCH 04/11] IMDraw benchmarks --- tools/benchmark/imdraw_bench.go | 128 ++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 tools/benchmark/imdraw_bench.go diff --git a/tools/benchmark/imdraw_bench.go b/tools/benchmark/imdraw_bench.go new file mode 100644 index 0000000..301a55e --- /dev/null +++ b/tools/benchmark/imdraw_bench.go @@ -0,0 +1,128 @@ +package benchmark + +import ( + "math" + "time" + + "github.com/gopxl/pixel/v2" + "github.com/gopxl/pixel/v2/backends/opengl" + "github.com/gopxl/pixel/v2/ext/imdraw" +) + +var ( + backgroundColor = pixel.RGB(0, 0, 0) +) + +func init() { + Benchmarks.Add( + Config{ + Name: "imdraw-static", + Description: "Stationary RGB triangles in a grid", + New: newStaticTriangles, + Duration: 30 * time.Second, + }, + Config{ + Name: "imdraw-moving", + Description: "Columns of RGB triangles moving in opposite directions", + New: newMovingTriangles, + Duration: 30 * time.Second, + }, + ) +} + +func newStaticTriangles(win *opengl.Window) (Benchmark, error) { + bounds := win.Bounds() + width := bounds.W() + height := bounds.H() + rows, cols := 32, 32 + cell := gridCell(width, height, rows, cols) + benchmark := &staticTriangles{ + imd: tri(cell), + rows: rows, + cols: cols, + cell: cell, + } + return benchmark, nil +} + +type staticTriangles struct { + imd *imdraw.IMDraw + rows, cols int + cell pixel.Vec +} + +func (st *staticTriangles) Step(win *opengl.Window) { + win.Clear(backgroundColor) + + for i := 0; i < st.cols; i++ { + for j := 0; j < st.rows; j++ { + pos := pixel.V(float64(i)*st.cell.X, float64(j)*st.cell.Y) + win.SetMatrix(pixel.IM.Moved(pos)) + st.imd.Draw(win) + } + } +} + +func newMovingTriangles(win *opengl.Window) (Benchmark, error) { + bounds := win.Bounds() + width := bounds.W() + height := bounds.H() + rows, cols := 32, 32 + cell := gridCell(width, height, rows, cols) + benchmark := &movingTriangles{ + imd: tri(cell), + rows: rows, + cols: cols, + cell: cell, + } + return benchmark, nil +} + +type movingTriangles struct { + imd *imdraw.IMDraw + rows, cols int + cell pixel.Vec + counter int +} + +func (mt *movingTriangles) Step(win *opengl.Window) { + win.Clear(backgroundColor) + + for i := 0; i < mt.cols; i++ { + yOffset := -mt.cell.Y + delta := float64(mt.counter % int(mt.cell.Y)) + if i%2 == 0 { + yOffset += delta + } else { + yOffset -= delta + } + + for j := 0; j < mt.rows+2; j++ { + pos := pixel.V(float64(i)*mt.cell.X, (float64(j)*mt.cell.Y)+yOffset) + matrix := pixel.IM.Moved(pos) + if i%2 == 1 { + matrix = matrix.Rotated(pos.Add(pixel.V(mt.cell.X/2, mt.cell.Y/2)), math.Pi) + } + win.SetMatrix(matrix) + mt.imd.Draw(win) + } + } + + mt.counter++ +} + +func tri(cell pixel.Vec) *imdraw.IMDraw { + imd := imdraw.New(nil) + imd.Color = pixel.RGB(1, 0, 0) + imd.Push(pixel.V(0, 0)) + imd.Color = pixel.RGB(0, 1, 0) + imd.Push(pixel.V(cell.X, 0)) + imd.Color = pixel.RGB(0, 0, 1) + imd.Push(pixel.V(cell.X/2, cell.Y)) + imd.Polygon(0) + return imd +} + +func gridCell(width, height float64, rows, cols int) (cell pixel.Vec) { + return pixel.V(width/float64(cols), height/float64(rows)) +} From 958daab1dc87beb8af0b2848882d01abff01442a Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:49:53 -0500 Subject: [PATCH 05/11] Sprite benchmarks --- tools/benchmark/sprite_bench.go | 218 ++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tools/benchmark/sprite_bench.go diff --git a/tools/benchmark/sprite_bench.go b/tools/benchmark/sprite_bench.go new file mode 100644 index 0000000..bcc3819 --- /dev/null +++ b/tools/benchmark/sprite_bench.go @@ -0,0 +1,218 @@ +package benchmark + +import ( + "image" + "image/png" + "os" + "path" + "path/filepath" + "runtime" + "time" + + "github.com/gopxl/pixel/v2" + "github.com/gopxl/pixel/v2/backends/opengl" +) + +var ( + basepath string + logoPath = "logo/LOGOTYPE-HORIZONTAL-BLUE2.png" + logoFrame = pixel.R(98, 44, 234, 180) +) + +func init() { + _, b, _, _ := runtime.Caller(0) + basepath = filepath.ToSlash(filepath.Dir(filepath.Dir(filepath.Dir(b)))) + logoPath = path.Join(basepath, logoPath) + + Benchmarks.Add( + Config{ + Name: "sprite-moving", + Description: "Columns of sprites moving in opposite directions", + New: newSpriteMoving, + Duration: 30 * time.Second, + }, + Config{ + Name: "sprite-moving-batched", + Description: "Columns of sprites moving in opposite directions with batched draw", + New: newSpriteMovingBatched, + Duration: 30 * time.Second, + }, + Config{ + Name: "sprite-static", + Description: "Draw a sprite to the window in a grid", + New: newSpriteStatic, + Duration: 30 * time.Second, + }, + Config{ + Name: "sprite-static-batched", + Description: "Draw a sprite to the window in a grid with batched draw", + New: newSpriteStaticBatched, + Duration: 30 * time.Second, + }, + ) +} + +func newSpriteStatic(win *opengl.Window) (Benchmark, error) { + sprite, err := loadSprite(logoPath, logoFrame) + if err != nil { + return nil, err + } + + bounds := win.Bounds() + width := bounds.W() + height := bounds.H() + rows, cols := 32, 32 + + benchmark := &spriteStatic{ + sprite: sprite, + rows: rows, + cols: rows, + cell: gridCell(width, height, rows, cols), + } + return benchmark, nil +} + +func newSpriteStaticBatched(win *opengl.Window) (Benchmark, error) { + benchmark, err := newSpriteStatic(win) + if err != nil { + return nil, err + } + ss := benchmark.(*spriteStatic) + ss.batch = pixel.NewBatch(&pixel.TrianglesData{}, ss.sprite.Picture()) + return ss, nil +} + +type spriteStatic struct { + sprite *pixel.Sprite + rows, cols int + cell pixel.Vec + batch *pixel.Batch +} + +func (ss *spriteStatic) Step(win *opengl.Window) { + win.Clear(backgroundColor) + var target pixel.Target + if ss.batch != nil { + ss.batch.Clear() + target = ss.batch + } else { + target = win + } + spriteGrid(ss.sprite, target, ss.rows, ss.cols, ss.cell) + if ss.batch != nil { + ss.batch.Draw(win) + } +} + +func newSpriteMoving(win *opengl.Window) (Benchmark, error) { + sprite, err := loadSprite(logoPath, logoFrame) + if err != nil { + return nil, err + } + bounds := win.Bounds() + width := bounds.W() + height := bounds.H() + rows, cols := 32, 32 + benchmark := &spriteMoving{ + sprite: sprite, + rows: rows, + cols: cols, + cell: gridCell(width, height, rows, cols), + } + return benchmark, nil +} + +func newSpriteMovingBatched(win *opengl.Window) (Benchmark, error) { + benchmark, err := newSpriteMoving(win) + if err != nil { + return nil, err + } + sm := benchmark.(*spriteMoving) + sm.batch = pixel.NewBatch(&pixel.TrianglesData{}, sm.sprite.Picture()) + return sm, nil +} + +type spriteMoving struct { + sprite *pixel.Sprite + rows, cols int + cell pixel.Vec + counter int + batch *pixel.Batch +} + +func (sm *spriteMoving) Step(win *opengl.Window) { + win.Clear(backgroundColor) + var target pixel.Target + if sm.batch != nil { + sm.batch.Clear() + target = sm.batch + } else { + target = win + } + spriteGridMoving(sm.sprite, target, sm.rows, sm.cols, sm.cell, sm.counter) + if sm.batch != nil { + sm.batch.Draw(win) + } + sm.counter += 1 +} + +func spriteGrid(sprite *pixel.Sprite, target pixel.Target, rows, cols int, cell pixel.Vec) { + spriteBounds := sprite.Frame().Bounds() + spriteWidth := spriteBounds.W() + spriteHeight := spriteBounds.H() + matrix := pixel.IM.ScaledXY(pixel.ZV, pixel.V(cell.X/spriteWidth, cell.Y/spriteHeight)) + offset := pixel.V(cell.X/2, cell.Y/2) + for i := 0; i < cols; i++ { + for j := 0; j < rows; j++ { + pos := pixel.V(float64(i)*cell.X, float64(j)*cell.Y).Add(offset) + sprite.Draw(target, matrix.Moved(pos)) + } + } +} + +func spriteGridMoving(sprite *pixel.Sprite, target pixel.Target, rows, cols int, cell pixel.Vec, counter int) { + spriteBounds := sprite.Frame().Bounds() + spriteWidth := spriteBounds.W() + spriteHeight := spriteBounds.H() + matrix := pixel.IM.ScaledXY(pixel.ZV, pixel.V(cell.X/spriteWidth, cell.Y/spriteHeight)) + offset := pixel.V(cell.X/2, cell.Y/2) + for i := 0; i < cols; i++ { + yOffset := -cell.Y + delta := float64(counter % int(cell.Y)) + if i%2 == 0 { + yOffset += delta + } else { + yOffset -= delta + } + + for j := 0; j < rows+2; j++ { + pos := pixel.V(float64(i)*cell.X, (float64(j)*cell.Y)+yOffset).Add(offset) + sprite.Draw(target, matrix.Moved(pos)) + } + } +} + +func loadSprite(file string, frame pixel.Rect) (sprite *pixel.Sprite, err error) { + image, err := loadPng(file) + if err != nil { + return nil, err + } + + pic := pixel.PictureDataFromImage(image) + if frame.Empty() { + frame = pic.Bounds() + } + sprite = pixel.NewSprite(pic, frame) + return sprite, nil +} + +func loadPng(file string) (i image.Image, err error) { + f, err := os.Open(file) + if err != nil { + return + } + defer f.Close() + + i, err = png.Decode(f) + return +} From 0e5012c854b2a0e57ac61536aec20d2956af8c88 Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:50:13 -0500 Subject: [PATCH 06/11] Tools readme with benchmark results --- tools/README.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 tools/README.md diff --git a/tools/README.md b/tools/README.md new file mode 100644 index 0000000..2b83a2a --- /dev/null +++ b/tools/README.md @@ -0,0 +1,65 @@ +# Benchmarking + +The `bench` command provides a set of tools used for benchmarking the performance of pixel under various scenarios. +It is intended to be a development tool for comparing the performance of new implementations in pixel against previous iterations. + +## Usage + +List available benchmarks +``` +go run main.go bench ls +``` + +Run a benchmark +``` +go run main.go bench run imdraw-static +``` + +Write benchmark stats to a file +``` +go run main.go bench run imdraw-static -o imdraw-static-stats.json +``` + +## Profiling +Run benchmark with cpu/mem profiling enabled +``` +go run main.go bench run -c cpu.prof -m mem.prof +``` + +View profile on cmdline +``` +go tool pprof cpu.prof +``` + +View profile in browser (requires [graphviz](https://graphviz.org/download/)) +``` +go tool pprof -http :9000 cpu.prof +``` + +## Results + +### Machine Info + +Information about the machines used to record benchmark stats + +| Machine | OS/Distro | CPU | Memory | GPU | +|--------------------|---------------------|-------------------------------|--------------------|----------------| +| bhperry-wsl | Linux Ubuntu 20.04 | Intel i7-8086K @ 4.00GHz | 8GiB | RTX 2080 | +| bhperry-win10 | Windows 10 | Intel i7-8086K @ 4.00GHz | 16GiB | RTX 2080 | + +### Stats + +| Machine | Pixel | Benchmark | Duration | Frames | FPS Avg | FPS Min | FPS Max | FPS Stdev | +|--------------------|--------|------------------------------|----------|--------|---------|---------|---------|-----------| +| bhperry-wsl | v2.2.0 | imdraw-moving | 30.01s | 2232 | 74.37 | 60 | 78 | 3.45 | +| bhperry-wsl | v2.2.0 | imdraw-static | 30.02s | 2334 | 77.75 | 73 | 80 | 1.2 | +| bhperry-wsl | v2.2.0 | sprite-moving | 30.03s | 1452 | 48.35 | 45 | 50 | 1.05 | +| bhperry-wsl | v2.2.0 | sprite-moving-batched | 30.01s | 4004 | 133.42 | 127 | 139 | 2.45 | +| bhperry-wsl | v2.2.0 | sprite-static | 30.02s | 1534 | 51.1 | 48 | 52 | 0.91 | +| bhperry-wsl | v2.2.0 | sprite-static-batched | 30s | 5293 | 176.43 | 163 | 179 | 2.99 | +| bhperry-win10 | v2.2.0 | imdraw-moving | 30.03s | 1425 | 47.45 | 21 | 49 | 4.96 | +| bhperry-win10 | v2.2.0 | imdraw-static | 30s | 1533 | 51.1 | 50 | 52 | 0.55 | +| bhperry-win10 | v2.2.0 | sprite-moving | 30.02s | 1145 | 38.15 | 37 | 39 | 0.46 | +| bhperry-win10 | v2.2.0 | sprite-moving-batched | 30s | 39753 | 1325.06 | 1269 | 1348 | 15.1 | +| bhperry-win10 | v2.2.0 | sprite-static | 30.01s | 1214 | 40.45 | 40 | 41 | 0.5 | +| bhperry-win10 | v2.2.0 | sprite-static-batched | 30s | 39513 | 1317.06 | 1299 | 1336 | 10.1 | From 43e99d93d2a234f72fab869f9b19e6e68435b88b Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:50:30 -0500 Subject: [PATCH 07/11] Update go.mod --- tools/go.mod | 10 ++++++---- tools/go.sum | 27 +++++++++++++++++++-------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/tools/go.mod b/tools/go.mod index e146c3d..41316b4 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -4,19 +4,21 @@ go 1.21 require ( github.com/gopxl/pixel/v2 v2.2.0-local + github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 ) replace github.com/gopxl/pixel/v2 v2.2.0-local => ../ require ( - github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect - github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect + github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect + github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect github.com/go-gl/mathgl v1.1.0 // indirect github.com/gopxl/glhf/v2 v2.0.0 // indirect - github.com/gopxl/mainthread/v2 v2.0.0 // indirect + github.com/gopxl/mainthread/v2 v2.1.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/image v0.18.0 // indirect + golang.org/x/image v0.19.0 // indirect ) diff --git a/tools/go.sum b/tools/go.sum index ad238f5..5273201 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -1,26 +1,37 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk= -github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA= +github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/mathgl v1.1.0 h1:0lzZ+rntPX3/oGrDzYGdowSLC2ky8Osirvf5uAwfIEA= github.com/go-gl/mathgl v1.1.0/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= github.com/gopxl/glhf/v2 v2.0.0 h1:SJtNy+TXuTBRjMersNx722VDJ0XHIooMH2+7+99LPIc= github.com/gopxl/glhf/v2 v2.0.0/go.mod h1:InKwj5OoVdOAkpzsS0ILwpB+RrWBLw1i7aFefiGmrp8= -github.com/gopxl/mainthread/v2 v2.0.0 h1:jRbeWFzX6/UyhRab00xS3xIVYywBgc0DgwPgwS6EVYw= -github.com/gopxl/mainthread/v2 v2.0.0/go.mod h1:/uFQhUiSP53SSU/RQ5w0FFkljRArJlaQkDPza3zE2V8= +github.com/gopxl/mainthread/v2 v2.1.1 h1:S7jIvQZth9s2k8qFePOxtEgtZLzW/Yjykum2mscGr0o= +github.com/gopxl/mainthread/v2 v2.1.1/go.mod h1:RLdqSRamocAGPzK9P4HsZf+WXL5bfHHtX78O6GkKaUw= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= -golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= +golang.org/x/image v0.19.0 h1:D9FX4QWkLfkeqaC62SonffIIuYdOk/UE2XKUBgRIBIQ= +golang.org/x/image v0.19.0/go.mod h1:y0zrRqlQRWQ5PXaYCOMLTW2fpsxZ8Qh9I/ohnInJEys= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 922dca5f08612c06c4c79b95a7bcfadc6bae9d96 Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 11:59:16 -0500 Subject: [PATCH 08/11] Create min/max fps estimate for 1s runs --- tools/benchmark/stats.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tools/benchmark/stats.go b/tools/benchmark/stats.go index 8817af5..a2480c8 100644 --- a/tools/benchmark/stats.go +++ b/tools/benchmark/stats.go @@ -50,6 +50,10 @@ func NewStats(name string, duration time.Duration, frames int, frameSeconds []in stats.MinFPS = slices.Min(fps) stats.MaxFPS = slices.Max(fps) stats.StdevFPS = standardDeviation(fps) + } else { + // 1s or less test. Use average as a stand-in. + stats.MinFPS = math.Floor(stats.AvgFPS) + stats.MaxFPS = math.Ceil(stats.AvgFPS) } return stats From a590af2786957ae41e11a05356efc2348257e65c Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 12:27:09 -0500 Subject: [PATCH 09/11] Pass frame delta for fixed movement rate in benchmarks --- tools/README.md | 24 ++++++++++++------------ tools/benchmark/benchmark.go | 7 +++++-- tools/benchmark/imdraw_bench.go | 22 ++++++++++++---------- tools/benchmark/sprite_bench.go | 28 ++++++++++++++++------------ 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/tools/README.md b/tools/README.md index 2b83a2a..cf33dff 100644 --- a/tools/README.md +++ b/tools/README.md @@ -51,15 +51,15 @@ Information about the machines used to record benchmark stats | Machine | Pixel | Benchmark | Duration | Frames | FPS Avg | FPS Min | FPS Max | FPS Stdev | |--------------------|--------|------------------------------|----------|--------|---------|---------|---------|-----------| -| bhperry-wsl | v2.2.0 | imdraw-moving | 30.01s | 2232 | 74.37 | 60 | 78 | 3.45 | -| bhperry-wsl | v2.2.0 | imdraw-static | 30.02s | 2334 | 77.75 | 73 | 80 | 1.2 | -| bhperry-wsl | v2.2.0 | sprite-moving | 30.03s | 1452 | 48.35 | 45 | 50 | 1.05 | -| bhperry-wsl | v2.2.0 | sprite-moving-batched | 30.01s | 4004 | 133.42 | 127 | 139 | 2.45 | -| bhperry-wsl | v2.2.0 | sprite-static | 30.02s | 1534 | 51.1 | 48 | 52 | 0.91 | -| bhperry-wsl | v2.2.0 | sprite-static-batched | 30s | 5293 | 176.43 | 163 | 179 | 2.99 | -| bhperry-win10 | v2.2.0 | imdraw-moving | 30.03s | 1425 | 47.45 | 21 | 49 | 4.96 | -| bhperry-win10 | v2.2.0 | imdraw-static | 30s | 1533 | 51.1 | 50 | 52 | 0.55 | -| bhperry-win10 | v2.2.0 | sprite-moving | 30.02s | 1145 | 38.15 | 37 | 39 | 0.46 | -| bhperry-win10 | v2.2.0 | sprite-moving-batched | 30s | 39753 | 1325.06 | 1269 | 1348 | 15.1 | -| bhperry-win10 | v2.2.0 | sprite-static | 30.01s | 1214 | 40.45 | 40 | 41 | 0.5 | -| bhperry-win10 | v2.2.0 | sprite-static-batched | 30s | 39513 | 1317.06 | 1299 | 1336 | 10.1 | +| bhperry-wsl | v2.2.0 | imdraw-moving | 30s | 2214 | 73.79 | 68 | 76 | 1.77 | +| bhperry-wsl | v2.2.0 | imdraw-static | 30s | 2355 | 78.5 | 72 | 81 | 1.89 | +| bhperry-wsl | v2.2.0 | sprite-moving | 30.03s | 1451 | 48.32 | 45 | 50 | 1.25 | +| bhperry-wsl | v2.2.0 | sprite-moving-batched | 30.01s | 4085 | 136.12 | 127 | 142 | 3.17 | +| bhperry-wsl | v2.2.0 | sprite-static | 30.01s | 1518 | 50.59 | 47 | 52 | 1.45 | +| bhperry-wsl | v2.2.0 | sprite-static-batched | 30.01s | 5318 | 177.2 | 159 | 182 | 6.01 | +| bhperry-win10 | v2.2.0 | imdraw-moving | 30.03s | 1430 | 47.61 | 22 | 50 | 5.85 | +| bhperry-win10 | v2.2.0 | imdraw-static | 30.02s | 1569 | 52.27 | 51 | 53 | 0.64 | +| bhperry-win10 | v2.2.0 | sprite-moving | 30.03s | 1148 | 38.23 | 35 | 39 | 0.9 | +| bhperry-win10 | v2.2.0 | sprite-moving-batched | 30s | 39085 | 1302.79 | 1205 | 1329 | 23.93 | +| bhperry-win10 | v2.2.0 | sprite-static | 30.04s | 1218 | 40.54 | 38 | 42 | 0.88 | +| bhperry-win10 | v2.2.0 | sprite-static-batched | 30s | 40570 | 1352.29 | 1245 | 1380 | 26.04 | diff --git a/tools/benchmark/benchmark.go b/tools/benchmark/benchmark.go index 9f562f4..d00c4c1 100644 --- a/tools/benchmark/benchmark.go +++ b/tools/benchmark/benchmark.go @@ -64,9 +64,12 @@ func (c Config) Run() (*Stats, error) { second := time.NewTicker(time.Second) done := time.NewTicker(duration) start := time.Now() + last := start loop: for frame = 0; !win.Closed(); frame++ { - benchmark.Step(win) + now := time.Now() + benchmark.Step(win, now.Sub(last).Seconds()) + last = now win.Update() select { @@ -90,7 +93,7 @@ loop: // Benchmark provides hooks into the stages of a window's lifecycle type Benchmark interface { - Step(win *opengl.Window) + Step(win *opengl.Window, delta float64) } // Registry is a collection of benchmark configs diff --git a/tools/benchmark/imdraw_bench.go b/tools/benchmark/imdraw_bench.go index 301a55e..0c7eeea 100644 --- a/tools/benchmark/imdraw_bench.go +++ b/tools/benchmark/imdraw_bench.go @@ -51,7 +51,7 @@ type staticTriangles struct { cell pixel.Vec } -func (st *staticTriangles) Step(win *opengl.Window) { +func (st *staticTriangles) Step(win *opengl.Window, delta float64) { win.Clear(backgroundColor) for i := 0; i < st.cols; i++ { @@ -82,23 +82,27 @@ type movingTriangles struct { imd *imdraw.IMDraw rows, cols int cell pixel.Vec - counter int + yOffset float64 } -func (mt *movingTriangles) Step(win *opengl.Window) { +func (mt *movingTriangles) Step(win *opengl.Window, delta float64) { win.Clear(backgroundColor) + mt.yOffset += mt.cell.Y * delta * 3 + if mt.yOffset >= mt.cell.Y { + mt.yOffset = 0 + } + for i := 0; i < mt.cols; i++ { - yOffset := -mt.cell.Y - delta := float64(mt.counter % int(mt.cell.Y)) + columnOffset := -mt.cell.Y if i%2 == 0 { - yOffset += delta + columnOffset += mt.yOffset } else { - yOffset -= delta + columnOffset -= mt.yOffset } for j := 0; j < mt.rows+2; j++ { - pos := pixel.V(float64(i)*mt.cell.X, (float64(j)*mt.cell.Y)+yOffset) + pos := pixel.V(float64(i)*mt.cell.X, (float64(j)*mt.cell.Y)+columnOffset) matrix := pixel.IM.Moved(pos) if i%2 == 1 { matrix = matrix.Rotated(pos.Add(pixel.V(mt.cell.X/2, mt.cell.Y/2)), math.Pi) @@ -107,8 +111,6 @@ func (mt *movingTriangles) Step(win *opengl.Window) { mt.imd.Draw(win) } } - - mt.counter++ } func tri(cell pixel.Vec) *imdraw.IMDraw { diff --git a/tools/benchmark/sprite_bench.go b/tools/benchmark/sprite_bench.go index bcc3819..14eccfc 100644 --- a/tools/benchmark/sprite_bench.go +++ b/tools/benchmark/sprite_bench.go @@ -89,7 +89,7 @@ type spriteStatic struct { batch *pixel.Batch } -func (ss *spriteStatic) Step(win *opengl.Window) { +func (ss *spriteStatic) Step(win *opengl.Window, delta float64) { win.Clear(backgroundColor) var target pixel.Target if ss.batch != nil { @@ -134,13 +134,13 @@ func newSpriteMovingBatched(win *opengl.Window) (Benchmark, error) { type spriteMoving struct { sprite *pixel.Sprite + batch *pixel.Batch rows, cols int cell pixel.Vec - counter int - batch *pixel.Batch + yOffset float64 } -func (sm *spriteMoving) Step(win *opengl.Window) { +func (sm *spriteMoving) Step(win *opengl.Window, delta float64) { win.Clear(backgroundColor) var target pixel.Target if sm.batch != nil { @@ -149,11 +149,16 @@ func (sm *spriteMoving) Step(win *opengl.Window) { } else { target = win } - spriteGridMoving(sm.sprite, target, sm.rows, sm.cols, sm.cell, sm.counter) + + sm.yOffset += sm.cell.Y * delta * 3 + if sm.yOffset >= sm.cell.Y { + sm.yOffset = 0 + } + + spriteGridMoving(sm.sprite, target, sm.rows, sm.cols, sm.cell, sm.yOffset) if sm.batch != nil { sm.batch.Draw(win) } - sm.counter += 1 } func spriteGrid(sprite *pixel.Sprite, target pixel.Target, rows, cols int, cell pixel.Vec) { @@ -170,23 +175,22 @@ func spriteGrid(sprite *pixel.Sprite, target pixel.Target, rows, cols int, cell } } -func spriteGridMoving(sprite *pixel.Sprite, target pixel.Target, rows, cols int, cell pixel.Vec, counter int) { +func spriteGridMoving(sprite *pixel.Sprite, target pixel.Target, rows, cols int, cell pixel.Vec, yOffset float64) { spriteBounds := sprite.Frame().Bounds() spriteWidth := spriteBounds.W() spriteHeight := spriteBounds.H() matrix := pixel.IM.ScaledXY(pixel.ZV, pixel.V(cell.X/spriteWidth, cell.Y/spriteHeight)) offset := pixel.V(cell.X/2, cell.Y/2) for i := 0; i < cols; i++ { - yOffset := -cell.Y - delta := float64(counter % int(cell.Y)) + columnOffset := -cell.Y if i%2 == 0 { - yOffset += delta + columnOffset += yOffset } else { - yOffset -= delta + columnOffset -= yOffset } for j := 0; j < rows+2; j++ { - pos := pixel.V(float64(i)*cell.X, (float64(j)*cell.Y)+yOffset).Add(offset) + pos := pixel.V(float64(i)*cell.X, (float64(j)*cell.Y)+columnOffset).Add(offset) sprite.Draw(target, matrix.Moved(pos)) } } From 87b1b529d0a6ade07f4ff0038194b7d3a3df7e15 Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Fri, 23 Aug 2024 12:50:05 -0500 Subject: [PATCH 10/11] Update readme --- tools/README.md | 46 +++++++++++++++++++++++++++++----------------- tools/go.mod | 4 ++-- 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/tools/README.md b/tools/README.md index cf33dff..2039523 100644 --- a/tools/README.md +++ b/tools/README.md @@ -12,18 +12,18 @@ go run main.go bench ls Run a benchmark ``` -go run main.go bench run imdraw-static +go run main.go bench run [names...] ``` Write benchmark stats to a file ``` -go run main.go bench run imdraw-static -o imdraw-static-stats.json +go run main.go bench run [names...] -o my-stats.json ``` ## Profiling Run benchmark with cpu/mem profiling enabled ``` -go run main.go bench run -c cpu.prof -m mem.prof +go run main.go bench run [names...] -c cpu.prof -m mem.prof ``` View profile on cmdline @@ -38,9 +38,21 @@ go tool pprof -http :9000 cpu.prof ## Results -### Machine Info +To add your own results to this file, create an entry in the [Machine Info](#machine-info) table with +a unique identifer and basic info about the computer where you are running the benchmarks. +On linux you can get most of the info from `lshw -short`. By default, benchmark stats will use the local username +from environment variables or the os package if `MACHINE_NAME` env is not provided. + +Then run all benchmarks: +``` +# Optional +export MACHINE_NAME= +export PIXEL_VERSION= -Information about the machines used to record benchmark stats +go run main.go bench run --all +``` + +### Machine Info | Machine | OS/Distro | CPU | Memory | GPU | |--------------------|---------------------|-------------------------------|--------------------|----------------| @@ -51,15 +63,15 @@ Information about the machines used to record benchmark stats | Machine | Pixel | Benchmark | Duration | Frames | FPS Avg | FPS Min | FPS Max | FPS Stdev | |--------------------|--------|------------------------------|----------|--------|---------|---------|---------|-----------| -| bhperry-wsl | v2.2.0 | imdraw-moving | 30s | 2214 | 73.79 | 68 | 76 | 1.77 | -| bhperry-wsl | v2.2.0 | imdraw-static | 30s | 2355 | 78.5 | 72 | 81 | 1.89 | -| bhperry-wsl | v2.2.0 | sprite-moving | 30.03s | 1451 | 48.32 | 45 | 50 | 1.25 | -| bhperry-wsl | v2.2.0 | sprite-moving-batched | 30.01s | 4085 | 136.12 | 127 | 142 | 3.17 | -| bhperry-wsl | v2.2.0 | sprite-static | 30.01s | 1518 | 50.59 | 47 | 52 | 1.45 | -| bhperry-wsl | v2.2.0 | sprite-static-batched | 30.01s | 5318 | 177.2 | 159 | 182 | 6.01 | -| bhperry-win10 | v2.2.0 | imdraw-moving | 30.03s | 1430 | 47.61 | 22 | 50 | 5.85 | -| bhperry-win10 | v2.2.0 | imdraw-static | 30.02s | 1569 | 52.27 | 51 | 53 | 0.64 | -| bhperry-win10 | v2.2.0 | sprite-moving | 30.03s | 1148 | 38.23 | 35 | 39 | 0.9 | -| bhperry-win10 | v2.2.0 | sprite-moving-batched | 30s | 39085 | 1302.79 | 1205 | 1329 | 23.93 | -| bhperry-win10 | v2.2.0 | sprite-static | 30.04s | 1218 | 40.54 | 38 | 42 | 0.88 | -| bhperry-win10 | v2.2.0 | sprite-static-batched | 30s | 40570 | 1352.29 | 1245 | 1380 | 26.04 | +| bhperry-wsl | v2.2.1 | imdraw-moving | 30s | 2214 | 73.79 | 68 | 76 | 1.77 | +| bhperry-wsl | v2.2.1 | imdraw-static | 30s | 2355 | 78.5 | 72 | 81 | 1.89 | +| bhperry-wsl | v2.2.1 | sprite-moving | 30.03s | 1451 | 48.32 | 45 | 50 | 1.25 | +| bhperry-wsl | v2.2.1 | sprite-moving-batched | 30.01s | 4085 | 136.12 | 127 | 142 | 3.17 | +| bhperry-wsl | v2.2.1 | sprite-static | 30.01s | 1518 | 50.59 | 47 | 52 | 1.45 | +| bhperry-wsl | v2.2.1 | sprite-static-batched | 30.01s | 5318 | 177.2 | 159 | 182 | 6.01 | +| bhperry-win10 | v2.2.1 | imdraw-moving | 30.03s | 1430 | 47.61 | 22 | 50 | 5.85 | +| bhperry-win10 | v2.2.1 | imdraw-static | 30.02s | 1569 | 52.27 | 51 | 53 | 0.64 | +| bhperry-win10 | v2.2.1 | sprite-moving | 30.03s | 1148 | 38.23 | 35 | 39 | 0.9 | +| bhperry-win10 | v2.2.1 | sprite-moving-batched | 30s | 39085 | 1302.79 | 1205 | 1329 | 23.93 | +| bhperry-win10 | v2.2.1 | sprite-static | 30.04s | 1218 | 40.54 | 38 | 42 | 0.88 | +| bhperry-win10 | v2.2.1 | sprite-static-batched | 30s | 40570 | 1352.29 | 1245 | 1380 | 26.04 | diff --git a/tools/go.mod b/tools/go.mod index 41316b4..e86ca7f 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -3,12 +3,12 @@ module github.com/gopxl/pixel/tools go 1.21 require ( - github.com/gopxl/pixel/v2 v2.2.0-local + github.com/gopxl/pixel/v2 v2.2.1-local github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 ) -replace github.com/gopxl/pixel/v2 v2.2.0-local => ../ +replace github.com/gopxl/pixel/v2 v2.2.1-local => ../ require ( github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect From 3127be3461df9a1efdf9c7089c3bf26bc29e1e34 Mon Sep 17 00:00:00 2001 From: Ben Perry Date: Mon, 26 Aug 2024 23:09:56 -0500 Subject: [PATCH 11/11] Batched imdraw benchmarks --- tools/README.md | 4 ++ tools/benchmark/imdraw_bench.go | 68 +++++++++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/tools/README.md b/tools/README.md index 2039523..b294062 100644 --- a/tools/README.md +++ b/tools/README.md @@ -64,13 +64,17 @@ go run main.go bench run --all | Machine | Pixel | Benchmark | Duration | Frames | FPS Avg | FPS Min | FPS Max | FPS Stdev | |--------------------|--------|------------------------------|----------|--------|---------|---------|---------|-----------| | bhperry-wsl | v2.2.1 | imdraw-moving | 30s | 2214 | 73.79 | 68 | 76 | 1.77 | +| bhperry-wsl | v2.2.1 | imdraw-moving-batched | 30s | 5658 | 188.57 | 166 | 195 | 5.86 | | bhperry-wsl | v2.2.1 | imdraw-static | 30s | 2355 | 78.5 | 72 | 81 | 1.89 | +| bhperry-wsl | v2.2.1 | imdraw-static-batched | 30.01s | 6171 | 205.64 | 168 | 212 | 9.62 | | bhperry-wsl | v2.2.1 | sprite-moving | 30.03s | 1451 | 48.32 | 45 | 50 | 1.25 | | bhperry-wsl | v2.2.1 | sprite-moving-batched | 30.01s | 4085 | 136.12 | 127 | 142 | 3.17 | | bhperry-wsl | v2.2.1 | sprite-static | 30.01s | 1518 | 50.59 | 47 | 52 | 1.45 | | bhperry-wsl | v2.2.1 | sprite-static-batched | 30.01s | 5318 | 177.2 | 159 | 182 | 6.01 | | bhperry-win10 | v2.2.1 | imdraw-moving | 30.03s | 1430 | 47.61 | 22 | 50 | 5.85 | +| bhperry-win10 | v2.2.1 | imdraw-moving-batched | 30s | 52017 | 1733.9 | 1635 | 1915 | 43.92 | | bhperry-win10 | v2.2.1 | imdraw-static | 30.02s | 1569 | 52.27 | 51 | 53 | 0.64 | +| bhperry-win10 | v2.2.1 | imdraw-static-batched | 30.01s | 1517 | 50.55 | 21 | 53 | 6.62 | | bhperry-win10 | v2.2.1 | sprite-moving | 30.03s | 1148 | 38.23 | 35 | 39 | 0.9 | | bhperry-win10 | v2.2.1 | sprite-moving-batched | 30s | 39085 | 1302.79 | 1205 | 1329 | 23.93 | | bhperry-win10 | v2.2.1 | sprite-static | 30.04s | 1218 | 40.54 | 38 | 42 | 0.88 | diff --git a/tools/benchmark/imdraw_bench.go b/tools/benchmark/imdraw_bench.go index 0c7eeea..c2b6387 100644 --- a/tools/benchmark/imdraw_bench.go +++ b/tools/benchmark/imdraw_bench.go @@ -21,12 +21,24 @@ func init() { New: newStaticTriangles, Duration: 30 * time.Second, }, + Config{ + Name: "imdraw-static-batched", + Description: "Stationary RGB triangles in a grid with batched draw", + New: newStaticTrianglesBatched, + Duration: 30 * time.Second, + }, Config{ Name: "imdraw-moving", Description: "Columns of RGB triangles moving in opposite directions", New: newMovingTriangles, Duration: 30 * time.Second, }, + Config{ + Name: "imdraw-moving-batched", + Description: "Columns of RGB triangles moving in opposite directions with batched draw", + New: newMovingTrianglesBatched, + Duration: 30 * time.Second, + }, ) } @@ -45,8 +57,20 @@ func newStaticTriangles(win *opengl.Window) (Benchmark, error) { return benchmark, nil } +func newStaticTrianglesBatched(win *opengl.Window) (Benchmark, error) { + benchmark, err := newStaticTriangles(win) + if err != nil { + return nil, err + } + st := benchmark.(*staticTriangles) + st.target = pixel.NewBatch(&pixel.TrianglesData{}, nil) + return st, nil +} + type staticTriangles struct { imd *imdraw.IMDraw + batch *pixel.Batch + target pixel.BasicTarget rows, cols int cell pixel.Vec } @@ -54,13 +78,25 @@ type staticTriangles struct { func (st *staticTriangles) Step(win *opengl.Window, delta float64) { win.Clear(backgroundColor) + var target pixel.BasicTarget + if st.batch != nil { + st.batch.Clear() + target = st.batch + } else { + target = win + } + for i := 0; i < st.cols; i++ { for j := 0; j < st.rows; j++ { pos := pixel.V(float64(i)*st.cell.X, float64(j)*st.cell.Y) - win.SetMatrix(pixel.IM.Moved(pos)) - st.imd.Draw(win) + target.SetMatrix(pixel.IM.Moved(pos)) + st.imd.Draw(target) } } + + if st.batch != nil { + st.batch.Draw(win) + } } func newMovingTriangles(win *opengl.Window) (Benchmark, error) { @@ -78,8 +114,20 @@ func newMovingTriangles(win *opengl.Window) (Benchmark, error) { return benchmark, nil } +func newMovingTrianglesBatched(win *opengl.Window) (Benchmark, error) { + benchmark, err := newMovingTriangles(win) + if err != nil { + return nil, err + } + + mt := benchmark.(*movingTriangles) + mt.batch = pixel.NewBatch(&pixel.TrianglesData{}, nil) + return mt, nil +} + type movingTriangles struct { imd *imdraw.IMDraw + batch *pixel.Batch rows, cols int cell pixel.Vec yOffset float64 @@ -88,6 +136,14 @@ type movingTriangles struct { func (mt *movingTriangles) Step(win *opengl.Window, delta float64) { win.Clear(backgroundColor) + var target pixel.BasicTarget + if mt.batch != nil { + mt.batch.Clear() + target = mt.batch + } else { + target = win + } + mt.yOffset += mt.cell.Y * delta * 3 if mt.yOffset >= mt.cell.Y { mt.yOffset = 0 @@ -107,10 +163,14 @@ func (mt *movingTriangles) Step(win *opengl.Window, delta float64) { if i%2 == 1 { matrix = matrix.Rotated(pos.Add(pixel.V(mt.cell.X/2, mt.cell.Y/2)), math.Pi) } - win.SetMatrix(matrix) - mt.imd.Draw(win) + target.SetMatrix(matrix) + mt.imd.Draw(target) } } + + if mt.batch != nil { + mt.batch.Draw(win) + } } func tri(cell pixel.Vec) *imdraw.IMDraw {