Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SetWindowSize() and Layout() sizes not matching at fractional display scale factors #2978

Open
1 of 11 tasks
tinne26 opened this issue May 2, 2024 · 9 comments
Open
1 of 11 tasks

Comments

@tinne26
Copy link

tinne26 commented May 2, 2024

Ebitengine Version

v2.7.2

Operating System

  • Windows
  • macOS
  • Linux
  • FreeBSD
  • OpenBSD
  • Android
  • iOS
  • Nintendo Switch
  • PlayStation 5
  • Xbox
  • Web Browsers

Go Version (go version)

go1.22.2

What steps will reproduce the problem?

Running the following program with a fractional display scale factor (I tested 125%):

package main

import "fmt"

import "github.com/hajimehoshi/ebiten/v2"

type Game struct {
	windowSize int
	waitLeft int
	lastW, lastH int
	lastWf, lastHf float64
}

func (self *Game) Layout(w, h int) (int, int) {
	if w != self.lastW || h != self.lastH {
		fmt.Printf("new dimensions at layout: %d x %d\n", w, h)
		self.lastW, self.lastH = w, h
	}
	return w, h
}

// (uncomment to test LayoutF() behavior)
// func (self *Game) LayoutF(w, h float64) (float64, float64) {
// 	if w != self.lastWf || h != self.lastHf {
// 		fmt.Printf("new dimensions at layout: %.02f x %.02f\n", w, h)
// 		self.lastWf, self.lastHf = w, h
// 	}
// 	return w, h
// }

func (self *Game) Update() error {
	if self.waitLeft > 0 {
		self.waitLeft -= 1
	} else { // self.waitLeft <= 0
		if self.windowSize >= 400 { return nil } // don't keep growing indefinitely
		self.windowSize += 1
		self.waitLeft = 120 // two seconds wait (lower is ok too)
		ebiten.SetWindowSize(self.windowSize, self.windowSize)
		fmt.Printf("setting window size to %d x %d\n", self.windowSize, self.windowSize)
	}

	return nil
}

func (self *Game) Draw(*ebiten.Image) {}

func main() {
	ebiten.SetWindowSize(300, 300)
	err := ebiten.RunGame(&Game{ windowSize: 300 })
	if err != nil { panic(err) }
}

What is the expected result?

Layout sizes match most recently set window sizes.

What happens instead?

Running the program with device scale factor 100% makes everything work as expected:

new dimensions at layout: 300 x 300
setting window size to 301 x 301
new dimensions at layout: 301 x 301
setting window size to 302 x 302
new dimensions at layout: 302 x 302
setting window size to 303 x 303
new dimensions at layout: 303 x 303
setting window size to 304 x 304
new dimensions at layout: 304 x 304
setting window size to 305 x 305
new dimensions at layout: 305 x 305
setting window size to 306 x 306
new dimensions at layout: 306 x 306
setting window size to 307 x 307
new dimensions at layout: 307 x 307
setting window size to 308 x 308
new dimensions at layout: 308 x 308
setting window size to 309 x 309
new dimensions at layout: 309 x 309
setting window size to 310 x 310
new dimensions at layout: 310 x 310

Running the program with device scale factor 125% shows discordances:

new dimensions at layout: 300 x 300
setting window size to 301 x 301
setting window size to 302 x 302
new dimensions at layout: 301 x 301
setting window size to 303 x 303
new dimensions at layout: 302 x 302
setting window size to 304 x 304
new dimensions at layout: 304 x 304
setting window size to 305 x 305
setting window size to 306 x 306
new dimensions at layout: 305 x 305
setting window size to 307 x 307
new dimensions at layout: 306 x 306
setting window size to 308 x 308
new dimensions at layout: 308 x 308
setting window size to 309 x 309
setting window size to 310 x 310
new dimensions at layout: 309 x 309

We see that we can get to pretty much any window size, but we tend to undershoot the target value.

Running the program with device scale factor 125% and LayoutF(), to have a more detailed view of what might be going on internally:

new dimensions at layout: 300.00 x 300.00
setting window size to 301 x 301
new dimensions at layout: 300.80 x 300.80
setting window size to 302 x 302
new dimensions at layout: 301.60 x 301.60
setting window size to 303 x 303
new dimensions at layout: 302.40 x 302.40
setting window size to 304 x 304
new dimensions at layout: 304.00 x 304.00
setting window size to 305 x 305
new dimensions at layout: 304.80 x 304.80
setting window size to 306 x 306
new dimensions at layout: 305.60 x 305.60
setting window size to 307 x 307
new dimensions at layout: 306.40 x 306.40
setting window size to 308 x 308
new dimensions at layout: 308.00 x 308.00
setting window size to 309 x 309
new dimensions at layout: 308.80 x 308.80
setting window size to 310 x 310
new dimensions at layout: 309.60 x 309.60

Here we can see that at each size increase, we keep getting further away from the target (-0.2, -0.4, -0.6) for a few steps, and then we snap back to the right size.

At first I thought the issue might be the OS not accepting certain sizes, but after the tests, I think this looks quite suspicious on Ebitengine's side.

Anything else you feel useful to add?

I wasn't sure that having SetWindowSize() and Layout() sizes match would even be possible, but after more detailed testing, I think it should be possible, this looks more like an internal scaling calculation mistake than an OS limitation.

For context, having this work correctly is useful for setting perfect windowed sizes on pixel art games.

@hajimehoshi
Copy link
Owner

hajimehoshi commented May 18, 2024

I wasn't sure that having SetWindowSize() and Layout() sizes match would even be possible, but after more detailed testing, I think it should be possible, this looks more like an internal scaling calculation mistake than an OS limitation.

Do you have an idea how to fix this?

For context, having this work correctly is useful for setting perfect windowed sizes on pixel art games.

I'm not sure we can render something in a pixel-perfect way with 125% mode.

@tinne26
Copy link
Author

tinne26 commented May 18, 2024

Do you have an idea how to fix this?

No, I'd have to go through the code to figure out if some floor or ceiling is being applied too early at some point or something like that.

I'm not sure we can render something in a pixel-perfect way with 125% mode.

Ignoring macOS and retina displays and all those apple things, yes, the device scaling is indifferent for rendering pixel perfect art. Notice that I'm not saying "render pixel art perfectly at an arbitrary scale", but "being able to set a window size compatible with our pixel art" (perfect multiple). This only depends on SetWindowSize() setting the requested size perfectly, which is what's not happening here.

@hajimehoshi
Copy link
Owner

hajimehoshi commented Sep 2, 2024

Does this issue happen with LayoutF instead of Layout?

@hajimehoshi
Copy link
Owner

The suspicious logic is

  • ebiten/gameforui.go

    Lines 83 to 101 in 3eda0dd

    func (g *gameForUI) Layout(outsideWidth, outsideHeight float64) (float64, float64) {
    if l, ok := g.game.(LayoutFer); ok {
    return l.LayoutF(outsideWidth, outsideHeight)
    }
    // Even if the original value is less than 1, the value must be a positive integer (#2340).
    // This is for a simple implementation of Layout, which returns the argument values without modifications.
    // TODO: Remove this hack when Game.Layout takes floats instead of integers.
    if outsideWidth < 1 {
    outsideWidth = 1
    }
    if outsideHeight < 1 {
    outsideHeight = 1
    }
    // TODO: Add a new Layout function taking float values (#2285).
    sw, sh := g.game.Layout(int(outsideWidth), int(outsideHeight))
    return float64(sw), float64(sh)
    }
  • sw := int(math.Ceil(c.screenWidth))
    sh := int(math.Ceil(c.screenHeight))
    ow := int(math.Ceil(c.offscreenWidth))
    oh := int(math.Ceil(c.offscreenHeight))

@hajimehoshi
Copy link
Owner

and/or

ebiten/internal/ui/ui_glfw.go

Lines 1200 to 1248 in 3eda0dd

func (u *UserInterface) outsideSize() (float64, float64, error) {
f, err := u.isFullscreen()
if err != nil {
return 0, 0, err
}
n, err := u.isNativeFullscreen()
if err != nil {
return 0, 0, err
}
if f && !n {
// On Linux, the window size is not reliable just after making the window
// fullscreened. Use the monitor size.
// On macOS's native fullscreen, the window's size returns a more precise size
// reflecting the adjustment of the view size (#1745).
var w, h float64
m, err := u.currentMonitor()
if err != nil {
return 0, 0, err
}
if m != nil {
w, h = m.sizeInDIP()
}
return w, h, nil
}
a, err := u.window.GetAttrib(glfw.Iconified)
if err != nil {
return 0, 0, err
}
if a == glfw.True {
return float64(u.origWindowWidthInDIP), float64(u.origWindowHeightInDIP), nil
}
// Instead of u.origWindow{Width,Height}InDIP, use the actual window size here.
// On Windows, the specified size at SetSize and the actual window size might
// not match (#1163).
ww, wh, err := u.window.GetSize()
if err != nil {
return 0, 0, err
}
m, err := u.currentMonitor()
if err != nil {
return 0, 0, err
}
s := m.DeviceScaleFactor()
w := dipFromGLFWPixel(float64(ww), s)
h := dipFromGLFWPixel(float64(wh), s)
return w, h, nil
}

@hajimehoshi
Copy link
Owner

We have to make a diagram or something to understand how the 'sizes' are converted... That's pretty compilicated. In the far future, we want to rewrite the GLFW part into Go and then the border line between GLFW and internal/ui will be blurred. I hope we would find a much clearer logic then.

@hajimehoshi hajimehoshi added this to the v2.9.0 milestone Sep 20, 2024
@venning
Copy link

venning commented Oct 1, 2024

Adding my experience, based on a Discord conversation today with @tinne26:

I am on macOS 13.1. Layout() and LayoutF() receive incorrect screen dimensions when in fullscreen.

Laptop Monitor
---
14" MacBook Pro, 2021 (with a notch)
Actual resolution:    3024 x 1964 [source: https://support.apple.com/en-us/111902]
OS resolution:        1800 x 1169
Monitor().Size():     1800 x 1169 [+0 x +0]
Usable resolution:    1800 x 1125 [what applications can actually use below the notch]
Layout() resolution:  1799 x 1126 [-1 x +1]
LayoutF() resolution: 1799 x 1126 [-1 x +1]
DeviceScaleFactor():  2


External Monitor
---
Gigabyte M32U
Actual resolution:    3840 x 2160
OS resolution:        3008 x 1692
Monitor().Size():     3008 x 1692 [+0 x +0]
Usable resolution:    n/a
Layout() resolution:  3007 x 1692 [-1 x +0]
LayoutF() resolution: 3007 x 1692 [-1 x +0]
DeviceScaleFactor():  2

I use ebiten.Monitor().Size() to calculate canvas size, but that does require me to have a separate routine to account for the notch; I don't think that is a great long-term strategy since I'm not sure how all other monitors may be notched or otherwise have unusable space.

For completeness, I use displayplacer to manage my monitors: https://github.com/jakehilborn/displayplacer
It reports both monitors as having "scaling: on".

Edited for clarity

@hajimehoshi
Copy link
Owner

Layout() resolution: 1799 x 1126 [-1 x +1]
Layout() resolution: 3007 x 1692 [-1 x +0]

What was the result of LayoutF? The same?

@venning
Copy link

venning commented Oct 1, 2024

Yes, they receive the same parameters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants