zorldo

Goofing around with Ebiten
git clone git://bsandro.tech/zorldo
Log | Files | Refs | README

uicontext.go (6653B)


      1 // Copyright 2014 Hajime Hoshi
      2 //
      3 // Licensed under the Apache License, Version 2.0 (the "License");
      4 // you may not use this file except in compliance with the License.
      5 // You may obtain a copy of the License at
      6 //
      7 //     http://www.apache.org/licenses/LICENSE-2.0
      8 //
      9 // Unless required by applicable law or agreed to in writing, software
     10 // distributed under the License is distributed on an "AS IS" BASIS,
     11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     12 // See the License for the specific language governing permissions and
     13 // limitations under the License.
     14 
     15 package ebiten
     16 
     17 import (
     18 	"fmt"
     19 	"math"
     20 	"sync"
     21 	"sync/atomic"
     22 
     23 	"github.com/hajimehoshi/ebiten/v2/internal/buffered"
     24 	"github.com/hajimehoshi/ebiten/v2/internal/clock"
     25 	"github.com/hajimehoshi/ebiten/v2/internal/debug"
     26 	"github.com/hajimehoshi/ebiten/v2/internal/driver"
     27 	"github.com/hajimehoshi/ebiten/v2/internal/graphics"
     28 	"github.com/hajimehoshi/ebiten/v2/internal/hooks"
     29 )
     30 
     31 type uiContext struct {
     32 	game      Game
     33 	offscreen *Image
     34 	screen    *Image
     35 
     36 	updateCalled bool
     37 
     38 	outsideSizeUpdated bool
     39 	outsideWidth       float64
     40 	outsideHeight      float64
     41 
     42 	err atomic.Value
     43 
     44 	m sync.Mutex
     45 }
     46 
     47 var theUIContext = &uiContext{}
     48 
     49 func (c *uiContext) set(game Game) {
     50 	c.m.Lock()
     51 	defer c.m.Unlock()
     52 	c.game = game
     53 }
     54 
     55 func (c *uiContext) setError(err error) {
     56 	c.err.Store(err)
     57 }
     58 
     59 func (c *uiContext) Layout(outsideWidth, outsideHeight float64) {
     60 	// The given outside size can be 0 e.g. just after restoring from the fullscreen mode on Windows (#1589)
     61 	// Just ignore such cases. Otherwise, creating a zero-sized framebuffer causes a panic.
     62 	if outsideWidth == 0 || outsideHeight == 0 {
     63 		return
     64 	}
     65 	c.outsideSizeUpdated = true
     66 	c.outsideWidth = outsideWidth
     67 	c.outsideHeight = outsideHeight
     68 }
     69 
     70 func (c *uiContext) updateOffscreen() {
     71 	sw, sh := c.game.Layout(int(c.outsideWidth), int(c.outsideHeight))
     72 	if sw <= 0 || sh <= 0 {
     73 		panic("ebiten: Layout must return positive numbers")
     74 	}
     75 
     76 	if c.offscreen != nil && !c.outsideSizeUpdated {
     77 		if w, h := c.offscreen.Size(); w == sw && h == sh {
     78 			return
     79 		}
     80 	}
     81 	c.outsideSizeUpdated = false
     82 
     83 	if c.screen != nil {
     84 		c.screen.Dispose()
     85 		c.screen = nil
     86 	}
     87 
     88 	if c.offscreen != nil {
     89 		if w, h := c.offscreen.Size(); w != sw || h != sh {
     90 			c.offscreen.Dispose()
     91 			c.offscreen = nil
     92 		}
     93 	}
     94 	if c.offscreen == nil {
     95 		c.offscreen = NewImage(sw, sh)
     96 		c.offscreen.mipmap.SetVolatile(IsScreenClearedEveryFrame())
     97 	}
     98 
     99 	// TODO: This is duplicated with mobile/ebitenmobileview/funcs.go. Refactor this.
    100 	d := uiDriver().DeviceScaleFactor()
    101 	c.screen = newScreenFramebufferImage(int(c.outsideWidth*d), int(c.outsideHeight*d))
    102 }
    103 
    104 func (c *uiContext) setScreenClearedEveryFrame(cleared bool) {
    105 	c.m.Lock()
    106 	defer c.m.Unlock()
    107 
    108 	if c.offscreen != nil {
    109 		c.offscreen.mipmap.SetVolatile(cleared)
    110 	}
    111 }
    112 
    113 func (c *uiContext) screenScale(deviceScaleFactor float64) float64 {
    114 	if c.offscreen == nil {
    115 		return 0
    116 	}
    117 	sw, sh := c.offscreen.Size()
    118 	scaleX := c.outsideWidth / float64(sw) * deviceScaleFactor
    119 	scaleY := c.outsideHeight / float64(sh) * deviceScaleFactor
    120 	return math.Min(scaleX, scaleY)
    121 }
    122 
    123 func (c *uiContext) offsets(deviceScaleFactor float64) (float64, float64) {
    124 	if c.offscreen == nil {
    125 		return 0, 0
    126 	}
    127 	sw, sh := c.offscreen.Size()
    128 	s := c.screenScale(deviceScaleFactor)
    129 	width := float64(sw) * s
    130 	height := float64(sh) * s
    131 	x := (c.outsideWidth*deviceScaleFactor - width) / 2
    132 	y := (c.outsideHeight*deviceScaleFactor - height) / 2
    133 	return x, y
    134 }
    135 
    136 func (c *uiContext) UpdateFrame() error {
    137 	// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
    138 	return c.updateFrame(clock.Update(MaxTPS()))
    139 }
    140 
    141 func (c *uiContext) ForceUpdateFrame() error {
    142 	// ForceUpdate can be invoked even if uiContext it not initialized yet (#1591).
    143 	if c.outsideWidth == 0 || c.outsideHeight == 0 {
    144 		return nil
    145 	}
    146 	return c.updateFrame(1)
    147 }
    148 
    149 func (c *uiContext) updateFrame(updateCount int) error {
    150 	if err, ok := c.err.Load().(error); ok && err != nil {
    151 		return err
    152 	}
    153 
    154 	debug.Logf("----\n")
    155 
    156 	if err := buffered.BeginFrame(); err != nil {
    157 		return err
    158 	}
    159 	if err := c.updateFrameImpl(updateCount); err != nil {
    160 		return err
    161 	}
    162 
    163 	// All the vertices data are consumed at the end of the frame, and the data backend can be
    164 	// available after that. Until then, lock the vertices backend.
    165 	return graphics.LockAndResetVertices(func() error {
    166 		if err := buffered.EndFrame(); err != nil {
    167 			return err
    168 		}
    169 		return nil
    170 	})
    171 }
    172 
    173 func (c *uiContext) updateFrameImpl(updateCount int) error {
    174 	c.updateOffscreen()
    175 
    176 	// Ensure that Update is called once before Draw so that Update can be used for initialization.
    177 	if !c.updateCalled && updateCount == 0 {
    178 		updateCount = 1
    179 		c.updateCalled = true
    180 	}
    181 	debug.Logf("Update count per frame: %d\n", updateCount)
    182 
    183 	for i := 0; i < updateCount; i++ {
    184 		if err := hooks.RunBeforeUpdateHooks(); err != nil {
    185 			return err
    186 		}
    187 		if err := c.game.Update(); err != nil {
    188 			return err
    189 		}
    190 		uiDriver().ResetForFrame()
    191 	}
    192 
    193 	// Even though updateCount == 0, the offscreen is cleared and Draw is called.
    194 	// Draw should not update the game state and then the screen should not be updated without Update, but
    195 	// users might want to process something at Draw with the time intervals of FPS.
    196 	if IsScreenClearedEveryFrame() {
    197 		c.offscreen.Clear()
    198 	}
    199 	c.game.Draw(c.offscreen)
    200 
    201 	if uiDriver().Graphics().NeedsClearingScreen() {
    202 		// This clear is needed for fullscreen mode or some mobile platforms (#622).
    203 		c.screen.Clear()
    204 	}
    205 
    206 	op := &DrawImageOptions{}
    207 
    208 	s := c.screenScale(uiDriver().DeviceScaleFactor())
    209 	switch vd := uiDriver().Graphics().FramebufferYDirection(); vd {
    210 	case driver.Upward:
    211 		op.GeoM.Scale(s, -s)
    212 		_, h := c.offscreen.Size()
    213 		op.GeoM.Translate(0, float64(h)*s)
    214 	case driver.Downward:
    215 		op.GeoM.Scale(s, s)
    216 	default:
    217 		panic(fmt.Sprintf("ebiten: invalid v-direction: %d", vd))
    218 	}
    219 
    220 	op.GeoM.Translate(c.offsets(uiDriver().DeviceScaleFactor()))
    221 	op.CompositeMode = CompositeModeCopy
    222 
    223 	// filterScreen works with >=1 scale, but does not well with <1 scale.
    224 	// Use regular FilterLinear instead so far (#669).
    225 	if s >= 1 {
    226 		op.Filter = filterScreen
    227 	} else {
    228 		op.Filter = FilterLinear
    229 	}
    230 	c.screen.DrawImage(c.offscreen, op)
    231 	return nil
    232 }
    233 
    234 func (c *uiContext) AdjustPosition(x, y float64, deviceScaleFactor float64) (float64, float64) {
    235 	ox, oy := c.offsets(deviceScaleFactor)
    236 	s := c.screenScale(deviceScaleFactor)
    237 	// The scale 0 indicates that the offscreen is not initialized yet.
    238 	// As any cursor values don't make sense, just return NaN.
    239 	if s == 0 {
    240 		return math.NaN(), math.NaN()
    241 	}
    242 	return (x*deviceScaleFactor - ox) / s, (y*deviceScaleFactor - oy) / s
    243 }