From 753312f32c8ca4bef8f2b70745b4558cd3aa9ffd Mon Sep 17 00:00:00 2001 From: Jia Sui Date: Fri, 11 Sep 2026 18:55:21 +0800 Subject: [PATCH] =?UTF-8?q?fix(server):=20RDP=20=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E4=B8=AD=E5=80=99=E9=80=89=E7=AA=97=E7=BC=A9=E6=94=BE=E8=B7=9F?= =?UTF-8?q?=E9=9A=8F=E5=AE=BF=E4=B8=BB=E5=BA=94=E7=94=A8=E5=AE=9E=E9=99=85?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E6=AF=94=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/src/utils/window_utils.cpp | 18 ++++++++ server/src/utils/window_utils.h | 28 ++++++++++++ server/src/window/candidate_presenter.cpp | 55 ++++++++++++++++++++--- server/src/window/candidate_presenter.h | 8 +++- 4 files changed, 103 insertions(+), 6 deletions(-) diff --git a/server/src/utils/window_utils.cpp b/server/src/utils/window_utils.cpp index 058b943c..258d4e16 100644 --- a/server/src/utils/window_utils.cpp +++ b/server/src/utils/window_utils.cpp @@ -110,6 +110,24 @@ FLOAT GetScaleForPoint(POINT pt) return ScaleFromMonitor(MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST)); } +ResolvedCandidateScale ResolveCandidateScaleForCaret(POINT caret) +{ + // SM_REMOTESESSION is only set inside a real remote session; console and + // local logons never see it, so the local path is byte-for-byte unchanged. + // GetDpiForWindow returns 0 for an invalid/absent foreground HWND, which + // drops to the monitor path instead of trusting a bogus 0 scale. + if (GetSystemMetrics(SM_REMOTESESSION)) + { + const UINT dpi = GetDpiForWindow(GetForegroundWindow()); + if (dpi != 0) + { + return {static_cast(dpi) / static_cast(USER_DEFAULT_SCREEN_DPI), + CandidateScaleSource::RdpForeground}; + } + } + return {GetScaleForPoint(caret), CandidateScaleSource::Monitor}; +} + HalfScreenDipLimits QueryHalfScreenDipLimitsForPoint(POINT pt) { HalfScreenDipLimits limits{}; diff --git a/server/src/utils/window_utils.h b/server/src/utils/window_utils.h index 22d9cb96..8948dd91 100644 --- a/server/src/utils/window_utils.h +++ b/server/src/utils/window_utils.h @@ -10,6 +10,34 @@ FLOAT GetForegroundWindowScale(); // Prefer the monitor that contains the caret / composition anchor. Foreground // HWND can disagree with the caret on Office extended-display setups. FLOAT GetScaleForPoint(POINT pt); + +// Where the candidate scale came from — diagnostic logging must be able to +// tell the RDP foreground path apart from the plain monitor path. +enum class CandidateScaleSource +{ + Monitor, // caret's monitor DPI (local sessions, and RDP fallback) + RdpForeground, // foreground window DPI inside an RDP session +}; + +struct ResolvedCandidateScale +{ + FLOAT scale = 0.0f; + CandidateScaleSource source = CandidateScaleSource::Monitor; +}; + +// Candidate window scale authority. RDP syncs the client's display scaling +// into the session's monitor DPI metadata (150% client -> 144), while the +// focused application in the session usually still renders at 96 DPI, so the +// monitor metadata is unreliable there and GetDpiForWindow(foreground) is the +// best proxy for what the user actually sees: +// - DPI-unaware / System Aware host -> virtualized 96, matches the app; +// - PMv2 and correctly following -> current monitor DPI, matches the app; +// - PMv2 but not following (pathological) -> mismatch, cannot be detected +// via public APIs; documented as a known residue, not probed. +// Local sessions keep the caret-monitor convention (mixed-DPI multi-monitor +// setups depend on it). Falls back to the monitor path when the foreground +// window is gone (lock screen, focus switch) so behavior matches pre-fix. +ResolvedCandidateScale ResolveCandidateScaleForCaret(POINT caret); // Candidate placement uses that monitor's work area to avoid taskbars/app bars. MonitorCoordinates GetMonitorCoordinatesFromPoint(POINT pt); diff --git a/server/src/window/candidate_presenter.cpp b/server/src/window/candidate_presenter.cpp index 3142ca3f..8bbb71b0 100644 --- a/server/src/window/candidate_presenter.cpp +++ b/server/src/window/candidate_presenter.cpp @@ -67,6 +67,23 @@ int AlignHostPixels(int value) return ((value + kBucket - 1) / kBucket) * kBucket; } +// Recompute half-screen DIP limits against `scale` when it differs from the +// monitor-derived scale, so clamping and window sizing stay on one scale +// (mirrors the webview path's rasterization-scale composition). +HalfScreenDipLimits ApplyScaleToHalfScreenLimits(POINT pt, FLOAT scale) +{ + HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(pt); + if (scale > 0.0f) + { + const double monitorWidthPx = static_cast((std::max)(1, limits.monitor.right - limits.monitor.left)); + const double monitorHeightPx = static_cast((std::max)(1, limits.monitor.bottom - limits.monitor.top)); + limits.scale = scale; + limits.maxWidthDip = (monitorWidthPx * 0.5) / static_cast(scale); + limits.maxHeightDip = (monitorHeightPx * 0.5) / static_cast(scale); + } + return limits; +} + D2D1_COLOR_F ParseCssColor(const std::string &text, D2D1_COLOR_F fallback) { std::string value = TrimCopy(text); @@ -848,14 +865,23 @@ void CandidatePresenter::RestoreHostAfterMenu() impl_->hostExpandedForMenu = false; } -void CandidatePresenter::PlaceAndShow(POINT caret, float widthDip, float heightDip, float cardLeftDip, float cardTopDip) +void CandidatePresenter::PlaceAndShow(POINT caret, float widthDip, float heightDip, float cardLeftDip, float cardTopDip, + const ResolvedCandidateScale &resolved) { - const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(caret); - FLOAT scale = limits.scale > 0.0f ? limits.scale : GetScaleForPoint(caret); + FLOAT scale = resolved.scale; + CandidateScaleSource scaleSource = resolved.source; + if (scale <= 0.0f) + { + const ResolvedCandidateScale now = ResolveCandidateScaleForCaret(caret); + scale = now.scale; + scaleSource = now.source; + } if (scale <= 0.0f) { scale = 1.0f; + scaleSource = CandidateScaleSource::Monitor; } + const HalfScreenDipLimits limits = ApplyScaleToHalfScreenLimits(caret, scale); widthDip = static_cast(ClampWidthDipToHalfScreen(widthDip, limits)); heightDip = static_cast(ClampHeightDipToHalfScreen(heightDip, limits)); lastLayoutWidthDip_ = widthDip; @@ -910,9 +936,24 @@ void CandidatePresenter::PlaceAndShow(POINT caret, float widthDip, float heightD y = monitor.top + 2 - cardTopPx; } SetWindowPos(hwnd_, HWND_TOPMOST, x, y, widthPx, heightPx, SWP_NOACTIVATE | SWP_SHOWWINDOW); + // Rendering must use the same scale the window was just sized with: the ui + // override replaces GetDpiForWindow (144 inside RDP) so DIPs map exactly + // onto the pixels computed above. Must land before EnsureForComposition — + // a freshly created target reads it through DpiForHwnd(). + const FLOAT dpiOverride = scale * 96.0f; + impl_->window->SetDpiOverride(dpiOverride); + impl_->resources.SetDpiOverride(dpiOverride); impl_->resources.EnsureForComposition(hwnd_); Present(); SetCandidateHostCloaked(false); + // Acceptance trace for the RDP candidate-scale fix: which scale authority + // won, and the system's own (potentially diverging) DPI values. Coordinates + // and DPI only — never user input, per the diagnostic logging red lines. + CAND_DIAG_LOGF(L"candidate-d2d place source={} scale={:.3f} hwnd_dpi={} system_dpi={} remote={} caret=({},{}) " + L"size_px=({},{})", + scaleSource == CandidateScaleSource::RdpForeground ? L"rdp-foreground" : L"monitor", scale, + GetDpiForWindow(hwnd_), GetDpiForSystem(), GetSystemMetrics(SM_REMOTESESSION) ? 1 : 0, caret.x, + caret.y, widthPx, heightPx); } void CandidatePresenter::ShowFromGlobalState() @@ -970,7 +1011,11 @@ void CandidatePresenter::ShowFromGlobalState(POINT caret) } FillItemsFromUi(); impl_->list->SetHoverEnabled(hoverArmed_); - const HalfScreenDipLimits limits = QueryHalfScreenDipLimitsForPoint(caret); + // Resolve the scale once per show and thread it through measure, clamping, + // window sizing and rendering DPI — a foreground window changing mid-show + // must not split measure (clamped at one scale) from placement (another). + const ResolvedCandidateScale scale = ResolveCandidateScaleForCaret(caret); + const HalfScreenDipLimits limits = ApplyScaleToHalfScreenLimits(caret, scale.scale); const float maxW = limits.maxWidthDip > 1.0 ? static_cast(limits.maxWidthDip) : 480.0f; const float maxH = limits.maxHeightDip > 1.0 ? static_cast(limits.maxHeightDip) : 640.0f; impl_->root->InvalidateMeasure(); @@ -992,7 +1037,7 @@ void CandidatePresenter::ShowFromGlobalState(POINT caret) cardLeftDip = card.x; cardTopDip = card.y; } - PlaceAndShow(caret, widthDip, heightDip, cardLeftDip, cardTopDip); + PlaceAndShow(caret, widthDip, heightDip, cardLeftDip, cardTopDip, scale); ::is_global_wnd_cand_shown = true; } diff --git a/server/src/window/candidate_presenter.h b/server/src/window/candidate_presenter.h index a0586c83..6563f96a 100644 --- a/server/src/window/candidate_presenter.h +++ b/server/src/window/candidate_presenter.h @@ -1,5 +1,7 @@ #pragma once +#include "utils/window_utils.h" + #include #include #include @@ -26,7 +28,11 @@ class CandidatePresenter void RebuildScene(); void ApplySkin(); void FillItemsFromUi(); - void PlaceAndShow(POINT caret, float widthDip, float heightDip, float cardLeftDip, float cardTopDip); + // `scale` carries the scale resolved once per show in ShowFromGlobalState + // so measure, clamping, sizing and rendering all share one source; an + // unset (scale == 0) value makes PlaceAndShow resolve it itself. + void PlaceAndShow(POINT caret, float widthDip, float heightDip, float cardLeftDip, float cardTopDip, + const ResolvedCandidateScale &scale = {}); void ArmHoverIfPointerMoved(); void CommitItem(size_t pageIndex); void ShowItemContextMenu(size_t pageIndex, POINT clientPoint);