internal/ui: defer GLFW initialization to RunGame

Move initializePlatform(), initializeGLFW(), and the monitor callback
registration from UserInterface.init() (called at package init time)
to initOnMainThread() (called only when RunGame() is invoked).

This prevents go test from hanging for packages that transitively
import Ebitengine, since GLFW initialization requires a display and
blocks in headless environments.

Also move initializeWindowPositionIfNeeded logic into initOnMainThread
since it depends on the monitor being available, which requires GLFW.
The window size and position-set flag are now passed through RunOptions.

Closes #3430

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hajime Hoshi
2026-04-08 17:46:07 +09:00
parent 03c0a55d41
commit 3e3b8b1662
4 changed files with 54 additions and 24 deletions
+3
View File
@@ -181,6 +181,9 @@ type RunOptions struct {
ApplePressAndHoldEnabled bool
X11ClassName string
X11InstanceName string
InitWindowWidthInDIP int
InitWindowHeightInDIP int
WindowPositionSet bool
}
// InitialWindowPosition returns the position for centering the given second width/height pair within the first width/height pair.
+47 -14
View File
@@ -118,6 +118,7 @@ type userInterfaceImpl struct {
cachedCurrentMonitor *Monitor
cachedCurrentMonitorTime int64
initOnce sync.Once
darwinInitOnce sync.Once
showWindowOnce sync.Once
bufferOnceSwappedOnce sync.Once
@@ -161,20 +162,6 @@ func (u *UserInterface) init() error {
u.iwindow.ui = u
if err := u.initializePlatform(); err != nil {
return err
}
if err := u.initializeGLFW(); err != nil {
return err
}
if _, err := glfw.SetMonitorCallback(func(monitor *glfw.Monitor, event glfw.PeripheralEvent) {
if err := theMonitors.update(); err != nil {
u.setError(err)
}
}); err != nil {
return err
}
return nil
}
@@ -265,6 +252,30 @@ func (u *UserInterface) initializeGLFW() error {
return nil
}
// ensureGLFWInit lazily initializes GLFW and related state on the first call.
// This is safe to call multiple times; initialization happens only once.
func (u *UserInterface) ensureGLFWInit() error {
u.initOnce.Do(func() {
if err := u.initializePlatform(); err != nil {
u.setError(err)
return
}
if err := u.initializeGLFW(); err != nil {
u.setError(err)
return
}
if _, err := glfw.SetMonitorCallback(func(monitor *glfw.Monitor, event glfw.PeripheralEvent) {
if err := theMonitors.update(); err != nil {
u.setError(err)
}
}); err != nil {
u.setError(err)
return
}
})
return u.error()
}
func (u *UserInterface) setInitMonitor(m *Monitor) {
u.initMonitor.Store(m)
}
@@ -275,12 +286,20 @@ func (u *UserInterface) getInitMonitor() *Monitor {
// AppendMonitors appends the current monitors to the passed in mons slice and returns it.
func (u *UserInterface) AppendMonitors(monitors []*Monitor) []*Monitor {
// Ensure GLFW is initialized so that the monitor list is available.
if err := u.ensureGLFWInit(); err != nil {
return monitors
}
return theMonitors.append(monitors)
}
// Monitor returns the window's current monitor. Returns nil if there is no current monitor yet.
func (u *UserInterface) Monitor() *Monitor {
if !u.isRunning() {
// Ensure GLFW is initialized so that the init monitor is available.
if err := u.ensureGLFWInit(); err != nil {
return nil
}
return u.getInitMonitor()
}
var monitor *Monitor
@@ -989,6 +1008,20 @@ event:
}
func (u *UserInterface) initOnMainThread(options *RunOptions) error {
if err := u.ensureGLFWInit(); err != nil {
return err
}
// Center the window on the monitor if the position was not explicitly set.
if !options.WindowPositionSet {
m := u.getInitMonitor()
if m != nil {
sw, sh := m.sizeInDIP()
x, y := InitialWindowPosition(int(sw), int(sh), options.InitWindowWidthInDIP, options.InitWindowHeightInDIP)
u.Window().SetPosition(x, y)
}
}
u.setApplePressAndHoldEnabled(options.ApplePressAndHoldEnabled)
if err := glfw.WindowHint(glfw.AutoIconify, glfw.False); err != nil {
+4 -2
View File
@@ -350,9 +350,11 @@ type RunGameOptions struct {
func RunGameWithOptions(game Game, options *RunGameOptions) error {
defer isRunGameEnded_.Store(true)
initializeWindowPositionIfNeeded(WindowSize())
op := toUIRunOptions(options)
ww, wh := WindowSize()
op.InitWindowWidthInDIP = ww
op.InitWindowHeightInDIP = wh
op.WindowPositionSet = windowPositionSetExplicitly.Load()
// This is necessary to change the result of IsScreenTransparent.
screenTransparent.Store(op.ScreenTransparent)
-8
View File
@@ -184,14 +184,6 @@ var (
windowPositionSetExplicitly atomic.Bool
)
func initializeWindowPositionIfNeeded(width, height int) {
if !windowPositionSetExplicitly.Load() {
sw, sh := ui.Get().Monitor().Size()
x, y := ui.InitialWindowPosition(sw, sh, width, height)
ui.Get().Window().SetPosition(x, y)
}
}
// WindowSize returns the window size on desktops.
// WindowSize returns (0, 0) on other environments.
//