Qt: Try to work out the "real" display scale on MacOS

Avoids the rendering at 2x and downsampling at fractional DPI scale.
This commit is contained in:
Stenzek
2025-11-29 01:19:43 +10:00
parent 9ee1b02cd7
commit 8bc3f04be5
8 changed files with 128 additions and 46 deletions

View File

@@ -48,4 +48,7 @@ bool DelayedLaunch(std::string_view file, std::span<const std::string_view> args
/// Returns the size of a NSView in pixels.
std::optional<std::pair<int, int>> GetViewSizeInPixels(const void* view);
/// Returns the "real" scaling factor for a given view, on its current display.
std::optional<double> GetViewRealScalingFactor(const void* view);
} // namespace CocoaTools

View File

@@ -4,6 +4,7 @@
#include "cocoa_tools.h"
#include "assert.h"
#include "error.h"
#include "log.h"
#include "small_string.h"
#include "fmt/format.h"
@@ -91,31 +92,36 @@ std::optional<std::string> CocoaTools::GetBundlePath()
std::optional<std::string> CocoaTools::GetNonTranslocatedBundlePath()
{
// See https://objective-see.com/blog/blog_0x15.html
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (!url)
return std::nullopt;
if (void* handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY))
std::optional<std::string> ret;
@autoreleasepool
{
auto IsTranslocatedURL =
reinterpret_cast<Boolean (*)(CFURLRef path, bool* isTranslocated, CFErrorRef* __nullable error)>(
dlsym(handle, "SecTranslocateIsTranslocatedURL"));
auto CreateOriginalPathForURL =
reinterpret_cast<CFURLRef __nullable (*)(CFURLRef translocatedPath, CFErrorRef* __nullable error)>(
dlsym(handle, "SecTranslocateCreateOriginalPathForURL"));
bool is_translocated = false;
if (IsTranslocatedURL)
IsTranslocatedURL((__bridge CFURLRef)url, &is_translocated, nullptr);
if (is_translocated)
NSURL* url = [NSURL fileURLWithPath:[[NSBundle mainBundle] bundlePath]];
if (!url)
return ret;
if (void* handle = dlopen("/System/Library/Frameworks/Security.framework/Security", RTLD_LAZY))
{
if (CFURLRef actual = CreateOriginalPathForURL((__bridge CFURLRef)url, nullptr))
url = (__bridge_transfer NSURL*)actual;
auto IsTranslocatedURL =
reinterpret_cast<Boolean (*)(CFURLRef path, bool* isTranslocated, CFErrorRef* __nullable error)>(
dlsym(handle, "SecTranslocateIsTranslocatedURL"));
auto CreateOriginalPathForURL =
reinterpret_cast<CFURLRef __nullable (*)(CFURLRef translocatedPath, CFErrorRef* __nullable error)>(
dlsym(handle, "SecTranslocateCreateOriginalPathForURL"));
bool is_translocated = false;
if (IsTranslocatedURL)
IsTranslocatedURL((__bridge CFURLRef)url, &is_translocated, nullptr);
if (is_translocated)
{
if (CFURLRef actual = CreateOriginalPathForURL((__bridge CFURLRef)url, nullptr))
url = (__bridge NSURL*)actual;
}
dlclose(handle);
}
dlclose(handle);
ret = std::string([url fileSystemRepresentation]);
}
return std::string([url fileSystemRepresentation]);
return ret;
}
bool CocoaTools::DelayedLaunch(std::string_view file, std::span<const std::string_view> args)
@@ -158,6 +164,51 @@ std::optional<std::pair<int, int>> CocoaTools::GetViewSizeInPixels(const void* v
return ret;
}
std::optional<double> CocoaTools::GetViewRealScalingFactor(const void* view)
{
if (!view)
return std::nullopt;
NSView* const nsview = (__bridge NSView*)view;
NSWindow* const nswindow = nsview.window;
if (nswindow == nil)
return std::nullopt;
NSScreen* const nsscreen = nswindow.screen;
if (nsscreen == nil)
return std::nullopt;
const u32 did = [[nsscreen.deviceDescription valueForKey:@"NSScreenNumber"] unsignedIntValue];
const NSArray* all_modes = (__bridge NSArray*)CGDisplayCopyAllDisplayModes(did, nil);
if (all_modes == nil)
{
GENERIC_LOG(Log::Channel::WindowInfo, Log::Level::Dev, Log::Color::Default,
"GetViewRealScalingFactor(): CGDisplayCopyAllDisplayModes() failed");
return std::nullopt;
}
u32 max_width = 0;
for (NSUInteger i = 0; i < all_modes.count; i++)
max_width = std::max(max_width, static_cast<u32>(CGDisplayModeGetPixelWidth((CGDisplayModeRef)all_modes[i])));
CFRelease(all_modes);
if (max_width == 0)
{
GENERIC_LOG(Log::Channel::WindowInfo, Log::Level::Dev, Log::Color::Default,
"GetViewRealScalingFactor(): Max width is zero");
return std::nullopt;
}
// Sanity check: Scale should not be less than 100%, and cannot be more than 200%.
const CGFloat frame_width = nsscreen.frame.size.width;
const CGFloat scale = static_cast<CGFloat>(max_width) / frame_width;
GENERIC_LOG(Log::Channel::WindowInfo, Log::Level::Dev, Log::Color::Default,
"GetViewRealScalingFactor(): MaxWidth={}, FrameWidth={}, Scale={}", max_width, frame_width, scale);
if (scale < 1.0f)
return std::nullopt;
return static_cast<double>(scale);
}
void Y_OnAssertFailed(const char* szMessage, const char* szFunction, const char* szFile, unsigned uLine)
{
if (![NSThread isMainThread])

View File

@@ -265,8 +265,13 @@ void AdvancedSettingsWidget::addTweakOptions()
static_cast<u32>(SaveStateCompressionMode::Count),
Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE);
#if defined(_WIN32)
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Disable Window Rounded Corners"), "Main",
"DisableWindowRoundedCorners", false);
#elif defined(__APPLE__)
addBooleanTweakOption(m_dialog, m_ui.tweakOptionTable, tr("Use Fractional Window Scale"), "Main",
"UseFractionalWindowScale", true);
#endif
if (m_dialog->isPerGameSettings())
{
@@ -340,7 +345,11 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Load Devices From Save States
setChoiceTweakOption(m_ui.tweakOptionTable, i++,
Settings::DEFAULT_SAVE_STATE_COMPRESSION_MODE); // Save State Compression
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Disable Window Rounded Corners
#if defined(_WIN32)
setBooleanTweakOption(m_ui.tweakOptionTable, i++, false); // Disable Window Rounded Corners
#elif defined(__APPLE__)
setBooleanTweakOption(m_ui.tweakOptionTable, i++, true); // Use Fractional Rendering Scale
#endif
setIntRangeTweakOption(m_ui.tweakOptionTable, i++,
static_cast<int>(Settings::DEFAULT_DMA_MAX_SLICE_TICKS)); // DMA max slice ticks
setIntRangeTweakOption(m_ui.tweakOptionTable, i++,
@@ -382,6 +391,7 @@ void AdvancedSettingsWidget::onResetToDefaultClicked()
sif->DeleteValue("Main", "LoadDevicesFromSaveStates");
sif->DeleteValue("Main", "CompressSaveStates");
sif->DeleteValue("Main", "DisableWindowRoundedCorners");
sif->DeleteValue("Main", "UseFractionalWindowScale");
sif->DeleteValue("Display", "ActiveStartOffset");
sif->DeleteValue("Display", "ActiveEndOffset");
sif->DeleteValue("Display", "LineStartOffset");

View File

@@ -165,10 +165,8 @@ bool DisplayWidget::isActuallyFullscreen() const
void DisplayWidget::checkForSizeChange()
{
const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this);
const QSize size = QtUtils::GetPixelSizeForWidget(this, dpr);
// avoid spamming resize events for paint events (sent on move on windows)
const auto& [size, dpr] = QtUtils::GetPixelSizeForWidget(this);
if (m_last_window_size != size || m_last_window_scale != dpr)
{
m_last_window_size = size;
@@ -270,11 +268,9 @@ bool DisplayWidget::event(QEvent* event)
{
if (!m_relative_mouse_enabled)
{
const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this);
const QPoint mouse_pos = static_cast<QMouseEvent*>(event)->pos();
const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * dpr);
const float scaled_y = static_cast<float>(static_cast<qreal>(mouse_pos.y()) * dpr);
const float scaled_x = static_cast<float>(static_cast<qreal>(mouse_pos.x()) * m_last_window_scale);
const float scaled_y = static_cast<float>(static_cast<qreal>(mouse_pos.y()) * m_last_window_scale);
InputManager::UpdatePointerAbsolutePosition(0, scaled_x, scaled_y);
}
else
@@ -596,10 +592,8 @@ bool AuxiliaryDisplayWidget::event(QEvent* event)
{
QWidget::event(event);
const qreal dpr = QtUtils::GetDevicePixelRatioForWidget(this);
const QSize size = QtUtils::GetPixelSizeForWidget(this, dpr);
// avoid spamming resize events for paint events (sent on move on windows)
const auto& [size, dpr] = QtUtils::GetPixelSizeForWidget(this);
if (m_last_window_size != size || m_last_window_scale != dpr)
{
m_last_window_size = size;

View File

@@ -510,25 +510,42 @@ qreal QtUtils::GetDevicePixelRatioForWidget(const QWidget* widget)
return screen_for_ratio ? screen_for_ratio->devicePixelRatio() : static_cast<qreal>(1);
}
QSize QtUtils::GetPixelSizeForWidget(const QWidget* widget, qreal device_pixel_ratio)
std::pair<QSize, qreal> QtUtils::GetPixelSizeForWidget(const QWidget* widget)
{
// 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(reinterpret_cast<HWND>(widget->winId()), &rc))
return QSize(static_cast<int>(rc.right - rc.left), static_cast<int>(rc.bottom - rc.top));
#elif defined(__APPLE__)
if (std::optional<std::pair<int, int>> size =
CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(widget->winId())))
{
return QSize(size->first, size->second);
const qreal device_pixel_ratio = GetDevicePixelRatioForWidget(widget);
return std::make_pair(QSize(static_cast<int>(rc.right - rc.left), static_cast<int>(rc.bottom - rc.top)),
device_pixel_ratio);
}
#elif defined(__APPLE__)
if (Host::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", true))
{
if (const std::optional<double> real_device_pixel_ratio =
CocoaTools::GetViewRealScalingFactor(reinterpret_cast<void*>(widget->winId())))
{
const qreal device_pixel_ratio = static_cast<qreal>(real_device_pixel_ratio.value());
return std::make_pair(ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio), device_pixel_ratio);
}
}
else
{
if (std::optional<std::pair<int, int>> size =
CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(widget->winId())))
{
const qreal device_pixel_ratio = GetDevicePixelRatioForWidget(widget);
return std::make_pair(QSize(size->first, size->second), device_pixel_ratio);
}
}
#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.
return ApplyDevicePixelRatioToSize(widget->size(), (device_pixel_ratio < 1) ? GetDevicePixelRatioForWidget(widget) :
device_pixel_ratio);
const qreal device_pixel_ratio = GetDevicePixelRatioForWidget(widget);
return std::make_pair(ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio), device_pixel_ratio);
}
std::optional<WindowInfo> QtUtils::GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error)
@@ -576,8 +593,7 @@ std::optional<WindowInfo> QtUtils::GetWindowInfoForWidget(QWidget* widget, Rende
}
#endif
const qreal dpr = GetDevicePixelRatioForWidget(widget);
const QSize size = GetPixelSizeForWidget(widget, dpr);
const auto& [size, dpr] = GetPixelSizeForWidget(widget);
wi.surface_width = static_cast<u16>(size.width());
wi.surface_height = static_cast<u16>(size.height());
wi.surface_scale = static_cast<float>(dpr);

View File

@@ -165,8 +165,9 @@ QSize GetDeviceIndependentSize(const QSize& size, qreal device_pixel_ratio);
/// Returns the pixel ratio/scaling factor for a widget.
qreal GetDevicePixelRatioForWidget(const QWidget* widget);
/// Returns the pixel size (real geometry) for a widget. DPR can be passed to avoid re-querying when needed.
QSize GetPixelSizeForWidget(const QWidget* widget, qreal device_pixel_ratio = -1);
/// Returns the pixel size (real geometry) for a widget.
/// Also returns the "real" DPR scale for the widget, ignoring any operating-system level downsampling.
std::pair<QSize, qreal> GetPixelSizeForWidget(const QWidget* widget);
/// Returns the common window info structure for a Qt widget.
std::optional<WindowInfo> GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error = nullptr);

View File

@@ -204,7 +204,12 @@ void OpenGLContextAGL::BindContextToView(WindowInfo& wi, NSOpenGLContext* contex
else
dispatch_sync(dispatch_get_main_queue(), block);
UpdateSurfaceSize(wi, context);
const NSSize window_size = [view frame].size;
const CGFloat window_scale = [[view window] backingScaleFactor];
wi.surface_width = static_cast<u32>(static_cast<CGFloat>(window_size.width) * window_scale);
wi.surface_height = static_cast<u32>(static_cast<CGFloat>(window_size.height) * window_scale);
wi.surface_scale = window_scale;
wi.surface_format = GPUTexture::Format::RGBA8;
}
void OpenGLContextAGL::UpdateSurfaceSize(WindowInfo& wi, NSOpenGLContext* context)
@@ -215,11 +220,12 @@ void OpenGLContextAGL::UpdateSurfaceSize(WindowInfo& wi, NSOpenGLContext* contex
const u32 new_width = static_cast<u32>(static_cast<CGFloat>(window_size.width) * window_scale);
const u32 new_height = static_cast<u32>(static_cast<CGFloat>(window_size.height) * window_scale);
if (wi.surface_width == new_width && wi.surface_height == new_height)
if (wi.surface_width == new_width && wi.surface_height == new_height && wi.surface_scale == window_scale)
return;
wi.surface_width = static_cast<u16>(new_width);
wi.surface_height = static_cast<u16>(new_height);
wi.surface_scale = static_cast<float>(window_scale);
dispatch_block_t block = ^{
[context update];

View File

@@ -385,8 +385,9 @@ bool VulkanSwapChain::CreateSwapChain(VulkanDevice& dev, Error* error)
// Determine the dimensions of the swap chain. Values of -1 indicate the size we specify here
// determines window size? Android sometimes lags updating currentExtent, so don't use it.
// We want to avoid the system-level downsampling with fractional scaling on MacOS too.
VkExtent2D size = surface_caps.surfaceCapabilities.currentExtent;
#ifndef __ANDROID__
#if defined(__ANDROID__) && !defined(__APPLE__)
if (size.width == UINT32_MAX)
#endif
{