mirror of
https://github.com/stenzek/duckstation.git
synced 2026-02-04 05:04:33 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user