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 (
m sync.Mutex
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".
// The device scale maps device dependent pixels to device independent pixels.
func GetAt(x, y int) float64 {
m.Lock()
defer m.Unlock()
@ -46,3 +46,16 @@ 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
}

View File

@ -103,3 +103,7 @@ func impl(x, y int) float64 {
}
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]
}
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 {
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
}
func videoModeScaleImpl(x, y int) float64 {
return 1
}

View File

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

View File

@ -25,9 +25,9 @@ import (
"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.
// 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,
// 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.
// So this function computes the ratio of physical per logical pixels.
m := monitorAt(x, y)
sx, _ := m.GetContentScale()
monitorX, monitorY := m.GetPos()
xconn, err := xgb.NewConn()
defer xconn.Close()
@ -43,19 +42,19 @@ func impl(x, y int) float64 {
// No X11 connection?
// Assume we're on pure Wayland then.
// GLFW/Wayland shouldn't be having this issue.
return float64(sx)
return 1
}
if err = randr.Init(xconn); err != nil {
// No RANDR extension?
// No problem.
return float64(sx)
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 float64(sx)
return 1
}
for _, crtc := range res.Crtcs[:res.NumCrtcs] {
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 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 float64(sx) * scale
return scale
}
}
// 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()
// TODO: This is tricky. Rename the function?
s := i.ui.deviceScaleFactor()
cx = fromGLFWMonitorPixel(cx, s)
cy = fromGLFWMonitorPixel(cy, s)
cx = i.ui.fromGLFWPixel(cx)
cy = i.ui.fromGLFWPixel(cy)
cx, cy = context.AdjustPosition(cx, cy, s)
// AdjustPosition can return NaN at the initialization.

View File

@ -189,9 +189,10 @@ func initialize() error {
m := currentMonitor(w)
theUI.initMonitor = m
v := m.GetVideoMode()
scale := devicescale.GetAt(currentMonitor(w).GetPos())
theUI.initFullscreenWidthInDP = int(fromGLFWMonitorPixel(float64(v.Width), scale))
theUI.initFullscreenHeightInDP = int(fromGLFWMonitorPixel(float64(v.Height), scale))
mx, my := currentMonitor(w).GetPos()
scale := devicescale.VideoModeScaleAt(mx, my)
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().
glfwSystemCursors[driver.CursorShapeDefault] = nil
@ -528,10 +529,12 @@ func (u *UserInterface) ScreenSizeInFullscreen() (int, int) {
var w, h int
_ = u.t.Call(func() error {
v := currentMonitor(u.window).GetVideoMode()
s := u.deviceScaleFactor()
w = int(fromGLFWMonitorPixel(float64(v.Width), s))
h = int(fromGLFWMonitorPixel(float64(v.Height), s))
m := currentMonitor(u.window)
v := m.GetVideoMode()
mx, my := m.GetPos()
s := devicescale.VideoModeScaleAt(mx, my)
w = int(u.fromGLFWMonitorPixel(float64(v.Width), s))
h = int(u.fromGLFWMonitorPixel(float64(v.Height), s))
return nil
})
return w, h
@ -707,7 +710,7 @@ func (u *UserInterface) SetCursorShape(shape driver.CursorShape) {
func (u *UserInterface) DeviceScaleFactor() float64 {
if !u.isRunning() {
// TODO: Use the initWindowPosition. This requires to convert the units correctly (#1575).
return devicescale.GetAt(u.initMonitor.GetPos())
return u.deviceScaleFactor()
}
f := 0.0
@ -724,7 +727,8 @@ func (u *UserInterface) deviceScaleFactor() float64 {
if u.window != nil {
m = currentMonitor(u.window)
}
return devicescale.GetAt(m.GetPos())
mx, my := m.GetPos()
return devicescale.GetAt(mx, my)
}
func init() {
@ -933,11 +937,13 @@ func (u *UserInterface) updateSize() (float64, float64, bool) {
var w, h float64
if u.isFullscreen() {
v := currentMonitor(u.window).GetVideoMode()
m := currentMonitor(u.window)
v := m.GetVideoMode()
ww, wh := v.Width, v.Height
s := u.deviceScaleFactor()
w = fromGLFWMonitorPixel(float64(ww), s)
h = fromGLFWMonitorPixel(float64(wh), s)
mx, my := m.GetPos()
s := devicescale.VideoModeScaleAt(mx, my)
w = u.fromGLFWMonitorPixel(float64(ww), s)
h = u.fromGLFWMonitorPixel(float64(wh), s)
} else {
// 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
@ -946,9 +952,6 @@ func (u *UserInterface) updateSize() (float64, float64, bool) {
w = u.fromGLFWPixel(float64(ww))
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
}

View File

@ -81,22 +81,27 @@ import (
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
func fromGLFWMonitorPixel(x float64, deviceScale float64) float64 {
return x
// fromGLFWMonitorPixel must be called from the main thread.
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 {
// 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
}
// toGLFWPixel must be called from the main thread.
func (u *UserInterface) toGLFWPixel(x float64) float64 {
return x
}
func (u *UserInterface) toFramebufferPixel(x float64) float64 {
return x
}
func (u *UserInterface) adjustWindowPosition(x, y int) (int, int) {
return x, y
}

View File

@ -19,37 +19,23 @@
package glfw
import (
"math"
"github.com/hajimehoshi/ebiten/v2/internal/driver"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
// fromGLFWMonitorPixel must be called from the main thread.
func fromGLFWMonitorPixel(x float64, deviceScale float64) float64 {
// deviceScaleFactor is sometimes an unnice value (e.g., 1.502361). Use math.Ceil to clean the vaule.
return math.Ceil(x / deviceScale)
func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 {
return x / (videoModeScale * u.deviceScaleFactor())
}
// fromGLFWPixel must be called from the main thread.
func (u *UserInterface) fromGLFWPixel(x float64) float64 {
// deviceScaleFactor() is a scale by desktop environment (e.g., Cinnamon), while GetContentScale() is X's scale.
// They are different things and then need to be treated in different ways (#1350).
s, _ := currentMonitor(u.window).GetContentScale()
return x / float64(s)
return x / u.deviceScaleFactor()
}
// toGLFWPixel must be called from the main thread.
func (u *UserInterface) toGLFWPixel(x float64) float64 {
s, _ := currentMonitor(u.window).GetContentScale()
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())
return x * u.deviceScaleFactor()
}
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.
func fromGLFWMonitorPixel(x float64, deviceScale float64) float64 {
return x / deviceScale
func (u *UserInterface) fromGLFWMonitorPixel(x float64, videoModeScale float64) float64 {
return x / (videoModeScale * u.deviceScaleFactor())
}
// fromGLFWPixel must be called from the main thread.
@ -113,11 +113,6 @@ func (u *UserInterface) toGLFWPixel(x float64) float64 {
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) {
mx, my := currentMonitor(u.window).GetPos()
// As the video width/height might be wrong,