// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin // SPDX-License-Identifier: CC-BY-NC-ND-4.0 #include "qtwindowinfo.h" #include "qtutils.h" #include "core/core.h" #include "core/host.h" #include "util/gpu_device.h" #include "common/error.h" #include "common/log.h" #include #include #include #if defined(_WIN32) #include "common/windows_headers.h" #include #elif defined(__APPLE__) #include "common/cocoa_tools.h" #include #include #elif defined(__linux__) #include #include #include #include #include #else #include #endif LOG_CHANNEL(Host); using namespace Qt::StringLiterals; namespace { struct WindowInfoLocals { #if defined(__APPLE__) IOPMAssertionID screensaver_inhibit_assertion; #elif defined(__linux__) // Prevent screensaver inhibits when running on platforms that don't have it (e.g. gamescope). std::optional screensaver_inhibit_interface; u32 screensaver_inhibit_cookie = 0; bool disable_screensaver_inhibit = false; #endif bool screensaver_inhibited = false; }; } // namespace static WindowInfoLocals s_window_info_locals; WindowInfoType QtUtils::GetWindowInfoType() { // Windows and Apple are easy here since there's no display connection. #if defined(_WIN32) return WindowInfoType::Win32; #elif defined(__APPLE__) return WindowInfoType::MacOS; #else const QString platform_name = QGuiApplication::platformName(); if (platform_name == "xcb"_L1) { // This is only used for determining the automatic Vulkan renderer, therefore XCB/XLib doesn't matter here. // See the comment below for information about this bullshit. return WindowInfoType::XCB; } else if (platform_name == "wayland"_L1) { return WindowInfoType::Wayland; } else { return WindowInfoType::Surfaceless; } #endif } std::optional QtUtils::GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error) { WindowInfo wi = {}; // Windows and Apple are easy here since there's no display connection. #if defined(_WIN32) wi.type = WindowInfoType::Win32; wi.window_handle = reinterpret_cast(widget->winId()); #elif defined(__APPLE__) wi.type = WindowInfoType::MacOS; wi.window_handle = reinterpret_cast(widget->winId()); #else QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface(); const QString platform_name = QGuiApplication::platformName(); if (platform_name == QStringLiteral("xcb")) { // This is fucking ridiculous. NVIDIA+XWayland doesn't support Xlib, and NVIDIA+Xorg doesn't support XCB. // Use Xlib if we're not running under Wayland, or we're not requesting OpenGL. Vulkan+XCB seems fine. const char* xdg_session_type = std::getenv("XDG_SESSION_TYPE"); const bool is_running_on_xwayland = (xdg_session_type && std::strstr(xdg_session_type, "wayland")); if (is_running_on_xwayland || render_api == RenderAPI::Vulkan) { wi.type = WindowInfoType::XCB; wi.display_connection = pni->nativeResourceForWindow("connection", widget->windowHandle()); } else { wi.type = WindowInfoType::Xlib; wi.display_connection = pni->nativeResourceForWindow("display", widget->windowHandle()); } wi.window_handle = reinterpret_cast(widget->winId()); } else if (platform_name == QStringLiteral("wayland")) { wi.type = WindowInfoType::Wayland; wi.display_connection = pni->nativeResourceForWindow("display", widget->windowHandle()); wi.window_handle = pni->nativeResourceForWindow("surface", widget->windowHandle()); } else { Error::SetStringFmt(error, "Unknown PNI platform {}", platform_name.toStdString()); return std::nullopt; } #endif UpdateSurfaceSize(widget, render_api, &wi); UpdateSurfaceRefreshRate(widget, &wi); INFO_LOG("Window size: {}x{} (Qt {}x{}), scale: {}, refresh rate {} hz", wi.surface_width, wi.surface_height, widget->width(), widget->height(), wi.surface_scale, wi.surface_refresh_rate); return wi; } void QtUtils::UpdateSurfaceSize(QWidget* widget, RenderAPI render_api, WindowInfo* wi) { // Why this nonsense? Qt's device independent sizes are integer, and fractional scaling is lossy. // We can't get back the "real" size of the window. So we have to platform natively query the actual client size. #if defined(_WIN32) if (RECT rc; GetClientRect(static_cast(wi->window_handle), &rc)) { const qreal device_pixel_ratio = widget->devicePixelRatio(); wi->surface_width = static_cast(rc.right - rc.left); wi->surface_height = static_cast(rc.bottom - rc.top); wi->surface_scale = static_cast(device_pixel_ratio); return; } #elif defined(__APPLE__) if (std::optional> size = CocoaTools::GetViewSizeInPixels(reinterpret_cast(widget->winId()))) { wi->surface_width = static_cast(size->first); wi->surface_height = static_cast(size->second); wi->surface_scale = static_cast(widget->devicePixelRatio()); // Only use "real" fractional window scale for Metal renderer. // Vulkan returns suboptimal constantly, triggering swap chain recreations. if (render_api == RenderAPI::Metal && Core::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", false)) { if (const std::optional real_device_pixel_ratio = CocoaTools::GetViewRealScalingFactor(wi->window_handle)) { const QSize scaled_size = ApplyDevicePixelRatioToSize(widget->size(), static_cast(real_device_pixel_ratio.value())); wi->surface_width = static_cast(scaled_size.width()); wi->surface_height = static_cast(scaled_size.height()); wi->surface_scale = static_cast(real_device_pixel_ratio.value()); } } return; } #endif // On Linux, fuck you, enjoy round trip to the X server, and on Wayland you can't query it in the first place... // I ain't dealing with this crap OS. Enjoy your mismatched sizes and shit experience. const qreal device_pixel_ratio = widget->devicePixelRatio(); const QSize scaled_size = ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio); wi->surface_width = static_cast(scaled_size.width()); wi->surface_height = static_cast(scaled_size.height()); wi->surface_scale = static_cast(device_pixel_ratio); } void QtUtils::UpdateSurfaceRefreshRate(QWidget* widget, WindowInfo* wi) { const QScreen* widget_screen = widget->screen(); if (!widget_screen) widget_screen = QGuiApplication::primaryScreen(); wi->surface_refresh_rate = widget_screen ? std::max(static_cast(widget_screen->refreshRate()), 0.0f) : 0.0f; } #ifdef __linux__ static void FormatQDBusReplyError(Error* error, const char* prefix, const QDBusError& qerror) { Error::SetStringFmt(error, "{}{}: {}", prefix, qerror.name().toStdString(), qerror.message().toStdString()); } #endif bool Host::SetScreensaverInhibit(bool inhibit, Error* error) { if (s_window_info_locals.screensaver_inhibited == inhibit) return true; #if defined(_WIN32) if (SetThreadExecutionState(ES_CONTINUOUS | (inhibit ? (ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) : 0)) == NULL) { Error::SetWin32(error, "SetThreadExecutionState() failed: ", GetLastError()); return false; } s_window_info_locals.screensaver_inhibited = inhibit; return true; #elif defined(__APPLE__) if (inhibit) { const CFStringRef reason = CFSTR("DuckStation VM is running."); const IOReturn ret = IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, reason, &s_window_info_locals.screensaver_inhibit_assertion); if (ret != kIOReturnSuccess) { Error::SetStringFmt(error, "IOPMAssertionCreateWithName() failed: {}", static_cast(ret)); return false; } s_window_info_locals.screensaver_inhibited = true; return true; } else { const IOReturn ret = IOPMAssertionRelease(s_window_info_locals.screensaver_inhibit_assertion); if (ret != kIOReturnSuccess) { Error::SetStringFmt(error, "IOPMAssertionRelease() failed: {}", static_cast(ret)); return false; } s_window_info_locals.screensaver_inhibit_assertion = kIOPMNullAssertionID; s_window_info_locals.screensaver_inhibited = false; return true; } #elif defined(__linux__) if (s_window_info_locals.disable_screensaver_inhibit) { // pretend it succeeded so the caller doesn't throw an error s_window_info_locals.screensaver_inhibited = inhibit; return true; } if (!s_window_info_locals.screensaver_inhibit_interface.has_value()) { const QDBusConnection connection = QDBusConnection::sessionBus(); if (!connection.isConnected()) { Error::SetStringView(error, "Failed to connect to the D-Bus session bus"); return false; } s_window_info_locals.screensaver_inhibit_interface.emplace( "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", connection); if (!s_window_info_locals.screensaver_inhibit_interface->isValid()) { s_window_info_locals.screensaver_inhibit_interface.reset(); Error::SetStringView(error, "org.freedesktop.ScreenSaver interface is invalid"); return false; } } if (inhibit) { const QDBusReply msg = s_window_info_locals.screensaver_inhibit_interface->call( "Inhibit", "DuckStation"_L1, "DuckStation VM is running."_L1); if (!msg.isValid()) { FormatQDBusReplyError(error, "Inhibit message call failed: ", msg.error()); return false; } s_window_info_locals.screensaver_inhibit_cookie = msg.value(); s_window_info_locals.screensaver_inhibited = true; return true; } else { const QDBusReply msg = s_window_info_locals.screensaver_inhibit_interface->call( "UnInhibit", s_window_info_locals.screensaver_inhibit_cookie); if (!msg.isValid()) { FormatQDBusReplyError(error, "UnInhibit message call failed: ", msg.error()); return false; } s_window_info_locals.screensaver_inhibit_cookie = 0; s_window_info_locals.screensaver_inhibited = false; return true; } #else Error::SetStringView(error, "Not implemented."); return false; #endif } #ifdef _WIN32 bool QtUtils::SetWindowRoundedCornerState(QWidget* widget, bool enabled) { const HWND window_handle = reinterpret_cast(widget->winId()); const DWM_WINDOW_CORNER_PREFERENCE value = enabled ? DWMWCP_DEFAULT : DWMWCP_DONOTROUND; const HRESULT hr = DwmSetWindowAttribute(window_handle, DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(value)); if (FAILED(hr)) { ERROR_LOG("DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE) failed: ", Error::CreateHResult(hr).GetDescription()); } return SUCCEEDED(hr); } #endif // _WIN32 void QtHost::DisableScreensaverInhibit() { #ifdef __linux__ s_window_info_locals.disable_screensaver_inhibit = true; #endif }