From 884922a1cb3e78d43f5964de5efc6580d248ba93 Mon Sep 17 00:00:00 2001 From: Collecting Date: Sun, 4 Jan 2026 22:00:08 +0000 Subject: [PATCH] fix(gamescope): DPI Re-architecture For GameScope Compatability Signed-off-by: Collecting --- src/citron/main.cpp | 99 +++++++++++++++------------------------------ 1 file changed, 33 insertions(+), 66 deletions(-) diff --git a/src/citron/main.cpp b/src/citron/main.cpp index 55a9b23e9..3c8c77d99 100644 --- a/src/citron/main.cpp +++ b/src/citron/main.cpp @@ -6089,96 +6089,71 @@ void VolumeButton::ResetMultiplier() { #endif static void SetHighDPIAttributes() { + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || qgetenv("XDG_CURRENT_DESKTOP") == "gamescope"; + #ifdef _WIN32 - // For Windows, we want to avoid scaling artifacts on fractional scaling ratios. - // This is done by setting the optimal scaling policy for the primary screen. + // Windows logic: Set policy globally. + // removed the 'temp QApplication' here because in Qt 6 it locks the DPI logic + // before our environment overrides in main() can take effect. + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::Round); - // Create a temporary QApplication. - int temp_argc = 0; - char** temp_argv = nullptr; - QApplication temp{temp_argc, temp_argv}; - - // Get the current screen geometry. - const QScreen* primary_screen = QGuiApplication::primaryScreen(); - if (primary_screen == nullptr) { - return; - } - - const QRect screen_rect = primary_screen->geometry(); - const int real_width = screen_rect.width(); - const int real_height = screen_rect.height(); - const float real_ratio = primary_screen->logicalDotsPerInch() / 96.0f; - - // Recommended minimum width and height for proper window fit. - // Any screen with a lower resolution than this will still have a scale of 1. - constexpr float minimum_width = 1350.0f; - constexpr float minimum_height = 900.0f; - - const float width_ratio = std::max(1.0f, real_width / minimum_width); - const float height_ratio = std::max(1.0f, real_height / minimum_height); - - // Get the lower of the 2 ratios and truncate, this is the maximum integer scale. - const float max_ratio = std::trunc(std::min(width_ratio, height_ratio)); - - if (max_ratio > real_ratio) { - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::Round); - } else { - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::Floor); - } -#else - // Other OSes should be better than Windows at fractional scaling. - QApplication::setHighDpiScaleFactorRoundingPolicy( - Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); -#endif - - // Set the DPI awareness for better scaling on Windows -#ifdef _WIN32 - // Enable Per Monitor DPI Awareness for Windows 8.1+ SetProcessDPIAware(); - // For Windows 10+, use Per Monitor v2 DPI Awareness - // This provides better scaling for multi-monitor setups HMODULE shcore = LoadLibrary(L"shcore.dll"); if (shcore) { typedef HRESULT(WINAPI* SetProcessDpiAwarenessFunc)(int); SetProcessDpiAwarenessFunc setProcessDpiAwareness = (SetProcessDpiAwarenessFunc)GetProcAddress(shcore, "SetProcessDpiAwareness"); if (setProcessDpiAwareness) { - // PROCESS_PER_MONITOR_DPI_AWARE_V2 = 2 - setProcessDpiAwareness(2); + setProcessDpiAwareness(2); // PROCESS_PER_MONITOR_DPI_AWARE_V2 } FreeLibrary(shcore); } +#else + if (is_gamescope) { + // Force 1:1 pixel mapping for Steam Deck to prevent bloated windows. + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::Floor); + } else { + // Standard Linux desktops handle fractional scaling better via PassThrough + QGuiApplication::setHighDpiScaleFactorRoundingPolicy( + Qt::HighDpiScaleFactorRoundingPolicy::PassThrough); + } #endif } int main(int argc, char* argv[]) { // 1. Detect Gamescope/Steam Deck hardware - const bool is_gamescope = qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" || - !qgetenv("GAMESCOPE_WIDTH").isEmpty() || + const bool is_gamescope = !qgetenv("GAMESCOPE_WIDTH").isEmpty() || + qgetenv("XDG_CURRENT_DESKTOP") == "gamescope" || !qgetenv("STEAM_DECK").isEmpty(); if (is_gamescope) { // Kill the SteamOS scaling requests before they can bloat the UI QGuiApplication::setDesktopSettingsAware(false); - QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::Floor); // Force 1:1 pixel ratio qputenv("QT_ENABLE_HIGHDPI_SCALING", "0"); qputenv("QT_SCALE_FACTOR", "1"); qputenv("QT_SCREEN_SCALE_FACTORS", "1"); qputenv("QT_AUTO_SCREEN_SCALE_FACTOR", "0"); + + // Steam Deck has a high physical DPI. Hard-coding 96 DPI prevents text + // from being oversized in dialogs like "About" or "Updater". qputenv("QT_FONT_DPI", "96"); + // FORCE X11 backend: Qt 6 scaling overrides are reliably respected under XCB in Gamescope. + // Wayland mode in Gamescope often ignores scaling overrides for child windows. + qputenv("QT_QPA_PLATFORM", "xcb"); + // Ensure Gamescope compositor handles Citron menus correctly QCoreApplication::setAttribute(Qt::AA_DontUseNativeMenuBar); QCoreApplication::setAttribute(Qt::AA_DontUseNativeDialogs); qputenv("QT_WAYLAND_SHELL_INTEGRATION", "xdg-shell"); } - // 2. NOW setup AppImage environment + // 2. Setup AppImage environment const bool is_appimage = !qgetenv("APPIMAGE").isEmpty(); if (is_appimage) { qputenv("QT_WAYLAND_DISABLE_EXPLICIT_SYNC", "1"); @@ -6231,6 +6206,7 @@ int main(int argc, char* argv[]) { QGuiApplication::setDesktopFileName(QStringLiteral("org.citron_emu.citron")); #endif + // Call policy attributes BEFORE creating the real QApplication instance SetHighDPIAttributes(); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) @@ -6252,21 +6228,16 @@ int main(int argc, char* argv[]) { #ifdef _WIN32 // On Windows, updates are applied by the helper script after the app exits. - // If we find a staging directory here, it means the helper script failed. - // Clean it up to avoid confusion. std::filesystem::path staging_path = app_dir / "update_staging"; if (std::filesystem::exists(staging_path)) { try { std::filesystem::remove_all(staging_path); - } catch (...) { - // Ignore cleanup errors - } + } catch (...) {} } #else - // On Linux, apply staged updates at startup as before + // On Linux, apply staged updates at startup if (Updater::UpdaterService::HasStagedUpdate(app_dir)) { if (Updater::UpdaterService::ApplyStagedUpdate(app_dir)) { - // Show a simple message that update was applied QMessageBox::information(nullptr, QObject::tr("Update Applied"), QObject::tr("Citron has been updated successfully!")); } @@ -6279,10 +6250,7 @@ int main(int argc, char* argv[]) { #endif - // Workaround for QTBUG-85409, for Suzhou numerals the number 1 is actually \u3021 - // so we can see if we get \u3008 instead - // TL;DR all other number formats are consecutive in unicode code points - // This bug is fixed in Qt6, specifically 6.0.0-alpha1 + // Workaround for QTBUG-85409 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const QLocale locale = QLocale::system(); if (QStringLiteral("\u3008") == locale.toString(1)) { @@ -6314,8 +6282,7 @@ int main(int argc, char* argv[]) { QObject::connect(&app, &QGuiApplication::applicationStateChanged, &main_window, &GMainWindow::OnAppFocusStateChanged); - int result = app.exec(); - return result; + return app.exec(); } void GMainWindow::OnCheckForUpdates() {