From b019a3723aaf02076fa6748e79a08897d97f2dbc Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Mon, 24 Oct 2022 01:51:37 +0900 Subject: [PATCH] internal/ui: optimize GPU usages when the screen doesn't have to be updated This change skips rendering when 1) the screen is not cleared every frame (`SetScreenClearedEveryFrame(false)`) and 2) Draw doesn't draw anything onto the screen. The GPU usages decreased on some machines (e.g. GPU usage was 10% with an empty Ebitengine project and became 2-3 % on a Windows machine). Updates #2341 --- examples/skipdraw/main.go | 107 ++++++++++++++++++ gameforui.go | 2 +- internal/graphicscommand/command.go | 3 +- .../graphicsdriver/metal/graphics_darwin.go | 6 +- internal/ui/context.go | 43 ++++--- internal/ui/image.go | 7 ++ 6 files changed, 152 insertions(+), 16 deletions(-) create mode 100644 examples/skipdraw/main.go diff --git a/examples/skipdraw/main.go b/examples/skipdraw/main.go new file mode 100644 index 000000000..2c118ea68 --- /dev/null +++ b/examples/skipdraw/main.go @@ -0,0 +1,107 @@ +// Copyright 2022 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build example +// +build example + +package main + +import ( + "bytes" + "image" + _ "image/jpeg" + "log" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/examples/resources/images" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +const ( + screenWidth = 640 + screenHeight = 480 +) + +var ( + gophersImage *ebiten.Image +) + +type Game struct { + count int + input bool + introShown bool +} + +func (g *Game) Update() error { + g.count++ + g.input = len(inpututil.AppendPressedKeys(nil)) > 0 + if inpututil.IsKeyJustPressed(ebiten.KeyF) { + ebiten.SetFullscreen(!ebiten.IsFullscreen()) + } + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + // If there is no inpnut, skip draw. Ebitengine skips GPU usages in this case. + if !g.input && g.introShown { + return + } + + screen.Clear() + + w, h := gophersImage.Size() + op := &ebiten.DrawImageOptions{} + + // Move the image's center to the screen's upper-left corner. + // This is a preparation for rotating. When geometry matrices are applied, + // the origin point is the upper-left corner. + op.GeoM.Translate(-float64(w)/2, -float64(h)/2) + + // Rotate the image. As a result, the anchor point of this rotate is + // the center of the image. + op.GeoM.Rotate(float64(g.count%360) * 2 * math.Pi / 360) + + // Move the image to the screen's center. + op.GeoM.Translate(screenWidth/2, screenHeight/2) + + screen.DrawImage(gophersImage, op) + + ebitenutil.DebugPrint(screen, "Press any keys to keep animations.\nPress F to switch fullscreen.") + + g.introShown = true +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { + return screenWidth, screenHeight +} + +func main() { + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + ebiten.SetScreenClearedEveryFrame(false) + + // Decode an image from the image file's byte slice. + img, _, err := image.Decode(bytes.NewReader(images.Gophers_jpg)) + if err != nil { + log.Fatal(err) + } + gophersImage = ebiten.NewImageFromImage(img) + + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowTitle("Skip Draw (Ebitengine Demo)") + if err := ebiten.RunGame(&Game{}); err != nil { + log.Fatal(err) + } +} diff --git a/gameforui.go b/gameforui.go index 1fd076ae5..710342dc0 100644 --- a/gameforui.go +++ b/gameforui.go @@ -147,7 +147,7 @@ func (g *gameForUI) DrawOffscreen() error { return nil } -func (g *gameForUI) DrawScreen() { +func (g *gameForUI) DrawFinalScreen() { scale, offsetX, offsetY := g.ScreenScaleAndOffsets() var geoM GeoM geoM.Scale(scale, scale) diff --git a/internal/graphicscommand/command.go b/internal/graphicscommand/command.go index f6e58a485..175fb4a96 100644 --- a/internal/graphicscommand/command.go +++ b/internal/graphicscommand/command.go @@ -168,7 +168,8 @@ func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bo // flush must be called the main thread. func (q *commandQueue) flush(graphicsDriver graphicsdriver.Graphics, endFrame bool) (err error) { - if len(q.commands) == 0 { + // If endFrame is true, Begin/End should be called to ensure the framebuffer is swapped. + if len(q.commands) == 0 && !endFrame { return nil } diff --git a/internal/graphicsdriver/metal/graphics_darwin.go b/internal/graphicsdriver/metal/graphics_darwin.go index 613c5e460..18bf06832 100644 --- a/internal/graphicsdriver/metal/graphics_darwin.go +++ b/internal/graphicsdriver/metal/graphics_darwin.go @@ -199,11 +199,15 @@ func (g *Graphics) SetVertices(vertices []float32, indices []uint16) error { } func (g *Graphics) flushIfNeeded(present bool) { - if g.cb == (mtl.CommandBuffer{}) { + if g.cb == (mtl.CommandBuffer{}) && !present { return } g.flushRenderCommandEncoderIfNeeded() + if present && g.screenDrawable == (ca.MetalDrawable{}) { + g.screenDrawable = g.view.nextDrawable() + } + if !g.view.presentsWithTransaction() && present && g.screenDrawable != (ca.MetalDrawable{}) { g.cb.PresentDrawable(g.screenDrawable) } diff --git a/internal/ui/context.go b/internal/ui/context.go index 624da6d95..054b2c665 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -38,7 +38,7 @@ type Game interface { Layout(outsideWidth, outsideHeight int) (int, int) Update() error DrawOffscreen() error - DrawScreen() + DrawFinalScreen() ScreenScaleAndOffsets() (scale, offsetX, offsetY float64) } @@ -53,6 +53,8 @@ type context struct { // The following members must be protected by the mutex m. outsideWidth float64 outsideHeight float64 + + skipCount int } func newContext(game Game) *context { @@ -63,7 +65,7 @@ func newContext(game Game) *context { func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *userInterfaceImpl) 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) + return c.updateFrameImpl(graphicsDriver, clock.UpdateFrame(), outsideWidth, outsideHeight, deviceScaleFactor, ui, false) } func (c *context) forceUpdateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *userInterfaceImpl) error { @@ -74,14 +76,14 @@ 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); err != nil { + if err := c.updateFrameImpl(graphicsDriver, 1, outsideWidth, outsideHeight, deviceScaleFactor, ui, true); err != nil { return err } } return nil } -func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *userInterfaceImpl) (err error) { +func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *userInterfaceImpl, forceDraw bool) (err error) { if err := theGlobalState.error(); err != nil { return err } @@ -142,14 +144,14 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update } // Draw the game. - if err := c.drawGame(graphicsDriver); err != nil { + if err := c.drawGame(graphicsDriver, forceDraw); err != nil { return err } return nil } -func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) error { +func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, forceDraw bool) error { if (c.offscreen.imageType == atlas.ImageTypeVolatile) != theGlobalState.isScreenClearedEveryFrame() { w, h := c.offscreen.width, c.offscreen.height c.offscreen.MarkDisposed() @@ -162,20 +164,35 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) error { if theGlobalState.isScreenClearedEveryFrame() { c.offscreen.clear() } + + c.offscreen.dirty = false if err := c.game.DrawOffscreen(); err != nil { return err } - if graphicsDriver.NeedsClearingScreen() { - // This clear is needed for fullscreen mode or some mobile platforms (#622). - c.screen.clear() + const maxSkipCount = 3 + + if !forceDraw && !theGlobalState.isScreenClearedEveryFrame() && !c.offscreen.dirty { + if c.skipCount < maxSkipCount { + c.skipCount++ + } + } else { + c.skipCount = 0 } - c.game.DrawScreen() + // If the offscreen is not updated and the framebuffers don't have to be updated, skip rendering to save GPU power. + if c.skipCount < maxSkipCount { + if graphicsDriver.NeedsClearingScreen() { + // This clear is needed for fullscreen mode or some mobile platforms (#622). + c.screen.clear() + } - // The final screen is never used as the rendering source. - // Flush its buffer here just in case. - c.screen.flushBufferIfNeeded() + c.game.DrawFinalScreen() + + // The final screen is never used as the rendering source. + // Flush its buffer here just in case. + c.screen.flushBufferIfNeeded() + } return nil } diff --git a/internal/ui/image.go b/internal/ui/image.go index 1370426a5..10e10aadf 100644 --- a/internal/ui/image.go +++ b/internal/ui/image.go @@ -45,6 +45,8 @@ type Image struct { bigOffscreenBuffer *Image bigOffscreenBufferBlend graphicsdriver.Blend bigOffscreenBufferDirty bool + + dirty bool } func NewImage(width, height int, imageType atlas.ImageType) *Image { @@ -68,9 +70,12 @@ func (i *Image) MarkDisposed() { i.mipmap.MarkDisposed() i.mipmap = nil i.dotsBuffer = nil + i.dirty = false } func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint16, blend graphicsdriver.Blend, dstRegion, srcRegion graphicsdriver.Region, subimageOffsets [graphics.ShaderImageCount - 1][2]float32, shader *Shader, uniforms [][]float32, evenOdd bool, canSkipMipmap bool, antialias bool) { + i.dirty = true + if antialias { // Flush the other buffer to make the buffers exclusive. i.flushDotsBufferIfNeeded() @@ -141,6 +146,8 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices [ } func (i *Image) WritePixels(pix []byte, x, y, width, height int) { + i.dirty = true + if width == 1 && height == 1 { // Flush the other buffer to make the buffers exclusive. i.flushBigOffscreenBufferIfNeeded()