Open erexo opened 7 months ago
Adjusting FPS from program is impossible so I don't think I'll introduce this feature.
Have you tried SetScreenClearedEveryFrame(false)
+ vsync off? This could save CPU and GPU consumption.
func main() {
SetScreenClearedEveryFrame(false)
SetVsyncEnabled(false)
if err := ebiten.RunGame(&Game{}); err != nil {
panic(err)
}
}
func (g *Game) Draw(screen *ebiten.Image) {
now := time.Now()
if now.Sub(g.lastRender) < time.Second / 60 {
return
}
g.lastRender = now
screen.Clear()
// ...
}
yes I did try something similar long time ago, but then when the vsync was off, the FPS (number of the actual Draw calls) was gigantic and the CPU was still terribly devastated.
It looks like the ebitengine with vsync off is trying it's best to squeeze every ounce of processing power that it can grab, no matter what's happening inside the Draw function
yes I did try something similar long time ago, but then when the vsync was off, the FPS (number of the actual Draw calls) was gigantic and the CPU was still terribly devastated.
Try this with the latest Ebitengine now. This should suppress CPU usages as long as Draw returns immediately.
https://ebitengine.org/en/documents/2.5.html#GPU_Optimization_with_SetScreenClearedEveryFrame(false)
Now I tested this
package main
import (
"sync"
"github.com/hajimehoshi/ebiten/v2"
)
type Game struct {
once sync.Once
}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
}
func (g *Game) Layout(width, height int) (int, int) {
return width, height
}
func main() {
ebiten.SetScreenClearedEveryFrame(false)
ebiten.SetVsyncEnabled(false)
if err := ebiten.RunGame(&Game{}); err != nil {
panic(err)
}
}
On Windows, the CPU usage was about 50%, which was not expected. This is a bug and I'll try to fix this.
On macOS, the CPU usage was less than 5%.
@erexo Please try v2.6 (256d40363bf1a331ffd1c6f391792a90507991a3) or main (8551cd350f287387b62f47b0a67594751add2ea8), thanks
it works a lot better now, good job!
one thing I've noticed is that the ebiten.ActualFPS()
doesn't seems to follow actual meaningful Draw calls, does it mean that the swaps are still happening when they shouldn't?
package main
import (
"fmt"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
const FPS = 10 // change_me
func main() {
ebiten.SetScreenClearedEveryFrame(false)
ebiten.SetVsyncEnabled(false)
if err := ebiten.RunGame(&Game{}); err != nil {
panic(err)
}
}
type Game struct {
lastRender time.Time
lastFpsCheck time.Time
lastRealFps int
realFps int
}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
now := time.Now()
if now.Sub(g.lastRender) < time.Second/FPS {
return
}
if now.Sub(g.lastFpsCheck) > time.Second {
g.lastRealFps = g.realFps
g.realFps = 0
g.lastFpsCheck = now
}
g.realFps++
g.lastRender = now
screen.Clear()
ebitenutil.DebugPrint(screen, fmt.Sprintf("Desired FPS: %v\nReal FPS: %v\n\nFPS: %v\nTPS: %v", FPS, g.lastRealFps, ebiten.ActualFPS(), ebiten.ActualTPS()))
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return outsideWidth, outsideHeight
}
Please try to run this code, on my machine when I set the FPS to 10, my ActualFPS is closer to around 80 (on previous versions of ebitengine swaps could be as high as 4k+ per second so we already have a massive upgrade)
This method doesn' affect the actual FPS (i.e. how many times Draw is called per second), so ActualFPS
would be useless, which is expected. However, the actual Draw implementation after if now.Sub(g.lastFpsCheck) > time.Second {
should be processed as you expected.
and for some weird reason the "real fps" seems to be incorrect as well at higher framerate 🤔 you have any idea why?
edit: it looks like when we exit Draw immediately a few (3) times in a row, then there is a sleep for 30ms, and that results in max FPS set at 33 with this method enabled, are my observations correct?
func (g *Game) Draw(screen *ebiten.Image) {
now := time.Now()
if now.Sub(g.lastRender) < time.Millisecond {
fmt.Println("skip")
return
}
fmt.Println("pass after", now.Sub(g.lastRender))
g.lastRender = now
screen.Clear()
}
the result on my machine:
3xskip
pass after 30.321ms
3x skip
pass after 31.4053ms
3x skip
pass after 32.1455ms
Try this one, 1ms of wait time will result usually result in 30ms delay between frames. If you remove the if statement then the Draw will be called a lot faster.
Sure, I'll take a look tomorrow.
g.lastRealFps = g.realFps
I guess this calculation can be more accurate by multiplying by time.Second / now.Sub(g.lastFpsCheck)
I tested this program based on your program:
package main
import (
"fmt"
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
const FPS = 10 // change_me
func main() {
ebiten.SetScreenClearedEveryFrame(false)
ebiten.SetVsyncEnabled(false)
if err := ebiten.RunGame(&Game{}); err != nil {
panic(err)
}
}
type Game struct {
lastRender time.Time
lastFpsCheck time.Time
lastRealFps float64
realFps int
}
func (g *Game) Update() error {
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
now := time.Now()
if now.Sub(g.lastRender) < time.Second/FPS {
return
}
if delta := now.Sub(g.lastFpsCheck); delta > time.Second {
g.lastRealFps = float64(g.realFps) * float64(time.Second) / float64(delta)
g.realFps = 0
g.lastFpsCheck = now
}
g.realFps++
g.lastRender = now
screen.Clear()
ebitenutil.DebugPrint(screen, fmt.Sprintf("Desired FPS: %v\nReal FPS: %v\n\nFPS: %v\nTPS: %v", FPS, g.lastRealFps, ebiten.ActualFPS(), ebiten.ActualTPS()))
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) {
return outsideWidth, outsideHeight
}
The result was about 8-9 real FPS.
Try this one, 1ms of wait time will result usually result in 30ms delay between frames. If you remove the if statement then the Draw will be called a lot faster.
I also tried this, and I could reproduce the 30ms things. My guess is that Windows sleeping timer is not so accurate as we expect unfortunately... This results seems to have a contradition with the previous experiment result (8-9 FPS), so I'll investigate more.
I can confirm that your program outputs around 8 Real FPS at FPS set to 10. But when you set the FPS to anything over 33, the Real FPS will stay at 30-33 due to previously mentioned 30ms delay between actual frames
This results seems to have a contradition with the previous experiment result (8-9 FPS), so I'll investigate more.
OK I think this is not a contradition. (Real) FPS means the average Draw count per second, while there can be a big gap between two Draw calls.
And, I found that the timer precision on Windows is pretty bad, and the smallest sleeping time was about 16[ms] unfortunately. This means that we cannot reach over 60FPS in this way. (Also, this means Ebitengine would not be able to implement good FPS adjustment). Sigh...
@erexo Can other game engines adjust FPS by the way?
of course, my previous game client could do that https://github.com/edubart/otclient/blob/e6861d79c90d1808bde3fd41d30b6458d1616bfe/src/framework/core/graphicalapplication.cpp#L198
I do however understand that this isn't that big of an issue and that it may be painful to implement, so please don't bother too much about it
Note for myself: https://github.com/golang/go/issues/44343
@erexo Try the main branch (7e4cdf5211ca57623bd4c8e30e05e14a525bcbb0). This is not a perfect solution and FPS might not reach to 60, but this should be better than before. In order to emulate higher FPS, we need shorter sleeping, which means more CPU consumption. I don't plan to backport this change as this is a little risky. Thanks!
Operating System
What feature would you like to be added?
Currently we are able to somewhat limit the FPS by mingling with the
SetVsyncEnabled
function. The problem I have with this approach is that it accepts boolean, so we are in control of the FPS limiting but our control is limited to two states. What I would really appreciate to see is something likeSetMaxFPS
which limits the FPS to given value when VSync is disabled.Why is this needed?
When VSync is off, the game can consume insane amount of processing power which is very inconvenient for a lot of users with older (and newer) PCs. When VSync is on, players sometimes report that the game is laggy and that generally the performance is throttled, this is usually because their Monitor's FPS has invalid configuration on their Windows system (ie. Monitor is 144hz and Windows states that it's 60hz). Usually a quick reconfiguration of player's system settings helps, but I encounter this problem over and over and I can't tell how many people just left the game in silence It would be much much easier for me as a developer to just limit the FPS to 140 in code, so that despite the Monitor settings user will have a smooth experience that is not killing his machine at the same time.