From 01eeab86f7b304252bdfdb56c16387eb93fb756d Mon Sep 17 00:00:00 2001 From: Thomas von Dein Date: Mon, 27 May 2024 13:38:14 +0200 Subject: [PATCH] add profiling support and window geom options to force geometry --- .gitignore | 2 + config.go | 164 +++++++++++++++++++++++++++++++++++++++-------------- main.go | 70 ++++++++++++++++++----- rle/rle.go | 19 +++++++ 4 files changed, 199 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 7fc9a42..77e14fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ golsky bak dump* +rect* +*profile diff --git a/config.go b/config.go index 3f33dc7..de89d11 100644 --- a/config.go +++ b/config.go @@ -1,9 +1,12 @@ package main import ( + "errors" "fmt" - "log" "os" + "runtime/pprof" + "strconv" + "strings" "github.com/spf13/pflag" "github.com/tlinden/golsky/rle" @@ -22,42 +25,136 @@ type Config struct { StateGrid *Grid // a grid from a statefile Wrap bool // wether wraparound mode is in place or not ShowVersion bool + + // for internal profiling + ProfileFile string + ProfileDraw bool + ProfileMaxLoops int64 } const ( - VERSION = "v0.0.6" + VERSION = "v0.0.7" Alive = 1 Dead = 0 ) -func GetRLE(filename string) *rle.RLE { +// parse given window geometry and adjust game settings according to it +func (config *Config) ParseGeom(geom string) error { + if geom == "" { + config.ScreenWidth = config.Cellsize * config.Width + config.ScreenHeight = config.Cellsize * config.Height + return nil + } + + // force a geom + geometry := strings.Split(geom, "x") + if len(geometry) != 2 { + return errors.New("failed to parse -g parameters, expecting WIDTHxHEIGHT") + } + + width, err := strconv.Atoi(geometry[0]) + if err != nil { + return errors.New("failed to parse width, expecting integer") + } + + height, err := strconv.Atoi(geometry[1]) + if err != nil { + return errors.New("failed to parse height, expecting integer") + } + + // adjust dimensions, account for grid width+height so that cells + // fit into window + config.ScreenWidth = width - (width % config.Width) + config.ScreenHeight = height - (height % config.Height) + config.Cellsize = config.ScreenWidth / config.Width + + return nil +} + +// check if we have been given an RLE file to load, then load it and +// adjust game settings accordingly +func (config *Config) ParseRLE(rlefile string) error { + if rlefile == "" { + return nil + } + + rleobj, err := rle.GetRLE(rlefile) + if err != nil { + return err + } + + if rleobj == nil { + return errors.New("failed to load RLE file (uncatched module error)") + } + + config.RLE = rleobj + + // adjust geometry if needed + if config.RLE.Width > config.Width || config.RLE.Height > config.Height { + config.Width = config.RLE.Width * 2 + config.Height = config.RLE.Height * 2 + config.Cellsize = config.ScreenWidth / config.Width + } + + // RLE needs an empty grid + config.Empty = true + + // it may come with its own rule + if config.RLE.Rule != "" { + config.Rule = ParseGameRule(config.RLE.Rule) + } + + return nil +} + +// parse a state file, if given, and adjust game settings accordingly +func (config *Config) ParseStatefile(statefile string) error { + if config.Statefile == "" { + return nil + } + + grid, err := LoadState(config.Statefile) + if err != nil { + return fmt.Errorf("failed to load game state: %s", err) + } + + config.Width = grid.Width + config.Height = grid.Height + config.Cellsize = config.ScreenWidth / config.Width + config.StateGrid = grid + + return nil +} + +func (config *Config) EnableCPUProfiling(filename string) error { if filename == "" { return nil } - content, err := os.ReadFile(filename) + fd, err := os.Create(filename) if err != nil { - log.Fatal(err) + return err } - parsedRle, err := rle.Parse(string(content)) - if err != nil { - log.Fatalf("failed to load RLE pattern file: %s", err) - } + pprof.StartCPUProfile(fd) + defer pprof.StopCPUProfile() - return &parsedRle + return nil } -func ParseCommandline() *Config { +func ParseCommandline() (*Config, error) { config := Config{} - var rule string - var rlefile string + var ( + rule, rlefile, geom string + ) // commandline params, most configure directly config flags pflag.IntVarP(&config.Width, "width", "W", 40, "grid width in cells") pflag.IntVarP(&config.Height, "height", "H", 40, "grid height in cells") pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels") + pflag.StringVarP(&geom, "geom", "g", "", "window geometry in WxH in pixels, overturns -c") + pflag.IntVarP(&config.Density, "density", "D", 10, "density of random cells") pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10, "game speed: the higher the slower (default: 10)") @@ -75,38 +172,21 @@ func ParseCommandline() *Config { pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution tracks") pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode") + pflag.StringVarP(&config.ProfileFile, "profile-file", "", "", "enable profiling") + pflag.BoolVarP(&config.ProfileDraw, "profile-draw", "", false, "profile draw method (default false)") + pflag.Int64VarP(&config.ProfileMaxLoops, "profile-max-loops", "", 10, "how many loops to execute (default 10)") + pflag.Parse() - // check if we have been given an RLE file to load - config.RLE = GetRLE(rlefile) - if config.RLE != nil { - if config.RLE.Width > config.Width || config.RLE.Height > config.Height { - config.Width = config.RLE.Width * 2 - config.Height = config.RLE.Height * 2 - fmt.Printf("rlew: %d, rleh: %d, w: %d, h: %d\n", - config.RLE.Width, config.RLE.Height, config.Width, config.Height) - } - - // RLE needs an empty grid - config.Empty = true - - // it may come with its own rule - if config.RLE.Rule != "" { - config.Rule = ParseGameRule(config.RLE.Rule) - } - } else if config.Statefile != "" { - grid, err := LoadState(config.Statefile) - if err != nil { - log.Fatalf("failed to load game state: %s", err) - } - - config.Width = grid.Width - config.Height = grid.Height - config.StateGrid = grid + err := config.ParseGeom(geom) + if err != nil { + return nil, err } - config.ScreenWidth = config.Cellsize * config.Width - config.ScreenHeight = config.Cellsize * config.Height + err = config.ParseRLE(rlefile) + if err != nil { + return nil, err + } // load rule from commandline when no rule came from RLE file, // default is B3/S23, aka conways game of life @@ -114,5 +194,5 @@ func ParseCommandline() *Config { config.Rule = ParseGameRule(rule) } - return &config + return &config, nil } diff --git a/main.go b/main.go index cdb6864..4fc4807 100644 --- a/main.go +++ b/main.go @@ -4,35 +4,77 @@ import ( "fmt" "log" "os" + "runtime/pprof" + "time" + + _ "net/http/pprof" "github.com/hajimehoshi/ebiten/v2" ) func main() { - config := ParseCommandline() + config, err := ParseCommandline() + if err != nil { + log.Fatal(err) + } if config.ShowVersion { fmt.Printf("This is golsky version %s\n", VERSION) os.Exit(0) } - // grid := [][]int64{ - // {0, 1, 1}, - // {0, 1, 0}, - // {1, 1, 0}, - // } - - // err := rle.StoreGridToRLE(grid, "test.rle", "B3/S23", 3, 3) - // if err != nil { - // panic(err) - // } - - // os.Exit(0) - game := NewGame(config, Play) + if config.ProfileFile != "" { + // enable cpu profiling and use fake game loop + fd, err := os.Create(config.ProfileFile) + if err != nil { + log.Fatal(err) + } + defer fd.Close() + + pprof.StartCPUProfile(fd) + defer pprof.StopCPUProfile() + + Ebitfake(game) + + pprof.StopCPUProfile() + fd.Close() + + os.Exit(0) + } + // main loop if err := ebiten.RunGame(game); err != nil { log.Fatal(err) } } + +// fake game loop, required to be able to profile the program using +// pprof. Otherwise any kind of program exit leads to an empty profile +// file. +func Ebitfake(game *Game) { + screen := ebiten.NewImage(game.ScreenWidth, game.ScreenHeight) + + var loops int64 + + for { + err := game.Update() + if err != nil { + log.Fatal(err) + } + + if game.Config.ProfileDraw { + game.Draw(screen) + } + + fmt.Print(".") + time.Sleep(16 * time.Millisecond) // around 60 TPS + + if loops >= game.Config.ProfileMaxLoops { + break + } + + loops++ + } +} diff --git a/rle/rle.go b/rle/rle.go index ac81d5a..e93c59b 100644 --- a/rle/rle.go +++ b/rle/rle.go @@ -20,6 +20,25 @@ type RLE struct { patternLineIndex int } +// wrapper to load a RLE file +func GetRLE(filename string) (*RLE, error) { + if filename == "" { + return nil, nil + } + + content, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + parsedRle, err := Parse(string(content)) + if err != nil { + return nil, fmt.Errorf("failed to load RLE pattern file: %s", err) + } + + return &parsedRle, nil +} + func Parse(input string) (RLE, error) { rle := RLE{ inputLines: strings.Split(input, "\n"),