Skip to content

Qt: Add display monitor selector - #14196

Open
jasaaved wants to merge 5 commits into
PCSX2:masterfrom
jasaaved:Multiple-Displays
Open

Qt: Add display monitor selector#14196
jasaaved wants to merge 5 commits into
PCSX2:masterfrom
jasaaved:Multiple-Displays

Conversation

@jasaaved

@jasaaved jasaaved commented Mar 23, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

Adds a display monitor selector to the Qt frontend, allowing users to choose which monitor PCSX2 opens on. Also restores the main window position when a game shuts down.

Rationale Behind Changes

Users with multi-monitor setups previously had no control over which display PCSX2 opened on. This change provides explicit control and ensures the window position is remembered per monitor.

Suggested Testing Steps

  • Open PCSX2 with multiple monitors connected
  • Go to Display Settings and select a monitor from the new selector
  • Verify the window opens on or moves to the selected monitor
  • Launch a game, then close it
  • Verify the main window restores to the original monitor

Did you use AI to help find, test, or implement this issue or feature?

Yes. I am unfamiliar with Qt, so I used AI to help learn it during implementation.

@F0bes

F0bes commented Mar 23, 2026

Copy link
Copy Markdown
Member

Fill out our PR template.

@jasaaved
jasaaved force-pushed the Multiple-Displays branch from e40df2d to 771078a Compare March 25, 2026 05:18
@TellowKrinkle

Copy link
Copy Markdown
Member

Please rebase instead of merging

Comment thread pcsx2-qt/MainWindow.cpp Outdated
m_display_surface = new DisplaySurface();
const int monitor_index = Host::GetBaseIntSettingValue("UI", "DisplayMonitor", 0);
const QList<QScreen*> screens = QGuiApplication::screens();
QScreen* target_screen = (monitor_index > 0 && monitor_index <= screens.size()) ? screens[monitor_index - 1] : QGuiApplication::primaryScreen();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why -1 on the index? isn't the combobox index (which gets saved to the settings) zero-based?

@jasaaved jasaaved Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the default monitor takes up two slots in the combo box (default and monitor 1, in this case monitor 1 is my default). For example, monitor 2 is index 2 in the combo box but monitor 2 is in index 1 in what Qt returns in screens. That's why I subtract 1.

In my setup, my combobox looks like: Default, Monitor 1, Monitor 2. Default is always at index 0.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, I missed the first addItem() on line 126. That explains it, thanks.

@TheLastRar TheLastRar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not tested, and have only skim read the pr code, so bear with if I've misunderstood something.

Strictly speaking, PCSX2 should fullscreen on the same monitor that the window currently exists on. Nevertheless, having explicit control could be useful.
This PR, however, seems to always override that logic, as it forces the primary screen when no screen is specified.

Your handling of render to separate (where render_to_main is false) appears incorrect.
You aren’t adjusting the separate window position in the fullscreen case, instead seemingly adjusting it only when fullscreen is false.

You do, however, have added additional code to restore the window position. this is probably plastering over the above issue.

What platform have you tested your changes on?

Comment thread pcsx2-qt/MainWindow.cpp Outdated
Comment on lines +2609 to +2616
if (monitor_index > 0)
{
const QSize windowSize = m_display_surface->size();
const QRect screenGeo = target_screen->availableGeometry();
const QPoint center(screenGeo.x() + (screenGeo.width() - windowSize.width()) / 2,
screenGeo.y() + (screenGeo.height() - windowSize.height()) / 2);
m_display_surface->setGeometry(QRect(center, windowSize));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these changes made?
This section of code looks like it would only run when fullscreen is false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I render to a separate window, I want to make sure the window is at good position on the screen. I've had cases where the window would open on a different monitor and the title bar is off screen and I couldn't move it around. Originally, I had it open on the top left, that's when I discovered the issue.

Putting the separate window on the center of the screen makes sure this does not happen. I don't do this with Fullscreen because accidentally making the title bar off screen is not really an issue.

What I can add, once the window is open at a safe position, the user can move the window around and it can save that position for the session so every time a separate windowed window is launched it will be in that position.

I can also fix it so if it is windowed, using the main window, and already at the desired monitor, I can skip re-centering. I know it's at a safe location already.

@TheLastRar TheLastRar Mar 30, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So restoreDisplayWindowGeometryFromConfig() ends up restoring the window offscreen?
Ideally restoreDisplayWindowGeometryFromConfig() or the associated Qt functions it calls would handle this.
What situations did you have the window appear offscreen?

If correcting the position does prove to be necessary, we should verify that the window is offscreen before repositioning it.

Code for saving and loading the window position already exists and is in use.
You, however, are currently overriding the restored position when you are centring the window.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restoreDisplayWindowGeometryFromConfig() returns the saved screen coordinates on whatever monitor it was last on, right? What happens if my display set up changes? My ini files are on the cloud so I'm not always launching pcsx2 on the same device.

We can check if a window is off screen and recenter it when it is not. I think we can use Qt's contains function.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restoreDisplayWindowGeometryFromConfig() will use Qt's restoreGeomatry() function.
Qt documents restoreGeometry() to adjust the window position if the saved geometry was offscreen.
See Qt's documentation.

As such, I would assume the window position would be corrected, but I must admit I've never tested this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated restoreDisplayWindowGeometryFromConfig() and tested restoreGeometry(). When I load from the config and the geometry is not within the target screen, I have to get the geometry from the target screen to pass it to restoreGeometry(), but if I do that, I have to saveGeometry() to get the target screen's geometry which requires me choosing a position anyways, and the center of the screen is still the safest position to save.

Anyways, I still do use restoreDisplayWindowGeometryFromConfig() and cleaned it up a bit. This way it also saves the position of the separate window when it matches the target screen within and across different sessions.

Comment thread pcsx2-qt/MainWindow.cpp Outdated
Comment on lines +2623 to +2630
if (monitor_index > 0)
{
const QSize windowSize = m_display_container->size();
const QRect screenGeo = target_screen->availableGeometry();
const QPoint center(screenGeo.x() + (screenGeo.width() - windowSize.width()) / 2,
screenGeo.y() + (screenGeo.height() - windowSize.height()) / 2);
m_display_container->setGeometry(QRect(center, windowSize));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Comment thread pcsx2-qt/MainWindow.cpp Outdated
Comment on lines +2635 to +2642
if (monitor_index > 0)
{
m_pre_game_main_window_geometry = saveGeometry();
const QRect screenGeo = target_screen->availableGeometry();
const QPoint center(screenGeo.x() + (screenGeo.width() - width()) / 2,
screenGeo.y() + (screenGeo.height() - height()) / 2);
move(center);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above

Comment thread pcsx2-qt/MainWindow.cpp Outdated
Comment on lines +2552 to +2556
const int monitor_index = Host::GetBaseIntSettingValue("UI", "DisplayMonitor", 0);
const QList<QScreen*> screens = QGuiApplication::screens();
QScreen* target_screen = (monitor_index > 0 && monitor_index <= screens.size()) ? screens[monitor_index - 1] : QGuiApplication::primaryScreen();
m_display_surface->setScreen(target_screen);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should only perform this when fullscreen is true.
I also found that setScreen() may also be ineffective.
positioning the window (for which you've adjusted/added code for) proved more reliable in my testing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right. I can move setScreen() in the fullscreen path.

I've only tested on windows.

@kamfretoz

Copy link
Copy Markdown
Contributor

Adding that, this doesn't work on Wayland.

@jasaaved

Copy link
Copy Markdown
Contributor Author

Adding that, this doesn't work on Wayland.

I don't have wayland to test it out right now, but what doesn't work specifically?

@kamfretoz

Copy link
Copy Markdown
Contributor

I don't have wayland to test it out right now, but what doesn't work specifically?

The entire PR, in which to be fair is caused by limiations of Wayland itself (It doesn't allow the application itself to set their own window position). I haven't checked X11 but it theoretically can work there.

@TheLastRar

Copy link
Copy Markdown
Contributor

I don't have wayland to test it out right now, but what doesn't work specifically?

The entire PR, in which to be fair is caused by limiations of Wayland itself (It doesn't allow the application itself to set their own window position). I haven't checked X11 but it theoretically can work there.

Wayland seems to allow apps to pick the screen they show up on. (at least that's how it seems to behave under weston)
But yes, positioning beyond what screen the window is on isn't supported.

@jasaaved

jasaaved commented Mar 30, 2026

Copy link
Copy Markdown
Contributor Author

So I should make sure to setScreen whenever possible for Wayland before any repositioning? Wayland will just ignore all the other repositioning logic but setScreen should, at least, put the window on the right screen.

@jasaaved

Copy link
Copy Markdown
Contributor Author

I created a Wayland VM to test this. When starting in fullscreen, Wayland respects the selected monitor. However, all windowed mode options are controlled by the compositor, which ignores the selected monitor. Even setScreen appears to be ignored.

I tried a few workarounds, such as starting in fullscreen and then switching to windowed mode, but that did not work. It seems like this would require rendering at least one frame first before switching, which adds a fair amount of complexity just to support monitor selection in windowed mode on Wayland.

@TheLastRar

Copy link
Copy Markdown
Contributor

I don't have wayland to test it out right now, but what doesn't work specifically?

The entire PR, in which to be fair is caused by limiations of Wayland itself (It doesn't allow the application itself to set their own window position). I haven't checked X11 but it theoretically can work there.

Testing this myself, this seems to work for me under both WSL, and a manjaro VM

@JordanTheToaster JordanTheToaster added this to the Release 2.X milestone Jun 2, 2026
@kamfretoz

Copy link
Copy Markdown
Contributor

can this be rebased?

@jasaaved
jasaaved force-pushed the Multiple-Displays branch from ddacf90 to ef12fbb Compare June 7, 2026 04:01
@jasaaved

jasaaved commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

can this be rebased?

Rebased and added a commit that fixed a bug when windowed, rendering to main, and changing the target screen.

@F0bes F0bes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm testing on macOS.

It seems to work fine, but it's a little confusing.

Primary Monitor -> Goes to the primary monitor only when start fullscreen is enabled. When it's disabled, the window will not move positions.

Setting it to another monitor (not primary) will make the PCSX2 window move to that monitor when a game starts with start fullscreen enabled or disabled.

This also breaks the default behaviour of PCSX2. I think you might want to make another option. Something like Active Monitor or Current Monitor? I'm not good at names. This should make it so PCSX2 goes fullscreen on whatever monitor it is currently on. This is the current behaviour of master.

@jasaaved

jasaaved commented Jun 7, 2026

Copy link
Copy Markdown
Contributor Author

Primary Monitor -> Goes to the primary monitor only when start fullscreen is enabled. When it's disabled, the window will not move positions.

That's a bug I need to fix... I like your idea about the active/current monitor though. I feel like the primary monitor option is a bit redundant since the primary monitor technically has two entries on the monitor list. Would it be alright if I remove primary and add current? or should I have both?

@kamfretoz

kamfretoz commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Just me two cents:

Current Monitor -> Follows which monitor the cursor is on
Primary Monitor -> Use whichever monitor set as the primary
<------- Separator ------->
Monitor A
Monitor B
etc

@jasaaved

Copy link
Copy Markdown
Contributor Author

Current Monitor -> Follows which monitor PCSX2 is on
Primary Monitor -> Use whichever monitor set as the primary
<------- Separator ------->
Monitor A
Monitor B
etc

Followed this logic. The refactoring I did for the target screen should fix any primary monitor bugs from before.

I also changed it so the settings window opens on the same screen as the main window. The settings window still retains its previous location, but if the main window has moved to a different screen since the last launch, the settings window will recenter on the new screen.

@kamfretoz

Copy link
Copy Markdown
Contributor

When does the monitor override actually gets enforced? during game startup? only when startin fullscreen?

I tried every option but none of them works (but this could be because I'm on Linux using Wayland)

@jasaaved

Copy link
Copy Markdown
Contributor Author

When does the monitor override actually gets enforced? during game startup? only when startin fullscreen?

I tried every option but none of them works (but this could be because I'm on Linux using Wayland)

game startup. It was a while ago when I tested using a Linux VM using Wayland. It was working when starting fullscreen, but not windowed. Did that stop working?

@kamfretoz

Copy link
Copy Markdown
Contributor

game startup. It was a while ago when I tested using a Linux VM using Wayland. It was working when starting fullscreen, but not windowed. Did that stop working?

Neither are working for me, it used to work the last few times i tested the PR

jasaaved added 2 commits June 17, 2026 19:35
Adds a "Display Monitor" dropdown to Interface settings, allowing the user to choose which monitor a game launches on. Options include the current monitor, the primary monitor, or any connected display by name.

The selected monitor is respected when launching in fullscreen, in a separate window, or when rendering to the main window. In the render-to-main case, the main window moves to the target screen before the game starts and restores to its previous position afterward. Window geometry is saved and restored independently for each screen.
The settings menu retains its location on screen even after closing. If the main window has moved to a different screen since the settings window was last opened, it recenters on that screen instead of appearing on the previous one.
@jasaaved
jasaaved force-pushed the Multiple-Displays branch from f17766a to 86422df Compare June 18, 2026 02:43
@jasaaved

Copy link
Copy Markdown
Contributor Author

Neither are working for me, it used to work the last few times i tested the PR

Starting in fullscreen should be working now with Wayland rendering to main or a separate window. The problem was I removed the setScreen() calls. I didn't need them for Windows / display surfaces anymore but display containers / Wayland still needed them.

Windowed screens still won't work since Wayland doesn't give control over repositioning those.

I squashed most of my commits since most were bug fixes.

@kamfretoz

Copy link
Copy Markdown
Contributor

Windowed screens still won't work since Wayland doesn't give control over repositioning those.

Probably worth adding a warning notice about those wayland issues on the setting's description box.

Added a description for the display monitor setting and included a warning that Start Fullscreen needs to be enabled for Linux Wayland.
@jasaaved

Copy link
Copy Markdown
Contributor Author

Probably worth adding a warning notice about those wayland issues on the setting's description box.

I have added a description to the setting and added a warning for wayland.

Comment thread pcsx2-qt/MainWindow.cpp Outdated
Comment on lines +2925 to +2927
const QRect screenGeo = target_screen->availableGeometry();
move(screenGeo.x() + (screenGeo.width() - width()) / 2,
screenGeo.y() + (screenGeo.height() - height()) / 2);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible that the target screen geometry is smaller than the current window geometry? e.g. moving to a smaller screen. In that case, should we clamp width and height to the available screen geometry?
I suggest writing a helper function for this, since the same logic is used two more times in restoreDisplayWindowGeometryFromConfig().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. We have to clamp slightly a little bit more than the available screen geometry though. availableGeometry() doesn't take to account the title bar, at least on windows, so it's possible it gets cut off. I need to use frameGeometry() to subtract the difference.

Comment thread pcsx2-qt/MainWindow.cpp
if (dlg->screen() != screen())
{
const QRect screenGeo = screen()->availableGeometry();
dlg->move(screenGeo.center() - QPoint(dlg->frameGeometry().width() / 2, dlg->frameGeometry().height() / 2));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe slightly easier to use: dlg->frameGeometry().moveCenter(screenGeo.center())

Also it's probably(?) more efficient to do this before calling show(), unless the dialog is required to be visible to get the correct frame geometry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try it and see it if it works.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dlg->frameGeometry().moveCenter(screenGeo.center())

dlg->frameGeometry() returns a copy of Qrect of the settings window so calling moveCenter() on it won't move the settings window directly.

unless the dialog is required to be visible to get the correct frame geometry

This part is correct, but it's more about the correct frame geometry than visibility. I looked into this a bit more. Before show() is called, frameGeometry() pretty much equals geometry() because the windows manager hasn't decorated the final window so the final size hasn't been determined yet. So if show() is called after frameGeometry(), frameGeometry() will be missing the title bar and borders and our calculations will be wrong. That's why I had to move some showNormals() to get clamping to work correctly.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dlg->frameGeometry().moveCenter(screenGeo.center())

dlg->frameGeometry() returns a copy of Qrect of the settings window so calling moveCenter() on it won't move the settings window directly.

The line I wrote was just meant as a hint to convey my idea, it's not the full code. Of course you still need to call move() or setGeometry().

m_ui.displayMonitor->addItem(tr("Monitor %1: %2").arg(i + 1).arg(screens[i]->name()));
m_ui.displayMonitor->setCurrentIndex(Host::GetBaseIntSettingValue("UI", "DisplayMonitor", 0));
connect(m_ui.displayMonitor, &QComboBox::currentIndexChanged, this, [](int index) {
Host::SetBaseIntSettingValue("UI", "DisplayMonitor", index);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the combobox index as persistent screen identifier seems unreliable to me. The documentation of QGuiApplication::screens() doesn't guarantee a consistent ordering of the returned list. Even if the order was stable, what happens if I have three screens connected, select the second screen, then unplug it? Won't the wrong screen be used then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently if an invalid index is used, the current monitor will be used so we don't have to worry about crashes. There are QT signals for display add and remove so we can have the interface settings update in real time whenever a monitor is connected or disconnected. I haven't pushed that yet, but it seems to be working fine so far.

But it is true, there is no guarantee the ordering will stable. We can use the names of the displays... but that won't work in case someone has multiple monitors of the same name. The monitors have to be sorted in a consistent, deterministic order. My idea right now is geometry position, but I haven't tested yet.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently if an invalid index is used, the current monitor will be used so we don't have to worry about crashes.

I wasn't worried about crashing (the code is already robust against that) but about the wrong monitor being used, e.g., when the index is still valid but refers to a different monitor because the screens() list changed.

We can use the names of the displays... but that won't work in case someone has multiple monitors of the same name. The monitors have to be sorted in a consistent, deterministic order. My idea right now is geometry position, but I haven't tested yet.

There is QScreen::serialNumber but I haven't checked what it returns and whether it's reliable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

QScreen::serialNumber doesn't work consistently so I went with sorting displays by geometry positions, so the order is always consistent. If a user rearranges their monitors in their OS, a game may launch on an unexpected display, but the interface settings will still reflect the correct index, and they can correct it from there.

jasaaved added 2 commits June 23, 2026 01:41
…geometry positions

Qt signals have been added so the interface settings are now updated in real time when a display is removed or added. If an index is no longer valid, "Current Monitor" is set.

Screens are now sorted by their geometry positions. QGuiApplication::screens() can't guarantee screen order so there's always a possibility the indexes on the interface setting does not match the indexes of the screen when a game launches. Sorting helps keep the order consistent.
When a window's saved geometry was recorded on a larger monitor, restoring it on a smaller screen could place it partially or fully off-screen. A centeredGeometry() helper now centers the window on the target screen and shrinks it to fit within the available area, accounting for frame offsets so the title bar is never clipped.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants