twitchapon-anim

Basic Twitchapon Receiver/Visuals
git clone git://bsandro.tech/twitchapon-anim
Log | Files | Refs | README | LICENSE

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 }