hajimehoshi / ebiten

Ebitengine - A dead simple 2D game engine for Go
https://ebitengine.org
Apache License 2.0
10.96k stars 654 forks source link

FPS is lower and irregular in fullscreen (macOS) #3054

Open venning opened 2 months ago

venning commented 2 months ago

Ebitengine Version

2.7.8

Operating System

Go Version (go version)

go1.22.2 darwin/arm64

What steps will reproduce the problem?

Run the following and toggle fullscreen by pressing [F]. It just measures the system time between Update calls (using SyncWithFPS) and calculates the effective FPS.

package main

import (
    "fmt"
    "log"
    "strings"
    "time"

    "github.com/hajimehoshi/ebiten/v2"
    "github.com/hajimehoshi/ebiten/v2/ebitenutil"
    "github.com/hajimehoshi/ebiten/v2/inpututil"
)

var (
    w, h = 800, 600
    then = time.Now().UnixNano()
    cols, rows = 10, 30
    msgs = make([]string, cols*rows)
    next = 0 // index of next msg to write
    x0, y0, xStroke = 10, 60, 80
    graphicsLibrary string
    paused bool
)

func addMsg(s string) {
    msgs[next] = s
    next++
    if next == len(msgs) {
        next = 0
    }
    msgs[next] = "" // so we can track new msgs after looping
}

func getCol(c int) string {
    return strings.Join(msgs[c*rows:(c+1)*rows], "\n")
}

type Game struct {}

func (g *Game) Update() error {
    if !paused {
        now := time.Now().UnixNano()
        tps := 1_000_000_000/float64(now-then)
        then = now
        // EDIT: updated to align tps >1000 since those results were a bit "hidden"
        addMsg(fmt.Sprintf("%6.1f", tps))
    }

    if inpututil.IsKeyJustPressed(ebiten.KeyF) {
        ebiten.SetFullscreen(!ebiten.IsFullscreen())
        addMsg("TOGGLED FS")
    }
    if inpututil.IsKeyJustPressed(ebiten.KeyQ) {
        return ebiten.Termination
    }
    if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
        paused = !paused
        if paused {
            addMsg("PAUSED")
        }
    }
    return nil
}

func (g *Game) Draw(screen *ebiten.Image) {
    if graphicsLibrary == "" {
        debugInfo := &ebiten.DebugInfo{}
        ebiten.ReadDebugInfo(debugInfo)
        graphicsLibrary = debugInfo.GraphicsLibrary.String()
    }
    msg := fmt.Sprintf("ebiten.ActualFPS():  %5.1f  (%s)\n\n", ebiten.ActualFPS(), graphicsLibrary)
    msg += "Press [F] to toggle fullscreen    Press [Q] to quit    Press [Space] to pause/unpause recording"
    ebitenutil.DebugPrint(screen, msg)

    for c := range cols {
        ebitenutil.DebugPrintAt(screen, getCol(c), x0+c*xStroke, y0)
    }
}

func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
    return w, h
}

func main() {
    // I'm using these settings, so for consistency:
    ebiten.SetTPS(ebiten.SyncWithFPS)
    op := &ebiten.RunGameOptions{}
    op.SingleThread = true

//  op.GraphicsLibrary = ebiten.GraphicsLibraryMetal
//  op.GraphicsLibrary = ebiten.GraphicsLibraryOpenGL

    ebiten.SetWindowSize(w, h)
    if err := ebiten.RunGameWithOptions(&Game{}, op); err != nil && err != ebiten.Termination {
        log.Fatal(err)
    }
}

What is the expected result?

My laptop monitor is 120 Hz. While not in fullscreen, Ebitengine runs at ~120 FPS and the values of the effective FPS (as established by the above code) is roughly 120 FPS for each frame. This is as I would expect.

While in fullscreen, the FPS drops to ~90 (though sometimes as low as 77, I'm not sure why). Moreover, the effective timing of frames oscillates between ~120 FPS and ~60 FPS (which would average to 90). This is unexpected.

What happens instead?

This is not fullscreen:

Screenshot 2024-08-03 at 11 56 56 AM

This is fullscreen:

Screenshot 2024-08-03 at 11 57 55 AM

(Note, the printed FPS at the top of the images is affected by me taking the screenshots themselves.)

Anything else you feel useful to add?

I'm running these under Metal. The OpenGL version of this is wildly irregular; which, I assume, is why Metal is the default. However, OpenGL maintains ~120 FPS in fullscreen.

My guess is that MacOS is limiting the application to 90 FPS for its own reasons, but Ebitengine is reading the refresh rate at 120 Hz and its Draw call timing calculation is compensating; though I really don't understand enough of Ebitengine's internals to feel confident in that.


MacBook Pro 14-inch, 2021 Apple M1 Pro 32 GB Ventura 13.1

hajimehoshi commented 2 months ago

I'll take a look, but I am not sure this is fixable.

hajimehoshi commented 2 months ago
        now := time.Now().UnixNano()
        tps := 1_000_000_000/float64(now-then)
        then = now
        addMsg(fmt.Sprintf("%5.1f", tps))

This measures TPS (ticks per second), not FPS (frames per second). Ticks can be flaky so that you can expect Update is called 60 times per second on average. This is expected.

EDIT: You used SyncWithFPS, so this measures FPS. I'm sorry.

hajimehoshi commented 2 months ago

I tested this on fullscreen (MacBook M3 Pro with power adapter):

image

Draw timings are pretty flaky and unstable, and as I said, I am not sure this is fixable. On average, FPS is stable as 120, so unfortunately I failed to reproduce your case.

venning commented 2 months ago

I am not sure this is fixable.

Okay, I understand.

Can you keep this issue open? I would be interested in revisiting this if and when I upgrade operating systems or machines.

The more I use Ebitengine, the more I appreciate how weird operating systems make rendering.

hajimehoshi commented 2 months ago

Can you keep this issue open? I would be interested in revisiting this if and when I upgrade operating systems or machines.

Sure. I don't set a milestone, but I'm fine keeping this open.

The more I use Ebitengine, the more I appreciate how weird operating systems make rendering.

Thanks! Yeah, Ebitengine does a lot of things to handle weird things under the hood...

gimby commented 2 months ago

As a side note, This reminds me of the troubles that the developers of the Zed editor went through, the performance characteristics were wildly different on the different ARM processors. https://zed.dev/blog/120fps

venning commented 2 months ago

(I'm recording this here more for posterity, as I will likely revisit this issue in the future. I don't expect this comment will result in any immediate change in behavior or understanding.)

@hajimehoshi Revisiting this, I studied your results a little more closely. (As a reminder: my computer was somewhat alternating between 120 FPS and 60 FPS to average to something around 90 FPS, but your computer seemed to be maintaining 120 FPS.)

However, I just now noticed that your computer seems to somewhat replicate what my computer was doing with the alternating 120 and 60, but occasionally your computer would have a tick-frame that happened so quickly it registered as >1000 FPS, which resulted in the average being 120. Presumably, this was Ebitengine attempting to compensate for the occasional 60 FPS "lag".

Is there some reason your computer would do that while mine would not? I imagine it's likely either just a difference between M1 and M3, or a difference between our operating system versions (I'm assuming you weren't on Ventura 13.1).