add profiling support and window geom options to force geometry

This commit is contained in:
2024-05-27 13:38:14 +02:00
parent 4b38bea5db
commit 01eeab86f7
4 changed files with 199 additions and 56 deletions

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
golsky golsky
bak bak
dump* dump*
rect*
*profile

160
config.go
View File

@@ -1,9 +1,12 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"log"
"os" "os"
"runtime/pprof"
"strconv"
"strings"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/tlinden/golsky/rle" "github.com/tlinden/golsky/rle"
@@ -22,42 +25,136 @@ type Config struct {
StateGrid *Grid // a grid from a statefile StateGrid *Grid // a grid from a statefile
Wrap bool // wether wraparound mode is in place or not Wrap bool // wether wraparound mode is in place or not
ShowVersion bool ShowVersion bool
// for internal profiling
ProfileFile string
ProfileDraw bool
ProfileMaxLoops int64
} }
const ( const (
VERSION = "v0.0.6" VERSION = "v0.0.7"
Alive = 1 Alive = 1
Dead = 0 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 == "" { if filename == "" {
return nil return nil
} }
content, err := os.ReadFile(filename) fd, err := os.Create(filename)
if err != nil { if err != nil {
log.Fatal(err) return err
} }
parsedRle, err := rle.Parse(string(content)) pprof.StartCPUProfile(fd)
if err != nil { defer pprof.StopCPUProfile()
log.Fatalf("failed to load RLE pattern file: %s", err)
}
return &parsedRle return nil
} }
func ParseCommandline() *Config { func ParseCommandline() (*Config, error) {
config := Config{} config := Config{}
var rule string var (
var rlefile string rule, rlefile, geom string
)
// commandline params, most configure directly config flags // commandline params, most configure directly config flags
pflag.IntVarP(&config.Width, "width", "W", 40, "grid width in cells") 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.Height, "height", "H", 40, "grid height in cells")
pflag.IntVarP(&config.Cellsize, "cellsize", "c", 8, "cell size in pixels") 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.Density, "density", "D", 10, "density of random cells")
pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10, pflag.IntVarP(&config.TPG, "ticks-per-generation", "t", 10,
"game speed: the higher the slower (default: 10)") "game speed: the higher the slower (default: 10)")
@@ -75,44 +172,27 @@ func ParseCommandline() *Config {
pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution tracks") pflag.BoolVarP(&config.ShowEvolution, "show-evolution", "s", false, "show evolution tracks")
pflag.BoolVarP(&config.Wrap, "wrap-around", "w", false, "wrap around grid mode") 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() pflag.Parse()
// check if we have been given an RLE file to load err := config.ParseGeom(geom)
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 { if err != nil {
log.Fatalf("failed to load game state: %s", err) return nil, err
} }
config.Width = grid.Width err = config.ParseRLE(rlefile)
config.Height = grid.Height if err != nil {
config.StateGrid = grid return nil, err
} }
config.ScreenWidth = config.Cellsize * config.Width
config.ScreenHeight = config.Cellsize * config.Height
// load rule from commandline when no rule came from RLE file, // load rule from commandline when no rule came from RLE file,
// default is B3/S23, aka conways game of life // default is B3/S23, aka conways game of life
if config.Rule == nil { if config.Rule == nil {
config.Rule = ParseGameRule(rule) config.Rule = ParseGameRule(rule)
} }
return &config return &config, nil
} }

70
main.go
View File

@@ -4,35 +4,77 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"runtime/pprof"
"time"
_ "net/http/pprof"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
func main() { func main() {
config := ParseCommandline() config, err := ParseCommandline()
if err != nil {
log.Fatal(err)
}
if config.ShowVersion { if config.ShowVersion {
fmt.Printf("This is golsky version %s\n", VERSION) fmt.Printf("This is golsky version %s\n", VERSION)
os.Exit(0) 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) 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 // main loop
if err := ebiten.RunGame(game); err != nil { if err := ebiten.RunGame(game); err != nil {
log.Fatal(err) 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++
}
}

View File

@@ -20,6 +20,25 @@ type RLE struct {
patternLineIndex int 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) { func Parse(input string) (RLE, error) {
rle := RLE{ rle := RLE{
inputLines: strings.Split(input, "\n"), inputLines: strings.Split(input, "\n"),