uicontext.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  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. package ebiten
  15. import (
  16. "fmt"
  17. "math"
  18. "sync"
  19. "sync/atomic"
  20. "github.com/hajimehoshi/ebiten/v2/internal/buffered"
  21. "github.com/hajimehoshi/ebiten/v2/internal/clock"
  22. "github.com/hajimehoshi/ebiten/v2/internal/debug"
  23. "github.com/hajimehoshi/ebiten/v2/internal/driver"
  24. "github.com/hajimehoshi/ebiten/v2/internal/hooks"
  25. )
  26. type uiContext struct {
  27. game Game
  28. offscreen *Image
  29. screen *Image
  30. updateCalled bool
  31. outsideSizeUpdated bool
  32. outsideWidth float64
  33. outsideHeight float64
  34. err atomic.Value
  35. m sync.Mutex
  36. }
  37. var theUIContext = &uiContext{}
  38. func (c *uiContext) set(game Game) {
  39. c.m.Lock()
  40. defer c.m.Unlock()
  41. c.game = game
  42. }
  43. func (c *uiContext) setError(err error) {
  44. c.err.Store(err)
  45. }
  46. func (c *uiContext) Layout(outsideWidth, outsideHeight float64) {
  47. // The given outside size can be 0 e.g. just after restoring from the fullscreen mode on Windows (#1589)
  48. // Just ignore such cases. Otherwise, creating a zero-sized framebuffer causes a panic.
  49. if outsideWidth == 0 || outsideHeight == 0 {
  50. return
  51. }
  52. c.outsideSizeUpdated = true
  53. c.outsideWidth = outsideWidth
  54. c.outsideHeight = outsideHeight
  55. }
  56. func (c *uiContext) updateOffscreen() {
  57. sw, sh := c.game.Layout(int(c.outsideWidth), int(c.outsideHeight))
  58. if sw <= 0 || sh <= 0 {
  59. panic("ebiten: Layout must return positive numbers")
  60. }
  61. if c.offscreen != nil && !c.outsideSizeUpdated {
  62. if w, h := c.offscreen.Size(); w == sw && h == sh {
  63. return
  64. }
  65. }
  66. c.outsideSizeUpdated = false
  67. if c.screen != nil {
  68. c.screen.Dispose()
  69. c.screen = nil
  70. }
  71. if c.offscreen != nil {
  72. if w, h := c.offscreen.Size(); w != sw || h != sh {
  73. c.offscreen.Dispose()
  74. c.offscreen = nil
  75. }
  76. }
  77. if c.offscreen == nil {
  78. c.offscreen = NewImage(sw, sh)
  79. c.offscreen.mipmap.SetVolatile(IsScreenClearedEveryFrame())
  80. }
  81. // TODO: This is duplicated with mobile/ebitenmobileview/funcs.go. Refactor this.
  82. d := uiDriver().DeviceScaleFactor()
  83. c.screen = newScreenFramebufferImage(int(c.outsideWidth*d), int(c.outsideHeight*d))
  84. }
  85. func (c *uiContext) setScreenClearedEveryFrame(cleared bool) {
  86. c.m.Lock()
  87. defer c.m.Unlock()
  88. if c.offscreen != nil {
  89. c.offscreen.mipmap.SetVolatile(cleared)
  90. }
  91. }
  92. func (c *uiContext) setWindowResizable(resizable bool) {
  93. c.m.Lock()
  94. defer c.m.Unlock()
  95. if w := uiDriver().Window(); w != nil {
  96. w.SetResizable(resizable)
  97. }
  98. }
  99. func (c *uiContext) screenScale(deviceScaleFactor float64) float64 {
  100. if c.offscreen == nil {
  101. return 0
  102. }
  103. sw, sh := c.offscreen.Size()
  104. scaleX := c.outsideWidth / float64(sw) * deviceScaleFactor
  105. scaleY := c.outsideHeight / float64(sh) * deviceScaleFactor
  106. return math.Min(scaleX, scaleY)
  107. }
  108. func (c *uiContext) offsets(deviceScaleFactor float64) (float64, float64) {
  109. if c.offscreen == nil {
  110. return 0, 0
  111. }
  112. sw, sh := c.offscreen.Size()
  113. s := c.screenScale(deviceScaleFactor)
  114. width := float64(sw) * s
  115. height := float64(sh) * s
  116. x := (c.outsideWidth*deviceScaleFactor - width) / 2
  117. y := (c.outsideHeight*deviceScaleFactor - height) / 2
  118. return x, y
  119. }
  120. func (c *uiContext) Update() error {
  121. // TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
  122. if err, ok := c.err.Load().(error); ok && err != nil {
  123. return err
  124. }
  125. if err := buffered.BeginFrame(); err != nil {
  126. return err
  127. }
  128. if err := c.update(clock.Update(MaxTPS())); err != nil {
  129. return err
  130. }
  131. if err := buffered.EndFrame(); err != nil {
  132. return err
  133. }
  134. return nil
  135. }
  136. func (c *uiContext) ForceUpdate() error {
  137. // ForceUpdate can be invoked even if uiContext it not initialized yet (#1591).
  138. if c.outsideWidth == 0 || c.outsideHeight == 0 {
  139. return nil
  140. }
  141. if err, ok := c.err.Load().(error); ok && err != nil {
  142. return err
  143. }
  144. if err := buffered.BeginFrame(); err != nil {
  145. return err
  146. }
  147. if err := c.update(1); err != nil {
  148. return err
  149. }
  150. if err := buffered.EndFrame(); err != nil {
  151. return err
  152. }
  153. return nil
  154. }
  155. func (c *uiContext) update(updateCount int) error {
  156. c.updateOffscreen()
  157. // Ensure that Update is called once before Draw so that Update can be used for initialization.
  158. if !c.updateCalled && updateCount == 0 {
  159. updateCount = 1
  160. c.updateCalled = true
  161. }
  162. debug.Logf("--\nUpdate count per frame: %d\n", updateCount)
  163. for i := 0; i < updateCount; i++ {
  164. if err := hooks.RunBeforeUpdateHooks(); err != nil {
  165. return err
  166. }
  167. if err := c.game.Update(); err != nil {
  168. return err
  169. }
  170. uiDriver().ResetForFrame()
  171. }
  172. // Even though updateCount == 0, the offscreen is cleared and Draw is called.
  173. // Draw should not update the game state and then the screen should not be updated without Update, but
  174. // users might want to process something at Draw with the time intervals of FPS.
  175. if IsScreenClearedEveryFrame() {
  176. c.offscreen.Clear()
  177. }
  178. c.game.Draw(c.offscreen)
  179. // This clear is needed for fullscreen mode or some mobile platforms (#622).
  180. c.screen.Clear()
  181. op := &DrawImageOptions{}
  182. s := c.screenScale(uiDriver().DeviceScaleFactor())
  183. switch vd := uiDriver().Graphics().FramebufferYDirection(); vd {
  184. case driver.Upward:
  185. op.GeoM.Scale(s, -s)
  186. _, h := c.offscreen.Size()
  187. op.GeoM.Translate(0, float64(h)*s)
  188. case driver.Downward:
  189. op.GeoM.Scale(s, s)
  190. default:
  191. panic(fmt.Sprintf("ebiten: invalid v-direction: %d", vd))
  192. }
  193. op.GeoM.Translate(c.offsets(uiDriver().DeviceScaleFactor()))
  194. op.CompositeMode = CompositeModeCopy
  195. // filterScreen works with >=1 scale, but does not well with <1 scale.
  196. // Use regular FilterLinear instead so far (#669).
  197. if s >= 1 {
  198. op.Filter = filterScreen
  199. } else {
  200. op.Filter = FilterLinear
  201. }
  202. c.screen.DrawImage(c.offscreen, op)
  203. return nil
  204. }
  205. func (c *uiContext) AdjustPosition(x, y float64, deviceScaleFactor float64) (float64, float64) {
  206. ox, oy := c.offsets(deviceScaleFactor)
  207. s := c.screenScale(deviceScaleFactor)
  208. // The scale 0 indicates that the offscreen is not initialized yet.
  209. // As any cursor values don't make sense, just return NaN.
  210. if s == 0 {
  211. return math.NaN(), math.NaN()
  212. }
  213. return (x*deviceScaleFactor - ox) / s, (y*deviceScaleFactor - oy) / s
  214. }