优化自动缩放机制 (#1227)

* fix: 重复缩放时不显示错误消息

* fix: 减小 ScalingService 和 ScalingRuntime 状态不一致的窗口期

* fix: 避免自动缩放和最小化窗口还原机制冲突

* chore: 修复 clang 编译警告

* refactor: ScalingRuntime::SwitchScalingState 重命名为 ScalingRuntime::ToggleScaling

* fix: 恢复检查自动缩放

* feat: 自动缩放可以终止当前缩放

* chore: 添加注释

* feat: 自动缩放不再等待最小化和不可见的窗口
现已支持直接缩放这类窗口,由 ScalingRuntime 等待
This commit is contained in:
Xu 2025-08-05 17:55:29 +08:00 committed by GitHub
commit ea107ac9c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 90 additions and 90 deletions

View file

@ -13,7 +13,6 @@ Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Magpie", "src\Magpie\Magpie
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{00AB63C3-0CD3-4944-B8E6-58C86138618D}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
src\BuildOptions.props = src\BuildOptions.props
src\Common.Post.props = src\Common.Post.props
src\Common.Pre.props = src\Common.Pre.props

View file

@ -47,7 +47,7 @@ bool AdaptivePresenter::_Initialize(HWND hwndAttach) noexcept {
};
ID3D11Device5* d3dDevice = _deviceResources->GetD3DDevice();
winrt::com_ptr<IDXGISwapChain1> dxgiSwapChain = nullptr;
winrt::com_ptr<IDXGISwapChain1> dxgiSwapChain;
HRESULT hr = _deviceResources->GetDXGIFactory()->CreateSwapChainForHwnd(
d3dDevice,
hwndAttach,

View file

@ -824,7 +824,7 @@ bool OverlayDrawer::_DrawToolbar(uint32_t fps) noexcept {
isWindowedMode ? L"Overlay_Toolbar_SwitchToFullscreen" : L"Overlay_Toolbar_SwitchToWindowed");
if (drawButton(icon, switchScalingStr.c_str())) {
ScalingWindow::Dispatcher().TryEnqueue([]() {
ScalingWindow::Get().SwitchScalingState(!ScalingWindow::Get().Options().IsWindowedMode());
ScalingWindow::Get().ToggleScaling(!ScalingWindow::Get().Options().IsWindowedMode());
});
}
}

View file

@ -14,8 +14,6 @@ ScalingRuntime::ScalingRuntime() : _scalingThread(&ScalingRuntime::_ScalingThrea
}
ScalingRuntime::~ScalingRuntime() {
Stop();
if (_scalingThread.joinable()) {
const HANDLE hScalingThread = _scalingThread.native_handle();
@ -50,27 +48,30 @@ ScalingRuntime::~ScalingRuntime() {
}
}
bool ScalingRuntime::Start(HWND hwndSrc, ScalingOptions&& options) {
bool ScalingRuntime::Start(HWND hwndSrc, ScalingOptions&& options, bool force) {
assert(!options.screenshotsDir.empty() && options.showToast && options.showError && options.save);
_Dispatcher().TryEnqueue([this, hwndSrc, options(std::move(options))]() mutable {
_Dispatcher().TryEnqueue([this, hwndSrc, options(std::move(options)), force]() mutable {
ScalingWindow& scalingWindow = ScalingWindow::Get();
if (scalingWindow.IsSrcRepositioning()) {
scalingWindow.CleanAfterSrcRepositioned();
// 如果正在缩放且 force 为假则忽略
if (scalingWindow && !force) {
return;
}
scalingWindow.Stop();
// 初始化时视为处于缩放状态
_IsScaling(true);
_State(ScalingState::Scaling);
scalingWindow.Start(hwndSrc, std::move(options));
});
return true;
}
void ScalingRuntime::SwitchScalingState(bool isWindowedMode) {
void ScalingRuntime::ToggleScaling(bool isWindowedMode) {
_Dispatcher().TryEnqueue([isWindowedMode]() {
if (ScalingWindow& scalingWindow = ScalingWindow::Get()) {
scalingWindow.SwitchScalingState(isWindowedMode);
scalingWindow.ToggleScaling(isWindowedMode);
};
});
}
@ -161,7 +162,6 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
scalingWindow.Stop();
_IsScaling(false);
return;
} else if (msg.message == CommonSharedConstants::WM_FRONTEND_RENDER &&
msg.hwnd == scalingWindow.Handle()) {
@ -172,9 +172,9 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
DispatchMessage(&msg);
}
_IsScaling(scalingWindow);
if (scalingWindow) {
_State(ScalingState::Scaling);
const auto now = steady_clock::now();
// 限制检测光标移动的频率
const milliseconds timeout(scalingWindow.Options().Is3DGameMode() ? 8 : 2);
@ -196,16 +196,20 @@ void ScalingRuntime::_ScalingThreadProc() noexcept {
if (repositioning.has_value()) {
if (*repositioning) {
// 等待调整完成
_State(ScalingState::Waiting);
MsgWaitForMultipleObjectsEx(0, nullptr, 10, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
} else {
// 重新缩放
// 重新缩放。初始化时视为处于缩放状态
_State(ScalingState::Scaling);
ScalingWindow::Get().RestartAfterSrcRepositioned();
}
} else {
// 取消缩放
ScalingWindow::Get().CleanAfterSrcRepositioned();
_State(ScalingState::Idle);
}
} else {
_State(ScalingState::Idle);
lastRenderTime = {};
WaitMessage();
}
@ -221,9 +225,9 @@ const winrt::DispatcherQueue& ScalingRuntime::_Dispatcher() noexcept {
return _dispatcher;
}
void ScalingRuntime::_IsScaling(bool value) {
if (_isScaling.exchange(value, std::memory_order_relaxed) != value) {
IsScalingChanged.Invoke(value);
void ScalingRuntime::_State(ScalingState value) {
if (_state.exchange(value, std::memory_order_relaxed) != value) {
StateChanged.Invoke(value);
}
}

View file

@ -322,10 +322,7 @@ ScalingError ScalingWindow::_StartImpl(HWND hwndSrc) noexcept {
}
void ScalingWindow::Start(HWND hwndSrc, ScalingOptions&& options) noexcept {
if (Handle()) {
options.showError(hwndSrc, ScalingError::ScalingFailedGeneral);
return;
}
assert(!Handle());
options.Log();
// 缩放结束后失效
@ -345,7 +342,7 @@ void ScalingWindow::Stop() noexcept {
CleanAfterSrcRepositioned();
}
void ScalingWindow::SwitchScalingState(bool isWindowedMode) noexcept {
void ScalingWindow::ToggleScaling(bool isWindowedMode) noexcept {
assert(Handle());
if (_options.IsWindowedMode() == isWindowedMode || !_srcTracker.IsFocused()) {

View file

@ -34,7 +34,7 @@ public:
void Stop() noexcept;
void SwitchScalingState(bool isWindowedMode) noexcept;
void ToggleScaling(bool isWindowedMode) noexcept;
void SwitchToolbarState() noexcept;

View file

@ -3,25 +3,31 @@
namespace Magpie {
enum class ScalingState {
Idle,
Scaling,
Waiting
};
class ScalingRuntime {
public:
ScalingRuntime();
~ScalingRuntime();
bool Start(HWND hwndSrc, struct ScalingOptions&& options);
bool Start(HWND hwndSrc, struct ScalingOptions&& options, bool force);
void SwitchScalingState(bool isWindowedMode);
void ToggleScaling(bool isWindowedMode);
void SwitchToolbarState();
void Stop();
bool IsScaling() const noexcept {
return _isScaling.load(std::memory_order_relaxed);
ScalingState State() const noexcept {
return _state.load(std::memory_order_relaxed);
}
// 调用者应处理线程同步
MultithreadEvent<bool> IsScalingChanged;
MultithreadEvent<ScalingState> StateChanged;
private:
void _ScalingThreadProc() noexcept;
@ -29,7 +35,7 @@ private:
// 确保 _dispatcher 完成初始化
const winrt::DispatcherQueue& _Dispatcher() noexcept;
void _IsScaling(bool value);
void _State(ScalingState value);
std::thread _scalingThread;
@ -38,7 +44,7 @@ private:
// 只能在主线程访问,省下检查 _dispatcherInitialized 的开销
bool _dispatcherInitializedCache = false;
std::atomic<bool> _isScaling = false;
std::atomic<ScalingState> _state = ScalingState::Idle;
};
}

View file

@ -7,7 +7,6 @@
#include "ProfileService.h"
#include "ScalingMode.h"
#include "ScalingModesService.h"
#include "ScalingRuntime.h"
#include "ScalingService.h"
#include "ShortcutService.h"
#include "ToastService.h"
@ -31,9 +30,9 @@ ScalingService& ScalingService::Get() noexcept {
ScalingService::~ScalingService() {}
void ScalingService::Initialize() {
_scalingRuntime = std::make_unique<ScalingRuntime>();
_scalingRuntime->IsScalingChanged(
std::bind_front(&ScalingService::_ScalingRuntime_IsScalingChanged, this));
_scalingRuntime.emplace();
_scalingRuntime->StateChanged(
std::bind_front(&ScalingService::_ScalingRuntime_StateChanged, this));
_countDownTimer.Interval(25ms);
_countDownTimer.Tick({ this, &ScalingService::_CountDownTimer_Tick });
@ -51,11 +50,14 @@ void ScalingService::Initialize() {
}
void ScalingService::Uninitialize() {
if (!_checkForegroundTimer) {
if (!_scalingRuntime) {
return;
}
_checkForegroundTimer.Cancel();
if (_checkForegroundTimer) {
_checkForegroundTimer.Cancel();
}
_countDownTimer.Stop();
_scalingRuntime.reset();
@ -97,7 +99,8 @@ double ScalingService::SecondsLeft() const noexcept {
}
bool ScalingService::IsScaling() const noexcept {
return _scalingRuntime->IsScaling();
// 等待状态视为未缩放
return _scalingRuntime->State() == ScalingState::Scaling;
}
void ScalingService::CheckForeground() {
@ -112,8 +115,8 @@ void ScalingService::_ShortcutService_ShortcutPressed(ShortcutAction action) {
{
const bool isWindowdMode = action == ShortcutAction::WindowedModeScale;
if (_scalingRuntime->IsScaling()) {
_scalingRuntime->SwitchScalingState(isWindowdMode);
if (_scalingRuntime->State() == ScalingState::Scaling) {
_scalingRuntime->ToggleScaling(isWindowdMode);
} else {
_ScaleForegroundWindow(isWindowdMode);
}
@ -122,10 +125,7 @@ void ScalingService::_ShortcutService_ShortcutPressed(ShortcutAction action) {
}
case ShortcutAction::Toolbar:
{
if (_scalingRuntime->IsScaling()) {
_scalingRuntime->SwitchToolbarState();
return;
}
_scalingRuntime->SwitchToolbarState();
break;
}
default:
@ -211,17 +211,6 @@ static void ShowError(HWND hWnd, ScalingError error) noexcept {
}
static bool IsReadyForScaling(HWND hwndFore) noexcept {
// GH#538
// 窗口还原过程中存在中间状态:虽然已经成为前台窗口,但仍是最小化的
if (Win32Helper::GetWindowShowCmd(hwndFore) == SW_SHOWMINIMIZED) {
return false;
}
// OS 允许不可见的窗口成为前台窗口,应等待窗口显示
if (!IsWindowVisible(hwndFore)) {
return false;
}
// GH#1148
// 有些游戏刚启动时将窗口创建在屏幕外,初始化完成后再移到屏幕内
if (!MonitorFromWindow(hwndFore, MONITOR_DEFAULTTONULL)) {
@ -234,52 +223,56 @@ static bool IsReadyForScaling(HWND hwndFore) noexcept {
}
fire_and_forget ScalingService::_CheckForegroundTimer_Tick(ThreadPoolTimer const& timer) {
// ThreadPoolTimer 是异步的Uninitialize 后仍可能执行
if (!_scalingRuntime || _scalingRuntime->IsScaling()) {
co_return;
}
if (timer) {
// ThreadPoolTimer 在后台线程触发
co_await App::Get().Dispatcher();
}
// ThreadPoolTimer 是异步的Uninitialize 后仍可能执行
if (!_scalingRuntime) {
co_return;
}
const HWND hwndFore = GetForegroundWindow();
if (!hwndFore || hwndFore == _hwndChecked) {
co_return;
}
// 检查自动缩放
if (const Profile* profile = ProfileService::Get().GetProfileForWindow(hwndFore, true)) {
// 如果窗口处于某种中间状态则跳过此次检查
if (!IsReadyForScaling(hwndFore)) {
co_return;
}
// 检查 _hwndCurSrc 使得缩放或等待状态下避免再次缩放源窗口
if (hwndFore != _hwndCurSrc) {
// 检查自动缩放
if (const Profile* profile = ProfileService::Get().GetProfileForWindow(hwndFore, true)) {
// 如果窗口处于某种中间状态则跳过此次检查
if (!IsReadyForScaling(hwndFore)) {
co_return;
}
_StartScale(hwndFore, *profile, profile->autoScale == AutoScale::Windowed);
// 自动缩放可以终止当前缩放
_StartScale(hwndFore, *profile, profile->autoScale == AutoScale::Windowed, true);
}
}
// 避免重复检查
_hwndChecked = hwndFore;
}
void ScalingService::_ScalingRuntime_IsScalingChanged(bool value) {
void ScalingService::_ScalingRuntime_StateChanged(ScalingState value) {
App::Get().Dispatcher().RunAsync(CoreDispatcherPriority::Normal, [this, value]() {
if (value) {
if (value == ScalingState::Scaling) {
StopTimer();
} else {
} else if (value == ScalingState::Idle) {
// 缩放结束后源窗口位于前台则不要检查自动缩放,用户可能刚通过快捷键或
// 工具栏终止缩放。_CheckForegroundTimer_Tick 也实现了类似功能,但它
// 的触发频率较低,容易错过时机。
if (GetForegroundWindow() == _hwndCurSrc) {
// 退出全屏后如果前台窗口不变视为通过热键退出
_hwndChecked = _hwndCurSrc;
}
// 缩放结束后清空 _hwndCurSrc等待状态下则保留
_hwndCurSrc = NULL;
// 立即检查前台窗口
_CheckForegroundTimer_Tick(nullptr);
}
IsScalingChanged.Invoke(value);
IsScalingChanged.Invoke(value == ScalingState::Scaling);
});
}
@ -290,25 +283,25 @@ void ScalingService::_ScaleForegroundWindow(bool windowedMode) {
}
const Profile& profile = *ProfileService::Get().GetProfileForWindow(hWnd, false);
_StartScale(hWnd, profile, windowedMode);
_StartScale(hWnd, profile, windowedMode, false);
}
void ScalingService::_StartScale(HWND hWnd, const Profile& profile, bool windowedMode) {
void ScalingService::_StartScale(HWND hWnd, const Profile& profile, bool windowedMode, bool force) {
assert(hWnd);
if (_scalingRuntime->IsScaling()) {
return;
}
const ScalingError error = _StartScaleImpl(hWnd, profile, windowedMode);
const ScalingError error = _StartScaleImpl(hWnd, profile, windowedMode, force);
if (error != ScalingError::NoError) {
ShowError(hWnd, error);
}
}
ScalingError ScalingService::_StartScaleImpl(HWND hWnd, const Profile& profile, bool windowedMode) {
ScalingError ScalingService::_StartScaleImpl(HWND hWnd, const Profile& profile, bool windowedMode, bool force) {
// ScalingRuntime::Start 会检查是否正在缩放,这里提前检查以避免无效操作
if (!force && _scalingRuntime->State() == ScalingState::Scaling) {
return ScalingError::NoError;
}
if (WindowHelper::IsForbiddenSystemWindow(hWnd)) {
// 不显示错误
return ScalingError::NoError;
}
@ -473,7 +466,7 @@ ScalingError ScalingService::_StartScaleImpl(HWND hWnd, const Profile& profile,
);
};
if (!_scalingRuntime->Start(hWnd, std::move(options))) {
if (!_scalingRuntime->Start(hWnd, std::move(options), force)) {
return ScalingError::ScalingFailedGeneral;
}

View file

@ -1,5 +1,6 @@
#pragma once
#include "Event.h"
#include "ScalingRuntime.h"
#include <winrt/Magpie.h>
#include <winrt/Windows.System.Threading.h>
@ -56,15 +57,15 @@ private:
winrt::fire_and_forget _CheckForegroundTimer_Tick(winrt::Threading::ThreadPoolTimer const& timer);
void _ScalingRuntime_IsScalingChanged(bool isRunning);
void _ScalingRuntime_StateChanged(ScalingState value);
void _ScaleForegroundWindow(bool windowedMode);
void _StartScale(HWND hWnd, const Profile& profile, bool windowedMode);
void _StartScale(HWND hWnd, const Profile& profile, bool windowedMode, bool force);
ScalingError _StartScaleImpl(HWND hWnd, const Profile& profile, bool windowedMode);
ScalingError _StartScaleImpl(HWND hWnd, const Profile& profile, bool windowedMode, bool force);
std::unique_ptr<ScalingRuntime> _scalingRuntime;
std::optional<ScalingRuntime> _scalingRuntime;
winrt::DispatcherTimer _countDownTimer;
// DispatcherTimer 在不显示主窗口时可能停滞,因此使用 ThreadPoolTimer