diff --git a/examples/fullscreen/main.go b/examples/fullscreen/main.go index 187aecd2b..b8de6df2e 100644 --- a/examples/fullscreen/main.go +++ b/examples/fullscreen/main.go @@ -72,7 +72,7 @@ func (g *Game) Update() error { } func (g *Game) Draw(screen *ebiten.Image) { - scale := ebiten.DeviceScaleFactor() + scale := ebiten.Monitor().DeviceScaleFactor() w, h := gophersImage.Bounds().Dx(), gophersImage.Bounds().Dy() op := &ebiten.DrawImageOptions{} @@ -99,15 +99,15 @@ func (g *Game) Draw(screen *ebiten.Image) { textOp := &text.DrawOptions{} textOp.GeoM.Translate(50*scale, 50*scale) textOp.ColorScale.ScaleWithColor(color.White) - textOp.LineSpacing = 12 * ebiten.DeviceScaleFactor() * 1.5 + textOp.LineSpacing = 12 * ebiten.Monitor().DeviceScaleFactor() * 1.5 text.Draw(screen, msg, &text.GoTextFace{ Source: mplusFaceSource, - Size: 12 * ebiten.DeviceScaleFactor(), + Size: 12 * ebiten.Monitor().DeviceScaleFactor(), }, textOp) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { - s := ebiten.DeviceScaleFactor() + s := ebiten.Monitor().DeviceScaleFactor() return int(float64(outsideWidth) * s), int(float64(outsideHeight) * s) } diff --git a/examples/highdpi/main.go b/examples/highdpi/main.go index 240197429..23f653593 100644 --- a/examples/highdpi/main.go +++ b/examples/highdpi/main.go @@ -84,7 +84,7 @@ func (g *Game) Draw(screen *ebiten.Image) { // Scale the image by the device ratio so that the rendering result can be same // on various (different-DPI) environments. - scale := ebiten.DeviceScaleFactor() + scale := ebiten.Monitor().DeviceScaleFactor() op.GeoM.Scale(scale, scale) // Move the image's center to the screen's center. @@ -99,7 +99,7 @@ func (g *Game) Draw(screen *ebiten.Image) { func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { // The unit of outsideWidth/Height is device-independent pixels. // By multiplying them by the device scale factor, we can get a hi-DPI screen size. - s := ebiten.DeviceScaleFactor() + s := ebiten.Monitor().DeviceScaleFactor() return int(float64(outsideWidth) * s), int(float64(outsideHeight) * s) } diff --git a/examples/windowsize/main.go b/examples/windowsize/main.go index be130f4d7..4d352c150 100644 --- a/examples/windowsize/main.go +++ b/examples/windowsize/main.go @@ -376,7 +376,7 @@ Window size limitation: (%d, %d) - (%d, %d) Cursor: (%d, %d) TPS: Current: %0.2f / Max: %s FPS: %0.2f -Device Scale Factor: %0.2f`, msgM, msgR, fg, wx, wy, ww, wh, minw, minh, maxw, maxh, cx, cy, ebiten.ActualTPS(), tpsStr, ebiten.ActualFPS(), ebiten.DeviceScaleFactor()) +Device Scale Factor: %0.2f`, msgM, msgR, fg, wx, wy, ww, wh, minw, minh, maxw, maxh, cx, cy, ebiten.ActualTPS(), tpsStr, ebiten.ActualFPS(), ebiten.Monitor().DeviceScaleFactor()) ebitenutil.DebugPrint(screen, msg) } @@ -400,7 +400,7 @@ func parseWindowPosition() (int, int, bool) { } func main() { - fmt.Printf("Device scale factor: %0.2f\n", ebiten.DeviceScaleFactor()) + fmt.Printf("Device scale factor: %0.2f\n", ebiten.Monitor().DeviceScaleFactor()) w, h := ebiten.ScreenSizeInFullscreen() fmt.Printf("Screen size in fullscreen: %d, %d\n", w, h) diff --git a/internal/ui/context.go b/internal/ui/context.go index 7abcf395a..d045e3b5e 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -286,5 +286,5 @@ func (c *context) screenScaleAndOffsets() (scale, offsetX, offsetY float64) { } func (u *UserInterface) LogicalPositionToClientPosition(x, y float64) (float64, float64) { - return u.context.logicalPositionToClientPosition(x, y, u.DeviceScaleFactor()) + return u.context.logicalPositionToClientPosition(x, y, u.Monitor().DeviceScaleFactor()) } diff --git a/internal/ui/input_glfw.go b/internal/ui/input_glfw.go index feee4a7eb..cff97e880 100644 --- a/internal/ui/input_glfw.go +++ b/internal/ui/input_glfw.go @@ -86,7 +86,7 @@ func (u *UserInterface) updateInputStateImpl() error { if err != nil { return err } - s := m.deviceScaleFactor() + s := m.DeviceScaleFactor() cx, cy := u.savedCursorX, u.savedCursorY defer func() { diff --git a/internal/ui/input_js.go b/internal/ui/input_js.go index 0d8ce47c6..4f5ff6c41 100644 --- a/internal/ui/input_js.go +++ b/internal/ui/input_js.go @@ -305,7 +305,7 @@ func (u *UserInterface) updateInputState() error { u.keyDurationsByKeyProperty[key]++ } - s := u.DeviceScaleFactor() + s := theMonitor.DeviceScaleFactor() if !math.IsNaN(u.savedCursorX) && !math.IsNaN(u.savedCursorY) { // If savedCursorX and savedCursorY are valid values, the cursor is saved just before entering or exiting from fullscreen. diff --git a/internal/ui/input_mobile.go b/internal/ui/input_mobile.go index 582cd3a18..e2fb5ca52 100644 --- a/internal/ui/input_mobile.go +++ b/internal/ui/input_mobile.go @@ -47,7 +47,7 @@ func (u *UserInterface) updateInputState() error { u.m.Lock() defer u.m.Unlock() - s := u.DeviceScaleFactor() + s := theMonitor.DeviceScaleFactor() u.inputState.Touches = u.inputState.Touches[:0] for _, t := range u.touches { diff --git a/internal/ui/input_nintendosdk.go b/internal/ui/input_nintendosdk.go index 281bf2cb0..f800f3947 100644 --- a/internal/ui/input_nintendosdk.go +++ b/internal/ui/input_nintendosdk.go @@ -60,7 +60,7 @@ func (u *UserInterface) updateInputStateImpl() error { u.inputState.Touches = u.inputState.Touches[:0] for _, t := range u.nativeTouches { - x, y := u.context.clientPositionToLogicalPosition(float64(t.x), float64(t.y), deviceScaleFactor) + x, y := u.context.clientPositionToLogicalPosition(float64(t.x), float64(t.y), theMonitor.DeviceScaleFactor()) u.inputState.Touches = append(u.inputState.Touches, Touch{ ID: TouchID(t.id), X: int(x), diff --git a/internal/ui/monitor_glfw.go b/internal/ui/monitor_glfw.go index 6509d3d89..fa9496db5 100644 --- a/internal/ui/monitor_glfw.go +++ b/internal/ui/monitor_glfw.go @@ -40,7 +40,8 @@ func (m *Monitor) Name() string { return m.name } -func (m *Monitor) deviceScaleFactor() float64 { +// DeviceScaleFactor is concurrent-safe as contentScale is immutable. +func (m *Monitor) DeviceScaleFactor() float64 { // It is rare, but monitor can be nil when glfw.GetPrimaryMonitor returns nil. // In this case, return 1 as a tentative scale (#1878). if m == nil { diff --git a/internal/ui/ui_glfw.go b/internal/ui/ui_glfw.go index 96ea90d75..90e863eb0 100644 --- a/internal/ui/ui_glfw.go +++ b/internal/ui/ui_glfw.go @@ -833,29 +833,6 @@ func (u *UserInterface) SetCursorShape(shape CursorShape) { }) } -func (u *UserInterface) DeviceScaleFactor() float64 { - if u.isTerminated() { - return 0 - } - if !u.isRunning() { - return u.getInitMonitor().deviceScaleFactor() - } - - var f float64 - u.mainThread.Call(func() { - if u.isTerminated() { - return - } - m, err := u.currentMonitor() - if err != nil { - u.setError(err) - return - } - f = m.deviceScaleFactor() - }) - return f -} - // createWindow creates a GLFW window. // // createWindow must be called from the main thread. @@ -988,7 +965,7 @@ func (u *UserInterface) registerWindowFramebufferSizeCallback() error { u.setError(err) return } - s := m.deviceScaleFactor() + s := m.DeviceScaleFactor() ww := int(float64(w) / s) wh := int(float64(h) / s) if err := u.setWindowSizeInDIP(ww, wh, false); err != nil { @@ -1460,7 +1437,7 @@ func (u *UserInterface) updateGame() error { if err != nil { return } - deviceScaleFactor = m.deviceScaleFactor() + deviceScaleFactor = m.DeviceScaleFactor() }); err != nil { return err } @@ -1639,7 +1616,7 @@ func (u *UserInterface) setWindowSizeInDIP(width, height int, callSetSize bool) if err != nil { return err } - scale := mon.deviceScaleFactor() + scale := mon.DeviceScaleFactor() if u.origWindowWidthInDIP == width && u.origWindowHeightInDIP == height && u.lastDeviceScaleFactor == scale { return nil } diff --git a/internal/ui/ui_js.go b/internal/ui/ui_js.go index ce1d956ca..fc16e05db 100644 --- a/internal/ui/ui_js.go +++ b/internal/ui/ui_js.go @@ -97,8 +97,6 @@ type userInterfaceImpl struct { onceUpdateCalled bool lastCaptureExitTime time.Time - deviceScaleFactor float64 - context *context inputState InputState keyDurationsByKeyProperty map[Key]int @@ -271,19 +269,6 @@ func (u *UserInterface) SetCursorShape(shape CursorShape) { } } -func (u *UserInterface) DeviceScaleFactor() float64 { - if u.deviceScaleFactor != 0 { - return u.deviceScaleFactor - } - - ratio := window.Get("devicePixelRatio").Float() - if ratio == 0 { - ratio = 1 - } - u.deviceScaleFactor = ratio - return u.deviceScaleFactor -} - func (u *UserInterface) outsideSize() (float64, float64) { if document.Truthy() { body := document.Get("body") @@ -365,11 +350,11 @@ func (u *UserInterface) updateImpl(force bool) error { w, h := u.outsideSize() if force { - if err := u.context.forceUpdateFrame(u.graphicsDriver, w, h, u.DeviceScaleFactor(), u); err != nil { + if err := u.context.forceUpdateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil { return err } } else { - if err := u.context.updateFrame(u.graphicsDriver, w, h, u.DeviceScaleFactor(), u); err != nil { + if err := u.context.updateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil { return err } } @@ -771,8 +756,9 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error { func (u *UserInterface) updateScreenSize() { if document.Truthy() { body := document.Get("body") - bw := int(body.Get("clientWidth").Float() * u.DeviceScaleFactor()) - bh := int(body.Get("clientHeight").Float() * u.DeviceScaleFactor()) + f := theMonitor.DeviceScaleFactor() + bw := int(body.Get("clientWidth").Float() * f) + bh := int(body.Get("clientHeight").Float() * f) canvas.Set("width", bw) canvas.Set("height", bh) } @@ -787,7 +773,9 @@ func (u *UserInterface) Window() Window { return &nullWindow{} } -type Monitor struct{} +type Monitor struct { + deviceScaleFactor float64 +} var theMonitor = &Monitor{} @@ -795,6 +783,19 @@ func (m *Monitor) Name() string { return "" } +func (m *Monitor) DeviceScaleFactor() float64 { + if m.deviceScaleFactor != 0 { + return m.deviceScaleFactor + } + + ratio := window.Get("devicePixelRatio").Float() + if ratio == 0 { + ratio = 1 + } + m.deviceScaleFactor = ratio + return m.deviceScaleFactor +} + func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor { return append(mons, theMonitor) } diff --git a/internal/ui/ui_linbsd.go b/internal/ui/ui_linbsd.go index bd07899d7..c94dddac2 100644 --- a/internal/ui/ui_linbsd.go +++ b/internal/ui/ui_linbsd.go @@ -123,11 +123,11 @@ func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) { } func dipFromGLFWPixel(x float64, monitor *Monitor) float64 { - return x / monitor.deviceScaleFactor() + return x / monitor.DeviceScaleFactor() } func dipToGLFWPixel(x float64, monitor *Monitor) float64 { - return x * monitor.deviceScaleFactor() + return x * monitor.DeviceScaleFactor() } func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) { diff --git a/internal/ui/ui_mobile.go b/internal/ui/ui_mobile.go index ff4633d11..c23dbd924 100644 --- a/internal/ui/ui_mobile.go +++ b/internal/ui/ui_mobile.go @@ -89,9 +89,6 @@ type userInterfaceImpl struct { outsideWidth float64 outsideHeight float64 - deviceScaleFactorOnce sync.Once - deviceScaleFactor float64 - foreground int32 errCh chan error @@ -179,8 +176,11 @@ func (u *UserInterface) update() error { renderEndCh <- struct{}{} }() + // The device scale factor can be obtained after the main function starts, especially on Android. + theMonitor.initDeviceScaleFactorIfNeeded() + w, h := u.outsideSize() - if err := u.context.updateFrame(u.graphicsDriver, w, h, u.DeviceScaleFactor(), u); err != nil { + if err := u.context.updateFrame(u.graphicsDriver, w, h, theMonitor.DeviceScaleFactor(), u); err != nil { return err } return nil @@ -256,14 +256,6 @@ func (u *UserInterface) updateExplicitRenderingModeIfNeeded(fpsMode FPSModeType) u.renderRequester.SetExplicitRenderingMode(fpsMode == FPSModeVsyncOffMinimum) } -func (u *UserInterface) DeviceScaleFactor() float64 { - // Assume that the device scale factor never changes on mobiles. - u.deviceScaleFactorOnce.Do(func() { - u.deviceScaleFactor = deviceScaleFactorImpl() - }) - return u.deviceScaleFactor -} - func (u *UserInterface) readInputState(inputState *InputState) { u.m.Lock() defer u.m.Unlock() @@ -274,7 +266,11 @@ func (u *UserInterface) Window() Window { return &nullWindow{} } -type Monitor struct{} +type Monitor struct { + deviceScaleFactor float64 + + m sync.Mutex +} var theMonitor = &Monitor{} @@ -282,6 +278,22 @@ func (m *Monitor) Name() string { return "" } +func (m *Monitor) initDeviceScaleFactorIfNeeded() { + // Assume that the device scale factor never changes on mobiles. + m.m.Lock() + defer m.m.Unlock() + if m.deviceScaleFactor != 0 { + return + } + m.deviceScaleFactor = deviceScaleFactorImpl() +} + +func (m *Monitor) DeviceScaleFactor() float64 { + m.m.Lock() + defer m.m.Unlock() + return m.deviceScaleFactor +} + func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor { return append(mons, theMonitor) } diff --git a/internal/ui/ui_nintendosdk.go b/internal/ui/ui_nintendosdk.go index a2f445dee..914703dd9 100644 --- a/internal/ui/ui_nintendosdk.go +++ b/internal/ui/ui_nintendosdk.go @@ -54,8 +54,6 @@ func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, er return nil, errors.New("ui: PlayStation 5 is not supported in this environment") } -const deviceScaleFactor = 1 - func init() { runtime.LockOSThread() } @@ -94,16 +92,12 @@ func (u *UserInterface) loopGame() error { for { recordProfilerHeartbeat() - if err := u.context.updateFrame(u.graphicsDriver, float64(C.kScreenWidth), float64(C.kScreenHeight), deviceScaleFactor, u); err != nil { + if err := u.context.updateFrame(u.graphicsDriver, float64(C.kScreenWidth), float64(C.kScreenHeight), theMonitor.DeviceScaleFactor(), u); err != nil { return err } } } -func (*UserInterface) DeviceScaleFactor() float64 { - return deviceScaleFactor -} - func (*UserInterface) IsFocused() bool { return true } @@ -172,6 +166,10 @@ func (m *Monitor) Name() string { return "" } +func (m *Monitor) DeviceScaleFactor() float64 { + return 1 +} + func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor { return append(mons, theMonitor) } diff --git a/internal/ui/ui_playstation5.go b/internal/ui/ui_playstation5.go index 0a95e50d9..faadc296e 100644 --- a/internal/ui/ui_playstation5.go +++ b/internal/ui/ui_playstation5.go @@ -50,9 +50,8 @@ func (*graphicsDriverCreatorImpl) newPlayStation5() (graphicsdriver.Graphics, er const ( // TODO: Get this value from the SDK. - screenWidth = 3840 - screenHeight = 2160 - deviceScaleFactor = 1 + screenWidth = 3840 + screenHeight = 2160 ) func init() { @@ -82,17 +81,13 @@ func (u *UserInterface) initOnMainThread(options *RunOptions) error { func (u *UserInterface) loopGame() error { for { - if err := u.context.updateFrame(u.graphicsDriver, screenWidth, screenHeight, deviceScaleFactor, u); err != nil { + if err := u.context.updateFrame(u.graphicsDriver, screenWidth, screenHeight, theMonitor.DeviceScaleFactor(), u); err != nil { return err } } return nil } -func (*UserInterface) DeviceScaleFactor() float64 { - return deviceScaleFactor -} - func (*UserInterface) IsFocused() bool { return true } @@ -164,6 +159,10 @@ func (m *Monitor) Name() string { return "" } +func (m *Monitor) DeviceScaleFactor() float64 { + return 1 +} + func (u *UserInterface) AppendMonitors(mons []*Monitor) []*Monitor { return append(mons, theMonitor) } diff --git a/internal/ui/ui_windows.go b/internal/ui/ui_windows.go index a6c423d66..ffe4830d8 100644 --- a/internal/ui/ui_windows.go +++ b/internal/ui/ui_windows.go @@ -102,11 +102,11 @@ func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) { } func dipFromGLFWPixel(x float64, monitor *Monitor) float64 { - return x / monitor.deviceScaleFactor() + return x / monitor.DeviceScaleFactor() } func dipToGLFWPixel(x float64, monitor *Monitor) float64 { - return x * monitor.deviceScaleFactor() + return x * monitor.DeviceScaleFactor() } func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) { diff --git a/mobile/ebitenmobileview/mobile.go b/mobile/ebitenmobileview/mobile.go index 7f5193564..d83e0090c 100644 --- a/mobile/ebitenmobileview/mobile.go +++ b/mobile/ebitenmobileview/mobile.go @@ -113,7 +113,7 @@ func Resume() error { } func DeviceScale() float64 { - return ui.Get().DeviceScaleFactor() + return ui.Get().Monitor().DeviceScaleFactor() } type RenderRequester interface { diff --git a/monitor.go b/monitor.go index 31a45a8a7..f5c7691f5 100644 --- a/monitor.go +++ b/monitor.go @@ -27,6 +27,20 @@ func (m *MonitorType) Name() string { return (*ui.Monitor)(m).Name() } +// DeviceScaleFactor returns the device scale factor of the monitor. +// +// DeviceScaleFactor returns a meaningful value on high-DPI display environment, +// otherwise DeviceScaleFactor returns 1. +// +// DeviceScaleFactor might panic on init function on some devices like Android. +// Then, it is not recommended to call DeviceScaleFactor from init functions. +// +// DeviceScaleFactor must be called on the main thread before the main loop, +// and is concurrent-safe after the main loop. +func (m *MonitorType) DeviceScaleFactor() float64 { + return (*ui.Monitor)(m).DeviceScaleFactor() +} + // Monitor returns the current monitor. func Monitor() *MonitorType { m := ui.Get().Monitor() diff --git a/run.go b/run.go index 813d174fd..668cfee31 100644 --- a/run.go +++ b/run.go @@ -480,11 +480,11 @@ func SetRunnableOnUnfocused(runnableOnUnfocused bool) { // DeviceScaleFactor must be called on the main thread before the main loop, and is concurrent-safe after the main // loop. // -// DeviceScaleFactor is concurrent-safe. -// // BUG: DeviceScaleFactor value is not affected by SetWindowPosition before RunGame (#1575). +// +// Deprecated: as of v2.6. Use Monitor().DeviceScaleFactor() instead. func DeviceScaleFactor() float64 { - return ui.Get().DeviceScaleFactor() + return Monitor().DeviceScaleFactor() } // IsVsyncEnabled returns a boolean value indicating whether