From 4bd3a9ef8f3c13d65b989ade533129d0838a2501 Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Thu, 13 Oct 2022 23:13:25 +0900 Subject: [PATCH] internal/ui: refactoring: move the screen rendering logic to ebiten package Updates #2046 --- gameforui.go | 104 ++++++++++++++++++++++++++++++++++-- internal/ui/context.go | 118 +++-------------------------------------- run.go | 4 +- 3 files changed, 108 insertions(+), 118 deletions(-) diff --git a/gameforui.go b/gameforui.go index f0867262b..7981ecdc2 100644 --- a/gameforui.go +++ b/gameforui.go @@ -15,21 +15,84 @@ package ebiten import ( + "fmt" "image" + "math" + "sync/atomic" "github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/ui" ) +const screenShaderSrc = `package main + +func Fragment(position vec4, texCoord vec2, color vec4) vec4 { + // TODO: Calculate the scale in the shader after pixels become the main unit in shaders (#1431) + _, dr := imageDstRegionOnTexture() + _, sr := imageSrcRegionOnTexture() + scale := (imageDstTextureSize() * dr) / (imageSrcTextureSize() * sr) + + sourceSize := imageSrcTextureSize() + // texelSize is one pixel size in texel sizes. + texelSize := 1 / sourceSize + halfScaledTexelSize := texelSize / 2 / scale + + // Shift 1/512 [texel] to avoid the tie-breaking issue. + pos := texCoord + p0 := pos - halfScaledTexelSize + (texelSize / 512) + p1 := pos + halfScaledTexelSize + (texelSize / 512) + + // Texels must be in the source rect, so it is not necessary to check. + c0 := imageSrc0UnsafeAt(p0) + c1 := imageSrc0UnsafeAt(vec2(p1.x, p0.y)) + c2 := imageSrc0UnsafeAt(vec2(p0.x, p1.y)) + c3 := imageSrc0UnsafeAt(p1) + + // p is the p1 value in one pixel assuming that the pixel's upper-left is (0, 0) and the lower-right is (1, 1). + p := fract(p1 * sourceSize) + + // rate indicates how much the 4 colors are mixed. rate is in between [0, 1]. + // + // 0 <= p <= 1/Scale: The rate is in between [0, 1] + // 1/Scale < p: Don't care. Adjacent colors (e.g. c0 vs c1 in an X direction) should be the same. + rate := clamp(p*scale, 0, 1) + return mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y) +} +` + +var screenFilterEnabled = int32(1) + +func isScreenFilterEnabled() bool { + return atomic.LoadInt32(&screenFilterEnabled) != 0 +} + +func setScreenFilterEnabled(enabled bool) { + v := int32(0) + if enabled { + v = 1 + } + atomic.StoreInt32(&screenFilterEnabled, v) +} + type gameForUI struct { - game Game - offscreen *Image + game Game + offscreen *Image + screen *Image + screenShader *Shader } func newGameForUI(game Game) *gameForUI { - return &gameForUI{ + g := &gameForUI{ game: game, } + + s, err := NewShader([]byte(screenShaderSrc)) + if err != nil { + panic(fmt.Sprintf("ebiten: compiling the screen shader failed: %v", err)) + } + g.screenShader = s + + return g } func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image { @@ -51,6 +114,16 @@ func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image { return c.offscreen.image } +func (c *gameForUI) NewScreenImage(width, height int) *ui.Image { + if c.screen != nil { + c.screen.Dispose() + c.screen = nil + } + + c.screen = newImage(image.Rect(0, 0, width, height), atlas.ImageTypeScreen) + return c.screen.image +} + func (c *gameForUI) Layout(outsideWidth, outsideHeight int) (int, int) { return c.game.Layout(outsideWidth, outsideHeight) } @@ -59,6 +132,29 @@ func (c *gameForUI) Update() error { return c.game.Update() } -func (c *gameForUI) Draw() { +func (c *gameForUI) DrawOffscreen() { c.game.Draw(c.offscreen) } + +func (g *gameForUI) DrawScreen(scale, offsetX, offsetY float64) { + switch { + case !isScreenFilterEnabled(), math.Floor(scale) == scale: + op := &DrawImageOptions{} + op.GeoM.Scale(scale, scale) + op.GeoM.Translate(offsetX, offsetY) + g.screen.DrawImage(g.offscreen, op) + case scale < 1: + op := &DrawImageOptions{} + op.GeoM.Scale(scale, scale) + op.GeoM.Translate(offsetX, offsetY) + op.Filter = FilterLinear + g.screen.DrawImage(g.offscreen, op) + default: + op := &DrawRectShaderOptions{} + op.Images[0] = g.offscreen + op.GeoM.Scale(scale, scale) + op.GeoM.Translate(offsetX, offsetY) + w, h := g.offscreen.Size() + g.screen.DrawRectShader(w, h, g.screenShader, op) + } +} diff --git a/internal/ui/context.go b/internal/ui/context.go index 71b2c5905..6d8a579f9 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -15,12 +15,10 @@ package ui import ( - "fmt" "math" "sync" "sync/atomic" - "github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/buffered" "github.com/hajimehoshi/ebiten/v2/internal/clock" "github.com/hajimehoshi/ebiten/v2/internal/debug" @@ -30,63 +28,18 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/mipmap" ) -const screenShaderSrc = `package main - -func Fragment(position vec4, texCoord vec2, color vec4) vec4 { - // TODO: Calculate the scale in the shader after pixels become the main unit in shaders (#1431) - _, dr := imageDstRegionOnTexture() - _, sr := imageSrcRegionOnTexture() - scale := (imageDstTextureSize() * dr) / (imageSrcTextureSize() * sr) - - sourceSize := imageSrcTextureSize() - // texelSize is one pixel size in texel sizes. - texelSize := 1 / sourceSize - halfScaledTexelSize := texelSize / 2 / scale - - // Shift 1/512 [texel] to avoid the tie-breaking issue. - pos := texCoord - p0 := pos - halfScaledTexelSize + (texelSize / 512) - p1 := pos + halfScaledTexelSize + (texelSize / 512) - - // Texels must be in the source rect, so it is not necessary to check. - c0 := imageSrc0UnsafeAt(p0) - c1 := imageSrc0UnsafeAt(vec2(p1.x, p0.y)) - c2 := imageSrc0UnsafeAt(vec2(p0.x, p1.y)) - c3 := imageSrc0UnsafeAt(p1) - - // p is the p1 value in one pixel assuming that the pixel's upper-left is (0, 0) and the lower-right is (1, 1). - p := fract(p1 * sourceSize) - - // rate indicates how much the 4 colors are mixed. rate is in between [0, 1]. - // - // 0 <= p <= 1/Scale: The rate is in between [0, 1] - // 1/Scale < p: Don't care. Adjacent colors (e.g. c0 vs c1 in an X direction) should be the same. - rate := clamp(p*scale, 0, 1) - return mix(mix(c0, c1, rate.x), mix(c2, c3, rate.x), rate.y) -} -` - var ( - screenShader *Shader NearestFilterShader = &Shader{shader: mipmap.NearestFilterShader} LinearFilterShader = &Shader{shader: mipmap.LinearFilterShader} ) -func init() { - { - ir, err := graphics.CompileShader([]byte(screenShaderSrc)) - if err != nil { - panic(fmt.Sprintf("ui: compiling the screen shader failed: %v", err)) - } - screenShader = NewShader(ir) - } -} - type Game interface { NewOffscreenImage(width, height int) *Image + NewScreenImage(width, height int) *Image Layout(outsideWidth, outsideHeight int) (int, int) Update() error - Draw() + DrawOffscreen() + DrawScreen(scale, offsetX, offsteY float64) } type context struct { @@ -211,51 +164,14 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) { if theGlobalState.isScreenClearedEveryFrame() { c.offscreen.clear() } - c.game.Draw() + c.game.DrawOffscreen() if graphicsDriver.NeedsClearingScreen() { // This clear is needed for fullscreen mode or some mobile platforms (#622). c.screen.clear() } - screenScale, offsetX, offsetY := c.screenScaleAndOffsets() - - var shader *Shader - switch { - case !theGlobalState.isScreenFilterEnabled(): - shader = NearestFilterShader - case math.Floor(screenScale) == screenScale: - shader = NearestFilterShader - case screenScale > 1: - shader = screenShader - default: - // screenShader works with >=1 scale, but does not well with <1 scale. - // Use regular FilterLinear instead so far (#669). - shader = LinearFilterShader - } - - dstRegion := graphicsdriver.Region{ - X: 0, - Y: 0, - Width: float32(c.screen.width), - Height: float32(c.screen.height), - } - srcRegion := graphicsdriver.Region{ - X: 0, - Y: 0, - Width: float32(c.offscreen.width), - Height: float32(c.offscreen.height), - } - - vs := graphics.QuadVertices( - 0, 0, float32(c.offscreen.width), float32(c.offscreen.height), - float32(screenScale), 0, 0, float32(screenScale), float32(offsetX), float32(offsetY), - 1, 1, 1, 1) - is := graphics.QuadIndices() - - srcs := [graphics.ShaderImageCount]*Image{c.offscreen} - - c.screen.DrawTriangles(srcs, vs, is, graphicsdriver.CompositeModeCopy, dstRegion, srcRegion, [graphics.ShaderImageCount - 1][2]float32{}, shader, nil, false, true) + c.game.DrawScreen(c.screenScaleAndOffsets()) } func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) { @@ -287,7 +203,7 @@ func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFac } } if c.screen == nil { - c.screen = NewImage(sw, sh, atlas.ImageTypeScreen) + c.screen = c.game.NewScreenImage(sw, sh) } if c.offscreen != nil { @@ -333,7 +249,6 @@ func (c *context) screenScaleAndOffsets() (float64, float64, float64) { var theGlobalState = globalState{ isScreenClearedEveryFrame_: 1, - screenFilterEnabled_: 1, } // globalState represents a global state in this package. @@ -344,7 +259,6 @@ type globalState struct { fpsMode_ int32 isScreenClearedEveryFrame_ int32 - screenFilterEnabled_ int32 graphicsLibrary_ int32 } @@ -382,18 +296,6 @@ func (g *globalState) setScreenClearedEveryFrame(cleared bool) { atomic.StoreInt32(&g.isScreenClearedEveryFrame_, v) } -func (g *globalState) isScreenFilterEnabled() bool { - return atomic.LoadInt32(&g.screenFilterEnabled_) != 0 -} - -func (g *globalState) setScreenFilterEnabled(enabled bool) { - v := int32(0) - if enabled { - v = 1 - } - atomic.StoreInt32(&g.screenFilterEnabled_, v) -} - func (g *globalState) setGraphicsLibrary(library GraphicsLibrary) { atomic.StoreInt32(&g.graphicsLibrary_, int32(library)) } @@ -419,14 +321,6 @@ func SetScreenClearedEveryFrame(cleared bool) { theGlobalState.setScreenClearedEveryFrame(cleared) } -func IsScreenFilterEnabled() bool { - return theGlobalState.isScreenFilterEnabled() -} - -func SetScreenFilterEnabled(enabled bool) { - theGlobalState.setScreenFilterEnabled(enabled) -} - func GetGraphicsLibrary() GraphicsLibrary { return theGlobalState.graphicsLibrary() } diff --git a/run.go b/run.go index 269bed2f2..5fede6fd8 100644 --- a/run.go +++ b/run.go @@ -133,14 +133,14 @@ func IsScreenClearedEveryFrame() bool { // // SetScreenFilterEnabled is concurrent-safe, but takes effect only at the next Draw call. func SetScreenFilterEnabled(enabled bool) { - ui.SetScreenFilterEnabled(enabled) + setScreenFilterEnabled(enabled) } // IsScreenFilterEnabled returns true if Ebitengine's "screen" filter is enabled. // // IsScreenFilterEnabled is concurrent-safe. func IsScreenFilterEnabled() bool { - return ui.IsScreenFilterEnabled() + return isScreenFilterEnabled() } type imageDumperGame struct {