uicontext.go (6171B)
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/driver" 26 "github.com/hajimehoshi/ebiten/v2/internal/hooks" 27 ) 28 29 type uiContext struct { 30 game Game 31 offscreen *Image 32 screen *Image 33 34 updateCalled bool 35 36 // scaleForWindow is the scale of a window. This doesn't represent the scale on fullscreen. This value works 37 // only on desktops. 38 // 39 // scaleForWindow is for backward compatibility and is used to calculate the window size when SetScreenSize 40 // is called. 41 scaleForWindow float64 42 43 outsideSizeUpdated bool 44 outsideWidth float64 45 outsideHeight float64 46 47 err atomic.Value 48 49 m sync.Mutex 50 } 51 52 var theUIContext = &uiContext{} 53 54 func (c *uiContext) set(game Game, scaleForWindow float64) { 55 c.m.Lock() 56 defer c.m.Unlock() 57 c.game = game 58 } 59 60 func (c *uiContext) setError(err error) { 61 c.err.Store(err) 62 } 63 64 func (c *uiContext) Layout(outsideWidth, outsideHeight float64) { 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 // Do not have to update scaleForWindow since this is used only for backward compatibility. 104 // Then, if a window is resizable, scaleForWindow (= ebiten.ScreenScale) might not match with the actual 105 // scale. This is fine since ebiten.ScreenScale will be deprecated. 106 } 107 108 func (c *uiContext) setScreenClearedEveryFrame(cleared bool) { 109 c.m.Lock() 110 defer c.m.Unlock() 111 112 if c.offscreen != nil { 113 c.offscreen.mipmap.SetVolatile(cleared) 114 } 115 } 116 117 func (c *uiContext) setWindowResizable(resizable bool) { 118 c.m.Lock() 119 defer c.m.Unlock() 120 121 if w := uiDriver().Window(); w != nil { 122 w.SetResizable(resizable) 123 } 124 } 125 126 func (c *uiContext) screenScale() float64 { 127 if c.offscreen == nil { 128 return 0 129 } 130 sw, sh := c.offscreen.Size() 131 d := uiDriver().DeviceScaleFactor() 132 scaleX := c.outsideWidth / float64(sw) * d 133 scaleY := c.outsideHeight / float64(sh) * d 134 return math.Min(scaleX, scaleY) 135 } 136 137 func (c *uiContext) offsets() (float64, float64) { 138 if c.offscreen == nil { 139 return 0, 0 140 } 141 sw, sh := c.offscreen.Size() 142 d := uiDriver().DeviceScaleFactor() 143 s := c.screenScale() 144 width := float64(sw) * s 145 height := float64(sh) * s 146 return (c.outsideWidth*d - width) / 2, (c.outsideHeight*d - height) / 2 147 } 148 149 func (c *uiContext) Update() error { 150 // TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped. 151 152 if err, ok := c.err.Load().(error); ok && err != nil { 153 return err 154 } 155 if err := buffered.BeginFrame(); err != nil { 156 return err 157 } 158 if err := c.update(); err != nil { 159 return err 160 } 161 if err := buffered.EndFrame(); err != nil { 162 return err 163 } 164 return nil 165 } 166 167 func (c *uiContext) Draw() error { 168 if err, ok := c.err.Load().(error); ok && err != nil { 169 return err 170 } 171 if err := buffered.BeginFrame(); err != nil { 172 return err 173 } 174 c.draw() 175 if err := buffered.EndFrame(); err != nil { 176 return err 177 } 178 return nil 179 } 180 181 func (c *uiContext) update() error { 182 // TODO: Move the clock usage to the UI driver side. 183 updateCount := clock.Update(MaxTPS()) 184 185 // Ensure that Update is called once before Draw so that Update can be used for initialization. 186 if !c.updateCalled && updateCount == 0 { 187 updateCount = 1 188 c.updateCalled = true 189 } 190 191 for i := 0; i < updateCount; i++ { 192 c.updateOffscreen() 193 194 if err := hooks.RunBeforeUpdateHooks(); err != nil { 195 return err 196 } 197 if err := c.game.Update(); err != nil { 198 return err 199 } 200 uiDriver().ResetForFrame() 201 } 202 return nil 203 } 204 205 func (c *uiContext) draw() { 206 // c.screen might be nil when updateCount is 0 in the initial state (#1039). 207 if c.screen == nil { 208 return 209 } 210 211 if IsScreenClearedEveryFrame() { 212 c.offscreen.Clear() 213 } 214 c.game.Draw(c.offscreen) 215 216 // This clear is needed for fullscreen mode or some mobile platforms (#622). 217 c.screen.Clear() 218 219 op := &DrawImageOptions{} 220 221 s := c.screenScale() 222 switch vd := uiDriver().Graphics().FramebufferYDirection(); vd { 223 case driver.Upward: 224 op.GeoM.Scale(s, -s) 225 _, h := c.offscreen.Size() 226 op.GeoM.Translate(0, float64(h)*s) 227 case driver.Downward: 228 op.GeoM.Scale(s, s) 229 default: 230 panic(fmt.Sprintf("ebiten: invalid v-direction: %d", vd)) 231 } 232 233 op.GeoM.Translate(c.offsets()) 234 op.CompositeMode = CompositeModeCopy 235 236 // filterScreen works with >=1 scale, but does not well with <1 scale. 237 // Use regular FilterLinear instead so far (#669). 238 if s >= 1 { 239 op.Filter = filterScreen 240 } else { 241 op.Filter = FilterLinear 242 } 243 c.screen.DrawImage(c.offscreen, op) 244 } 245 246 func (c *uiContext) AdjustPosition(x, y float64) (float64, float64) { 247 d := uiDriver().DeviceScaleFactor() 248 ox, oy := c.offsets() 249 s := c.screenScale() 250 return (x*d - ox) / s, (y*d - oy) / s 251 }