mirror of https://github.com/stenzek/duckstation
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
349 lines
11 KiB
C++
349 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
|
|
// 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 <QtGui/QGuiApplication>
|
|
#include <QtGui/QScreen>
|
|
#include <QtWidgets/QWidget>
|
|
|
|
#if defined(_WIN32)
|
|
|
|
#include "common/windows_headers.h"
|
|
#include <dwmapi.h>
|
|
|
|
#elif defined(__APPLE__)
|
|
|
|
#include "common/cocoa_tools.h"
|
|
#include <CoreFoundation/CFString.h>
|
|
#include <IOKit/pwr_mgt/IOPMLib.h>
|
|
|
|
#elif defined(__linux__)
|
|
|
|
#include <QtDBus/QDBusConnection>
|
|
#include <QtDBus/QDBusInterface>
|
|
#include <QtDBus/QDBusMessage>
|
|
#include <QtDBus/QDBusReply>
|
|
#include <qpa/qplatformnativeinterface.h>
|
|
|
|
#else
|
|
|
|
#include <qpa/qplatformnativeinterface.h>
|
|
|
|
#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<QDBusInterface> 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<WindowInfo> 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<void*>(widget->winId());
|
|
#elif defined(__APPLE__)
|
|
wi.type = WindowInfoType::MacOS;
|
|
wi.window_handle = reinterpret_cast<void*>(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<void*>(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<HWND>(wi->window_handle), &rc))
|
|
{
|
|
const qreal device_pixel_ratio = widget->devicePixelRatio();
|
|
wi->surface_width = static_cast<u16>(rc.right - rc.left);
|
|
wi->surface_height = static_cast<u16>(rc.bottom - rc.top);
|
|
wi->surface_scale = static_cast<float>(device_pixel_ratio);
|
|
return;
|
|
}
|
|
#elif defined(__APPLE__)
|
|
if (std::optional<std::pair<int, int>> size =
|
|
CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(widget->winId())))
|
|
{
|
|
wi->surface_width = static_cast<u16>(size->first);
|
|
wi->surface_height = static_cast<u16>(size->second);
|
|
wi->surface_scale = static_cast<float>(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<double> real_device_pixel_ratio = CocoaTools::GetViewRealScalingFactor(wi->window_handle))
|
|
{
|
|
const QSize scaled_size =
|
|
ApplyDevicePixelRatioToSize(widget->size(), static_cast<qreal>(real_device_pixel_ratio.value()));
|
|
wi->surface_width = static_cast<u16>(scaled_size.width());
|
|
wi->surface_height = static_cast<u16>(scaled_size.height());
|
|
wi->surface_scale = static_cast<float>(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<u16>(scaled_size.width());
|
|
wi->surface_height = static_cast<u16>(scaled_size.height());
|
|
wi->surface_scale = static_cast<float>(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<float>(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<s32>(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<s32>(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<quint32> 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<void> 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<HWND>(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
|
|
}
|