Switch out the devicescale implementation by one that relies on glfw/xrandr. (#1800)

This should fix fullscreen mode on Linux/X11 systems in general,
while not affecting other systems.

Note that this deletes a bunch of OS X specific and Windows specific code,
as GLFW already provides this functionality.

This change is not expected to cause regressions, however,
the current behavior is still wrong and leads to wrong/unintended window sizes.
To be fixed in further PRs.

Updates #1774
This commit is contained in:
divVerent 2021-09-13 23:35:02 -04:00 committed by GitHub
parent cc1ac47387
commit 60df512352
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 138 additions and 554 deletions

1
go.mod
View File

@ -9,6 +9,7 @@ require (
github.com/hajimehoshi/go-mp3 v0.3.2
github.com/hajimehoshi/oto/v2 v2.1.0-alpha.0.20210912073017-18657977e3dc
github.com/jakecoffman/cp v1.1.0
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
github.com/jfreymuth/oggvorbis v1.0.3
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
golang.org/x/mobile v0.0.0-20210902104108-5d9a33257ab5

2
go.sum
View File

@ -13,6 +13,8 @@ github.com/hajimehoshi/oto/v2 v2.1.0-alpha.0.20210912073017-18657977e3dc h1:ztXP
github.com/hajimehoshi/oto/v2 v2.1.0-alpha.0.20210912073017-18657977e3dc/go.mod h1:rUKQmwMkqmRxe+IAof9+tuYA2ofm8cAWXFmSfzDN8vQ=
github.com/jakecoffman/cp v1.1.0 h1:bhKvCNbAddYegYHSV5abG3G23vZdsISgqXa4X/lK8Oo=
github.com/jakecoffman/cp v1.1.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 h1:dy+DS31tGEGCsZzB45HmJJNHjur8GDgtRNX9U7HnSX4=
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240/go.mod h1:3P4UH/k22rXyHIJD2w4h2XMqPX4Of/eySEZq9L6wqc4=
github.com/jfreymuth/oggvorbis v1.0.3 h1:MLNGGyhOMiVcvea9Dp5+gbs2SAwqwQbtrWnonYa0M0Y=
github.com/jfreymuth/oggvorbis v1.0.3/go.mod h1:1U4pqWmghcoVsCJJ4fRBKv9peUJMBHixthRlBeD6uII=
github.com/jfreymuth/vorbis v1.0.2 h1:m1xH6+ZI4thH927pgKD8JOH4eaGRm18rEE9/0WKjvNE=

View File

@ -1,152 +0,0 @@
// Copyright 2020 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 (dragonfly || freebsd || linux || netbsd || openbsd || solaris) && !android
// +build dragonfly freebsd linux netbsd openbsd solaris
// +build !android
package devicescale
import (
"encoding/xml"
"os"
"os/exec"
"path/filepath"
"strconv"
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
type xmlBool bool
func (b *xmlBool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var s string
if err := d.DecodeElement(&s, &start); err != nil {
return err
}
*b = xmlBool(s == "yes")
return nil
}
type cinnamonMonitors struct {
XMLName xml.Name `xml:"monitors"`
Version string `xml:"version,attr"`
Configuration []cinnamonMonitorsConfiguration `xml:"configuration"`
}
type cinnamonMonitorsConfiguration struct {
BaseScale float64 `xml:"base_scale"`
Output []struct {
X int `xml:"x"`
Y int `xml:"y"`
Width int `xml:"width"`
Height int `xml:"height"`
Scale float64 `xml:"scale"`
Primary xmlBool `xml:"primary"`
} `xml:"output"`
}
func (c *cinnamonMonitorsConfiguration) matchesWithGLFWMonitors(monitors []*glfw.Monitor) bool {
type area struct {
X, Y, Width, Height int
}
areas := map[area]struct{}{}
for _, o := range c.Output {
if o.Width == 0 || o.Height == 0 {
continue
}
areas[area{
X: o.X,
Y: o.Y,
Width: o.Width,
Height: o.Height,
}] = struct{}{}
}
if len(areas) != len(monitors) {
return false
}
for _, m := range monitors {
x, y := m.GetPos()
v := m.GetVideoMode()
a := area{
X: x,
Y: y,
Width: v.Width,
Height: v.Height,
}
if _, ok := areas[a]; !ok {
return false
}
}
return true
}
func cinnamonScaleFromXML() (float64, error) {
home, err := os.UserHomeDir()
if err != nil {
return 0, err
}
f, err := os.Open(filepath.Join(home, ".config", "cinnamon-monitors.xml"))
if err != nil {
return 0, err
}
defer f.Close()
d := xml.NewDecoder(f)
var monitors cinnamonMonitors
if err = d.Decode(&monitors); err != nil {
return 0, err
}
for _, c := range monitors.Configuration {
if !c.matchesWithGLFWMonitors(glfw.GetMonitors()) {
continue
}
for _, v := range c.Output {
// TODO: Get the monitor at the specified position.
// TODO: Consider the base scale?
if v.Primary && v.Scale != 0.0 {
return v.Scale, nil
}
}
}
return 0, nil
}
func cinnamonScale() float64 {
if s, err := cinnamonScaleFromXML(); err == nil && s > 0 {
return s
}
out, err := exec.Command("gsettings", "get", "org.cinnamon.desktop.interface", "scaling-factor").Output()
if err != nil {
if err == exec.ErrNotFound {
return 0
}
if _, ok := err.(*exec.ExitError); ok {
return 0
}
panic(err)
}
m := gsettingsRe.FindStringSubmatch(string(out))
s, err := strconv.Atoi(m[1])
if err != nil {
return 0
}
return float64(s)
}

View File

@ -28,7 +28,8 @@ var (
)
// GetAt returns the device scale at (x, y).
// x and y are in device-dependent pixels.
// 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()

View File

@ -0,0 +1,35 @@
// 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 && !ios && !js
// +build !android,!ios,!js
package devicescale
import (
"github.com/hajimehoshi/ebiten/v2/internal/glfw"
)
func monitorAt(x, y int) *glfw.Monitor {
// Note: this assumes that x, y are exact monitor origins.
// If they're not, this arbitrarily returns the first monitor.
monitors := glfw.GetMonitors()
for _, mon := range monitors {
mx, my := mon.GetPos()
if x == mx && y == my {
return mon
}
}
return monitors[0]
}

View File

@ -1,42 +0,0 @@
// Copyright 2018 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
// +build darwin,!ios
package devicescale
// #cgo CFLAGS: -x objective-c
// #cgo LDFLAGS: -framework AppKit
//
// #import <AppKit/AppKit.h>
//
// static float scaleAt(int x, int y) {
// // On macOS, the direction of Y axis is inverted from GLFW monitors (#807).
// // This is an inverse function of _glfwTransformYNS in GLFW (#1113).
// y = CGDisplayBounds(CGMainDisplayID()).size.height - y - 1;
//
// NSArray<NSScreen*>* screens = [NSScreen screens];
// for (NSScreen* screen in screens) {
// if (NSPointInRect(NSMakePoint(x, y), [screen frame])) {
// return [screen backingScaleFactor];
// }
// }
// return 0;
// }
import "C"
func impl(x, y int) float64 {
return float64(C.scaleAt(C.int(x), C.int(y)))
}

View File

@ -1,4 +1,4 @@
// Copyright 2018 The Ebiten Authors
// 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.
@ -12,8 +12,12 @@
// 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 impl(x, y int) float64 {
return 1
sx, _ := monitorAt(x, y).GetContentScale()
return float64(sx)
}

View File

@ -1,101 +0,0 @@
// Copyright 2018 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 (dragonfly || freebsd || linux || netbsd || openbsd || solaris) && !android
// +build dragonfly freebsd linux netbsd openbsd solaris
// +build !android
package devicescale
import (
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)
type desktop int
const (
desktopUnknown desktop = iota
desktopGnome
desktopCinnamon
desktopUnity
desktopKDE
desktopXfce
)
func currentDesktop() desktop {
tokens := strings.Split(os.Getenv("XDG_CURRENT_DESKTOP"), ":")
switch tokens[len(tokens)-1] {
case "GNOME":
return desktopGnome
case "X-Cinnamon":
return desktopCinnamon
case "Unity":
return desktopUnity
case "KDE":
return desktopKDE
case "XFCE":
return desktopXfce
default:
return desktopUnknown
}
}
var gsettingsRe = regexp.MustCompile(`\Auint32 (\d+)\s*\z`)
func gnomeScale() float64 {
// TODO: Should 'monitors.xml' be loaded?
out, err := exec.Command("gsettings", "get", "org.gnome.desktop.interface", "scaling-factor").Output()
if err != nil {
if err == exec.ErrNotFound {
return 0
}
if _, ok := err.(*exec.ExitError); ok {
return 0
}
panic(err)
}
m := gsettingsRe.FindStringSubmatch(string(out))
s, err := strconv.Atoi(m[1])
if err != nil {
return 0
}
return float64(s)
}
func impl(x, y int) float64 {
s := -1.0
switch currentDesktop() {
case desktopGnome:
// TODO: Support wayland and per-monitor scaling https://wiki.gnome.org/HowDoI/HiDpi
s = gnomeScale()
case desktopCinnamon:
s = cinnamonScale()
case desktopUnity:
// TODO: Implement, supports per-monitor scaling
case desktopKDE:
// TODO: Implement, appears to support per-monitor scaling
case desktopXfce:
// TODO: Implement
}
if s <= 0 {
s = 1
}
return s
}

View File

@ -1,256 +0,0 @@
// Copyright 2018 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.
package devicescale
import (
"fmt"
"runtime"
"unsafe"
"golang.org/x/sys/windows"
)
const (
logPixelsX = 88
monitorDefaultToNearest = 2
mdtEffectiveDpi = 0
)
type rect struct {
left int32
top int32
right int32
bottom int32
}
var (
user32 = windows.NewLazySystemDLL("user32")
gdi32 = windows.NewLazySystemDLL("gdi32")
shcore = windows.NewLazySystemDLL("shcore")
)
var (
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
procGetWindowDC = user32.NewProc("GetWindowDC")
procReleaseDC = user32.NewProc("ReleaseDC")
procMonitorFromRect = user32.NewProc("MonitorFromRect")
procGetMonitorInfo = user32.NewProc("GetMonitorInfoW")
procGetDeviceCaps = gdi32.NewProc("GetDeviceCaps")
// GetScaleFactorForMonitor function can return unrelaiavle value (e.g. returning 180
// for 200% scale). Use GetDpiForMonitor instead.
procGetDpiForMonitor = shcore.NewProc("GetDpiForMonitor")
)
var shcoreAvailable = false
type winErr struct {
FuncName string
Code windows.Errno
Return uintptr
}
func (e *winErr) Error() string {
return fmt.Sprintf("devicescale: %s failed: error code: %d", e.FuncName, e.Code)
}
func init() {
if shcore.Load() == nil {
shcoreAvailable = true
}
}
func setProcessDPIAware() error {
r, _, e := procSetProcessDPIAware.Call()
if e != nil && e.(windows.Errno) != 0 {
return &winErr{
FuncName: "SetProcessDPIAware",
Code: e.(windows.Errno),
}
}
if r == 0 {
return &winErr{
FuncName: "SetProcessDPIAware",
Return: r,
}
}
return nil
}
func getWindowDC(hwnd uintptr) (uintptr, error) {
r, _, e := procGetWindowDC.Call(hwnd)
if e != nil && e.(windows.Errno) != 0 {
return 0, &winErr{
FuncName: "GetWindowDC",
Code: e.(windows.Errno),
}
}
if r == 0 {
return 0, &winErr{
FuncName: "GetWindowDC",
Return: r,
}
}
return r, nil
}
func releaseDC(hwnd, hdc uintptr) error {
r, _, e := procReleaseDC.Call(hwnd, hdc)
if e != nil && e.(windows.Errno) != 0 {
return &winErr{
FuncName: "ReleaseDC",
Code: e.(windows.Errno),
}
}
if r == 0 {
return &winErr{
FuncName: "ReleaseDC",
Return: r,
}
}
return nil
}
func getDeviceCaps(hdc uintptr, nindex int) (int, error) {
r, _, e := procGetDeviceCaps.Call(hdc, uintptr(nindex))
if e != nil && e.(windows.Errno) != 0 {
return 0, &winErr{
FuncName: "GetDeviceCaps",
Code: e.(windows.Errno),
}
}
return int(r), nil
}
func monitorFromRect(lprc *rect, dwFlags int) (uintptr, error) {
r, _, e := procMonitorFromRect.Call(uintptr(unsafe.Pointer(lprc)), uintptr(dwFlags))
runtime.KeepAlive(lprc)
if e != nil && e.(windows.Errno) != 0 {
return 0, &winErr{
FuncName: "MonitorFromRect",
Code: e.(windows.Errno),
}
}
if r == 0 {
return 0, &winErr{
FuncName: "MonitorFromRect",
Return: r,
}
}
return r, nil
}
func getMonitorInfo(hMonitor uintptr, lpMonitorInfo uintptr) error {
r, _, e := procGetMonitorInfo.Call(hMonitor, lpMonitorInfo)
if e != nil && e.(windows.Errno) != 0 {
return &winErr{
FuncName: "GetMonitorInfo",
Code: e.(windows.Errno),
}
}
if r == 0 {
return &winErr{
FuncName: "GetMonitorInfo",
Return: r,
}
}
return nil
}
func getDpiForMonitor(hMonitor uintptr, dpiType uintptr, dpiX, dpiY *uint32) error {
r, _, e := procGetDpiForMonitor.Call(hMonitor, dpiType, uintptr(unsafe.Pointer(dpiX)), uintptr(unsafe.Pointer(dpiY)))
if e != nil && e.(windows.Errno) != 0 {
return &winErr{
FuncName: "GetDpiForMonitor",
Code: e.(windows.Errno),
}
}
if r != 0 {
return &winErr{
FuncName: "GetDpiForMonitor",
Return: r,
}
}
return nil
}
func getFromLogPixelSx() float64 {
dc, err := getWindowDC(0)
if err != nil {
const (
errorInvalidWindowHandle = 1400
errorResourceDataNotFound = 1812
)
// On Wine, it looks like GetWindowDC(0) doesn't work (#738, #743).
code := err.(*winErr).Code
if code == errorInvalidWindowHandle {
return 1
}
if code == errorResourceDataNotFound {
return 1
}
panic(err)
}
// Note that GetDeviceCaps with LOGPIXELSX always returns a same value for any monitors
// even if multiple monitors are used.
dpi, err := getDeviceCaps(dc, logPixelsX)
if err != nil {
panic(err)
}
if err := releaseDC(0, dc); err != nil {
panic(err)
}
return float64(dpi) / 96
}
func impl(x, y int) float64 {
if err := setProcessDPIAware(); err != nil {
panic(err)
}
// On Windows 7 or older, shcore.dll is not available.
if !shcoreAvailable {
return getFromLogPixelSx()
}
lprc := rect{
left: int32(x),
right: int32(x + 1),
top: int32(y),
bottom: int32(y + 1),
}
// MonitorFromPoint requires to pass a POINT value, and there seems no portable way to
// do this with Cgo. Use MonitorFromRect instead.
m, err := monitorFromRect(&lprc, monitorDefaultToNearest)
if err != nil {
// monitorFromRect can fail in some environments (#1612)
return getFromLogPixelSx()
}
dpiX := uint32(0)
dpiY := uint32(0) // Passing dpiY is needed even though this is not used, or GetDpiForMonitor returns an error.
if err := getDpiForMonitor(m, mdtEffectiveDpi, &dpiX, &dpiY); err != nil {
// getDpiForMonitor can fail in some environments (#1612)
return getFromLogPixelSx()
}
runtime.KeepAlive(dpiY)
return float64(dpiX) / 96
}

View File

@ -0,0 +1,83 @@
// 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 impl(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.
// 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)
sx, _ := m.GetContentScale()
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 float64(sx)
}
if err = randr.Init(xconn); err != nil {
// No RANDR extension?
// No problem.
return float64(sx)
}
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)
}
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 float64(sx) * scale
}
}
// Monitor not known to XRandR. Weird.
return float64(sx)
}

View File

@ -80,6 +80,13 @@ type Monitor struct {
m uintptr
}
func (m *Monitor) GetContentScale() (float32, float32) {
var sx, sy float32
glfwDLL.call("glfwGetMonitorContentScale", m.m, uintptr(unsafe.Pointer(&sx)), uintptr(unsafe.Pointer(&sy)))
panicError()
return sx, sy
}
func (m *Monitor) GetPos() (int, int) {
var x, y int32
glfwDLL.call("glfwGetMonitorPos", m.m, uintptr(unsafe.Pointer(&x)), uintptr(unsafe.Pointer(&y)))

View File

@ -246,6 +246,8 @@ func ensureMonitors() []*monitor {
// getMonitorFromPosition must be called on the main thread.
func getMonitorFromPosition(wx, wy int) *monitor {
for _, m := range ensureMonitors() {
// TODO: Fix incorrectness in the cases of https://github.com/glfw/glfw/issues/1961.
// See also internal/devicescale/impl_desktop.go for a maybe better way of doing this.
if m.x <= wx && wx < m.x+m.vm.Width && m.y <= wy && wy < m.y+m.vm.Height {
return m
}