From 24e5751ece712abfc8d54a0497a556e919b25d2e Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Tue, 17 Sep 2024 01:44:19 +0900 Subject: [PATCH] internal/ui: add sleep for an environment where vsync doesn't work Updates #2952 --- internal/ui/context.go | 86 ++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/internal/ui/context.go b/internal/ui/context.go index 82e94c8f4..fc6f320f6 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -70,7 +70,14 @@ func newContext(game Game) *context { func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface) error { // TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped. - return c.updateFrameImpl(graphicsDriver, clock.UpdateFrame(), outsideWidth, outsideHeight, deviceScaleFactor, ui, false) + needsSwapBuffers, err := c.updateFrameImpl(graphicsDriver, clock.UpdateFrame(), outsideWidth, outsideHeight, deviceScaleFactor, ui, false) + if err != nil { + return err + } + if err := c.swapBuffersOrWait(needsSwapBuffers, graphicsDriver, ui.FPSMode() == FPSModeVsyncOn); err != nil { + return err + } + return nil } func (c *context) forceUpdateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface) error { @@ -81,63 +88,50 @@ func (c *context) forceUpdateFrame(graphicsDriver graphicsdriver.Graphics, outsi n = 2 } for i := 0; i < n; i++ { - if err := c.updateFrameImpl(graphicsDriver, 1, outsideWidth, outsideHeight, deviceScaleFactor, ui, true); err != nil { + needsSwapBuffers, err := c.updateFrameImpl(graphicsDriver, 1, outsideWidth, outsideHeight, deviceScaleFactor, ui, true) + if err != nil { + return err + } + if err := c.swapBuffersOrWait(needsSwapBuffers, graphicsDriver, ui.FPSMode() == FPSModeVsyncOn); err != nil { return err } } return nil } -func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool) (err error) { +func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool) (needsSwapBuffers bool, err error) { // The given outside size can be 0 e.g. just after restoring from the fullscreen mode on Windows (#1589) // Just ignore such cases. Otherwise, creating a zero-sized framebuffer causes a panic. if outsideWidth == 0 || outsideHeight == 0 { - return nil + return false, nil } debug.FrameLogf("----\n") - var needSwapBuffers bool - if err := atlas.BeginFrame(graphicsDriver); err != nil { - return err + return false, err } defer func() { if err1 := atlas.EndFrame(); err1 != nil && err == nil { + needsSwapBuffers = false err = err1 return } - - if needSwapBuffers { - if err1 := atlas.SwapBuffers(graphicsDriver); err1 != nil && err == nil { - err = err1 - return - } - } else { - now := time.Now() - defer func() { - c.lastSwapBufferTime = now - }() - if delta := time.Second/60 - now.Sub(c.lastSwapBufferTime); delta > 0 { - // When swapping buffers is skipped and Draw is called too early, sleep for a while to suppress CPU usages (#2890). - time.Sleep(delta) - } - } }() // Flush deferred functions, like reading pixels from GPU. if err := c.processFuncsInFrame(ui); err != nil { - return err + return false, err } // ForceUpdate can be invoked even if the context is not initialized yet (#1591). if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 { - return nil + return false, nil } // Update the input state after the layout is updated as a cursor position is affected by the layout. if err := ui.updateInputState(); err != nil { - return err + return false, err } // Ensure that Update is called once before Draw so that Update can be used for initialization. @@ -155,15 +149,15 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update }) if err := hook.RunBeforeUpdateHooks(); err != nil { - return err + return false, err } if err := c.game.Update(); err != nil { - return err + return false, err } // Catch the error that happened at (*Image).At. if err := ui.error(); err != nil { - return err + return false, err } ui.tick.Add(1) @@ -172,13 +166,39 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update // Update window icons during a frame, since an icon might be *ebiten.Image and // getting pixels from it needs to be in a frame (#1468). if err := ui.updateIconIfNeeded(); err != nil { - return err + return false, err } // Draw the game. - needSwapBuffers, err = c.drawGame(graphicsDriver, ui, forceDraw) - if err != nil { - return err + return c.drawGame(graphicsDriver, ui, forceDraw) +} + +func (c *context) swapBuffersOrWait(needsSwapBuffers bool, graphicsDriver graphicsdriver.Graphics, vsyncEnabled bool) error { + now := time.Now() + defer func() { + c.lastSwapBufferTime = now + }() + + if needsSwapBuffers { + if err := atlas.SwapBuffers(graphicsDriver); err != nil { + return err + } + } + + var waitTime time.Duration + if !needsSwapBuffers { + // When swapping buffers is skipped and Draw is called too early, sleep for a while to suppress CPU usages (#2890). + waitTime = time.Second / 60 + } else if vsyncEnabled { + // In some environments, e.g. Linux on Parallels, SwapBuffers doesn't wait for the vsync (#2952). + // In the case when the display has high refresh rates like 240 [Hz], the wait time should be small. + waitTime = time.Millisecond + } + if waitTime > 0 { + if delta := waitTime - now.Sub(c.lastSwapBufferTime); delta > 0 { + println(waitTime.String(), now.Sub(c.lastSwapBufferTime).String()) + time.Sleep(delta) + } } return nil