Split the concept of device scale and screen scale (#1811)

We now set deviceScale to always the mapping from logical window system pixels
to device independent pixels (which is what Ebiten API users expect), and
introduce a new concept of videoModeScale that maps from video mode to logical
window system pixels.

videoModeScale is now only used for computing full-screen resolutions, while
deviceScale is used for any other conversion.

Fixes window sizes on X11, should be a NOP otherwise.

Closes #1774.
This commit is contained in:
divVerent 2021-09-14 12:03:04 -04:00 committed by GitHub
parent 60df512352
commit 923c84a3d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 81 additions and 64 deletions

View File

@ -25,11 +25,11 @@ type pos struct {
var ( var (
m sync.Mutex m sync.Mutex
cache = map[pos]float64{} cache = map[pos]float64{}
videoModeScaleCache = map[pos]float64{}
) )
// GetAt returns the device scale at (x, y). // GetAt returns the device scale at (x, y), i.e. the number of device-dependent pixels per device-independent 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". // 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".
// The device scale maps device dependent pixels to device independent pixels.
func GetAt(x, y int) float64 { func GetAt(x, y int) float64 {
m.Lock() m.Lock()
defer m.Unlock() defer m.Unlock()
@ -46,3 +46,16 @@ func GetAt(x, y int) float64 {
return s 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
}

View File

@ -103,3 +103,7 @@ func impl(x, y int) float64 {
} }
return s return s
} }
func videoModeScaleImpl(x, y int) float64 {
return 1
}

View File

@ -33,3 +33,8 @@ func monitorAt(x, y int) *glfw.Monitor {
} }
return monitors[0] return monitors[0]
} }
func impl(x, y int) float64 {
sx, _ := monitorAt(x, y).GetContentScale()
return float64(sx)
}

View File

@ -30,3 +30,7 @@ import "C"
func impl(x, y int) float64 { func impl(x, y int) float64 {
return float64(C.devicePixelRatio()) return float64(C.devicePixelRatio())
} }
func videoModeScaleImpl(x, y int) float64 {
return 1
}

View File

@ -33,3 +33,7 @@ func impl(x, y int) float64 {
} }
return ratio return ratio
} }
func videoModeScaleImpl(x, y int) float64 {
return 1
}

View File

@ -17,7 +17,6 @@
package devicescale package devicescale
func impl(x, y int) float64 { func videoModeScaleImpl(x, y int) float64 {
sx, _ := monitorAt(x, y).GetContentScale() return 1
return float64(sx)
} }

View File

@ -25,9 +25,9 @@ import (
"github.com/jezek/xgb/xproto" "github.com/jezek/xgb/xproto"
) )
func impl(x, y int) float64 { func videoModeScaleImpl(x, y int) float64 {
// TODO: if https://github.com/glfw/glfw/issues/1961 gets fixed, this function may need revising. // 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.0. // In case GLFW decides to switch to returning logical pixels, we can just return 1.
// Note: GLFW currently returns physical pixel sizes, // Note: GLFW currently returns physical pixel sizes,
// but we need to predict the window system-side size of the fullscreen window // but we need to predict the window system-side size of the fullscreen window
@ -35,7 +35,6 @@ func impl(x, y int) float64 {
// Also at the moment we need this prior to switching to fullscreen, but that might be replacable. // 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. // So this function computes the ratio of physical per logical pixels.
m := monitorAt(x, y) m := monitorAt(x, y)
sx, _ := m.GetContentScale()
monitorX, monitorY := m.GetPos() monitorX, monitorY := m.GetPos()
xconn, err := xgb.NewConn() xconn, err := xgb.NewConn()
defer xconn.Close() defer xconn.Close()
@ -43,19 +42,19 @@ func impl(x, y int) float64 {
// No X11 connection? // No X11 connection?
// Assume we're on pure Wayland then. // Assume we're on pure Wayland then.
// GLFW/Wayland shouldn't be having this issue. // GLFW/Wayland shouldn't be having this issue.
return float64(sx) return 1
} }
if err = randr.Init(xconn); err != nil { if err = randr.Init(xconn); err != nil {
// No RANDR extension? // No RANDR extension?
// No problem. // No problem.
return float64(sx) return 1
} }
root := xproto.Setup(xconn).DefaultScreen(xconn).Root root := xproto.Setup(xconn).DefaultScreen(xconn).Root
res, err := randr.GetScreenResourcesCurrent(xconn, root).Reply() res, err := randr.GetScreenResourcesCurrent(xconn, root).Reply()
if err != nil { if err != nil {
// Likely means RANDR is not working. // Likely means RANDR is not working.
// No problem. // No problem.
return float64(sx) return 1
} }
for _, crtc := range res.Crtcs[:res.NumCrtcs] { for _, crtc := range res.Crtcs[:res.NumCrtcs] {
info, err := randr.GetCrtcInfo(xconn, crtc, res.ConfigTimestamp).Reply() info, err := randr.GetCrtcInfo(xconn, crtc, res.ConfigTimestamp).Reply()
@ -75,9 +74,9 @@ func impl(x, y int) float64 {
// Return one scale, even though there may be separate X and Y scales. // 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. // 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)) scale := math.Max(float64(physWidth)/float64(xWidth), float64(physHeight)/float64(xHeight))
return float64(sx) * scale return scale
} }
} }
// Monitor not known to XRandR. Weird. // Monitor not known to XRandR. Weird.
return float64(sx) return 1
} }

View File

@ -305,8 +305,8 @@ func (i *Input) update(window *glfw.Window, context driver.UIContext) {
cx, cy := window.GetCursorPos() cx, cy := window.GetCursorPos()
// TODO: This is tricky. Rename the function? // TODO: This is tricky. Rename the function?
s := i.ui.deviceScaleFactor() s := i.ui.deviceScaleFactor()
cx = fromGLFWMonitorPixel(cx, s) cx = i.ui.fromGLFWPixel(cx)
cy = fromGLFWMonitorPixel(cy, s) cy = i.ui.fromGLFWPixel(cy)
cx, cy = context.AdjustPosition(cx, cy, s) cx, cy = context.AdjustPosition(cx, cy, s)
// AdjustPosition can return NaN at the initialization. // AdjustPosition can return NaN at the initialization.

View File

@ -189,9 +189,10 @@ func initialize() error {
m := currentMonitor(w) m := currentMonitor(w)
theUI.initMonitor = m theUI.initMonitor = m
v := m.GetVideoMode() v := m.GetVideoMode()
scale := devicescale.GetAt(currentMonitor(w).GetPos()) mx, my := currentMonitor(w).GetPos()
theUI.initFullscreenWidthInDP = int(fromGLFWMonitorPixel(float64(v.Width), scale)) scale := devicescale.VideoModeScaleAt(mx, my)
theUI.initFullscreenHeightInDP = int(fromGLFWMonitorPixel(float64(v.Height), scale)) theUI.initFullscreenWidthInDP = int(theUI.fromGLFWMonitorPixel(float64(v.Width), scale))
theUI.initFullscreenHeightInDP = int(theUI.fromGLFWMonitorPixel(float64(v.Height), scale))
// Create system cursors. These cursors are destroyed at glfw.Terminate(). // Create system cursors. These cursors are destroyed at glfw.Terminate().
glfwSystemCursors[driver.CursorShapeDefault] = nil glfwSystemCursors[driver.CursorShapeDefault] = nil
@ -528,10 +529,12 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
var w, h int var w, h int
_ = u.t.Call(func() error { _ = u.t.Call(func() error {
v := currentMonitor(u.window).GetVideoMode() m := currentMonitor(u.window)
s := u.deviceScaleFactor() v := m.GetVideoMode()
w = int(fromGLFWMonitorPixel(float64(v.Width), s)) mx, my := m.GetPos()
h = int(fromGLFWMonitorPixel(float64(v.Height), s)) s := devicescale.VideoModeScaleAt(mx, my)
w = int(u.fromGLFWMonitorPixel(float64(v.Width), s))
h = int(u.fromGLFWMonitorPixel(float64(v.Height), s))
return nil return nil
}) })
return w, h return w, h
@ -707,7 +710,7 @@ func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
func (u *UserInterface) DeviceScaleFactor() float64 { func (u *UserInterface) DeviceScaleFactor() float64 {
if !u.isRunning() { if !u.isRunning() {
// TODO: Use the initWindowPosition. This requires to convert the units correctly (#1575). // TODO: Use the initWindowPosition. This requires to convert the units correctly (#1575).
return devicescale.GetAt(u.initMonitor.GetPos()) return u.deviceScaleFactor()
} }
f := 0.0 f := 0.0
@ -724,7 +727,8 @@ func (u *UserInterface) deviceScaleFactor() float64 {
if u.window != nil { if u.window != nil {
m = currentMonitor(u.window) m = currentMonitor(u.window)
} }
return devicescale.GetAt(m.GetPos()) mx, my := m.GetPos()
return devicescale.GetAt(mx, my)
} }
func init() { func init() {
@ -933,11 +937,13 @@ func (u *UserInterface) updateSize() (float64, float64, bool) {
var w, h float64 var w, h float64
if u.isFullscreen() { if u.isFullscreen() {
v := currentMonitor(u.window).GetVideoMode() m := currentMonitor(u.window)
v := m.GetVideoMode()
ww, wh := v.Width, v.Height ww, wh := v.Width, v.Height
s := u.deviceScaleFactor() mx, my := m.GetPos()
w = fromGLFWMonitorPixel(float64(ww), s) s := devicescale.VideoModeScaleAt(mx, my)
h = fromGLFWMonitorPixel(float64(wh), s) w = u.fromGLFWMonitorPixel(float64(ww), s)
h = u.fromGLFWMonitorPixel(float64(wh), s)
} else { } else {
// Instead of u.windowWidth and u.windowHeight, use the actual window size here. // Instead of u.windowWidth and u.windowHeight, use the actual window size here.
// On Windows, the specified size at SetSize and the actual window size might not // On Windows, the specified size at SetSize and the actual window size might not
@ -946,9 +952,6 @@ func (u *UserInterface) updateSize() (float64, float64, bool) {
w = u.fromGLFWPixel(float64(ww)) w = u.fromGLFWPixel(float64(ww))
h = u.fromGLFWPixel(float64(wh)) h = u.fromGLFWPixel(float64(wh))
} }
// On Linux/UNIX, further adjusting is required (#1307).
w = u.toFramebufferPixel(w)
h = u.toFramebufferPixel(h)
return w, h, true return w, h, true
} }

View File

@ -81,22 +81,27 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/glfw" "github.com/hajimehoshi/ebiten/v2/internal/glfw"
) )
func fromGLFWMonitorPixel(x float64, deviceScale float64) float64 { // fromGLFWMonitorPixel must be called from the main thread.
return x func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 {
// videoModeScale is always 1 on macOS,
// however leaving the divison in place for consistency.
return x / videoModeScale
} }
// fromGLFWPixel must be called from the main thread.
func (u *UserInterface) fromGLFWPixel(x float64) float64 { func (u *UserInterface) fromGLFWPixel(x float64) float64 {
// NOTE: On macOS, GLFW exposes the device independent coordinate system.
// Thus, the conversion functions are unnecessary,
// however we still need the deviceScaleFactor internally
// so we can create and maintain a HiDPI frame buffer.
return x return x
} }
// toGLFWPixel must be called from the main thread.
func (u *UserInterface) toGLFWPixel(x float64) float64 { func (u *UserInterface) toGLFWPixel(x float64) float64 {
return x return x
} }
func (u *UserInterface) toFramebufferPixel(x float64) float64 {
return x
}
func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) { func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) {
return x, y return x, y
} }

View File

@ -19,37 +19,23 @@
package glfw package glfw
import ( import (
"math"
"github.com/hajimehoshi/ebiten/v2/internal/driver" "github.com/hajimehoshi/ebiten/v2/internal/driver"
"github.com/hajimehoshi/ebiten/v2/internal/glfw" "github.com/hajimehoshi/ebiten/v2/internal/glfw"
) )
// fromGLFWMonitorPixel must be called from the main thread. // fromGLFWMonitorPixel must be called from the main thread.
func fromGLFWMonitorPixel(x float64, deviceScale float64) float64 { func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 {
// deviceScaleFactor is sometimes an unnice value (e.g., 1.502361). Use math.Ceil to clean the vaule. return x / (videoModeScale * u.deviceScaleFactor())
return math.Ceil(x / deviceScale)
} }
// fromGLFWPixel must be called from the main thread. // fromGLFWPixel must be called from the main thread.
func (u *UserInterface) fromGLFWPixel(x float64) float64 { func (u *UserInterface) fromGLFWPixel(x float64) float64 {
// deviceScaleFactor() is a scale by desktop environment (e.g., Cinnamon), while GetContentScale() is X's scale. return x / u.deviceScaleFactor()
// They are different things and then need to be treated in different ways (#1350).
s, _ := currentMonitor(u.window).GetContentScale()
return x / float64(s)
} }
// toGLFWPixel must be called from the main thread. // toGLFWPixel must be called from the main thread.
func (u *UserInterface) toGLFWPixel(x float64) float64 { func (u *UserInterface) toGLFWPixel(x float64) float64 {
s, _ := currentMonitor(u.window).GetContentScale() return x * u.deviceScaleFactor()
return x * float64(s)
}
// toFramebufferPixel must be called from the main thread.
func (u *UserInterface) toFramebufferPixel(x float64) float64 {
s, _ := currentMonitor(u.window).GetContentScale()
// deviceScaleFactor is sometimes an unnice value (e.g., 1.502361). Use math.Ceil to clean the vaule.
return math.Ceil(x * float64(s) / u.deviceScaleFactor())
} }
func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) { func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) {

View File

@ -99,8 +99,8 @@ func getMonitorInfoW(hMonitor uintptr, lpmi *monitorInfo) error {
} }
// fromGLFWMonitorPixel must be called from the main thread. // fromGLFWMonitorPixel must be called from the main thread.
func fromGLFWMonitorPixel(x float64, deviceScale float64) float64 { func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 {
return x / deviceScale return x / (videoModeScale * u.deviceScaleFactor())
} }
// fromGLFWPixel must be called from the main thread. // fromGLFWPixel must be called from the main thread.
@ -113,11 +113,6 @@ func (u *UserInterface) toGLFWPixel(x float64) float64 {
return x * u.deviceScaleFactor() return x * u.deviceScaleFactor()
} }
// toFramebufferPixel must be called from the main thread.
func (u *UserInterface) toFramebufferPixel(x float64) float64 {
return x
}
func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) { func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) {
mx, my := currentMonitor(u.window).GetPos() mx, my := currentMonitor(u.window).GetPos()
// As the video width/height might be wrong, // As the video width/height might be wrong,