Skip to content

Instantly share code, notes, and snippets.

@venning
Last active November 27, 2023 11:45
Show Gist options
  • Save venning/09e0b76dd913123acf5d3ae6cabb93d4 to your computer and use it in GitHub Desktop.
Save venning/09e0b76dd913123acf5d3ae6cabb93d4 to your computer and use it in GitHub Desktop.
Ebiten drawing benchmark
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