ci-platforms / build (amd64, windows, windows-2022) (push) Skipped
ci / build (amd64, linux, ubuntu-24.04) (push) Successful in 5m59s
ci / release (push) Successful in 4m44s
ci-platforms / build (amd64, darwin, macos-15-intel) (push) Skipped
ci-platforms / build (arm64, darwin, macos-15) (push) Skipped
ci / checks (push) Successful in 3m53s
217 lines
6.0 KiB
Go
217 lines
6.0 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"runtime/pprof"
|
|
"strings"
|
|
|
|
"aego/demos"
|
|
_ "aego/demos/asset-hot-reload"
|
|
_ "aego/demos/audio"
|
|
_ "aego/demos/basic"
|
|
_ "aego/demos/camera"
|
|
_ "aego/demos/headless"
|
|
_ "aego/demos/hierarchy"
|
|
_ "aego/demos/input"
|
|
_ "aego/demos/layers"
|
|
_ "aego/demos/mesh"
|
|
_ "aego/demos/mixed"
|
|
_ "aego/demos/offscreen"
|
|
_ "aego/demos/physics2d"
|
|
_ "aego/demos/resize"
|
|
_ "aego/demos/scene-load"
|
|
_ "aego/demos/sprites"
|
|
_ "aego/demos/stress"
|
|
_ "aego/demos/stress-mesh"
|
|
_ "aego/demos/stress-scene"
|
|
_ "aego/demos/text"
|
|
"aego/engine"
|
|
"aego/platform"
|
|
renderhost "aego/render/host"
|
|
)
|
|
|
|
func main() {
|
|
os.Exit(run(os.Args[1:]))
|
|
}
|
|
|
|
func run(args []string) int {
|
|
fs := flag.NewFlagSet("sandbox", flag.ContinueOnError)
|
|
name := fs.String("demo", "basic", "demo to run")
|
|
vsyncFlag := fs.String("vsync", "on", "off|on|adaptive")
|
|
fullscreen := fs.Bool("fullscreen", false, "start fullscreen")
|
|
headless := fs.Bool("headless", false, "run without a window on the null gpu backend")
|
|
frames := fs.Int("frames", 0, "run a fixed number of frames and exit")
|
|
warmup := fs.Int("warmup", 60, "frames excluded from the allocation measurement")
|
|
debug := fs.Bool("debug", false, "enable gpu debug output and verbose logging")
|
|
hidden := fs.Bool("hidden", false, "create the window without showing it (benchmarks)")
|
|
res := fs.String("res", "demos/res", "directory mounted as res://")
|
|
width := fs.Int("width", 1280, "window width")
|
|
height := fs.Int("height", 720, "window height")
|
|
strict := fs.Bool("strict-allocs", false, "exit non-zero if frames allocate after warmup")
|
|
allocProf := fs.String("alloc-profile", "", "with -strict-allocs, write an allocation profile of the measured frames here when they allocate")
|
|
list := fs.Bool("list", false, "list demos and exit")
|
|
if err := fs.Parse(args); err != nil {
|
|
return 2
|
|
}
|
|
if *strict && *allocProf != "" {
|
|
// set before any goroutine starts: a goroutine keeps the sample interval it began with
|
|
runtime.MemProfileRate = 1
|
|
}
|
|
if *list {
|
|
for _, n := range demos.Names() {
|
|
fmt.Println(n)
|
|
}
|
|
return 0
|
|
}
|
|
ctor, ok := demos.Registry[*name]
|
|
if !ok {
|
|
fmt.Fprintf(os.Stderr, "unknown demo %q, available: %s\n", *name, strings.Join(demos.Names(), ", "))
|
|
return 2
|
|
}
|
|
mode, err := platform.ParseVSync(*vsyncFlag)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
return 2
|
|
}
|
|
|
|
demo := ctor()
|
|
cfg := engine.Config{
|
|
Title: "aego sandbox: " + demo.Name(),
|
|
Width: *width,
|
|
Height: *height,
|
|
Fullscreen: *fullscreen,
|
|
VSync: mode,
|
|
Headless: *headless,
|
|
Debug: *debug,
|
|
Hidden: *hidden,
|
|
ResRoot: *res,
|
|
AppID: "aego-sandbox",
|
|
}
|
|
if c, ok := demo.(demos.Configurer); ok {
|
|
c.Configure(&cfg)
|
|
}
|
|
if cfg.Headless {
|
|
if *frames <= 0 {
|
|
*frames = 600
|
|
}
|
|
if cfg.HeadlessFrameTime <= 0 {
|
|
cfg.HeadlessFrameTime = 1.0 / 60
|
|
}
|
|
}
|
|
|
|
builder := engine.NewBuilder(cfg)
|
|
if m, ok := demo.(interface{ Module() *renderhost.Module }); ok {
|
|
builder.Use(m.Module())
|
|
}
|
|
eng, err := builder.Build()
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "build:", err)
|
|
return 1
|
|
}
|
|
if *frames <= 0 {
|
|
if err := eng.Run(demo); err != nil {
|
|
fmt.Fprintln(os.Stderr, "run:", err)
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
return runFrames(eng, demo, *frames, *warmup, *strict, *allocProf)
|
|
}
|
|
|
|
func runFrames(eng *engine.Engine, app engine.App, frames, warmup int, strict bool, profDir string) int {
|
|
if warmup >= frames {
|
|
warmup = frames / 10
|
|
}
|
|
profile := strict && profDir != ""
|
|
n := 0
|
|
for ; n < warmup; n++ {
|
|
if !eng.Step(app) {
|
|
break
|
|
}
|
|
}
|
|
// No GC or per-frame MemStats here: both let late goroutines settle and hide the allocations being looked for.
|
|
var base bytes.Buffer
|
|
var before, after runtime.MemStats
|
|
measured := 0
|
|
var draws, tris uint64
|
|
var elapsed float64
|
|
window := func(count int) {
|
|
base.Reset()
|
|
if profile {
|
|
pprof.Lookup("allocs").WriteTo(&base, 0)
|
|
}
|
|
measured, draws, tris, elapsed = 0, 0, 0, 0
|
|
runtime.ReadMemStats(&before)
|
|
for end := n + count; n < end; n++ {
|
|
if !eng.Step(app) {
|
|
break
|
|
}
|
|
s := eng.Stats()
|
|
draws += uint64(s.DrawCalls)
|
|
tris += uint64(s.Triangles)
|
|
elapsed += s.FrameTime
|
|
measured++
|
|
}
|
|
runtime.ReadMemStats(&after)
|
|
}
|
|
window(frames - n)
|
|
if first := after.Mallocs - before.Mallocs; strict && first > 0 && first < uint64(measured) && n == frames {
|
|
// a second window tells a one-off runtime allocation (a blocked worker's sudog, GC workers) from frames that allocate
|
|
fmt.Fprintf(os.Stderr, "first window allocated %d times, measuring again\n", first)
|
|
window(measured)
|
|
}
|
|
err := eng.Close()
|
|
allocs := after.Mallocs - before.Mallocs
|
|
bytes := after.TotalAlloc - before.TotalAlloc
|
|
perFrame := func(v uint64) float64 {
|
|
if measured == 0 {
|
|
return 0
|
|
}
|
|
return float64(v) / float64(measured)
|
|
}
|
|
fmt.Printf("frames=%d measured=%d sim_time=%.3fs draws/frame=%.2f tris/frame=%.2f allocs=%d alloc_bytes=%d gc=%d\n",
|
|
n, measured, elapsed, perFrame(draws), perFrame(tris), allocs, bytes, after.NumGC-before.NumGC)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "run:", err)
|
|
return 1
|
|
}
|
|
if strict && allocs > 0 {
|
|
fmt.Fprintf(os.Stderr, "allocations after warmup: %d\n", allocs)
|
|
if profile {
|
|
writeAllocProfile(profDir, &base)
|
|
}
|
|
return 3
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// writeAllocProfile saves the allocation profiles from the end of warmup and of the run; pprof -base shows the measured frames.
|
|
func writeAllocProfile(dir string, base *bytes.Buffer) {
|
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
fmt.Fprintln(os.Stderr, "alloc profile:", err)
|
|
return
|
|
}
|
|
basePath, headPath := filepath.Join(dir, "smoke-allocs-base.pb.gz"), filepath.Join(dir, "smoke-allocs.pb.gz")
|
|
runtime.GC()
|
|
f, err := os.Create(headPath)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "alloc profile:", err)
|
|
return
|
|
}
|
|
err = pprof.Lookup("allocs").WriteTo(f, 0)
|
|
f.Close()
|
|
if err == nil {
|
|
err = os.WriteFile(basePath, base.Bytes(), 0o644)
|
|
}
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, "alloc profile:", err)
|
|
return
|
|
}
|
|
fmt.Fprintf(os.Stderr, "allocation profile: go tool pprof -traces -sample_index=alloc_objects -base %s %s\n", basePath, headPath)
|
|
}
|