diff --git a/internal/devicescale/devicescale.go b/internal/devicescale/devicescale.go index 5f888ba6e..9239b1bda 100644 --- a/internal/devicescale/devicescale.go +++ b/internal/devicescale/devicescale.go @@ -23,9 +23,8 @@ type pos struct { } var ( - m sync.Mutex - cache = map[pos]float64{} - videoModeScaleCache = map[pos]float64{} + m sync.Mutex + cache = map[pos]float64{} ) // GetAt returns the device scale at (x, y), i.e. the number of device-dependent pixels per device-independent pixel. @@ -46,16 +45,3 @@ func GetAt(x, y int) float64 { return s } - -// VideoModeScaleAt returns the video mode scale scale at (x, y), i.e. the number of video mode pixels per device-dependent pixel. -// x and y are in device-dependent pixels and must be the top-left coordinate of a monitor, or 0,0 to request a "global scale". -func VideoModeScaleAt(x, y int) float64 { - m.Lock() - defer m.Unlock() - if s, ok := videoModeScaleCache[pos{x, y}]; ok { - return s - } - s := videoModeScaleImpl(x, y) - videoModeScaleCache[pos{x, y}] = s - return s -} diff --git a/internal/devicescale/impl_android.go b/internal/devicescale/impl_android.go index 0b9d0bb46..271f422b7 100644 --- a/internal/devicescale/impl_android.go +++ b/internal/devicescale/impl_android.go @@ -103,7 +103,3 @@ func impl(x, y int) float64 { } return s } - -func videoModeScaleImpl(x, y int) float64 { - return 1 -} diff --git a/internal/devicescale/impl_ios.go b/internal/devicescale/impl_ios.go index 2aa54b3f5..6735c5218 100644 --- a/internal/devicescale/impl_ios.go +++ b/internal/devicescale/impl_ios.go @@ -30,7 +30,3 @@ import "C" func impl(x, y int) float64 { return float64(C.devicePixelRatio()) } - -func videoModeScaleImpl(x, y int) float64 { - return 1 -} diff --git a/internal/devicescale/impl_js.go b/internal/devicescale/impl_js.go index 158218c8a..ea2f9cb90 100644 --- a/internal/devicescale/impl_js.go +++ b/internal/devicescale/impl_js.go @@ -33,7 +33,3 @@ func impl(x, y int) float64 { } return ratio } - -func videoModeScaleImpl(x, y int) float64 { - return 1 -} diff --git a/internal/devicescale/impl_notx.go b/internal/devicescale/impl_notx.go deleted file mode 100644 index c10cdc113..000000000 --- a/internal/devicescale/impl_notx.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2021 The Ebiten 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 (darwin && !ios) || windows -// +build darwin,!ios windows - -package devicescale - -func videoModeScaleImpl(x, y int) float64 { - return 1 -} diff --git a/internal/devicescale/impl_x.go b/internal/devicescale/impl_x.go deleted file mode 100644 index 4139dc96b..000000000 --- a/internal/devicescale/impl_x.go +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright 2021 The Ebiten 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 !android && !darwin && !js && !windows -// +build !android,!darwin,!js,!windows - -package devicescale - -import ( - "math" - - "github.com/jezek/xgb" - "github.com/jezek/xgb/randr" - "github.com/jezek/xgb/xproto" -) - -func videoModeScaleImpl(x, y int) float64 { - // TODO: if https://github.com/glfw/glfw/issues/1961 gets fixed, this function may need revising. - // In case GLFW decides to switch to returning logical pixels, we can just return 1. - - // Note: GLFW currently returns physical pixel sizes, - // but we need to predict the window system-side size of the fullscreen window - // for our `ScreenSizeInFullscreen` public API. - // Also at the moment we need this prior to switching to fullscreen, but that might be replacable. - // So this function computes the ratio of physical per logical pixels. - m := monitorAt(x, y) - monitorX, monitorY := m.GetPos() - xconn, err := xgb.NewConn() - defer xconn.Close() - if err != nil { - // No X11 connection? - // Assume we're on pure Wayland then. - // GLFW/Wayland shouldn't be having this issue. - return 1 - } - if err = randr.Init(xconn); err != nil { - // No RANDR extension? - // No problem. - return 1 - } - root := xproto.Setup(xconn).DefaultScreen(xconn).Root - res, err := randr.GetScreenResourcesCurrent(xconn, root).Reply() - if err != nil { - // Likely means RANDR is not working. - // No problem. - return 1 - } - for _, crtc := range res.Crtcs[:res.NumCrtcs] { - info, err := randr.GetCrtcInfo(xconn, crtc, res.ConfigTimestamp).Reply() - if err != nil { - // This Crtc is bad. Maybe just got disconnected? - continue - } - if info.NumOutputs == 0 { - // This Crtc is not connected to any output. - // In other words, a disabled monitor. - continue - } - if int(info.X) == monitorX && int(info.Y) == monitorY { - xWidth, xHeight := info.Width, info.Height - vm := m.GetVideoMode() - physWidth, physHeight := vm.Width, vm.Height - // Return one scale, even though there may be separate X and Y scales. - // Return the _larger_ scale, as this would yield a letterboxed display on mismatch, rather than a cut-off one. - scale := math.Max(float64(physWidth)/float64(xWidth), float64(physHeight)/float64(xHeight)) - return scale - } - } - // Monitor not known to XRandR. Weird. - return 1 -} diff --git a/internal/uidriver/glfw/ui.go b/internal/uidriver/glfw/ui.go index a5880b9bc..c0befa833 100644 --- a/internal/uidriver/glfw/ui.go +++ b/internal/uidriver/glfw/ui.go @@ -189,8 +189,7 @@ func initialize() error { m := currentMonitor(w) theUI.initMonitor = m v := m.GetVideoMode() - mx, my := currentMonitor(w).GetPos() - scale := devicescale.VideoModeScaleAt(mx, my) + scale := videoModeScale(currentMonitor(w)) theUI.initFullscreenWidthInDP = int(theUI.fromGLFWMonitorPixel(float64(v.Width), scale)) theUI.initFullscreenHeightInDP = int(theUI.fromGLFWMonitorPixel(float64(v.Height), scale)) @@ -232,6 +231,7 @@ func updateMonitors() { y: y, }) } + clearVideoModeScaleCache() } func ensureMonitors() []*monitor { @@ -531,8 +531,7 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) { _ = u.t.Call(func() error { m := currentMonitor(u.window) v := m.GetVideoMode() - mx, my := m.GetPos() - s := devicescale.VideoModeScaleAt(mx, my) + s := videoModeScale(m) w = int(u.fromGLFWMonitorPixel(float64(v.Width), s)) h = int(u.fromGLFWMonitorPixel(float64(v.Height), s)) return nil @@ -940,8 +939,7 @@ func (u *UserInterface) updateSize() (float64, float64, bool) { m := currentMonitor(u.window) v := m.GetVideoMode() ww, wh := v.Width, v.Height - mx, my := m.GetPos() - s := devicescale.VideoModeScaleAt(mx, my) + s := videoModeScale(m) w = u.fromGLFWMonitorPixel(float64(ww), s) h = u.fromGLFWMonitorPixel(float64(wh), s) } else { diff --git a/internal/uidriver/glfw/ui_darwin.go b/internal/uidriver/glfw/ui_darwin.go index 7985c75e0..e5f7fd1b4 100644 --- a/internal/uidriver/glfw/ui_darwin.go +++ b/internal/uidriver/glfw/ui_darwin.go @@ -81,6 +81,14 @@ import ( "github.com/hajimehoshi/ebiten/v2/internal/glfw" ) +// clearVideoModeScaleCache must be called from the main thread. +func clearVideoModeScaleCache() {} + +// videoModeScale must be called from the main thread. +func videoModeScale(m *glfw.Monitor) float64 { + return 1 +} + // fromGLFWMonitorPixel must be called from the main thread. func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 { // videoModeScale is always 1 on macOS, diff --git a/internal/uidriver/glfw/ui_unix.go b/internal/uidriver/glfw/ui_unix.go index 2b281f623..e9b816758 100644 --- a/internal/uidriver/glfw/ui_unix.go +++ b/internal/uidriver/glfw/ui_unix.go @@ -19,10 +19,96 @@ package glfw import ( + "math" + "github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/glfw" + "github.com/jezek/xgb" + "github.com/jezek/xgb/randr" + "github.com/jezek/xgb/xproto" ) +type videoModeScaleCacheKey struct{ X, Y int } + +var videoModeScaleCache = map[videoModeScaleCacheKey]float64{} + +// clearVideoModeScaleCache must be called from the main thread. +func clearVideoModeScaleCache() { + for k := range videoModeScaleCache { + delete(videoModeScaleCache, k) + } +} + +// videoModeScale must be called from the main thread. +func videoModeScale(m *glfw.Monitor) float64 { + // Caching wrapper for videoModeScaleUncached as + // videoModeScaleUncached may be expensive (uses blocking calls on X connection) + // and public ScreenSizeInFullscreen API needs the videoModeScale. + monitorX, monitorY := m.GetPos() + cacheKey := videoModeScaleCacheKey{X: monitorX, Y: monitorY} + if cached, ok := videoModeScaleCache[cacheKey]; ok { + return cached + } + scale := videoModeScaleUncached(m, monitorX, monitorY) + videoModeScaleCache[cacheKey] = scale + return scale +} + +// videoModeScaleUncached must be called from the main thread. +func videoModeScaleUncached(m *glfw.Monitor, monitorX, monitorY int) float64 { + // TODO: if https://github.com/glfw/glfw/issues/1961 gets fixed, this function may need revising. + // In case GLFW decides to switch to returning logical pixels, we can just return 1. + + // Note: GLFW currently returns physical pixel sizes, + // but we need to predict the window system-side size of the fullscreen window + // for our `ScreenSizeInFullscreen` public API. + // Also at the moment we need this prior to switching to fullscreen, but that might be replacable. + // So this function computes the ratio of physical per logical pixels. + xconn, err := xgb.NewConn() + defer xconn.Close() + if err != nil { + // No X11 connection? + // Assume we're on pure Wayland then. + // GLFW/Wayland shouldn't be having this issue. + return 1 + } + if err = randr.Init(xconn); err != nil { + // No RANDR extension? + // No problem. + return 1 + } + root := xproto.Setup(xconn).DefaultScreen(xconn).Root + res, err := randr.GetScreenResourcesCurrent(xconn, root).Reply() + if err != nil { + // Likely means RANDR is not working. + // No problem. + return 1 + } + for _, crtc := range res.Crtcs[:res.NumCrtcs] { + info, err := randr.GetCrtcInfo(xconn, crtc, res.ConfigTimestamp).Reply() + if err != nil { + // This Crtc is bad. Maybe just got disconnected? + continue + } + if info.NumOutputs == 0 { + // This Crtc is not connected to any output. + // In other words, a disabled monitor. + continue + } + if int(info.X) == monitorX && int(info.Y) == monitorY { + xWidth, xHeight := info.Width, info.Height + vm := m.GetVideoMode() + physWidth, physHeight := vm.Width, vm.Height + // Return one scale, even though there may be separate X and Y scales. + // Return the _larger_ scale, as this would yield a letterboxed display on mismatch, rather than a cut-off one. + scale := math.Max(float64(physWidth)/float64(xWidth), float64(physHeight)/float64(xHeight)) + return scale + } + } + // Monitor not known to XRandR. Weird. + return 1 +} + // fromGLFWMonitorPixel must be called from the main thread. func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 { return x / (videoModeScale * u.deviceScaleFactor()) diff --git a/internal/uidriver/glfw/ui_windows.go b/internal/uidriver/glfw/ui_windows.go index 6c1059eba..68d2685dd 100644 --- a/internal/uidriver/glfw/ui_windows.go +++ b/internal/uidriver/glfw/ui_windows.go @@ -98,6 +98,14 @@ func getMonitorInfoW(hMonitor uintptr, lpmi *monitorInfo) error { return nil } +// clearVideoModeScaleCache must be called from the main thread. +func clearVideoModeScaleCache() {} + +// videoModeScale must be called from the main thread. +func videoModeScale(m *glfw.Monitor) float64 { + return 1 +} + // fromGLFWMonitorPixel must be called from the main thread. func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 { return x / (videoModeScale * u.deviceScaleFactor())