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 }