internal/ui: refactoring: move the logic in gameForUI to context

This commit is contained in:
Hajime Hoshi 2022-04-01 18:16:03 +09:00
parent 9c448d207a
commit 5c7917897c
5 changed files with 187 additions and 153 deletions

View File

@ -15,16 +15,12 @@
package ebiten
import (
"fmt"
"math"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/ui"
)
type gameForUI struct {
game Game
offscreen *Image
screen *Image
}
func newGameForUI(game Game) *gameForUI {
@ -33,90 +29,23 @@ func newGameForUI(game Game) *gameForUI {
}
}
func (c *gameForUI) Layout(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
ow, oh := c.game.Layout(int(outsideWidth), int(outsideHeight))
if ow <= 0 || oh <= 0 {
panic("ebiten: Layout must return positive numbers")
}
sw, sh := int(outsideWidth*deviceScaleFactor), int(outsideHeight*deviceScaleFactor)
if c.screen != nil {
if w, h := c.screen.Size(); w != sw || h != sh {
c.screen.Dispose()
c.screen = nil
}
}
if c.screen == nil {
c.screen = newScreenFramebufferImage(sw, sh)
}
func (c *gameForUI) NewOffscreenImage(width, height int) *ui.Image {
if c.offscreen != nil {
if w, h := c.offscreen.Size(); w != ow || h != oh {
c.offscreen.Dispose()
c.offscreen = nil
}
c.offscreen.Dispose()
c.offscreen = nil
}
if c.offscreen == nil {
c.offscreen = NewImage(ow, oh)
c.offscreen = NewImage(width, height)
return c.offscreen.image
}
// Keep the offscreen an independent image from an atlas (#1938).
// The shader program for the screen is special and doesn't work well with an image on an atlas.
// An image on an atlas is surrounded by a transparent edge,
// and the shader program unexpectedly picks the pixel on the edges.
c.offscreen.image.SetIndependent(true)
}
return ow, oh
func (c *gameForUI) Layout(outsideWidth, outsideHeight int) (int, int) {
return c.game.Layout(outsideWidth, outsideHeight)
}
func (c *gameForUI) Update() error {
return c.game.Update()
}
func (c *gameForUI) Draw(screenScale float64, offsetX, offsetY float64, needsClearingScreen bool, framebufferYDirection graphicsdriver.YDirection, clearScreenEveryFrame, filterEnabled bool) {
c.offscreen.image.SetVolatile(clearScreenEveryFrame)
// Even though updateCount == 0, the offscreen is cleared and Draw is called.
// Draw should not update the game state and then the screen should not be updated without Update, but
// users might want to process something at Draw with the time intervals of FPS.
if clearScreenEveryFrame {
c.offscreen.Clear()
}
func (c *gameForUI) Draw() {
c.game.Draw(c.offscreen)
if needsClearingScreen {
// This clear is needed for fullscreen mode or some mobile platforms (#622).
c.screen.Clear()
}
op := &DrawImageOptions{}
s := screenScale
switch framebufferYDirection {
case graphicsdriver.Upward:
op.GeoM.Scale(s, -s)
_, h := c.offscreen.Size()
op.GeoM.Translate(0, float64(h)*s)
case graphicsdriver.Downward:
op.GeoM.Scale(s, s)
default:
panic(fmt.Sprintf("ebiten: invalid v-direction: %d", framebufferYDirection))
}
op.GeoM.Translate(offsetX, offsetY)
op.CompositeMode = CompositeModeCopy
switch {
case !filterEnabled:
op.Filter = FilterNearest
case math.Floor(s) == s:
op.Filter = FilterNearest
case s > 1:
op.Filter = filterScreen
default:
// filterScreen works with >=1 scale, but does not well with <1 scale.
// Use regular FilterLinear instead so far (#669).
op.Filter = FilterLinear
}
c.screen.DrawImage(c.offscreen, op)
}

View File

@ -27,11 +27,6 @@ const (
// FilterLinear represents linear filter
FilterLinear Filter = Filter(graphicsdriver.FilterLinear)
// filterScreen represents a special filter for screen. Inner usage only.
//
// Some parameters like a color matrix or color vertex values can be ignored when filterScreen is used.
filterScreen Filter = Filter(graphicsdriver.FilterScreen)
)
// CompositeMode represents Porter-Duff composition mode.

View File

@ -66,39 +66,22 @@ func (i *Image) Clear() {
i.Fill(color.Transparent)
}
var (
emptyImage = NewImage(3, 3)
emptySubImage = emptyImage.SubImage(image.Rect(1, 1, 2, 2)).(*Image)
)
func init() {
w, h := emptyImage.Size()
pix := make([]byte, 4*w*h)
for i := range pix {
pix[i] = 0xff
}
// As emptyImage is used at Fill, use ReplacePixels instead.
emptyImage.ReplacePixels(pix)
}
// Fill fills the image with a solid color.
//
// When the image is disposed, Fill does nothing.
func (i *Image) Fill(clr color.Color) {
// Use the original size to cover the entire region (#1691).
// DrawImage automatically clips the rendering region.
orig := i
if i.isSubImage() {
orig = i.original
i.copyCheck()
var crf, cgf, cbf, caf float32
cr, cg, cb, ca := clr.RGBA()
if ca != 0 {
crf = float32(cr) / float32(ca)
cgf = float32(cg) / float32(ca)
cbf = float32(cb) / float32(ca)
caf = float32(ca) / 0xffff
}
w, h := orig.Size()
op := &DrawImageOptions{}
op.GeoM.Scale(float64(w), float64(h))
op.ColorM.ScaleWithColor(clr)
op.CompositeMode = CompositeModeCopy
i.DrawImage(emptySubImage, op)
b := i.Bounds()
i.image.Fill(crf, cgf, cbf, caf, b.Min.X, b.Min.Y, b.Dx(), b.Dy())
}
func canSkipMipmap(geom GeoM, filter graphicsdriver.Filter) bool {
@ -845,12 +828,3 @@ func NewImageFromImage(source image.Image) *Image {
i.ReplacePixels(imageToBytes(source))
return i
}
func newScreenFramebufferImage(width, height int) *Image {
i := &Image{
image: ui.NewScreenFramebufferImage(width, height),
bounds: image.Rect(0, 0, width, height),
}
i.addr = i
return i
}

View File

@ -15,10 +15,12 @@
package ui
import (
"fmt"
"math"
"sync"
"sync/atomic"
"github.com/hajimehoshi/ebiten/v2/internal/affine"
"github.com/hajimehoshi/ebiten/v2/internal/buffered"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/debug"
@ -30,9 +32,10 @@ import (
const DefaultTPS = 60
type Game interface {
Layout(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int)
NewOffscreenImage(width, height int) *Image
Layout(outsideWidth, outsideHeight int) (int, int)
Update() error
Draw(screenScale float64, offsetX, offsetY float64, needsClearingScreen bool, framebufferYDirection graphicsdriver.YDirection, screenClearedEveryFrame, filterEnabled bool)
Draw()
}
type context struct {
@ -40,11 +43,12 @@ type context struct {
updateCalled bool
offscreen *Image
screen *Image
// The following members must be protected by the mutex m.
outsideWidth float64
outsideHeight float64
screenWidth int
screenHeight int
m sync.Mutex
}
@ -116,12 +120,11 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
if err := theGlobalState.error(); err != nil {
return err
}
Get().resetForTick()
theUI.resetForTick()
}
// Draw the game.
screenScale, offsetX, offsetY := c.screenScaleAndOffsets(deviceScaleFactor)
c.game.Draw(screenScale, offsetX, offsetY, graphicsDriver.NeedsClearingScreen(), graphicsDriver.FramebufferYDirection(), theGlobalState.isScreenClearedEveryFrame(), theGlobalState.isScreenFilterEnabled())
c.drawGame(graphicsDriver)
// All the vertices data are consumed at the end of the frame, and the data backend can be
// available after that. Until then, lock the vertices backend.
@ -133,20 +136,119 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
})
}
func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics) {
c.offscreen.mipmap.SetVolatile(theGlobalState.isScreenClearedEveryFrame())
// Even though updateCount == 0, the offscreen is cleared and Draw is called.
// Draw should not update the game state and then the screen should not be updated without Update, but
// users might want to process something at Draw with the time intervals of FPS.
if theGlobalState.isScreenClearedEveryFrame() {
c.offscreen.clear()
}
c.game.Draw()
if graphicsDriver.NeedsClearingScreen() {
// This clear is needed for fullscreen mode or some mobile platforms (#622).
c.screen.clear()
}
ga := 1.0
gd := 1.0
gtx := 0.0
gty := 0.0
screenScale, offsetX, offsetY := c.screenScaleAndOffsets()
s := screenScale
switch y := graphicsDriver.FramebufferYDirection(); y {
case graphicsdriver.Upward:
ga *= s
gd *= -s
gty += float64(c.offscreen.height) * s
case graphicsdriver.Downward:
ga *= s
gd *= s
default:
panic(fmt.Sprintf("ui: invalid y-direction: %d", y))
}
gtx += offsetX
gty += offsetY
var filter graphicsdriver.Filter
switch {
case !theGlobalState.isScreenFilterEnabled():
filter = graphicsdriver.FilterNearest
case math.Floor(s) == s:
filter = graphicsdriver.FilterNearest
case s > 1:
filter = graphicsdriver.FilterScreen
default:
// FilterScreen works with >=1 scale, but does not well with <1 scale.
// Use regular FilterLinear instead so far (#669).
filter = graphicsdriver.FilterLinear
}
dstRegion := graphicsdriver.Region{
X: 0,
Y: 0,
Width: float32(c.screen.width),
Height: float32(c.screen.height),
}
vs := graphics.QuadVertices(
0, 0, float32(c.offscreen.width), float32(c.offscreen.height),
float32(ga), 0, 0, float32(gd), float32(gtx), float32(gty),
1, 1, 1, 1)
is := graphics.QuadIndices()
srcs := [graphics.ShaderImageNum]*Image{c.offscreen}
c.screen.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, filter, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, true)
}
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
c.m.Lock()
defer c.m.Unlock()
c.outsideWidth = outsideWidth
c.outsideHeight = outsideHeight
w, h := c.game.Layout(outsideWidth, outsideHeight, deviceScaleFactor)
c.screenWidth = w
c.screenHeight = h
return w, h
ow, oh := c.game.Layout(int(outsideWidth), int(outsideHeight))
if ow <= 0 || oh <= 0 {
panic("ui: Layout must return positive numbers")
}
sw, sh := int(outsideWidth*deviceScaleFactor), int(outsideHeight*deviceScaleFactor)
if c.screen != nil {
if c.screen.width != sw || c.screen.height != sh {
c.screen.MarkDisposed()
c.screen = nil
}
}
if c.screen == nil {
c.screen = newScreenFramebufferImage(sw, sh)
}
if c.offscreen != nil {
if c.offscreen.width != ow || c.offscreen.height != oh {
c.offscreen.MarkDisposed()
c.offscreen = nil
}
}
if c.offscreen == nil {
c.offscreen = c.game.NewOffscreenImage(ow, oh)
// Keep the offscreen an independent image from an atlas (#1938).
// The shader program for the screen is special and doesn't work well with an image on an atlas.
// An image on an atlas is surrounded by a transparent edge,
// and the shader program unexpectedly picks the pixel on the edges.
c.offscreen.mipmap.SetIndependent(true)
}
return ow, oh
}
func (c *context) adjustPosition(x, y float64, deviceScaleFactor float64) (float64, float64) {
s, ox, oy := c.screenScaleAndOffsets(deviceScaleFactor)
s, ox, oy := c.screenScaleAndOffsets()
// The scale 0 indicates that the screen is not initialized yet.
// As any cursor values don't make sense, just return NaN.
if s == 0 {
@ -155,21 +257,21 @@ func (c *context) adjustPosition(x, y float64, deviceScaleFactor float64) (float
return (x*deviceScaleFactor - ox) / s, (y*deviceScaleFactor - oy) / s
}
func (c *context) screenScaleAndOffsets(deviceScaleFactor float64) (float64, float64, float64) {
func (c *context) screenScaleAndOffsets() (float64, float64, float64) {
c.m.Lock()
defer c.m.Unlock()
if c.screenWidth == 0 || c.screenHeight == 0 {
if c.screen == nil {
return 0, 0, 0
}
scaleX := c.outsideWidth / float64(c.screenWidth) * deviceScaleFactor
scaleY := c.outsideHeight / float64(c.screenHeight) * deviceScaleFactor
scaleX := float64(c.screen.width) / float64(c.offscreen.width)
scaleY := float64(c.screen.height) / float64(c.offscreen.height)
scale := math.Min(scaleX, scaleY)
width := float64(c.screenWidth) * scale
height := float64(c.screenHeight) * scale
x := (c.outsideWidth*deviceScaleFactor - width) / 2
y := (c.outsideHeight*deviceScaleFactor - height) / 2
width := float64(c.offscreen.width) * scale
height := float64(c.offscreen.height) * scale
x := (float64(c.screen.width) - width) / 2
y := (float64(c.screen.height) - height) / 2
return scale, x, y
}
@ -257,7 +359,7 @@ func FPSMode() FPSModeType {
func SetFPSMode(fpsMode FPSModeType) {
theGlobalState.setFPSMode(fpsMode)
Get().SetFPSMode(fpsMode)
theUI.SetFPSMode(fpsMode)
}
func MaxTPS() int {

View File

@ -31,17 +31,23 @@ func SetPanicOnErrorOnReadingPixelsForTesting(value bool) {
type Image struct {
mipmap *mipmap.Mipmap
width int
height int
}
func NewImage(width, height int) *Image {
return &Image{
mipmap: mipmap.New(width, height),
width: width,
height: height,
}
}
func NewScreenFramebufferImage(width, height int) *Image {
func newScreenFramebufferImage(width, height int) *Image {
return &Image{
mipmap: mipmap.NewScreenFramebufferMipmap(width, height),
width: width,
height: height,
}
}
@ -92,14 +98,42 @@ func (i *Image) DumpScreenshot(name string, blackbg bool) error {
return theUI.dumpScreenshot(i.mipmap, name, blackbg)
}
func (i *Image) SetIndependent(independent bool) {
i.mipmap.SetIndependent(independent)
}
func (i *Image) SetVolatile(volatile bool) {
i.mipmap.SetVolatile(volatile)
}
func DumpImages(dir string) error {
return theUI.dumpImages(dir)
}
var (
emptyImage = NewImage(3, 3)
)
func init() {
pix := make([]byte, 4*emptyImage.width*emptyImage.height)
for i := range pix {
pix[i] = 0xff
}
// As emptyImage is used at Fill, use ReplacePixels instead.
emptyImage.ReplacePixels(pix, 0, 0, emptyImage.width, emptyImage.height)
}
func (i *Image) clear() {
i.Fill(0, 0, 0, 0, 0, 0, i.width, i.height)
}
func (i *Image) Fill(r, g, b, a float32, x, y, width, height int) {
dstRegion := graphicsdriver.Region{
X: float32(x),
Y: float32(y),
Width: float32(width),
Height: float32(height),
}
vs := graphics.QuadVertices(
1, 1, float32(emptyImage.width-1), float32(emptyImage.height-1),
float32(i.width), 0, 0, float32(i.height), 0, 0,
r, g, b, a)
is := graphics.QuadIndices()
srcs := [graphics.ShaderImageNum]*Image{emptyImage}
i.DrawTriangles(srcs, vs, is, affine.ColorMIdentity{}, graphicsdriver.CompositeModeCopy, graphicsdriver.FilterNearest, graphicsdriver.AddressUnsafe, dstRegion, graphicsdriver.Region{}, [graphics.ShaderImageNum - 1][2]float32{}, nil, nil, false, true)
}