ui.go (11090B)
1 // Copyright 2016 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 // +build android ios 16 17 package mobile 18 19 import ( 20 "fmt" 21 "runtime/debug" 22 "sync" 23 "sync/atomic" 24 "time" 25 "unicode" 26 27 "golang.org/x/mobile/app" 28 "golang.org/x/mobile/event/key" 29 "golang.org/x/mobile/event/lifecycle" 30 "golang.org/x/mobile/event/paint" 31 "golang.org/x/mobile/event/size" 32 "golang.org/x/mobile/event/touch" 33 "golang.org/x/mobile/gl" 34 35 "github.com/hajimehoshi/ebiten/v2/internal/devicescale" 36 "github.com/hajimehoshi/ebiten/v2/internal/driver" 37 "github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver/opengl" 38 "github.com/hajimehoshi/ebiten/v2/internal/hooks" 39 "github.com/hajimehoshi/ebiten/v2/internal/restorable" 40 "github.com/hajimehoshi/ebiten/v2/internal/thread" 41 ) 42 43 var ( 44 glContextCh = make(chan gl.Context, 1) 45 46 // renderCh receives when updating starts. 47 renderCh = make(chan struct{}) 48 49 // renderEndCh receives when updating finishes. 50 renderEndCh = make(chan struct{}) 51 52 theUI = &UserInterface{ 53 foreground: 1, 54 errCh: make(chan error), 55 56 // Give a default outside size so that the game can start without initializing them. 57 outsideWidth: 640, 58 outsideHeight: 480, 59 sizeChanged: true, 60 } 61 ) 62 63 func init() { 64 theUI.input.ui = theUI 65 } 66 67 func Get() *UserInterface { 68 return theUI 69 } 70 71 // Update is called from mobile/ebitenmobileview. 72 // 73 // Update must be called on the rendering thread. 74 func (u *UserInterface) Update() error { 75 select { 76 case err := <-u.errCh: 77 return err 78 default: 79 } 80 81 if !u.IsFocused() { 82 return nil 83 } 84 85 renderCh <- struct{}{} 86 if u.Graphics().IsGL() { 87 if u.glWorker == nil { 88 panic("mobile: glWorker must be initialized but not") 89 } 90 91 workAvailable := u.glWorker.WorkAvailable() 92 for { 93 // When the two channels don't receive for a while, call DoWork forcibly to avoid freeze 94 // (#1322, #1332). 95 // 96 // In theory, this timeout should not be necessary. However, it looks like this 'select' 97 // statement sometimes blocks forever on some Android devices like Pixel 4(a). Apparently 98 // workAvailable sometimes not receives even though there are queued OpenGL functions. 99 // Call DoWork for such case as a symptomatic treatment. 100 // 101 // Calling DoWork without waiting for workAvailable is safe. If there are no tasks, DoWork 102 // should return immediately. 103 // 104 // TODO: Fix the root cause. Note that this is pretty hard since e.g., logging affects the 105 // scheduling and freezing might not happen with logging. 106 t := time.NewTimer(100 * time.Millisecond) 107 108 select { 109 case <-workAvailable: 110 if !t.Stop() { 111 <-t.C 112 } 113 u.glWorker.DoWork() 114 case <-renderEndCh: 115 if !t.Stop() { 116 <-t.C 117 } 118 return nil 119 case <-t.C: 120 u.glWorker.DoWork() 121 } 122 } 123 } 124 125 go func() { 126 <-renderEndCh 127 u.t.Call(func() error { 128 return thread.BreakLoop 129 }) 130 }() 131 u.t.Loop() 132 return nil 133 } 134 135 type UserInterface struct { 136 outsideWidth float64 137 outsideHeight float64 138 139 sizeChanged bool 140 foreground int32 141 errCh chan error 142 143 // Used for gomobile-build 144 gbuildWidthPx int 145 gbuildHeightPx int 146 setGBuildSizeCh chan struct{} 147 once sync.Once 148 149 context driver.UIContext 150 151 input Input 152 153 t *thread.Thread 154 glWorker gl.Worker 155 156 m sync.RWMutex 157 } 158 159 func deviceScale() float64 { 160 return devicescale.GetAt(0, 0) 161 } 162 163 // appMain is the main routine for gomobile-build mode. 164 func (u *UserInterface) appMain(a app.App) { 165 var glctx gl.Context 166 var sizeInited bool 167 168 touches := map[touch.Sequence]*Touch{} 169 keys := map[driver.Key]struct{}{} 170 171 for e := range a.Events() { 172 var updateInput bool 173 var runes []rune 174 175 switch e := a.Filter(e).(type) { 176 case lifecycle.Event: 177 switch e.Crosses(lifecycle.StageVisible) { 178 case lifecycle.CrossOn: 179 u.SetForeground(true) 180 restorable.OnContextLost() 181 glctx, _ = e.DrawContext.(gl.Context) 182 // Assume that glctx is always a same instance. 183 // Then, only once initializing should be enough. 184 if glContextCh != nil { 185 glContextCh <- glctx 186 glContextCh = nil 187 } 188 a.Send(paint.Event{}) 189 case lifecycle.CrossOff: 190 u.SetForeground(false) 191 glctx = nil 192 } 193 case size.Event: 194 u.setGBuildSize(e.WidthPx, e.HeightPx) 195 sizeInited = true 196 case paint.Event: 197 if !sizeInited { 198 a.Send(paint.Event{}) 199 continue 200 } 201 if glctx == nil || e.External { 202 continue 203 } 204 renderCh <- struct{}{} 205 <-renderEndCh 206 a.Publish() 207 a.Send(paint.Event{}) 208 case touch.Event: 209 if !sizeInited { 210 continue 211 } 212 switch e.Type { 213 case touch.TypeBegin, touch.TypeMove: 214 s := deviceScale() 215 x, y := float64(e.X)/s, float64(e.Y)/s 216 // TODO: Is it ok to cast from int64 to int here? 217 touches[e.Sequence] = &Touch{ 218 ID: driver.TouchID(e.Sequence), 219 X: int(x), 220 Y: int(y), 221 } 222 case touch.TypeEnd: 223 delete(touches, e.Sequence) 224 } 225 updateInput = true 226 case key.Event: 227 k, ok := gbuildKeyToDriverKey[e.Code] 228 if ok { 229 switch e.Direction { 230 case key.DirPress, key.DirNone: 231 keys[k] = struct{}{} 232 case key.DirRelease: 233 delete(keys, k) 234 } 235 } 236 237 switch e.Direction { 238 case key.DirPress, key.DirNone: 239 if e.Rune != -1 && unicode.IsPrint(e.Rune) { 240 runes = []rune{e.Rune} 241 } 242 } 243 updateInput = true 244 } 245 246 if updateInput { 247 ts := []*Touch{} 248 for _, t := range touches { 249 ts = append(ts, t) 250 } 251 u.input.update(keys, runes, ts, nil) 252 } 253 } 254 } 255 256 func (u *UserInterface) SetForeground(foreground bool) { 257 var v int32 258 if foreground { 259 v = 1 260 } 261 atomic.StoreInt32(&u.foreground, v) 262 263 if foreground { 264 hooks.ResumeAudio() 265 } else { 266 hooks.SuspendAudio() 267 } 268 } 269 270 func (u *UserInterface) Run(context driver.UIContext) error { 271 u.setGBuildSizeCh = make(chan struct{}) 272 go func() { 273 if err := u.run(context, true); err != nil { 274 // As mobile apps never ends, Loop can't return. Just panic here. 275 panic(err) 276 } 277 }() 278 app.Main(u.appMain) 279 return nil 280 } 281 282 func (u *UserInterface) RunWithoutMainLoop(context driver.UIContext) { 283 go func() { 284 // title is ignored? 285 if err := u.run(context, false); err != nil { 286 u.errCh <- err 287 } 288 }() 289 } 290 291 func (u *UserInterface) run(context driver.UIContext, mainloop bool) (err error) { 292 // Convert the panic to a regular error so that Java/Objective-C layer can treat this easily e.g., for 293 // Crashlytics. A panic is treated as SIGABRT, and there is no way to handle this on Java/Objective-C layer 294 // unfortunately. 295 // TODO: Panic on other goroutines cannot be handled here. 296 defer func() { 297 if r := recover(); r != nil { 298 err = fmt.Errorf("%v\n%q", r, string(debug.Stack())) 299 } 300 }() 301 302 u.m.Lock() 303 u.sizeChanged = true 304 u.m.Unlock() 305 306 u.context = context 307 308 if u.Graphics().IsGL() { 309 var ctx gl.Context 310 if mainloop { 311 ctx = <-glContextCh 312 } else { 313 ctx, u.glWorker = gl.NewContext() 314 } 315 u.Graphics().(*opengl.Graphics).SetMobileGLContext(ctx) 316 } else { 317 u.t = thread.New() 318 u.Graphics().SetThread(u.t) 319 } 320 321 // If gomobile-build is used, wait for the outside size fixed. 322 if u.setGBuildSizeCh != nil { 323 <-u.setGBuildSizeCh 324 } 325 326 // Force to set the screen size 327 u.layoutIfNeeded() 328 for { 329 if err := u.update(); err != nil { 330 return err 331 } 332 } 333 } 334 335 // layoutIfNeeded must be called on the same goroutine as update(). 336 func (u *UserInterface) layoutIfNeeded() { 337 var outsideWidth, outsideHeight float64 338 339 u.m.RLock() 340 sizeChanged := u.sizeChanged 341 if sizeChanged { 342 if u.gbuildWidthPx == 0 || u.gbuildHeightPx == 0 { 343 outsideWidth = u.outsideWidth 344 outsideHeight = u.outsideHeight 345 } else { 346 // gomobile build 347 d := deviceScale() 348 outsideWidth = float64(u.gbuildWidthPx) / d 349 outsideHeight = float64(u.gbuildHeightPx) / d 350 } 351 } 352 u.sizeChanged = false 353 u.m.RUnlock() 354 355 if sizeChanged { 356 u.context.Layout(outsideWidth, outsideHeight) 357 } 358 } 359 360 func (u *UserInterface) update() error { 361 <-renderCh 362 defer func() { 363 renderEndCh <- struct{}{} 364 }() 365 366 if err := u.context.Update(); err != nil { 367 return err 368 } 369 if err := u.context.Draw(); err != nil { 370 return err 371 } 372 return nil 373 } 374 375 func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { 376 // TODO: This function should return gbuildWidthPx, gbuildHeightPx, 377 // but these values are not initialized until the main loop starts. 378 return 0, 0 379 } 380 381 // SetOutsideSize is called from mobile/ebitenmobileview. 382 // 383 // SetOutsideSize is concurrent safe. 384 func (u *UserInterface) SetOutsideSize(outsideWidth, outsideHeight float64) { 385 u.m.Lock() 386 if u.outsideWidth != outsideWidth || u.outsideHeight != outsideHeight { 387 u.outsideWidth = outsideWidth 388 u.outsideHeight = outsideHeight 389 u.sizeChanged = true 390 } 391 u.m.Unlock() 392 } 393 394 func (u *UserInterface) setGBuildSize(widthPx, heightPx int) { 395 u.m.Lock() 396 u.gbuildWidthPx = widthPx 397 u.gbuildHeightPx = heightPx 398 u.sizeChanged = true 399 u.m.Unlock() 400 401 u.once.Do(func() { 402 close(u.setGBuildSizeCh) 403 }) 404 } 405 406 func (u *UserInterface) adjustPosition(x, y int) (int, int) { 407 xf, yf := u.context.AdjustPosition(float64(x), float64(y)) 408 return int(xf), int(yf) 409 } 410 411 func (u *UserInterface) CursorMode() driver.CursorMode { 412 return driver.CursorModeHidden 413 } 414 415 func (u *UserInterface) SetCursorMode(mode driver.CursorMode) { 416 // Do nothing 417 } 418 419 func (u *UserInterface) IsFullscreen() bool { 420 return false 421 } 422 423 func (u *UserInterface) SetFullscreen(fullscreen bool) { 424 // Do nothing 425 } 426 427 func (u *UserInterface) IsFocused() bool { 428 return atomic.LoadInt32(&u.foreground) != 0 429 } 430 431 func (u *UserInterface) IsRunnableOnUnfocused() bool { 432 return false 433 } 434 435 func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) { 436 // Do nothing 437 } 438 439 func (u *UserInterface) IsVsyncEnabled() bool { 440 return true 441 } 442 443 func (u *UserInterface) SetVsyncEnabled(enabled bool) { 444 // Do nothing 445 } 446 447 func (u *UserInterface) DeviceScaleFactor() float64 { 448 return deviceScale() 449 } 450 451 func (u *UserInterface) SetScreenTransparent(transparent bool) { 452 // Do nothing 453 } 454 455 func (u *UserInterface) IsScreenTransparent() bool { 456 return false 457 } 458 459 func (u *UserInterface) ResetForFrame() { 460 u.layoutIfNeeded() 461 u.input.resetForFrame() 462 } 463 464 func (u *UserInterface) SetInitFocused(focused bool) { 465 // Do nothing 466 } 467 468 func (u *UserInterface) Input() driver.Input { 469 return &u.input 470 } 471 472 func (u *UserInterface) Window() driver.Window { 473 return nil 474 } 475 476 type Touch struct { 477 ID driver.TouchID 478 X int 479 Y int 480 } 481 482 type Gamepad struct { 483 ID driver.GamepadID 484 SDLID string 485 Name string 486 Buttons [driver.GamepadButtonNum]bool 487 ButtonNum int 488 Axes [32]float32 489 AxisNum int 490 } 491 492 func (u *UserInterface) UpdateInput(keys map[driver.Key]struct{}, runes []rune, touches []*Touch, gamepads []Gamepad) { 493 u.input.update(keys, runes, touches, gamepads) 494 }