updated ebiten version from 2.7.9 to 2.9.9

This commit is contained in:
2026-06-15 19:06:55 +02:00
parent 21edbc41c4
commit db1b625069
405 changed files with 31913 additions and 12595 deletions
+11
View File
@@ -68,9 +68,12 @@ type _POINT struct {
}
var (
imm32 = windows.NewLazySystemDLL("imm32.dll")
ole32 = windows.NewLazySystemDLL("ole32.dll")
user32 = windows.NewLazySystemDLL("user32.dll")
procImmAssociateContext = imm32.NewProc("ImmAssociateContext")
procCoCreateInstance = ole32.NewProc("CoCreateInstance")
procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
@@ -79,6 +82,14 @@ var (
procGetCursorPos = user32.NewProc("GetCursorPos")
)
func _ImmAssociateContext(hwnd windows.HWND, hIMC uintptr) (uintptr, error) {
r, _, e := procImmAssociateContext.Call(uintptr(hwnd), hIMC)
if e != nil && !errors.Is(e, windows.ERROR_SUCCESS) {
return 0, fmt.Errorf("ui: ImmAssociateContext failed: error code: %w", e)
}
return r, nil
}
func _CoCreateInstance(rclsid *windows.GUID, pUnkOuter unsafe.Pointer, dwClsContext uint32, riid *windows.GUID) (unsafe.Pointer, error) {
var ptr unsafe.Pointer
r, _, _ := procCoCreateInstance.Call(uintptr(unsafe.Pointer(rclsid)), uintptr(pUnkOuter), uintptr(dwClsContext), uintptr(unsafe.Pointer(riid)), uintptr(unsafe.Pointer(&ptr)))
+87 -51
View File
@@ -54,7 +54,7 @@ type context struct {
offscreenHeight float64
isOffscreenModified bool
lastDrawTime time.Time
lastSwapBufferTime time.Time
skipCount int
@@ -70,7 +70,14 @@ func newContext(game Game) *context {
func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface) error {
// TODO: If updateCount is 0 and vsync is disabled, swapping buffers can be skipped.
return c.updateFrameImpl(graphicsDriver, clock.UpdateFrame(), outsideWidth, outsideHeight, deviceScaleFactor, ui, false)
needsSwapBuffers, err := c.updateFrameImpl(graphicsDriver, clock.UpdateFrame(), outsideWidth, outsideHeight, deviceScaleFactor, ui, false)
if err != nil {
return err
}
if err := c.swapBuffersOrWait(needsSwapBuffers, graphicsDriver, ui.FPSMode() == FPSModeVsyncOn); err != nil {
return err
}
return nil
}
func (c *context) forceUpdateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface) error {
@@ -81,33 +88,32 @@ func (c *context) forceUpdateFrame(graphicsDriver graphicsdriver.Graphics, outsi
n = 2
}
for i := 0; i < n; i++ {
if err := c.updateFrameImpl(graphicsDriver, 1, outsideWidth, outsideHeight, deviceScaleFactor, ui, true); err != nil {
needsSwapBuffers, err := c.updateFrameImpl(graphicsDriver, 1, outsideWidth, outsideHeight, deviceScaleFactor, ui, true)
if err != nil {
return err
}
if err := c.swapBuffersOrWait(needsSwapBuffers, graphicsDriver, ui.FPSMode() == FPSModeVsyncOn); err != nil {
return err
}
}
return nil
}
func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool) (err error) {
func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool) (needsSwapBuffers bool, err error) {
// The given outside size can be 0 e.g. just after restoring from the fullscreen mode on Windows (#1589)
// Just ignore such cases. Otherwise, creating a zero-sized framebuffer causes a panic.
if outsideWidth == 0 || outsideHeight == 0 {
return nil
return false, nil
}
debug.Logf("----\n")
debug.FrameLogf("----\n")
if err := atlas.BeginFrame(graphicsDriver); err != nil {
return err
return false, err
}
defer func() {
if err1 := atlas.EndFrame(); err1 != nil && err == nil {
err = err1
return
}
if err1 := atlas.SwapBuffers(graphicsDriver); err1 != nil && err == nil {
needsSwapBuffers = false
err = err1
return
}
@@ -115,17 +121,17 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
// Flush deferred functions, like reading pixels from GPU.
if err := c.processFuncsInFrame(ui); err != nil {
return err
return false, err
}
// ForceUpdate can be invoked even if the context is not initialized yet (#1591).
if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 {
return nil
return false, nil
}
// Update the input state after the layout is updated as a cursor position is affected by the layout.
if err := ui.updateInputState(); err != nil {
return err
if err := ui.updateInputStateForFrame(); err != nil {
return false, err
}
// Ensure that Update is called once before Draw so that Update can be used for initialization.
@@ -133,7 +139,7 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
updateCount = 1
c.updateCalled = true
}
debug.Logf("Update count per frame: %d\n", updateCount)
debug.FrameLogf("Update count per frame: %d\n", updateCount)
// Update the game.
for i := 0; i < updateCount; i++ {
@@ -143,27 +149,55 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
})
if err := hook.RunBeforeUpdateHooks(); err != nil {
return err
return false, err
}
if err := c.game.Update(); err != nil {
return err
return false, err
}
// Catch the error that happened at (*Image).At.
if err := ui.error(); err != nil {
return err
return false, err
}
ui.incrementTick()
}
// Update window icons during a frame, since an icon might be *ebiten.Image and
// getting pixels from it needs to be in a frame (#1468).
if err := ui.updateIconIfNeeded(); err != nil {
return err
return false, err
}
// Draw the game.
if err := c.drawGame(graphicsDriver, ui, forceDraw); err != nil {
return err
return c.drawGame(graphicsDriver, ui, forceDraw)
}
func (c *context) swapBuffersOrWait(needsSwapBuffers bool, graphicsDriver graphicsdriver.Graphics, vsyncEnabled bool) error {
now := time.Now()
defer func() {
c.lastSwapBufferTime = now
}()
if needsSwapBuffers {
if err := atlas.SwapBuffers(graphicsDriver); err != nil {
return err
}
}
var waitTime time.Duration
if !needsSwapBuffers {
// When swapping buffers is skipped and Draw is called too early, sleep for a while to suppress CPU usages (#2890).
waitTime = time.Second / 60
} else if vsyncEnabled {
// In some environments, e.g. Linux on Parallels, SwapBuffers doesn't wait for the vsync (#2952).
// In the case when the display has high refresh rates like 240 [Hz], the wait time should be small.
waitTime = time.Millisecond
}
if waitTime > 0 {
if delta := waitTime - now.Sub(c.lastSwapBufferTime); delta > 0 {
time.Sleep(delta)
}
}
return nil
@@ -177,7 +211,7 @@ func (c *context) newOffscreenImage(w, h int) *Image {
return img
}
func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInterface, forceDraw bool) error {
func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInterface, forceDraw bool) (needSwapBuffers bool, err error) {
if (c.offscreen.imageType == atlas.ImageTypeVolatile) != ui.IsScreenClearedEveryFrame() {
w, h := c.offscreen.width, c.offscreen.height
c.offscreen.Deallocate()
@@ -195,10 +229,10 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInter
}
if err := c.game.DrawOffscreen(); err != nil {
return err
return false, err
}
const maxSkipCount = 3
const maxSkipCount = 4
if !forceDraw && !c.isOffscreenModified {
if c.skipCount < maxSkipCount {
@@ -208,28 +242,26 @@ func (c *context) drawGame(graphicsDriver graphicsdriver.Graphics, ui *UserInter
c.skipCount = 0
}
now := time.Now()
defer func() {
c.lastDrawTime = now
}()
if c.skipCount < maxSkipCount {
if graphicsDriver.NeedsClearingScreen() {
// This clear is needed for fullscreen mode or some mobile platforms (#622).
c.screen.clear()
}
c.game.DrawFinalScreen(c.screenScaleAndOffsets())
// The final screen is never used as the rendering source.
// Flush its buffer here just in case.
c.screen.flushBufferIfNeeded()
} else if delta := time.Second/60 - now.Sub(c.lastDrawTime); delta > 0 {
// When swapping buffers is skipped and Draw is called too early, sleep for a while to suppress CPU usages (#2890).
time.Sleep(delta)
if c.skipCount >= maxSkipCount {
return false, nil
}
return nil
// screen can be nil for some edge cases (#3121).
if c.screen == nil {
return false, nil
}
if graphicsDriver.NeedsClearingScreen() {
// This clear is needed for fullscreen mode or some mobile platforms (#622).
c.screen.clear()
}
c.game.DrawFinalScreen(c.screenScaleAndOffsets())
// The final screen is never used as the rendering source.
// Flush its buffer here just in case.
c.screen.flushBufferIfNeeded()
return true, nil
}
func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFactor float64) (int, int) {
@@ -238,8 +270,13 @@ func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFac
panic("ui: Layout must return positive numbers")
}
c.screenWidth = outsideWidth * deviceScaleFactor
c.screenHeight = outsideHeight * deviceScaleFactor
screenWidth := outsideWidth * deviceScaleFactor
screenHeight := outsideHeight * deviceScaleFactor
if c.screenWidth != screenWidth || c.screenHeight != screenHeight {
c.skipCount = 0
}
c.screenWidth = screenWidth
c.screenHeight = screenHeight
c.offscreenWidth = owf
c.offscreenHeight = ohf
@@ -252,7 +289,7 @@ func (c *context) layoutGame(outsideWidth, outsideHeight float64, deviceScaleFac
c.screen.Deallocate()
c.screen = nil
}
if c.screen == nil {
if c.screen == nil && sw > 0 && sh > 0 {
c.screen = c.game.NewScreenImage(sw, sh)
}
@@ -308,7 +345,6 @@ func (c *context) runInFrame(f func()) {
f()
}
<-ch
return
}
func (c *context) processFuncsInFrame(ui *UserInterface) error {
+21 -15
View File
@@ -23,6 +23,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/graphics"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
)
// panicOnErrorOnReadingPixels indicates whether reading pixels panics on an error or not.
@@ -77,7 +78,7 @@ func (i *Image) Deallocate() {
i.mipmap.Deallocate()
}
func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule, canSkipMipmap bool, antialias bool) {
func (i *Image) DrawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule, canSkipMipmap bool, antialias bool, hint restorable.Hint) {
if i.modifyCallback != nil {
i.modifyCallback()
}
@@ -98,13 +99,13 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices [
i.bigOffscreenBuffer = i.ui.newBigOffscreenImage(i, imageType)
}
i.bigOffscreenBuffer.drawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap, false)
i.bigOffscreenBuffer.drawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap)
return
}
i.flushBufferIfNeeded()
var srcMipmaps [graphics.ShaderImageCount]*mipmap.Mipmap
var srcMipmaps [graphics.ShaderSrcImageCount]*mipmap.Mipmap
for i, src := range srcs {
if src == nil {
continue
@@ -113,7 +114,7 @@ func (i *Image) DrawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices [
srcMipmaps[i] = src.mipmap
}
i.mipmap.DrawTriangles(srcMipmaps, vertices, indices, blend, dstRegion, srcRegions, shader.shader, uniforms, fillRule, canSkipMipmap)
i.mipmap.DrawTriangles(srcMipmaps, vertices, indices, blend, dstRegion, srcRegions, shader.shader, uniforms, fillRule, canSkipMipmap, hint)
}
func (i *Image) WritePixels(pix []byte, region image.Rectangle) {
@@ -168,22 +169,23 @@ func (i *Image) Fill(r, g, b, a float32, region image.Rectangle) {
i.tmpVerticesForFill = make([]float32, 4*graphics.VertexFloatCount)
}
// i.tmpVerticesForFill can be reused as this is sent to DrawTriangles immediately.
graphics.QuadVertices(
graphics.QuadVerticesFromSrcAndMatrix(
i.tmpVerticesForFill,
1, 1, float32(i.ui.whiteImage.width-1), float32(i.ui.whiteImage.height-1),
float32(i.width), 0, 0, float32(i.height), 0, 0,
r, g, b, a)
is := graphics.QuadIndices()
srcs := [graphics.ShaderImageCount]*Image{i.ui.whiteImage}
srcs := [graphics.ShaderSrcImageCount]*Image{i.ui.whiteImage}
blend := graphicsdriver.BlendCopy
// If possible, use BlendSourceOver to encourage batching (#2817).
if a == 1 && i.lastBlend == graphicsdriver.BlendSourceOver {
blend = graphicsdriver.BlendSourceOver
}
sr := image.Rect(0, 0, i.ui.whiteImage.width, i.ui.whiteImage.height)
// i.lastBlend is updated in DrawTriangles.
i.DrawTriangles(srcs, i.tmpVerticesForFill, is, blend, region, [graphics.ShaderImageCount]image.Rectangle{}, NearestFilterShader, nil, graphicsdriver.FillAll, true, false)
i.DrawTriangles(srcs, i.tmpVerticesForFill, is, blend, region, [graphics.ShaderSrcImageCount]image.Rectangle{sr}, NearestFilterShader, nil, graphicsdriver.FillRuleFillAll, true, false, restorable.HintOverwriteDstRegion)
}
type bigOffscreenImage struct {
@@ -217,7 +219,7 @@ func (i *bigOffscreenImage) deallocate() {
i.dirty = false
}
func (i *bigOffscreenImage) drawTriangles(srcs [graphics.ShaderImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule, canSkipMipmap bool, antialias bool) {
func (i *bigOffscreenImage) drawTriangles(srcs [graphics.ShaderSrcImageCount]*Image, vertices []float32, indices []uint32, blend graphicsdriver.Blend, dstRegion image.Rectangle, srcRegions [graphics.ShaderSrcImageCount]image.Rectangle, shader *Shader, uniforms []uint32, fillRule graphicsdriver.FillRule, canSkipMipmap bool) {
if i.blend != blend {
i.flush()
}
@@ -240,19 +242,20 @@ func (i *bigOffscreenImage) drawTriangles(srcs [graphics.ShaderImageCount]*Image
// Copy the current rendering result to get the correct blending result.
if blend != graphicsdriver.BlendSourceOver && !i.dirty {
srcs := [graphics.ShaderImageCount]*Image{i.orig}
srcs := [graphics.ShaderSrcImageCount]*Image{i.orig}
if len(i.tmpVerticesForCopying) < 4*graphics.VertexFloatCount {
i.tmpVerticesForCopying = make([]float32, 4*graphics.VertexFloatCount)
}
// i.tmpVerticesForCopying can be reused as this is sent to DrawTriangles immediately.
graphics.QuadVertices(
graphics.QuadVerticesFromSrcAndMatrix(
i.tmpVerticesForCopying,
float32(i.region.Min.X), float32(i.region.Min.Y), float32(i.region.Max.X), float32(i.region.Max.Y),
bigOffscreenScale, 0, 0, bigOffscreenScale, 0, 0,
1, 1, 1, 1)
is := graphics.QuadIndices()
dstRegion := image.Rect(0, 0, i.region.Dx()*bigOffscreenScale, i.region.Dy()*bigOffscreenScale)
i.image.DrawTriangles(srcs, i.tmpVerticesForCopying, is, graphicsdriver.BlendCopy, dstRegion, [graphics.ShaderImageCount]image.Rectangle{}, NearestFilterShader, nil, graphicsdriver.FillAll, true, false)
srcRegion := i.region
i.image.DrawTriangles(srcs, i.tmpVerticesForCopying, is, graphicsdriver.BlendCopy, dstRegion, [graphics.ShaderSrcImageCount]image.Rectangle{srcRegion}, NearestFilterShader, nil, graphicsdriver.FillRuleFillAll, true, false, restorable.HintOverwriteDstRegion)
}
for idx := 0; idx < len(vertices); idx += graphics.VertexFloatCount {
@@ -268,7 +271,7 @@ func (i *bigOffscreenImage) drawTriangles(srcs [graphics.ShaderImageCount]*Image
dstRegion.Max.X *= bigOffscreenScale
dstRegion.Max.Y *= bigOffscreenScale
i.image.DrawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap, false)
i.image.DrawTriangles(srcs, vertices, indices, blend, dstRegion, srcRegions, shader, uniforms, fillRule, canSkipMipmap, false, restorable.HintNone)
i.dirty = true
}
@@ -284,23 +287,26 @@ func (i *bigOffscreenImage) flush() {
// Mark the offscreen clean earlier to avoid recursive calls.
i.dirty = false
srcs := [graphics.ShaderImageCount]*Image{i.image}
srcs := [graphics.ShaderSrcImageCount]*Image{i.image}
if len(i.tmpVerticesForFlushing) < 4*graphics.VertexFloatCount {
i.tmpVerticesForFlushing = make([]float32, 4*graphics.VertexFloatCount)
}
// i.tmpVerticesForFlushing can be reused as this is sent to DrawTriangles in this function.
graphics.QuadVertices(
graphics.QuadVerticesFromSrcAndMatrix(
i.tmpVerticesForFlushing,
0, 0, float32(i.region.Dx()*bigOffscreenScale), float32(i.region.Dy()*bigOffscreenScale),
1.0/bigOffscreenScale, 0, 0, 1.0/bigOffscreenScale, float32(i.region.Min.X), float32(i.region.Min.Y),
1, 1, 1, 1)
is := graphics.QuadIndices()
dstRegion := i.region
srcRegion := image.Rect(0, 0, i.region.Dx()*bigOffscreenScale, i.region.Dy()*bigOffscreenScale)
blend := graphicsdriver.BlendSourceOver
hint := restorable.HintNone
if i.blend != graphicsdriver.BlendSourceOver {
blend = graphicsdriver.BlendCopy
hint = restorable.HintOverwriteDstRegion
}
i.orig.DrawTriangles(srcs, i.tmpVerticesForFlushing, is, blend, dstRegion, [graphics.ShaderImageCount]image.Rectangle{}, LinearFilterShader, nil, graphicsdriver.FillAll, true, false)
i.orig.DrawTriangles(srcs, i.tmpVerticesForFlushing, is, blend, dstRegion, [graphics.ShaderSrcImageCount]image.Rectangle{srcRegion}, LinearFilterShader, nil, graphicsdriver.FillRuleFillAll, true, false, hint)
i.image.clear()
i.dirty = false
+1 -1
View File
@@ -14,7 +14,7 @@
//go:build nintendosdk
// The actual implementation will be provided by -overlay.
// The actual implementation will be provided by github.com/hajimehoshi/uwagaki.
#include "init_nintendosdk.h"
+171 -12
View File
@@ -39,21 +39,180 @@ type Touch struct {
}
type InputState struct {
KeyPressed [KeyMax + 1]bool
MouseButtonPressed [MouseButtonMax + 1]bool
CursorX float64
CursorY float64
WheelX float64
WheelY float64
Touches []Touch
Runes []rune
WindowBeingClosed bool
DroppedFiles fs.FS
KeyPressedTimes [KeyMax + 1]InputTime
KeyReleasedTimes [KeyMax + 1]InputTime
MouseButtonPressedTimes [MouseButtonMax + 1]InputTime
MouseButtonReleasedTimes [MouseButtonMax + 1]InputTime
CursorX float64
CursorY float64
WheelX float64
WheelY float64
Touches []Touch
Runes []rune
WindowBeingClosed bool
DroppedFiles fs.FS
}
func (i *InputState) setKeyPressed(key Key, t InputTime) {
if key < 0 || KeyMax < key {
return
}
i.KeyPressedTimes[key] = t
}
func (i *InputState) setKeyReleased(key Key, t InputTime) {
if key < 0 || KeyMax < key {
return
}
// Ignore duplicated key releases (#3326).
if i.KeyPressedTimes[key] <= i.KeyReleasedTimes[key] {
return
}
i.KeyReleasedTimes[key] = t
}
func (i *InputState) setMouseButtonPressed(button MouseButton, t InputTime) {
if button < 0 || MouseButtonMax < button {
return
}
i.MouseButtonPressedTimes[button] = t
}
func (i *InputState) setMouseButtonReleased(button MouseButton, t InputTime) {
if button < 0 || MouseButtonMax < button {
return
}
if i.MouseButtonPressedTimes[button] <= i.MouseButtonReleasedTimes[button] {
return
}
i.MouseButtonReleasedTimes[button] = t
}
// releaseAllButtons is called when the browser window loses focus.
func (i *InputState) releaseAllButtons(t InputTime) {
for j := range i.KeyPressedTimes {
if i.KeyPressedTimes[Key(j)] <= i.KeyReleasedTimes[Key(j)] {
continue
}
i.KeyReleasedTimes[Key(j)] = t
}
for j := range i.MouseButtonPressedTimes {
if i.MouseButtonPressedTimes[MouseButton(j)] <= i.MouseButtonReleasedTimes[MouseButton(j)] {
continue
}
i.MouseButtonReleasedTimes[j] = t
}
i.Touches = i.Touches[:0]
}
func (i *InputState) IsKeyPressed(key Key, tick int64) bool {
switch key {
case KeyAlt:
return i.IsKeyPressed(KeyAltLeft, tick) || i.IsKeyPressed(KeyAltRight, tick)
case KeyControl:
return i.IsKeyPressed(KeyControlLeft, tick) || i.IsKeyPressed(KeyControlRight, tick)
case KeyShift:
return i.IsKeyPressed(KeyShiftLeft, tick) || i.IsKeyPressed(KeyShiftRight, tick)
case KeyMeta:
return i.IsKeyPressed(KeyMetaLeft, tick) || i.IsKeyPressed(KeyMetaRight, tick)
}
if key < 0 || KeyMax < key {
return false
}
p := i.KeyPressedTimes[key]
r := i.KeyReleasedTimes[key]
return inputStatePressed(p, r, tick)
}
func (i *InputState) IsKeyJustPressed(key Key, tick int64) bool {
if key < 0 || KeyMax < key {
return false
}
p := i.KeyPressedTimes[key]
return inputStateJustPressed(p, tick)
}
func (i *InputState) IsKeyJustReleased(key Key, tick int64) bool {
if key < 0 || KeyMax < key {
return false
}
r := i.KeyReleasedTimes[key]
return inputStateJustReleased(r, tick)
}
func (i *InputState) KeyPressDuration(key Key, tick int64) int64 {
if key < 0 || KeyMax < key {
return 0
}
p := i.KeyPressedTimes[key]
r := i.KeyReleasedTimes[key]
return inputStateDuration(p, r, tick)
}
func (i *InputState) IsMouseButtonPressed(button MouseButton, tick int64) bool {
if button < 0 || MouseButtonMax < button {
return false
}
p := i.MouseButtonPressedTimes[button]
r := i.MouseButtonReleasedTimes[button]
return inputStatePressed(p, r, tick)
}
func (i *InputState) IsMouseButtonJustPressed(button MouseButton, tick int64) bool {
if button < 0 || MouseButtonMax < button {
return false
}
p := i.MouseButtonPressedTimes[button]
return inputStateJustPressed(p, tick)
}
func (i *InputState) IsMouseButtonJustReleased(button MouseButton, tick int64) bool {
if button < 0 || MouseButtonMax < button {
return false
}
r := i.MouseButtonReleasedTimes[button]
return inputStateJustReleased(r, tick)
}
func (i *InputState) MouseButtonPressDuration(button MouseButton, tick int64) int64 {
if button < 0 || MouseButtonMax < button {
return 0
}
p := i.MouseButtonPressedTimes[button]
r := i.MouseButtonReleasedTimes[button]
return inputStateDuration(p, r, tick)
}
func inputStatePressed(pressed, released InputTime, tick int64) bool {
return released < pressed || inputStateJustPressed(pressed, tick)
}
func inputStateJustPressed(pressed InputTime, tick int64) bool {
return pressed > 0 && pressed.Tick() == tick
}
func inputStateJustReleased(released InputTime, tick int64) bool {
return released > 0 && released.Tick() == tick
}
func inputStateDuration(pressed, released InputTime, tick int64) int64 {
if pressed == 0 {
return 0
}
if pressed < released {
return 0
}
return tick - pressed.Tick() + 1
}
func (i *InputState) copyAndReset(dst *InputState) {
dst.KeyPressed = i.KeyPressed
dst.MouseButtonPressed = i.MouseButtonPressed
dst.KeyPressedTimes = i.KeyPressedTimes
dst.KeyReleasedTimes = i.KeyReleasedTimes
dst.MouseButtonPressedTimes = i.MouseButtonPressedTimes
dst.MouseButtonReleasedTimes = i.MouseButtonReleasedTimes
dst.CursorX = i.CursorX
dst.CursorY = i.CursorY
dst.WheelX = i.WheelX
+50 -19
View File
@@ -32,6 +32,52 @@ var glfwMouseButtonToMouseButton = map[glfw.MouseButton]MouseButton{
}
func (u *UserInterface) registerInputCallbacks() error {
if _, err := u.window.SetKeyCallback(func(w *glfw.Window, key glfw.Key, scancode int, action glfw.Action, mods glfw.ModifierKey) {
// Ignore key repeats for now.
if action == glfw.Repeat {
return
}
// As this function is called from GLFW callbacks, the current thread is main.
u.m.Lock()
defer u.m.Unlock()
uk, ok := glfwKeyToUIKey[key]
if !ok {
return
}
if action == glfw.Press {
u.inputState.setKeyPressed(uk, u.InputTime())
} else {
u.inputState.setKeyReleased(uk, u.InputTime())
}
}); err != nil {
return err
}
if _, err := u.window.SetMouseButtonCallback(func(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
// Ignore key repeats for now.
if action == glfw.Repeat {
return
}
// As this function is called from GLFW callbacks, the current thread is main.
u.m.Lock()
defer u.m.Unlock()
ub, ok := glfwMouseButtonToMouseButton[button]
if !ok {
return
}
if action == glfw.Press {
u.inputState.setMouseButtonPressed(ub, u.InputTime())
} else {
u.inputState.setMouseButtonReleased(ub, u.InputTime())
}
}); err != nil {
return err
}
if _, err := u.window.SetCharModsCallback(func(w *glfw.Window, char rune, mods glfw.ModifierKey) {
// As this function is called from GLFW callbacks, the current thread is main.
u.m.Lock()
@@ -54,34 +100,19 @@ func (u *UserInterface) registerInputCallbacks() error {
return nil
}
func (u *UserInterface) updateInputState() error {
func (u *UserInterface) updateInputStateForFrame() error {
var err error
u.mainThread.Call(func() {
err = u.updateInputStateImpl()
err = u.updateInputStateForFrameImpl()
})
return err
}
// updateInputStateImpl must be called from the main thread.
func (u *UserInterface) updateInputStateImpl() error {
// updateInputStateForFrameImpl must be called from the main thread.
func (u *UserInterface) updateInputStateForFrameImpl() error {
u.m.Lock()
defer u.m.Unlock()
for uk, gk := range uiKeyToGLFWKey {
s, err := u.window.GetKey(gk)
if err != nil {
return err
}
u.inputState.KeyPressed[uk] = s == glfw.Press
}
for gb, ub := range glfwMouseButtonToMouseButton {
s, err := u.window.GetMouseButton(gb)
if err != nil {
return err
}
u.inputState.MouseButtonPressed[ub] = s == glfw.Press
}
m, err := u.currentMonitor()
if err != nil {
return err
+61 -63
View File
@@ -16,6 +16,7 @@ package ui
import (
"math"
"strings"
"syscall/js"
"unicode"
)
@@ -62,10 +63,15 @@ var codeToMouseButton = map[int]MouseButton{
4: MouseButton4,
}
func eventToKeys(e js.Value) (key0, key1 Key, fromKeyProperty bool) {
func eventToKeys(e js.Value) (key0, key1 Key) {
id := jsCodeToID(e.Get("code"))
// On mobile browsers, treat enter key as if this is from a `key` property.
if IsVirtualKeyboard() && id == KeyEnter {
return KeyEnter, -1
}
if id >= 0 {
return id, -1, false
return id, -1
}
// With a virtual keyboard on mobile devices, e.code is empty. Use a 'key' property instead (#2898).
@@ -75,69 +81,56 @@ func eventToKeys(e js.Value) (key0, key1 Key, fromKeyProperty bool) {
// Let's assume both keys are pressed.
switch {
case key.Equal(stringAlt):
return KeyAltLeft, KeyAltRight, true
return KeyAltLeft, KeyAltRight
case key.Equal(stringControl):
return KeyControlLeft, KeyControlRight, true
return KeyControlLeft, KeyControlRight
case key.Equal(stringMeta):
return KeyMetaLeft, KeyMetaRight, true
return KeyMetaLeft, KeyMetaRight
case key.Equal(stringShift):
return KeyShiftLeft, KeyShiftRight, true
return KeyShiftLeft, KeyShiftRight
}
for uiKey, jsKey := range uiKeyToJSKey {
if key.Equal(jsKey) {
return uiKey, -1, true
return uiKey, -1
}
}
return -1, -1, false
return -1, -1
}
func (u *UserInterface) keyDown(event js.Value) {
key0, key1, fromKeyProperty := eventToKeys(event)
// Ignore key repeats for now.
if event.Get("repeat").Bool() {
return
}
now := u.InputTime()
key0, key1 := eventToKeys(event)
if key0 >= 0 {
// If the key value comes from a 'key' property, a 'keydown' and 'keyup' event might be fired too quickly.
// Record the key duration to prevent immediate resetting a key state by a 'keyup' event.
// Resetting a key state is delayed until the next tick. See updateInputState.
if fromKeyProperty && !u.inputState.KeyPressed[key0] {
if u.keyDurationsByKeyProperty == nil {
u.keyDurationsByKeyProperty = map[Key]int{}
}
u.keyDurationsByKeyProperty[key0] = 1
}
u.inputState.KeyPressed[key0] = true
u.inputState.setKeyPressed(key0, now)
}
if key1 >= 0 {
if fromKeyProperty && !u.inputState.KeyPressed[key1] {
if u.keyDurationsByKeyProperty == nil {
u.keyDurationsByKeyProperty = map[Key]int{}
}
u.keyDurationsByKeyProperty[key1] = 1
}
u.inputState.KeyPressed[key1] = true
u.inputState.setKeyPressed(key1, now)
}
}
func (u *UserInterface) keyUp(event js.Value) {
key0, key1, fromKeyProperty := eventToKeys(event)
now := u.InputTime()
key0, key1 := eventToKeys(event)
if key0 >= 0 {
if !fromKeyProperty || u.keyDurationsByKeyProperty[key0] == 0 {
u.inputState.KeyPressed[key0] = false
}
u.inputState.setKeyReleased(key0, now)
}
if key1 >= 0 {
if !fromKeyProperty || u.keyDurationsByKeyProperty[key1] == 0 {
u.inputState.KeyPressed[key1] = false
}
u.inputState.setKeyReleased(key1, now)
}
}
func (u *UserInterface) mouseDown(code int) {
u.inputState.MouseButtonPressed[codeToMouseButton[code]] = true
u.inputState.setMouseButtonPressed(codeToMouseButton[code], u.InputTime())
}
func (u *UserInterface) mouseUp(code int) {
u.inputState.MouseButtonPressed[codeToMouseButton[code]] = false
u.inputState.setMouseButtonReleased(codeToMouseButton[code], u.InputTime())
}
func (u *UserInterface) updateInputFromEvent(e js.Value) error {
@@ -239,10 +232,12 @@ func isKeyString(str string) bool {
}
var (
jsKeyboard = js.Global().Get("navigator").Get("keyboard")
jsKeyboardGetLayoutMap js.Value
jsKeyboardGetLayoutMapCh chan js.Value
jsKeyboardGetLayoutMapCallback js.Func
jsKeyboard = js.Global().Get("navigator").Get("keyboard")
jsKeyboardLayoutAvailable bool
jsKeyboardGetLayoutMap js.Value
jsKeyboardGetLayoutMapCh chan js.Value
jsKeyboardGetLayoutMapThenCallback js.Func
jsKeyboardGetLayoutMapCatchCallback js.Func
)
func init() {
@@ -252,10 +247,18 @@ func init() {
jsKeyboardGetLayoutMap = jsKeyboard.Get("getLayoutMap").Call("bind", jsKeyboard)
jsKeyboardGetLayoutMapCh = make(chan js.Value, 1)
jsKeyboardGetLayoutMapCallback = js.FuncOf(func(this js.Value, args []js.Value) any {
jsKeyboardGetLayoutMapThenCallback = js.FuncOf(func(this js.Value, args []js.Value) any {
jsKeyboardGetLayoutMapCh <- args[0]
return nil
})
jsKeyboardGetLayoutMapCatchCallback = js.FuncOf(func(this js.Value, args []js.Value) any {
err := args[0]
js.Global().Get("console").Call("error", "ui: navigator.keyboard.getLayoutMap() failed:", err)
jsKeyboardLayoutAvailable = false
jsKeyboardGetLayoutMapCh <- js.Undefined()
return nil
})
jsKeyboardLayoutAvailable = true
}
func (u *UserInterface) KeyName(key Key) string {
@@ -263,17 +266,20 @@ func (u *UserInterface) KeyName(key Key) string {
return ""
}
if !jsKeyboardLayoutAvailable {
return ""
}
// keyboardLayoutMap is reset every tick.
if u.keyboardLayoutMap.IsUndefined() {
if !jsKeyboard.Truthy() {
return ""
}
// Invoke getLayoutMap every tick to detect the keyboard change.
// TODO: Calling this every tick might be inefficient. Is there a way to detect a keyboard change?
jsKeyboardGetLayoutMap.Invoke().Call("then", jsKeyboardGetLayoutMapCallback)
jsKeyboardGetLayoutMap.Invoke().Call("then", jsKeyboardGetLayoutMapThenCallback).Call("catch", jsKeyboardGetLayoutMapCatchCallback)
u.keyboardLayoutMap = <-jsKeyboardGetLayoutMapCh
}
if u.keyboardLayoutMap.IsUndefined() {
return ""
}
n := u.keyboardLayoutMap.Call("get", uiKeyToJSCode[key])
if n.IsUndefined() {
@@ -294,17 +300,7 @@ func (u *UserInterface) saveCursorPosition() {
u.savedOutsideHeight = h
}
func (u *UserInterface) updateInputState() error {
// Reset the key state if a key is pressed by a 'key' property and the key's duration is big enough.
for key, duration := range u.keyDurationsByKeyProperty {
if duration >= 2 {
delete(u.keyDurationsByKeyProperty, key)
u.inputState.KeyPressed[key] = false
continue
}
u.keyDurationsByKeyProperty[key]++
}
func (u *UserInterface) updateInputStateForFrame() error {
s := theMonitor.DeviceScaleFactor()
if !math.IsNaN(u.savedCursorX) && !math.IsNaN(u.savedCursorY) {
@@ -417,12 +413,14 @@ var uiKeyToJSKey = map[Key]js.Value{
KeyNumpad9: js.ValueOf("9"),
}
func (i *InputState) resetForBlur() {
for j := range i.KeyPressed {
i.KeyPressed[j] = false
func IsVirtualKeyboard() bool {
// Detect a virtual keyboard by the user agent.
// Note that this is not a correct way to detect a virtual keyboard.
// In the future, we should use the `navigator.virtualKeyboard` API.
// https://developer.mozilla.org/en-US/docs/Web/API/Navigator/virtualKeyboard
ua := js.Global().Get("navigator").Get("userAgent").String()
if strings.Contains(ua, "Android") || strings.Contains(ua, "iPhone") || strings.Contains(ua, "iPad") || strings.Contains(ua, "iPod") {
return true
}
for j := range i.MouseButtonPressed {
i.MouseButtonPressed[j] = false
}
i.Touches = i.Touches[:0]
return false
}
+4 -8
View File
@@ -26,24 +26,20 @@ type TouchForInput struct {
Y float64
}
func (u *UserInterface) updateInputStateFromOutside(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
func (u *UserInterface) updateInputStateFromOutside(keyPressedTimes, keyReleasedTimes [KeyMax + 1]InputTime, runes []rune, touches []TouchForInput) {
u.m.Lock()
defer u.m.Unlock()
for k := range u.inputState.KeyPressed {
_, ok := keys[Key(k)]
u.inputState.KeyPressed[k] = ok
}
u.inputState.KeyPressedTimes = keyPressedTimes
u.inputState.KeyReleasedTimes = keyReleasedTimes
u.inputState.Runes = append(u.inputState.Runes, runes...)
u.touches = u.touches[:0]
for _, t := range touches {
u.touches = append(u.touches, t)
}
}
func (u *UserInterface) updateInputState() error {
func (u *UserInterface) updateInputStateForFrame() error {
u.m.Lock()
defer u.m.Unlock()
+1 -1
View File
@@ -14,7 +14,7 @@
//go:build nintendosdk
// The actual implementaiton will be provided by -overlay.
// The actual implementaiton will be provided by github.com/hajimehoshi/uwagaki.
#include "input_nintendosdk.h"
+4 -4
View File
@@ -29,16 +29,16 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
)
func (u *UserInterface) updateInputState() error {
func (u *UserInterface) updateInputStateForFrame() error {
var err error
u.mainThread.Call(func() {
err = u.updateInputStateImpl()
err = u.updateInputStateForFrameImpl()
})
return err
}
// updateInputStateImpl must be called from the main thread.
func (u *UserInterface) updateInputStateImpl() error {
// updateInputStateForFrameImpl must be called from the main thread.
func (u *UserInterface) updateInputStateForFrameImpl() error {
if err := gamepad.Update(); err != nil {
return err
}
+17 -2
View File
@@ -16,8 +16,23 @@
package ui
func (u *UserInterface) updateInputState() error {
// TODO: Implement this
import (
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
)
func (u *UserInterface) updateInputStateForFrame() error {
var err error
u.mainThread.Call(func() {
err = u.updateInputStateForFrameImpl()
})
return err
}
// updateInputStateForFrameImpl must be called from the main thread.
func (u *UserInterface) updateInputStateForFrameImpl() error {
if err := gamepad.Update(); err != nil {
return err
}
return nil
}
+5 -5
View File
@@ -141,11 +141,11 @@ const (
KeySlash
KeySpace
KeyTab
KeyReserved0
KeyReserved1
KeyReserved2
KeyReserved3
KeyMax = KeyReserved3
KeyAlt
KeyControl
KeyShift
KeyMeta
KeyMax = KeyMeta
)
func (k Key) String() string {
+121
View File
@@ -142,3 +142,124 @@ var uiKeyToGLFWKey = map[Key]glfw.Key{
KeyY: glfw.KeyY,
KeyZ: glfw.KeyZ,
}
var glfwKeyToUIKey = map[glfw.Key]Key{
glfw.KeyA: KeyA,
glfw.KeyLeftAlt: KeyAltLeft,
glfw.KeyRightAlt: KeyAltRight,
glfw.KeyDown: KeyArrowDown,
glfw.KeyLeft: KeyArrowLeft,
glfw.KeyRight: KeyArrowRight,
glfw.KeyUp: KeyArrowUp,
glfw.KeyB: KeyB,
glfw.KeyGraveAccent: KeyBackquote,
glfw.KeyBackslash: KeyBackslash,
glfw.KeyBackspace: KeyBackspace,
glfw.KeyLeftBracket: KeyBracketLeft,
glfw.KeyRightBracket: KeyBracketRight,
glfw.KeyC: KeyC,
glfw.KeyCapsLock: KeyCapsLock,
glfw.KeyComma: KeyComma,
glfw.KeyMenu: KeyContextMenu,
glfw.KeyLeftControl: KeyControlLeft,
glfw.KeyRightControl: KeyControlRight,
glfw.KeyD: KeyD,
glfw.KeyDelete: KeyDelete,
glfw.Key0: KeyDigit0,
glfw.Key1: KeyDigit1,
glfw.Key2: KeyDigit2,
glfw.Key3: KeyDigit3,
glfw.Key4: KeyDigit4,
glfw.Key5: KeyDigit5,
glfw.Key6: KeyDigit6,
glfw.Key7: KeyDigit7,
glfw.Key8: KeyDigit8,
glfw.Key9: KeyDigit9,
glfw.KeyE: KeyE,
glfw.KeyEnd: KeyEnd,
glfw.KeyEnter: KeyEnter,
glfw.KeyEqual: KeyEqual,
glfw.KeyEscape: KeyEscape,
glfw.KeyF: KeyF,
glfw.KeyF1: KeyF1,
glfw.KeyF10: KeyF10,
glfw.KeyF11: KeyF11,
glfw.KeyF12: KeyF12,
glfw.KeyF13: KeyF13,
glfw.KeyF14: KeyF14,
glfw.KeyF15: KeyF15,
glfw.KeyF16: KeyF16,
glfw.KeyF17: KeyF17,
glfw.KeyF18: KeyF18,
glfw.KeyF19: KeyF19,
glfw.KeyF2: KeyF2,
glfw.KeyF20: KeyF20,
glfw.KeyF21: KeyF21,
glfw.KeyF22: KeyF22,
glfw.KeyF23: KeyF23,
glfw.KeyF24: KeyF24,
glfw.KeyF3: KeyF3,
glfw.KeyF4: KeyF4,
glfw.KeyF5: KeyF5,
glfw.KeyF6: KeyF6,
glfw.KeyF7: KeyF7,
glfw.KeyF8: KeyF8,
glfw.KeyF9: KeyF9,
glfw.KeyG: KeyG,
glfw.KeyH: KeyH,
glfw.KeyHome: KeyHome,
glfw.KeyI: KeyI,
glfw.KeyInsert: KeyInsert,
glfw.KeyWorld1: KeyIntlBackslash,
glfw.KeyJ: KeyJ,
glfw.KeyK: KeyK,
glfw.KeyL: KeyL,
glfw.KeyM: KeyM,
glfw.KeyLeftSuper: KeyMetaLeft,
glfw.KeyRightSuper: KeyMetaRight,
glfw.KeyMinus: KeyMinus,
glfw.KeyN: KeyN,
glfw.KeyNumLock: KeyNumLock,
glfw.KeyKP0: KeyNumpad0,
glfw.KeyKP1: KeyNumpad1,
glfw.KeyKP2: KeyNumpad2,
glfw.KeyKP3: KeyNumpad3,
glfw.KeyKP4: KeyNumpad4,
glfw.KeyKP5: KeyNumpad5,
glfw.KeyKP6: KeyNumpad6,
glfw.KeyKP7: KeyNumpad7,
glfw.KeyKP8: KeyNumpad8,
glfw.KeyKP9: KeyNumpad9,
glfw.KeyKPAdd: KeyNumpadAdd,
glfw.KeyKPDecimal: KeyNumpadDecimal,
glfw.KeyKPDivide: KeyNumpadDivide,
glfw.KeyKPEnter: KeyNumpadEnter,
glfw.KeyKPEqual: KeyNumpadEqual,
glfw.KeyKPMultiply: KeyNumpadMultiply,
glfw.KeyKPSubtract: KeyNumpadSubtract,
glfw.KeyO: KeyO,
glfw.KeyP: KeyP,
glfw.KeyPageDown: KeyPageDown,
glfw.KeyPageUp: KeyPageUp,
glfw.KeyPause: KeyPause,
glfw.KeyPeriod: KeyPeriod,
glfw.KeyPrintScreen: KeyPrintScreen,
glfw.KeyQ: KeyQ,
glfw.KeyApostrophe: KeyQuote,
glfw.KeyR: KeyR,
glfw.KeyS: KeyS,
glfw.KeyScrollLock: KeyScrollLock,
glfw.KeySemicolon: KeySemicolon,
glfw.KeyLeftShift: KeyShiftLeft,
glfw.KeyRightShift: KeyShiftRight,
glfw.KeySlash: KeySlash,
glfw.KeySpace: KeySpace,
glfw.KeyT: KeyT,
glfw.KeyTab: KeyTab,
glfw.KeyU: KeyU,
glfw.KeyV: KeyV,
glfw.KeyW: KeyW,
glfw.KeyX: KeyX,
glfw.KeyY: KeyY,
glfw.KeyZ: KeyZ,
}
+15 -12
View File
@@ -18,6 +18,7 @@ package ui
import (
"image"
"slices"
"sync"
"sync/atomic"
@@ -65,13 +66,13 @@ type monitors struct {
m sync.Mutex
updateCalled int32
updateCalled atomic.Bool
}
var theMonitors monitors
func (m *monitors) append(ms []*Monitor) []*Monitor {
if atomic.LoadInt32(&m.updateCalled) == 0 {
if !m.updateCalled.Load() {
panic("ui: (*monitors).update must be called before (*monitors).append is called")
}
@@ -81,15 +82,24 @@ func (m *monitors) append(ms []*Monitor) []*Monitor {
return append(ms, m.monitors...)
}
func (m *monitors) contains(monitor *Monitor) bool {
if !m.updateCalled.Load() {
return false
}
m.m.Lock()
defer m.m.Unlock()
return slices.Contains(m.monitors, monitor)
}
func (m *monitors) primaryMonitor() *Monitor {
if atomic.LoadInt32(&m.updateCalled) == 0 {
if !m.updateCalled.Load() {
panic("ui: (*monitors).update must be called before (*monitors).primaryMonitor is called")
}
m.m.Lock()
defer m.m.Unlock()
// GetMonitors might return nil in theory (#1878, #1887).
// GetMonitors might return nil in theory (#1878, #1887, #3241).
// primaryMonitor can be called at the initialization, so monitors can be nil.
if len(m.monitors) == 0 {
return nil
@@ -97,13 +107,6 @@ func (m *monitors) primaryMonitor() *Monitor {
return m.monitors[0]
}
func (m *monitors) monitorFromID(id int) *Monitor {
m.m.Lock()
defer m.m.Unlock()
return m.monitors[id]
}
// monitorFromPosition returns a monitor for the given position (x, y),
// or returns nil if monitor is not found.
// The position is in GLFW pixels.
@@ -179,6 +182,6 @@ func (m *monitors) update() error {
m.monitors = newMonitors
m.m.Unlock()
atomic.StoreInt32(&m.updateCalled, 1)
m.updateCalled.Store(true)
return nil
}
+15 -10
View File
@@ -37,18 +37,8 @@ func (u *UserInterface) runMultiThread(game Game, options *RunOptions) error {
u.mainThread = thread.NewOSThread()
graphicscommand.SetOSThreadAsRenderThread()
// Set the running state true after the main thread is set, and before initOnMainThread is called (#2742).
// TODO: As the existence of the main thread is the same as the value of `running`, this is redundant.
// Make `mainThread` atomic and remove `running` if possible.
u.setRunning(true)
defer u.setRunning(false)
u.context = newContext(game)
if err := u.initOnMainThread(options); err != nil {
return err
}
ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
defer cancel()
@@ -57,6 +47,7 @@ func (u *UserInterface) runMultiThread(game Game, options *RunOptions) error {
// Run the render thread.
wg.Go(func() error {
defer cancel()
graphicscommand.LoopRenderThread(ctx)
return nil
})
@@ -64,6 +55,20 @@ func (u *UserInterface) runMultiThread(game Game, options *RunOptions) error {
// Run the game thread.
wg.Go(func() error {
defer cancel()
var err error
u.mainThread.Call(func() {
if err1 := u.initOnMainThread(options); err1 != nil {
err = err1
}
})
if err != nil {
return err
}
// setRunning(true) should be called in initOnMainThread for each platform.
defer u.setRunning(false)
return u.loopGame()
})
+32 -15
View File
@@ -27,14 +27,14 @@ import (
type Shader struct {
shader *atlas.Shader
uniformNames []string
uniformTypes []shaderir.Type
uniformUint32Count int
uniformNames []string
uniformTypes []shaderir.Type
uniformDwordCount int
}
func NewShader(ir *shaderir.Program) *Shader {
func NewShader(ir *shaderir.Program, name string) *Shader {
return &Shader{
shader: atlas.NewShader(ir),
shader: atlas.NewShader(ir, name),
uniformNames: ir.UniformNames[graphics.PreservedUniformVariablesCount:],
uniformTypes: ir.Uniforms[graphics.PreservedUniformVariablesCount:],
}
@@ -45,20 +45,20 @@ func (s *Shader) Deallocate() {
}
func (s *Shader) AppendUniforms(dst []uint32, uniforms map[string]any) []uint32 {
if s.uniformUint32Count == 0 {
if s.uniformDwordCount == 0 {
for _, typ := range s.uniformTypes {
s.uniformUint32Count += typ.Uint32Count()
s.uniformDwordCount += typ.DwordCount()
}
}
origLen := len(dst)
if cap(dst)-len(dst) >= s.uniformUint32Count {
dst = dst[:len(dst)+s.uniformUint32Count]
if cap(dst)-len(dst) >= s.uniformDwordCount {
dst = dst[:len(dst)+s.uniformDwordCount]
for i := origLen; i < len(dst); i++ {
dst[i] = 0
}
} else {
dst = append(dst, make([]uint32, s.uniformUint32Count)...)
dst = append(dst, make([]uint32, s.uniformDwordCount)...)
}
idx := origLen
@@ -70,27 +70,44 @@ func (s *Shader) AppendUniforms(dst []uint32, uniforms map[string]any) []uint32
v := reflect.ValueOf(uv)
t := v.Type()
switch t.Kind() {
case reflect.Bool:
if typ.DwordCount() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
if v.Bool() {
dst[idx] = 1
} else {
dst[idx] = 0
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
if typ.Uint32Count() != 1 {
if typ.DwordCount() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
dst[idx] = uint32(v.Int())
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
if typ.Uint32Count() != 1 {
if typ.DwordCount() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
dst[idx] = uint32(v.Uint())
case reflect.Float32, reflect.Float64:
if typ.Uint32Count() != 1 {
if typ.DwordCount() != 1 {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
dst[idx] = math.Float32bits(float32(v.Float()))
case reflect.Slice, reflect.Array:
l := v.Len()
if typ.Uint32Count() != l {
if typ.DwordCount() != l {
panic(fmt.Sprintf("ui: unexpected uniform value for %s (%s)", name, typ.String()))
}
switch t.Elem().Kind() {
case reflect.Bool:
for i := 0; i < l; i++ {
if v.Index(i).Bool() {
dst[idx+i] = 1
} else {
dst[idx+i] = 0
}
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
for i := 0; i < l; i++ {
dst[idx+i] = uint32(v.Index(i).Int())
@@ -111,7 +128,7 @@ func (s *Shader) AppendUniforms(dst []uint32, uniforms map[string]any) []uint32
}
}
idx += typ.Uint32Count()
idx += typ.DwordCount()
}
return dst
+70 -31
View File
@@ -23,6 +23,7 @@ import (
_ "github.com/ebitengine/hideconsole"
"github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/mipmap"
"github.com/hajimehoshi/ebiten/v2/internal/thread"
)
@@ -75,10 +76,12 @@ type UserInterface struct {
err error
errM sync.Mutex
isScreenClearedEveryFrame int32
graphicsLibrary int32
running int32
terminated int32
isScreenClearedEveryFrame atomic.Bool
graphicsLibrary atomic.Int32
running atomic.Bool
terminated atomic.Bool
tick atomic.Int64
inputTime atomic.Int64
whiteImage *Image
@@ -106,10 +109,9 @@ func Get() *UserInterface {
// newUserInterface must be called from the main thread.
func newUserInterface() (*UserInterface, error) {
u := &UserInterface{
isScreenClearedEveryFrame: 1,
graphicsLibrary: int32(GraphicsLibraryUnknown),
}
u := &UserInterface{}
u.isScreenClearedEveryFrame.Store(true)
u.graphicsLibrary.Store(int32(GraphicsLibraryUnknown))
u.whiteImage = u.NewImage(3, 3, atlas.ImageTypeRegular)
pix := make([]byte, 4*u.whiteImage.width*u.whiteImage.height)
@@ -127,6 +129,10 @@ func newUserInterface() (*UserInterface, error) {
}
func (u *UserInterface) readPixels(mipmap *mipmap.Mipmap, pixels []byte, region image.Rectangle) error {
if !u.running.Load() {
panic("ui: ReadPixels cannot be called before the game starts")
}
ok, err := mipmap.ReadPixels(u.graphicsDriver, pixels, region)
if err != nil {
return err
@@ -167,13 +173,17 @@ func (u *UserInterface) dumpImages(dir string) (string, error) {
}
type RunOptions struct {
GraphicsLibrary GraphicsLibrary
InitUnfocused bool
ScreenTransparent bool
SkipTaskbar bool
SingleThread bool
X11ClassName string
X11InstanceName string
GraphicsLibrary GraphicsLibrary
InitUnfocused bool
ScreenTransparent bool
SkipTaskbar bool
SingleThread bool
DisableHiDPI bool
ColorSpace graphicsdriver.ColorSpace
ApplePressAndHoldEnabled bool
X11ClassName string
X11InstanceName string
StrictContextRestoration bool
}
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair.
@@ -196,41 +206,70 @@ func (u *UserInterface) setError(err error) {
}
func (u *UserInterface) IsScreenClearedEveryFrame() bool {
return atomic.LoadInt32(&u.isScreenClearedEveryFrame) != 0
return u.isScreenClearedEveryFrame.Load()
}
func (u *UserInterface) SetScreenClearedEveryFrame(cleared bool) {
v := int32(0)
if cleared {
v = 1
}
atomic.StoreInt32(&u.isScreenClearedEveryFrame, v)
u.isScreenClearedEveryFrame.Store(cleared)
}
func (u *UserInterface) setGraphicsLibrary(library GraphicsLibrary) {
atomic.StoreInt32(&u.graphicsLibrary, int32(library))
u.graphicsLibrary.Store(int32(library))
}
func (u *UserInterface) GraphicsLibrary() GraphicsLibrary {
return GraphicsLibrary(atomic.LoadInt32(&u.graphicsLibrary))
return GraphicsLibrary(u.graphicsLibrary.Load())
}
func (u *UserInterface) isRunning() bool {
return atomic.LoadInt32(&u.running) != 0 && !u.isTerminated()
return u.running.Load() && !u.isTerminated()
}
func (u *UserInterface) setRunning(running bool) {
if running {
atomic.StoreInt32(&u.running, 1)
} else {
atomic.StoreInt32(&u.running, 0)
}
u.running.Store(running)
}
func (u *UserInterface) isTerminated() bool {
return atomic.LoadInt32(&u.terminated) != 0
return u.terminated.Load()
}
func (u *UserInterface) setTerminated() {
atomic.StoreInt32(&u.terminated, 1)
u.terminated.Store(true)
}
func (u *UserInterface) Tick() int64 {
return u.tick.Load()
}
func (u *UserInterface) incrementTick() {
u.tick.Add(1)
u.inputTime.Store(int64(NewInputTimeFromTick(u.tick.Load())))
}
func (u *UserInterface) InputTime() InputTime {
t := InputTime(u.inputTime.Add(1))
if t.Subtick() == 0 {
panic("ui: too many input events in a tick")
}
return t
}
// inputTimeSubtickBits is the number of bits for a counter in a tick.
// An input time consists of a tick and a counter in a tick.
// This means that an input time will be invalid when 2^20 = 1048576 inputs are handled in a tick,
// but this should unlikely happen.
const inputTimeSubtickBits = 20
type InputTime int64
func NewInputTimeFromTick(tick int64) InputTime {
return InputTime(tick << inputTimeSubtickBits)
}
func (i InputTime) Tick() int64 {
return int64(i >> inputTimeSubtickBits)
}
func (i InputTime) Subtick() int64 {
return int64(i & ((1 << inputTimeSubtickBits) - 1))
}
+40 -19
View File
@@ -18,15 +18,21 @@ package ui
#include <jni.h>
#include <stdlib.h>
// Basically same as:
// The following JNI code works as this pseudo Java code:
//
// WindowService windowService = context.getSystemService(Context.WINDOW_SERVICE);
// Display display = windowManager.getDefaultDisplay();
// DisplayMetrics displayMetrics = new DisplayMetrics();
// display.getRealMetrics(displayMetrics);
// this.deviceScale = displayMetrics.density;
// return displayMetrics.widthPixels, displayMetrics.heightPixels, displayMetrics.density;
//
static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
#cgo noescape displayInfo
#cgo nocallback displayInfo
static void displayInfo(int* width, int* height, float* scale, uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
*width = 0;
*height = 0;
*scale = 1;
JavaVM* vm = (JavaVM*)java_vm;
JNIEnv* env = (JNIEnv*)jni_env;
jobject context = (jobject)ctx;
@@ -64,7 +70,15 @@ static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
env, display,
(*env)->GetMethodID(env, android_view_Display, "getRealMetrics", "(Landroid/util/DisplayMetrics;)V"),
displayMetrics);
const float density =
*width =
(*env)->GetIntField(
env, displayMetrics,
(*env)->GetFieldID(env, android_util_DisplayMetrics, "widthPixels", "I"));
*height =
(*env)->GetIntField(
env, displayMetrics,
(*env)->GetFieldID(env, android_util_DisplayMetrics, "heightPixels", "I"));
*scale =
(*env)->GetFloatField(
env, displayMetrics,
(*env)->GetFieldID(env, android_util_DisplayMetrics, "density", "F"));
@@ -78,15 +92,12 @@ static float deviceScale(uintptr_t java_vm, uintptr_t jni_env, uintptr_t ctx) {
(*env)->DeleteLocalRef(env, windowManager);
(*env)->DeleteLocalRef(env, display);
(*env)->DeleteLocalRef(env, displayMetrics);
return density;
}
*/
import "C"
import (
"errors"
"fmt"
"github.com/ebitengine/gomobile/app"
@@ -95,6 +106,7 @@ import (
)
type graphicsDriverCreatorImpl struct {
colorSpace graphicsdriver.ColorSpace
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@@ -118,18 +130,27 @@ func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, er
return nil, errors.New("ui: PlayStation 5 is not supported in this environment")
}
func deviceScaleFactorImpl() float64 {
var s float64
if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
// TODO: This might be crash when this is called from init(). How can we detect this?
s = float64(C.deviceScale(C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx)))
return nil
}); err != nil {
panic(fmt.Sprintf("devicescale: error %v", err))
}
return s
}
func dipToNativePixels(x float64, scale float64) float64 {
return x * scale
}
func dipFromNativePixels(x float64, scale float64) float64 {
return x / scale
}
func (u *UserInterface) displayInfo() (int, int, float64, bool) {
var cWidth, cHeight C.int
var cScale C.float
if err := app.RunOnJVM(func(vm, env, ctx uintptr) error {
C.displayInfo(&cWidth, &cHeight, &cScale, C.uintptr_t(vm), C.uintptr_t(env), C.uintptr_t(ctx))
return nil
}); err != nil {
// JVM is not ready yet.
// TODO: Fix gomobile to detect the error type for this case.
return 0, 0, 1, false
}
scale := float64(cScale)
width := int(dipFromNativePixels(float64(cWidth), scale))
height := int(dipFromNativePixels(float64(cHeight), scale))
return width, height, scale, true
}
+45 -17
View File
@@ -20,7 +20,6 @@ import (
"errors"
"fmt"
"reflect"
"unsafe"
"github.com/ebitengine/purego/objc"
@@ -79,8 +78,8 @@ func (u *UserInterface) initializePlatform() error {
// See cocoa_window.m in GLFW.
{
Cmd: sel_windowShouldClose,
Fn: func(id objc.ID, cmd objc.SEL, notification objc.ID) bool {
return id.Send(sel_origDelegate).Send(cmd, notification) != 0
Fn: func(id objc.ID, cmd objc.SEL, sender objc.ID) bool {
return id.Send(sel_origDelegate).Send(cmd, sender) != 0
},
},
{
@@ -165,8 +164,22 @@ func (u *UserInterface) initializePlatform() error {
return nil
}
func (u *UserInterface) setApplePressAndHoldEnabled(enabled bool) {
var val int
if enabled {
val = 1
}
defaults := objc.ID(class_NSMutableDictionary).Send(sel_alloc).Send(sel_init)
defaults.Send(sel_setObjectForKey,
objc.ID(class_NSNumber).Send(sel_alloc).Send(sel_initWithBool, val),
cocoa.NSString_alloc().InitWithUTF8String("ApplePressAndHoldEnabled").ID)
ud := objc.ID(class_NSUserDefaults).Send(sel_standardUserDefaults)
ud.Send(sel_registerDefaults, defaults)
}
type graphicsDriverCreatorImpl struct {
transparent bool
colorSpace graphicsdriver.ColorSpace
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@@ -189,8 +202,8 @@ func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error)
return nil, errors.New("ui: DirectX is not supported in this environment")
}
func (*graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics()
func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics(g.colorSpace)
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
@@ -218,13 +231,16 @@ func dipToGLFWPixel(x float64, scale float64) float64 {
return x
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {
return x, y
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int, error) {
return x, y, nil
}
var (
class_NSCursor = objc.GetClass("NSCursor")
class_NSEvent = objc.GetClass("NSEvent")
class_NSCursor = objc.GetClass("NSCursor")
class_NSEvent = objc.GetClass("NSEvent")
class_NSMutableDictionary = objc.GetClass("NSMutableDictionary")
class_NSNumber = objc.GetClass("NSNumber")
class_NSUserDefaults = objc.GetClass("NSUserDefaults")
)
var (
@@ -232,17 +248,21 @@ var (
sel_collectionBehavior = objc.RegisterName("collectionBehavior")
sel_delegate = objc.RegisterName("delegate")
sel_init = objc.RegisterName("init")
sel_initWithBool = objc.RegisterName("initWithBool:")
sel_initWithOrigDelegate = objc.RegisterName("initWithOrigDelegate:")
sel_mouseLocation = objc.RegisterName("mouseLocation")
sel_origDelegate = objc.RegisterName("origDelegate")
sel_origResizable = objc.RegisterName("isOrigResizable")
sel_registerDefaults = objc.RegisterName("registerDefaults:")
sel_setCollectionBehavior = objc.RegisterName("setCollectionBehavior:")
sel_setDelegate = objc.RegisterName("setDelegate:")
sel_setDocumentEdited = objc.RegisterName("setDocumentEdited:")
sel_setObjectForKey = objc.RegisterName("setObject:forKey:")
sel_setOrigDelegate = objc.RegisterName("setOrigDelegate:")
sel_setOrigResizable = objc.RegisterName("setOrigResizable:")
sel_standardUserDefaults = objc.RegisterName("standardUserDefaults")
sel_toggleFullScreen = objc.RegisterName("toggleFullScreen:")
sel_windowDidBecomeKey = objc.RegisterName("windowDidBecomeKey:")
sel_windowDidDeminiaturize = objc.RegisterName("windowDidDeminiaturize:")
sel_windowDidEnterFullScreen = objc.RegisterName("windowDidEnterFullScreen:")
sel_windowDidExitFullScreen = objc.RegisterName("windowDidExitFullScreen:")
sel_windowDidMiniaturize = objc.RegisterName("windowDidMiniaturize:")
@@ -256,13 +276,7 @@ var (
)
func currentMouseLocation() (x, y int) {
sig := cocoa.NSMethodSignature_signatureWithObjCTypes("{NSPoint=dd}@:")
inv := cocoa.NSInvocation_invocationWithMethodSignature(sig)
inv.SetTarget(objc.ID(class_NSEvent))
inv.SetSelector(sel_mouseLocation)
inv.Invoke()
var point cocoa.NSPoint
inv.GetReturnValue(unsafe.Pointer(&point))
point := objc.Send[cocoa.NSPoint](objc.ID(class_NSEvent), sel_mouseLocation)
x, y = int(point.X), int(point.Y)
@@ -434,3 +448,17 @@ func initializeWindowAfterCreation(w *glfw.Window) error {
func (u *UserInterface) skipTaskbar() error {
return nil
}
// setDocumentEdited must be called from the main thread.
func (u *UserInterface) setDocumentEdited(edited bool) error {
w, err := u.window.GetCocoaWindow()
if err != nil {
return err
}
objc.ID(w).Send(sel_setDocumentEdited, edited)
return nil
}
func (u *UserInterface) afterWindowCreation() error {
return nil
}
+81 -13
View File
@@ -24,8 +24,10 @@ import (
"os"
"runtime"
"sync"
"sync/atomic"
"time"
"github.com/hajimehoshi/ebiten/v2/internal/clock"
"github.com/hajimehoshi/ebiten/v2/internal/file"
"github.com/hajimehoshi/ebiten/v2/internal/gamepad"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
@@ -69,7 +71,7 @@ type userInterfaceImpl struct {
lastDeviceScaleFactor float64
initMonitor *Monitor
initMonitor atomic.Pointer[Monitor]
initFullscreen bool
initCursorMode CursorMode
initWindowDecorated bool
@@ -81,6 +83,8 @@ type userInterfaceImpl struct {
initWindowMaximized bool
initWindowMousePassthrough bool
initUnfocused bool
// bufferOnceSwapped must be accessed from the main thread.
bufferOnceSwapped bool
@@ -96,17 +100,22 @@ type userInterfaceImpl struct {
savedCursorX float64
savedCursorY float64
sizeCallback glfw.SizeCallback
closeCallback glfw.CloseCallback
framebufferSizeCallback glfw.FramebufferSizeCallback
defaultFramebufferSizeCallback glfw.FramebufferSizeCallback
dropCallback glfw.DropCallback
framebufferSizeCallbackCh chan struct{}
cachedCurrentMonitor *Monitor
cachedCurrentMonitorTime int64
darwinInitOnce sync.Once
showWindowOnce sync.Once
bufferOnceSwappedOnce sync.Once
// immContext is used only in Windows.
immContext uintptr
m sync.RWMutex
}
@@ -246,15 +255,11 @@ func (u *UserInterface) initializeGLFW() error {
}
func (u *UserInterface) setInitMonitor(m *Monitor) {
u.m.Lock()
defer u.m.Unlock()
u.initMonitor = m
u.initMonitor.Store(m)
}
func (u *UserInterface) getInitMonitor() *Monitor {
u.m.RLock()
defer u.m.RUnlock()
return u.initMonitor
return u.initMonitor.Load()
}
// AppendMonitors appends the current monitors to the passed in mons slice and returns it.
@@ -569,6 +574,22 @@ func (u *UserInterface) setWindowClosingHandled(handled bool) {
u.m.Lock()
u.windowClosingHandled = handled
u.m.Unlock()
if !u.isRunning() {
return
}
if u.isTerminated() {
return
}
u.mainThread.Call(func() {
if u.isTerminated() {
return
}
if err := u.setDocumentEdited(handled); err != nil {
u.setError(err)
return
}
})
}
// isFullscreen must be called from the main thread.
@@ -822,6 +843,8 @@ func (u *UserInterface) createWindow() error {
return err
}
u.window = window
// Set the running state true just a window is set (#2742).
u.setRunning(true)
// The position must be set before the size is set (#1982).
// setWindowSizeInDIP refers the current monitor's device scale.
@@ -873,6 +896,17 @@ func (u *UserInterface) createWindow() error {
return err
}
u.m.Lock()
closingHandled := u.windowClosingHandled
u.m.Unlock()
if err := u.setDocumentEdited(closingHandled); err != nil {
return err
}
if err := u.afterWindowCreation(); err != nil {
return err
}
return nil
}
@@ -1020,6 +1054,8 @@ event:
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
u.setApplePressAndHoldEnabled(options.ApplePressAndHoldEnabled)
if err := glfw.WindowHint(glfw.AutoIconify, glfw.False); err != nil {
return err
}
@@ -1058,6 +1094,7 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error {
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
transparent: options.ScreenTransparent,
colorSpace: options.ColorSpace,
}, options.GraphicsLibrary)
if err != nil {
return err
@@ -1087,6 +1124,7 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error {
return err
}
u.initUnfocused = options.InitUnfocused
focused := glfw.True
if options.InitUnfocused {
focused = glfw.False
@@ -1288,8 +1326,10 @@ func (u *UserInterface) update() (float64, float64, error) {
if err = u.window.Show(); err != nil {
return
}
if err = u.window.Focus(); err != nil {
return
if !u.initUnfocused {
if err = u.window.Focus(); err != nil {
return
}
}
if runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
@@ -1351,7 +1391,9 @@ func (u *UserInterface) update() (float64, float64, error) {
}
}
for !u.isRunnableOnUnfocused() {
// If isRunnableOnUnfocused is false and the window is not focused, wait here.
// For the first update, skip this check as the window might not be seen yet in some environments like ChromeOS (#3091).
for !u.isRunnableOnUnfocused() && u.bufferOnceSwapped {
// In the initial state on macOS, the window is not shown (#2620).
visible, err := u.window.GetAttrib(glfw.Visible)
if err != nil {
@@ -1830,6 +1872,21 @@ func (u *UserInterface) minimumWindowWidth() (int, error) {
//
// currentMonitor must be called on the main thread.
func (u *UserInterface) currentMonitor() (*Monitor, error) {
if u.cachedCurrentMonitor != nil && u.cachedCurrentMonitorTime > u.Tick()-int64(clock.TPS()) && theMonitors.contains(u.cachedCurrentMonitor) {
return u.cachedCurrentMonitor, nil
}
m, err := u.currentMonitorImpl()
if err != nil {
return nil, err
}
u.cachedCurrentMonitor = m
u.cachedCurrentMonitorTime = u.Tick()
return m, nil
}
// currentMonitorImpl must be called from the main thread.
func (u *UserInterface) currentMonitorImpl() (*Monitor, error) {
if u.window == nil {
return u.getInitMonitor(), nil
}
@@ -1863,7 +1920,13 @@ func (u *UserInterface) currentMonitor() (*Monitor, error) {
return m, nil
}
return theMonitors.primaryMonitor(), nil
if m := theMonitors.primaryMonitor(); m != nil {
return m, nil
}
// The primiary monitor might be missing even after the initialization (#3094, #3241).
// The reason is still unknown. As a workaround, return the initial monitor.
return u.getInitMonitor(), nil
}
func (u *UserInterface) readInputState(inputState *InputState) {
@@ -2074,7 +2137,12 @@ func (u *UserInterface) setWindowPositionInDIP(x, y int, monitor *Monitor) error
s := monitor.DeviceScaleFactor()
xf := dipToGLFWPixel(float64(x), s)
yf := dipToGLFWPixel(float64(y), s)
if x, y := u.adjustWindowPosition(mx+int(xf), my+int(yf), monitor); f {
x, y, err = u.adjustWindowPosition(mx+int(xf), my+int(yf), monitor)
if err != nil {
return err
}
if f {
u.setOrigWindowPos(x, y)
} else {
if err := u.window.SetPos(x, y); err != nil {
+60 -8
View File
@@ -19,8 +19,45 @@ package ui
//
// #import <UIKit/UIKit.h>
//
// static double devicePixelRatio() {
// return [[UIScreen mainScreen] nativeScale];
// static void displayInfoOnMainThread(float* width, float* height, float* scale, UIView* view) {
// *width = 0;
// *height = 0;
// *scale = 1;
// UIWindow* window = view.window;
// if (!window) {
// return;
// }
// UIWindowScene* scene = window.windowScene;
// if (!scene) {
// return;
// }
// CGRect bounds = scene.screen.bounds;
// *width = bounds.size.width;
// *height = bounds.size.height;
// *scale = scene.screen.nativeScale;
// }
//
// #cgo noescape displayInfo
// #cgo nocallback displayInfo
// static void displayInfo(float* width, float* height, float* scale, uintptr_t viewPtr) {
// *width = 0;
// *height = 0;
// *scale = 1;
// if (!viewPtr) {
// return;
// }
// UIView* view = (__bridge UIView*)(void*)viewPtr;
// if ([NSThread isMainThread]) {
// displayInfoOnMainThread(width, height, scale, view);
// return;
// }
// __block float w, h, s;
// dispatch_sync(dispatch_get_main_queue(), ^{
// displayInfoOnMainThread(&w, &h, &s, view);
// });
// *width = w;
// *height = h;
// *scale = s;
// }
import "C"
@@ -34,6 +71,7 @@ import (
)
type graphicsDriverCreatorImpl struct {
colorSpace graphicsdriver.ColorSpace
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@@ -57,7 +95,7 @@ func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error)
}
func (g *graphicsDriverCreatorImpl) newMetal() (graphicsdriver.Graphics, error) {
return metal.NewGraphics()
return metal.NewGraphics(g.colorSpace)
}
func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, error) {
@@ -65,6 +103,7 @@ func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, er
}
func (u *UserInterface) SetUIView(uiview uintptr) error {
u.uiView.Store(uiview)
select {
case err := <-u.errCh:
return err
@@ -88,11 +127,24 @@ func (u *UserInterface) IsGL() (bool, error) {
return u.GraphicsLibrary() == GraphicsLibraryOpenGL, nil
}
func deviceScaleFactorImpl() float64 {
// TODO: Can this be called from non-main threads?
return float64(C.devicePixelRatio())
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}
func dipFromNativePixels(x float64, scale float64) float64 {
return x
}
func (u *UserInterface) displayInfo() (int, int, float64, bool) {
view := u.uiView.Load()
if view == 0 {
return 0, 0, 1, false
}
var cWidth, cHeight, cScale C.float
C.displayInfo(&cWidth, &cHeight, &cScale, C.uintptr_t(view))
scale := float64(cScale)
width := int(dipFromNativePixels(float64(cWidth), scale))
height := int(dipFromNativePixels(float64(cHeight), scale))
return width, height, scale, true
}
+59 -22
View File
@@ -29,7 +29,8 @@ import (
)
type graphicsDriverCreatorImpl struct {
canvas js.Value
canvas js.Value
colorSpace graphicsdriver.ColorSpace
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@@ -38,7 +39,7 @@ func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, Graphics
}
func (g *graphicsDriverCreatorImpl) newOpenGL() (graphicsdriver.Graphics, error) {
return opengl.NewGraphics(g.canvas)
return opengl.NewGraphics(g.canvas, g.colorSpace)
}
func (*graphicsDriverCreatorImpl) newDirectX() (graphicsdriver.Graphics, error) {
@@ -96,15 +97,15 @@ type userInterfaceImpl struct {
cursorShape CursorShape
onceUpdateCalled bool
lastCaptureExitTime time.Time
hiDPIEnabled bool
context *context
inputState InputState
keyDurationsByKeyProperty map[Key]int
cursorXInClient float64
cursorYInClient float64
origCursorXInClient float64
origCursorYInClient float64
touchesInClient []touchInClient
context *context
inputState InputState
cursorXInClient float64
cursorYInClient float64
origCursorXInClient float64
origCursorYInClient float64
touchesInClient []touchInClient
savedCursorX float64
savedCursorY float64
@@ -369,7 +370,7 @@ func (u *UserInterface) needsUpdate() bool {
}
func (u *UserInterface) loopGame() error {
// Initialize the screen size first (#3033).
// Initialize the screen size first (#3034).
// If ebiten.SetRunnableOnUnfocused(false) and the canvas is not focused,
// suspended() returns true and the update routine cannot start.
u.updateScreenSize()
@@ -465,6 +466,7 @@ func (u *UserInterface) init() error {
runnableOnUnfocused: true,
savedCursorX: math.NaN(),
savedCursorY: math.NaN(),
hiDPIEnabled: true,
}
// document is undefined on node.js
@@ -512,6 +514,7 @@ func (u *UserInterface) init() error {
canvasStyle.Set("height", "100%")
canvasStyle.Set("margin", "0")
canvasStyle.Set("padding", "0")
canvasStyle.Set("display", "block")
// Make the canvas focusable.
canvas.Call("setAttribute", "tabindex", 1)
@@ -535,6 +538,10 @@ func (u *UserInterface) init() error {
}))
document.Call("addEventListener", "pointerlockerror", js.FuncOf(func(this js.Value, args []js.Value) any {
js.Global().Get("console").Call("error", "pointerlockerror event is fired. 'sandbox=\"allow-pointer-lock\"' might be required at an iframe. This function on browsers must be called as a result of a gestural interaction or orientation change.")
if u.cursorMode == CursorModeCaptured {
u.recoverCursorMode()
}
u.recoverCursorPosition()
return nil
}))
document.Call("addEventListener", "fullscreenerror", js.FuncOf(func(this js.Value, args []js.Value) any {
@@ -697,7 +704,7 @@ func (u *UserInterface) setCanvasEventHandlers(v js.Value) {
// Blur
v.Call("addEventListener", "blur", js.FuncOf(func(this js.Value, args []js.Value) any {
u.inputState.resetForBlur()
u.inputState.releaseAllButtons(u.InputTime())
return nil
}))
}
@@ -707,14 +714,21 @@ func (u *UserInterface) appendDroppedFiles(data js.Value) {
defer u.dropFileM.Unlock()
items := data.Get("items")
var entries []js.Value
for i := 0; i < items.Length(); i++ {
kind := items.Index(i).Get("kind").String()
switch kind {
case "file":
fs := items.Index(i).Call("webkitGetAsEntry").Get("filesystem").Get("root")
u.inputState.DroppedFiles = file.NewFileEntryFS(fs)
entries = append(entries, items.Index(i).Call("webkitGetAsEntry").Get("filesystem").Get("root"))
}
}
if len(entries) > 0 {
fs, err := file.NewFileEntryFS(entries)
if err != nil {
u.setError(err)
return
}
u.inputState.DroppedFiles = fs
}
}
@@ -732,18 +746,37 @@ func (u *UserInterface) forceUpdateOnMinimumFPSMode() {
}()
}
func (u *UserInterface) shouldFocusFirst(options *RunOptions) bool {
if options.InitUnfocused {
return false
}
if !window.Truthy() {
return false
}
// Do not focus the canvas when the current document is in an iframe.
// Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373).
parent := window.Get("parent")
isInIframe := !window.Get("location").Equal(parent.Get("location"))
if !isInIframe {
return true
}
return false
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
if !options.InitUnfocused && window.Truthy() {
// Do not focus the canvas when the current document is in an iframe.
// Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373).
isInIframe := !window.Get("location").Equal(window.Get("parent").Get("location"))
if !isInIframe {
canvas.Call("focus")
}
u.setRunning(true)
u.hiDPIEnabled = !options.DisableHiDPI
if u.shouldFocusFirst(options) {
canvas.Call("focus")
}
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
canvas: canvas,
canvas: canvas,
colorSpace: options.ColorSpace,
}, options.GraphicsLibrary)
if err != nil {
return err
@@ -791,6 +824,10 @@ func (m *Monitor) Name() string {
}
func (m *Monitor) DeviceScaleFactor() float64 {
if !theUI.hiDPIEnabled {
return 1
}
if m.deviceScaleFactor != 0 {
return m.deviceScaleFactor
}
+15 -2
View File
@@ -34,8 +34,13 @@ func (u *UserInterface) initializePlatform() error {
return nil
}
func (u *UserInterface) setApplePressAndHoldEnabled(enabled bool) {
// Do nothings.
}
type graphicsDriverCreatorImpl struct {
transparent bool
colorSpace graphicsdriver.ColorSpace
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@@ -130,8 +135,8 @@ func dipToGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x * deviceScaleFactor
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {
return x, y
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int, error) {
return x, y, nil
}
func initialMonitorByOS() (*Monitor, error) {
@@ -200,3 +205,11 @@ func initializeWindowAfterCreation(w *glfw.Window) error {
func (u *UserInterface) skipTaskbar() error {
return nil
}
func (u *UserInterface) setDocumentEdited(edited bool) error {
return nil
}
func (u *UserInterface) afterWindowCreation() error {
return nil
}
+65 -36
View File
@@ -28,6 +28,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/graphicscommand"
"github.com/hajimehoshi/ebiten/v2/internal/graphicsdriver"
"github.com/hajimehoshi/ebiten/v2/internal/hook"
"github.com/hajimehoshi/ebiten/v2/internal/restorable"
)
var (
@@ -40,7 +41,6 @@ var (
func (u *UserInterface) init() error {
u.userInterfaceImpl = userInterfaceImpl{
foreground: 1,
graphicsLibraryInitCh: make(chan struct{}),
errCh: make(chan error),
@@ -48,6 +48,7 @@ func (u *UserInterface) init() error {
outsideWidth: 640,
outsideHeight: 480,
}
u.foreground.Store(true)
return nil
}
@@ -89,7 +90,7 @@ type userInterfaceImpl struct {
outsideWidth float64
outsideHeight float64
foreground int32
foreground atomic.Bool
errCh chan error
context *context
@@ -97,18 +98,20 @@ type userInterfaceImpl struct {
inputState InputState
touches []TouchForInput
fpsMode int32
renderRequester RenderRequester
fpsMode atomic.Int32
renderer Renderer
strictContextRestoration atomic.Bool
strictContextRestorationOnce sync.Once
// uiView is used only on iOS.
uiView atomic.Uintptr
m sync.RWMutex
}
func (u *UserInterface) SetForeground(foreground bool) error {
var v int32
if foreground {
v = 1
}
atomic.StoreInt32(&u.foreground, v)
u.foreground.Store(foreground)
if foreground {
return hook.ResumeAudio()
@@ -147,13 +150,20 @@ func (u *UserInterface) runMobile(game Game, options *RunOptions) (err error) {
u.context = newContext(game)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
colorSpace: options.ColorSpace,
}, options.GraphicsLibrary)
if err != nil {
return err
}
u.graphicsDriver = g
u.setGraphicsLibrary(lib)
close(u.graphicsLibraryInitCh)
if options.StrictContextRestoration {
u.strictContextRestoration.Store(true)
} else {
restorable.Disable()
}
for {
if err := u.update(); err != nil {
@@ -220,7 +230,7 @@ func (u *UserInterface) SetFullscreen(fullscreen bool) {
}
func (u *UserInterface) IsFocused() bool {
return atomic.LoadInt32(&u.foreground) != 0
return u.foreground.Load()
}
func (u *UserInterface) IsRunnableOnUnfocused() bool {
@@ -232,19 +242,19 @@ func (u *UserInterface) SetRunnableOnUnfocused(runnableOnUnfocused bool) {
}
func (u *UserInterface) FPSMode() FPSModeType {
return FPSModeType(atomic.LoadInt32(&u.fpsMode))
return FPSModeType(u.fpsMode.Load())
}
func (u *UserInterface) SetFPSMode(mode FPSModeType) {
atomic.StoreInt32(&u.fpsMode, int32(mode))
u.fpsMode.Store(int32(mode))
u.updateExplicitRenderingModeIfNeeded(mode)
}
func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) {
if u.renderRequester == nil {
if u.renderer == nil {
return
}
u.renderRequester.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
u.renderer.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum)
}
func (u *UserInterface) readInputState(inputState *InputState) {
@@ -258,8 +268,10 @@ func (u *UserInterface) Window() Window {
}
type Monitor struct {
deviceScaleFactor float64
deviceScaleFactorOnce sync.Once
width int
height int
deviceScaleFactor float64
inited atomic.Bool
m sync.Mutex
}
@@ -270,22 +282,35 @@ func (m *Monitor) Name() string {
return ""
}
func (m *Monitor) DeviceScaleFactor() float64 {
func (m *Monitor) ensureInit() {
if m.inited.Load() {
return
}
m.m.Lock()
defer m.m.Unlock()
// Re-check the state since the state might be changed while locking.
if m.inited.Load() {
return
}
width, height, scale, ok := theUI.displayInfo()
if !ok {
return
}
m.width = width
m.height = height
m.deviceScaleFactor = scale
m.inited.Store(true)
}
// The device scale factor can be obtained after the main function starts, especially on Android.
// Initialize this lazily.
m.deviceScaleFactorOnce.Do(func() {
// Assume that the device scale factor never changes on mobiles.
m.deviceScaleFactor = deviceScaleFactorImpl()
})
func (m *Monitor) DeviceScaleFactor() float64 {
m.ensureInit()
return m.deviceScaleFactor
}
func (m *Monitor) Size() (int, int) {
// TODO: Return a valid value.
return 0, 0
m.ensureInit()
return m.width, m.height
}
func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor {
@@ -296,26 +321,26 @@ func (u *UserInterface) Monitor() *Monitor {
return theMonitor
}
func (u *UserInterface) UpdateInput(keys map[Key]struct{}, runes []rune, touches []TouchForInput) {
u.updateInputStateFromOutside(keys, runes, touches)
if FPSModeType(atomic.LoadInt32(&u.fpsMode)) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
func (u *UserInterface) UpdateInput(keyPressedTimes, keyReleasedTimes [KeyMax + 1]InputTime, runes []rune, touches []TouchForInput) {
u.updateInputStateFromOutside(keyPressedTimes, keyReleasedTimes, runes, touches)
if FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderer.RequestRenderIfNeeded()
}
}
type RenderRequester interface {
type Renderer interface {
SetExplicitRenderingMode(explicitRendering bool)
RequestRenderIfNeeded()
}
func (u *UserInterface) SetRenderRequester(renderRequester RenderRequester) {
u.renderRequester = renderRequester
u.updateExplicitRenderingModeIfNeeded(FPSModeType(atomic.LoadInt32(&u.fpsMode)))
func (u *UserInterface) SetRenderer(renderer Renderer) {
u.renderer = renderer
u.updateExplicitRenderingModeIfNeeded(FPSModeType(u.fpsMode.Load()))
}
func (u *UserInterface) ScheduleFrame() {
if u.renderRequester != nil && FPSModeType(atomic.LoadInt32(&u.fpsMode)) == FPSModeVsyncOffMinimum {
u.renderRequester.RequestRenderIfNeeded()
if u.renderer != nil && FPSModeType(u.fpsMode.Load()) == FPSModeVsyncOffMinimum {
u.renderer.RequestRenderIfNeeded()
}
}
@@ -323,6 +348,10 @@ func (u *UserInterface) updateIconIfNeeded() error {
return nil
}
func (u *UserInterface) UsesStrictContextRestoration() bool {
return u.strictContextRestoration.Load()
}
func IsScreenTransparentAvailable() bool {
return false
}
@@ -73,6 +73,8 @@ func (u *UserInterface) init() error {
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
u.setRunning(true)
n := C.ebitengine_Initialize()
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
nativeWindow: n,
@@ -69,6 +69,8 @@ func (u *UserInterface) init() error {
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
u.setRunning(true)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary)
if err != nil {
return err
+54 -4
View File
@@ -34,8 +34,13 @@ func (u *UserInterface) initializePlatform() error {
return nil
}
func (u *UserInterface) setApplePressAndHoldEnabled(enabled bool) {
// Do nothings.
}
type graphicsDriverCreatorImpl struct {
transparent bool
colorSpace graphicsdriver.ColorSpace
}
func (g *graphicsDriverCreatorImpl) newAuto() (graphicsdriver.Graphics, GraphicsLibrary, error) {
@@ -109,9 +114,18 @@ func dipToGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x * deviceScaleFactor
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int, error) {
if microsoftgdk.IsXbox() {
return x, y
return x, y, nil
}
// If a window is not decorated, the window should be able to reach the top of the screen (#3118).
d, err := u.window.GetAttrib(glfw.Decorated)
if err != nil {
return 0, 0, err
}
if d == glfw.False {
return x, y, nil
}
mx := monitor.boundsInGLFWPixels.Min.X
@@ -123,12 +137,12 @@ func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, i
}
t, err := _GetSystemMetrics(_SM_CYCAPTION)
if err != nil {
panic(err)
return 0, 0, err
}
if y < my+int(t) {
y = my + int(t)
}
return x, y
return x, y, nil
}
func initialMonitorByOS() (*Monitor, error) {
@@ -242,6 +256,42 @@ func (u *UserInterface) skipTaskbar() error {
return nil
}
func (u *UserInterface) setDocumentEdited(edited bool) error {
return nil
}
func (u *UserInterface) afterWindowCreation() error {
if microsoftgdk.IsXbox() {
return nil
}
// By default, IME should be disabled (#2918).
w, err := u.window.GetWin32Window()
if err != nil {
return err
}
c, err := _ImmAssociateContext(w, 0)
if err != nil {
return err
}
u.immContext = c
return nil
}
// RestoreIMMContextOnMainThread is called from the main thread.
// The textinput package invokes RestoreIMMContextOnMainThread to enable IME inputting.
func (u *UserInterface) RestoreIMMContextOnMainThread() error {
w, err := u.window.GetWin32Window()
if err != nil {
return err
}
if _, err := _ImmAssociateContext(w, u.immContext); err != nil {
return err
}
u.immContext = 0
return nil
}
func init() {
if microsoftgdk.IsXbox() {
// TimeBeginPeriod might not be defined in Xbox.
+4
View File
@@ -43,6 +43,7 @@ type Window interface {
IsClosingHandled() bool
SetMousePassthrough(enabled bool)
IsMousePassthrough() bool
RequestAttention()
}
type nullWindow struct{}
@@ -128,3 +129,6 @@ func (*nullWindow) SetMousePassthrough(enabled bool) {
func (*nullWindow) IsMousePassthrough() bool {
return false
}
func (*nullWindow) RequestAttention() {
}
+19
View File
@@ -506,3 +506,22 @@ func (w *glfwWindow) IsMousePassthrough() bool {
})
return v
}
func (w *glfwWindow) RequestAttention() {
if w.ui.isTerminated() {
return
}
if !w.ui.isRunning() {
// Do nothing
return
}
w.ui.mainThread.Call(func() {
if w.ui.isTerminated() {
return
}
if err := w.ui.window.RequestAttention(); err != nil {
w.ui.setError(err)
return
}
})
}