internal/ui: bug fix: panic at ReadPixels before running

Closes #2820
This commit is contained in:
Hajime Hoshi 2023-10-27 11:01:01 +09:00
parent 4eb9b3a152
commit 2f6df3d4d6
11 changed files with 98 additions and 36 deletions

View File

@ -0,0 +1,47 @@
// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build ignore
package main
import (
"time"
"github.com/hajimehoshi/ebiten/v2"
)
func init() {
defer func() {
if e := recover(); e == nil {
panic("(*ebiten.Image).At before the main loop must panic but not (1)")
}
}()
done := make(chan struct{})
go func() {
select {
case <-done:
panic("(*ebiten.Image).At before the main loop must panic but not (2)")
case <-time.After(time.Second):
panic("(*ebiten.Image).At times out unexpectedly")
}
}()
img := ebiten.NewImage(1, 1)
img.At(0, 0)
close(done)
}
func main() {
}

View File

@ -154,6 +154,10 @@ func (i *Image) WritePixels(pix []byte, region image.Rectangle) {
} }
func (i *Image) ReadPixels(pixels []byte, region image.Rectangle) { func (i *Image) ReadPixels(pixels []byte, region image.Rectangle) {
if !i.ui.isRunning() {
panic("ui: ReadPixels cannot be called before the game starts")
}
// Check the error existence and avoid unnecessary calls. // Check the error existence and avoid unnecessary calls.
if i.ui.error() != nil { if i.ui.error() != nil {
return return

View File

@ -200,7 +200,7 @@ func init() {
} }
func (u *UserInterface) KeyName(key Key) string { func (u *UserInterface) KeyName(key Key) string {
if !u.running { if !u.isRunning() {
return "" return ""
} }

View File

@ -26,8 +26,6 @@ import (
) )
func (u *UserInterface) Run(game Game, options *RunOptions) error { func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.context = newContext(game)
u.mainThread = thread.NewOSThread() u.mainThread = thread.NewOSThread()
u.renderThread = thread.NewOSThread() u.renderThread = thread.NewOSThread()
graphicscommand.SetRenderThread(u.renderThread) graphicscommand.SetRenderThread(u.renderThread)
@ -38,6 +36,8 @@ func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.setRunning(true) u.setRunning(true)
defer u.setRunning(false) defer u.setRunning(false)
u.context = newContext(game)
if err := u.initOnMainThread(options); err != nil { if err := u.initOnMainThread(options); err != nil {
return err return err
} }

View File

@ -22,8 +22,6 @@ import (
) )
func (u *UserInterface) Run(game Game, options *RunOptions) error { func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.context = newContext(game)
// Initialize the main thread first so the thread is available at u.run (#809). // Initialize the main thread first so the thread is available at u.run (#809).
u.mainThread = thread.NewNoopThread() u.mainThread = thread.NewNoopThread()
u.renderThread = thread.NewNoopThread() u.renderThread = thread.NewNoopThread()
@ -32,6 +30,8 @@ func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.setRunning(true) u.setRunning(true)
defer u.setRunning(false) defer u.setRunning(false)
u.context = newContext(game)
if err := u.initOnMainThread(options); err != nil { if err := u.initOnMainThread(options); err != nil {
return err return err
} }

View File

@ -74,6 +74,8 @@ type UserInterface struct {
isScreenClearedEveryFrame int32 isScreenClearedEveryFrame int32
graphicsLibrary int32 graphicsLibrary int32
running int32
terminated int32
whiteImage *Image whiteImage *Image
@ -176,3 +178,23 @@ func (u *UserInterface) setGraphicsLibrary(library GraphicsLibrary) {
func (u *UserInterface) GraphicsLibrary() GraphicsLibrary { func (u *UserInterface) GraphicsLibrary() GraphicsLibrary {
return GraphicsLibrary(atomic.LoadInt32(&u.graphicsLibrary)) return GraphicsLibrary(atomic.LoadInt32(&u.graphicsLibrary))
} }
func (u *UserInterface) isRunning() bool {
return atomic.LoadInt32(&u.running) != 0 && !u.isTerminated()
}
func (u *UserInterface) setRunning(running bool) {
if running {
atomic.StoreInt32(&u.running, 1)
} else {
atomic.StoreInt32(&u.running, 0)
}
}
func (u *UserInterface) isTerminated() bool {
return atomic.LoadInt32(&u.terminated) != 0
}
func (u *UserInterface) setTerminated() {
atomic.StoreInt32(&u.terminated, 1)
}

View File

@ -24,7 +24,6 @@ import (
"os" "os"
"runtime" "runtime"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/hajimehoshi/ebiten/v2/internal/file" "github.com/hajimehoshi/ebiten/v2/internal/file"
@ -61,8 +60,6 @@ type userInterfaceImpl struct {
maxWindowWidthInDIP int maxWindowWidthInDIP int
maxWindowHeightInDIP int maxWindowHeightInDIP int
running uint32
terminated uint32
runnableOnUnfocused bool runnableOnUnfocused bool
fpsMode FPSModeType fpsMode FPSModeType
iconImages []image.Image iconImages []image.Image
@ -286,26 +283,6 @@ func (u *UserInterface) Monitor() *Monitor {
return monitor return monitor
} }
func (u *UserInterface) isRunning() bool {
return atomic.LoadUint32(&u.running) != 0 && !u.isTerminated()
}
func (u *UserInterface) isTerminated() bool {
return atomic.LoadUint32(&u.terminated) != 0
}
func (u *UserInterface) setRunning(running bool) {
if running {
atomic.StoreUint32(&u.running, 1)
} else {
atomic.StoreUint32(&u.running, 0)
}
}
func (u *UserInterface) setTerminated() {
atomic.StoreUint32(&u.terminated, 1)
}
// setWindowMonitor must be called on the main thread. // setWindowMonitor must be called on the main thread.
func (u *UserInterface) setWindowMonitor(monitor *Monitor) error { func (u *UserInterface) setWindowMonitor(monitor *Monitor) error {
if microsoftgdk.IsXbox() { if microsoftgdk.IsXbox() {

View File

@ -91,7 +91,6 @@ type userInterfaceImpl struct {
runnableOnUnfocused bool runnableOnUnfocused bool
fpsMode FPSModeType fpsMode FPSModeType
renderingScheduled bool renderingScheduled bool
running bool
cursorMode CursorMode cursorMode CursorMode
cursorPrevMode CursorMode cursorPrevMode CursorMode
captureCursorLater bool captureCursorLater bool
@ -744,6 +743,9 @@ func (u *UserInterface) forceUpdateOnMinimumFPSMode() {
} }
func (u *UserInterface) Run(game Game, options *RunOptions) error { func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.setRunning(true)
defer u.setRunning(false)
if !options.InitUnfocused && window.Truthy() { if !options.InitUnfocused && window.Truthy() {
// Do not focus the canvas when the current document is in an iframe. // 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). // Otherwise, the parent page tries to focus the iframe on every loading, which is annoying (#1373).
@ -752,7 +754,7 @@ func (u *UserInterface) Run(game Game, options *RunOptions) error {
canvas.Call("focus") canvas.Call("focus")
} }
} }
u.running = true
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
canvas: canvas, canvas: canvas,
}, options.GraphicsLibrary) }, options.GraphicsLibrary)

View File

@ -269,8 +269,6 @@ func (u *UserInterface) run(game Game, mainloop bool, options *RunOptions) (err
} }
}() }()
u.context = newContext(game)
var mgl gl.Context var mgl gl.Context
if mainloop { if mainloop {
// When gomobile-build is used, GL functions must be called via // When gomobile-build is used, GL functions must be called via
@ -281,6 +279,11 @@ func (u *UserInterface) run(game Game, mainloop bool, options *RunOptions) (err
graphicscommand.SetRenderThread(u.renderThread) graphicscommand.SetRenderThread(u.renderThread)
} }
u.setRunning(true)
defer u.setRunning(false)
u.context = newContext(game)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{ g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{
gomobileContext: mgl, gomobileContext: mgl,
}, options.GraphicsLibrary) }, options.GraphicsLibrary)

View File

@ -84,7 +84,15 @@ func (u *UserInterface) init() error {
} }
func (u *UserInterface) Run(game Game, options *RunOptions) error { func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.mainThread = thread.NewOSThread()
u.renderThread = thread.NewOSThread()
graphicscommand.SetRenderThread(u.renderThread)
u.setRunning(true)
defer u.setRunning(false)
u.context = newContext(game) u.context = newContext(game)
g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary) g, lib, err := newGraphicsDriver(&graphicsDriverCreatorImpl{}, options.GraphicsLibrary)
if err != nil { if err != nil {
return err return err
@ -99,10 +107,6 @@ func (u *UserInterface) Run(game Game, options *RunOptions) error {
initializeProfiler() initializeProfiler()
u.mainThread = thread.NewOSThread()
u.renderThread = thread.NewOSThread()
graphicscommand.SetRenderThread(u.renderThread)
ctx, cancel := stdcontext.WithCancel(stdcontext.Background()) ctx, cancel := stdcontext.WithCancel(stdcontext.Background())
defer cancel() defer cancel()

View File

@ -64,6 +64,9 @@ func (u *UserInterface) init() error {
} }
func (u *UserInterface) Run(game Game, options *RunOptions) error { func (u *UserInterface) Run(game Game, options *RunOptions) error {
u.setRunning(true)
defer u.setRunning(false)
// TODO: Implement this. // TODO: Implement this.
return nil return nil
} }