Last active
November 27, 2023 11:45
-
-
Save venning/09e0b76dd913123acf5d3ae6cabb93d4 to your computer and use it in GitHub Desktop.
Ebiten drawing benchmark
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"errors" | |
"fmt" | |
"image/color" | |
"log" | |
"math/rand" | |
"runtime/debug" | |
"time" | |
"github.com/hajimehoshi/ebiten/v2" | |
"github.com/hajimehoshi/ebiten/v2/ebitenutil" | |
"github.com/hajimehoshi/ebiten/v2/inpututil" | |
) | |
// adjust these to change the test | |
const ( | |
screenWidth, screenHeight = 1920, 1080 | |
numObjs = 500 | |
objW, objH = 200, 200 | |
// it's best if this is an even division of numObjs, so we don't skip drawing any | |
numLayers = 10 | |
// it's best if X*Y is an even division of numObjs, so we don't skip drawing any | |
numXSubdivs = 5 | |
numYSubdivs = 2 | |
) | |
var ( | |
ticks, frames = 0, 0 | |
lastSwitchTick = 1*60 // a little more for the initial grace period | |
gracePeriod = 3*60 // num ticks after a mode switch before we track the FPS | |
terminated = errors.New("regular termination") | |
startTime time.Time | |
perLayer = numObjs / numLayers | |
perSubdiv = numObjs / numXSubdivs / numYSubdivs | |
subdivW = screenWidth / numXSubdivs | |
subdivH = screenHeight / numYSubdivs | |
objs = make([]*ebiten.Image, numObjs) | |
screenOps = make([]*ebiten.DrawImageOptions, numObjs) | |
layers = make([]*ebiten.Image, numLayers) | |
layerOps = make([]*ebiten.DrawImageOptions, numObjs) | |
subdivs = make([][]*ebiten.Image, numXSubdivs) | |
subdivSelfOps = make([][]*ebiten.DrawImageOptions, numXSubdivs) | |
subdivObjOps = make([]*ebiten.DrawImageOptions, numObjs) | |
mode = 0 // current mode | |
modeKeys = []ebiten.Key{ | |
ebiten.Key1, | |
ebiten.Key2, | |
ebiten.Key3, | |
ebiten.Key4, | |
ebiten.Key5, | |
} | |
prefixes = []string{ | |
"Press", | |
"Press", | |
"Press", | |
"Press", | |
"Press", | |
} | |
builders = []func(){ | |
BuildOnScreen, | |
BuildLayers, | |
BuildLayers, | |
BuildSubdivs, | |
BuildSubdivs, | |
} | |
drawers = []func(*ebiten.Image){ | |
DrawOnScreen, | |
DrawLayersSeq, | |
DrawLayersOnce, | |
DrawSubdivsSeq, | |
DrawSubdivsOnce, | |
} | |
modeFrames = []int{ | |
0, | |
0, | |
0, | |
0, | |
0, | |
} | |
modeFps = []float64{ | |
0, | |
0, | |
0, | |
0, | |
0, | |
} | |
) | |
func init() { | |
for i := 0; i < numObjs; i++ { | |
objs[i] = ebiten.NewImage(objW, objH) | |
objs[i].Fill(color.NRGBA{uint8(rand.Intn(256)), uint8(rand.Intn(256)), uint8(rand.Intn(256)), 0x80}) | |
} | |
for x := 0; x < numXSubdivs; x++ { | |
subdivs[x] = make([]*ebiten.Image, numYSubdivs) | |
subdivSelfOps[x] = make([]*ebiten.DrawImageOptions, numYSubdivs) | |
for y := 0; y < numYSubdivs; y++ { | |
subdivSelfOps[x][y] = &ebiten.DrawImageOptions{} | |
subdivSelfOps[x][y].GeoM.Translate(float64(x*subdivW), float64(y*subdivH)) | |
} | |
} | |
builders[mode]() | |
prefixes[mode] = " »»»»" | |
} | |
type Game struct { | |
} | |
func (g *Game) Update() error { | |
ticks++ | |
if ticks == 1 { | |
startTime = time.Now() | |
} | |
if inpututil.IsKeyJustPressed(ebiten.KeyQ) || ebiten.IsWindowBeingClosed() { | |
t := time.Now().Sub(startTime) | |
s := t.Seconds() | |
tps, fps := float64(ticks) / s, float64(frames) / s | |
fmt.Printf("Run Time: %s; Avg TPS: %.1f; Avg FPS: %.1f\n", t.Round(time.Second), tps, fps) | |
return terminated | |
} | |
if ebiten.IsFullscreen() && (inpututil.IsKeyJustPressed(ebiten.KeyF) || inpututil.IsKeyJustPressed(ebiten.KeyEscape)) { | |
ebiten.SetFullscreen(false) | |
lastSwitchTick = ticks | |
} else if inpututil.IsKeyJustPressed(ebiten.KeyF) { | |
ebiten.SetFullscreen(true) | |
lastSwitchTick = ticks | |
} | |
for i, key := range modeKeys { | |
if inpututil.IsKeyJustPressed(key) && mode != i { | |
Dispose() | |
mode = i | |
builders[mode]() | |
prefixes[mode] = " »»»»" | |
lastSwitchTick = ticks | |
} | |
} | |
return nil | |
} | |
func (g *Game) Draw(screen *ebiten.Image) { | |
frames++ | |
if ticks > lastSwitchTick+gracePeriod { | |
modeFrames[mode]++ | |
modeFps[mode] += ebiten.CurrentFPS() | |
} | |
drawers[mode](screen) | |
msg := fmt.Sprintf("%d objects @ %dx%d; ", numObjs, screenWidth, screenHeight) | |
msg += fmt.Sprintf("TPS: %.1f; FPS: %.1f; ", ebiten.CurrentTPS(), ebiten.CurrentFPS()) | |
msg += fmt.Sprintf("\nPress [F] to toggle fullscreen, [Q] to quit\n") | |
msg += fmt.Sprintf("\nCurrent mode: %d", mode+1) | |
msg += fmt.Sprintf("\n%s [1] to draw directly on the screen%s", prefixes[0], FPS(0)) | |
msg += fmt.Sprintf("\n%s [2] to draw on %d layers, sequentially%s", prefixes[1], numLayers, FPS(1)) | |
msg += fmt.Sprintf("\n%s [3] to draw on %d layers, all at once%s", prefixes[2], numLayers, FPS(2)) | |
msg += fmt.Sprintf("\n%s [4] to draw on %dx%d subdivisions, sequentially%s", prefixes[3], numXSubdivs, numYSubdivs, FPS(3)) | |
msg += fmt.Sprintf("\n%s [5] to draw on %dx%d subdivisions, all at once%s", prefixes[4], numXSubdivs, numYSubdivs, FPS(4)) | |
msg += fmt.Sprintf("\n(switching modes may take a few seconds to stabilize FPS)") // due to GC call | |
ebitenutil.DebugPrint(screen, msg) | |
} | |
func FPS(m int) string { | |
if modeFrames[m] == 0 { | |
return "" | |
} | |
return fmt.Sprintf(" - %.1f avg FPS", modeFps[m] / float64(modeFrames[m])) | |
} | |
func BuildOnScreen() { | |
for i := 0; i < numObjs; i++ { | |
screenOps[i] = &ebiten.DrawImageOptions{} | |
screenOps[i].GeoM.Translate(float64(rand.Intn(screenWidth-objW)), float64(rand.Intn(screenHeight-objH))) | |
} | |
} | |
func DrawOnScreen(screen *ebiten.Image) { | |
for i := 0; i < numObjs; i++ { | |
screen.DrawImage(objs[i], screenOps[i]) | |
} | |
} | |
func BuildLayers() { | |
for i := 0; i < numLayers; i++ { | |
layers[i] = ebiten.NewImage(screenWidth, screenHeight) | |
} | |
for i := 0; i < numObjs; i++ { | |
layerOps[i] = &ebiten.DrawImageOptions{} | |
layerOps[i].GeoM.Translate(float64(rand.Intn(screenWidth-objW)), float64(rand.Intn(screenHeight-objH))) | |
} | |
} | |
func DrawLayersSeq(screen *ebiten.Image) { | |
o := 0 | |
for i := 0; i < numLayers; i++ { | |
layers[i].Clear() | |
for l := 0; l < perLayer; l++ { | |
layers[i].DrawImage(objs[o], layerOps[o]) | |
o++ | |
} | |
screen.DrawImage(layers[i], nil) | |
} | |
} | |
func DrawLayersOnce(screen *ebiten.Image) { | |
o := 0 | |
for i := 0; i < numLayers; i++ { | |
layers[i].Clear() | |
for l := 0; l < perLayer; l++ { | |
layers[i].DrawImage(objs[o], layerOps[o]) | |
o++ | |
} | |
} | |
for i := 0; i < numLayers; i++ { | |
screen.DrawImage(layers[i], nil) | |
} | |
} | |
func BuildSubdivs() { | |
for x := 0; x < numXSubdivs; x++ { | |
for y := 0; y < numYSubdivs; y++ { | |
subdivs[x][y] = ebiten.NewImage(subdivW, subdivH) | |
} | |
} | |
for i := 0; i < numObjs; i++ { | |
subdivObjOps[i] = &ebiten.DrawImageOptions{} | |
subdivObjOps[i].GeoM.Translate(float64(rand.Intn(subdivW-objW)), float64(rand.Intn(subdivH-objH))) | |
} | |
} | |
func DrawSubdivsSeq(screen *ebiten.Image) { | |
o := 0 | |
for x := 0; x < numXSubdivs; x++ { | |
for y := 0; y < numYSubdivs; y++ { | |
subdivs[x][y].Clear() | |
for s := 0; s < perSubdiv; s++ { | |
subdivs[x][y].DrawImage(objs[o], subdivObjOps[o]) | |
o++ | |
} | |
screen.DrawImage(subdivs[x][y], subdivSelfOps[x][y]) | |
} | |
} | |
} | |
func DrawSubdivsOnce(screen *ebiten.Image) { | |
o := 0 | |
for x := 0; x < numXSubdivs; x++ { | |
for y := 0; y < numYSubdivs; y++ { | |
subdivs[x][y].Clear() | |
for s := 0; s < perSubdiv; s++ { | |
subdivs[x][y].DrawImage(objs[o], subdivObjOps[o]) | |
o++ | |
} | |
} | |
} | |
for x := 0; x < numXSubdivs; x++ { | |
for y := 0; y < numYSubdivs; y++ { | |
screen.DrawImage(subdivs[x][y], subdivSelfOps[x][y]) | |
} | |
} | |
} | |
func Dispose() { | |
prefixes[mode] = "Press" | |
switch mode { | |
case 0: | |
for i := 0; i < numObjs; i++ { | |
screenOps[i] = nil // probably unnecessary to reset these, but just being complete | |
} | |
case 1: | |
fallthrough | |
case 2: | |
for i := 0; i < numLayers; i++ { | |
layers[i].Dispose() | |
layers[i] = nil | |
} | |
for i := 0; i < numObjs; i++ { | |
screenOps[i] = nil // probably unnecessary to reset these, but just being complete | |
} | |
case 3: | |
fallthrough | |
case 4: | |
for x := 0; x < numXSubdivs; x++ { | |
for y := 0; y < numYSubdivs; y++ { | |
subdivs[x][y].Dispose() | |
subdivs[x][y] = nil | |
} | |
} | |
for i := 0; i < numObjs; i++ { | |
subdivObjOps[i] = nil // probably unnecessary to reset these, but just being complete | |
} | |
} | |
debug.FreeOSMemory() | |
} | |
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { | |
return screenWidth, screenHeight | |
} | |
func main() { | |
ebiten.SetFPSMode(ebiten.FPSModeVsyncOffMaximum) | |
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) | |
ebiten.SetWindowClosingHandled(true) | |
ebiten.SetWindowSize(screenWidth, screenHeight) | |
ebiten.SetWindowTitle("Draw Test") | |
if err := ebiten.RunGame(&Game{}); err != nil && err != terminated { | |
log.Fatal(err) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment