From 4f236b7dd045ee1e85dc855fb5e999b1907704c9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 09:05:32 +0000 Subject: [PATCH] =?UTF-8?q?feat(rdp):=20skalowanie=20pulpitu=20zdalnego=20?= =?UTF-8?q?poni=C5=BCej=20100%=20(50=E2=80=9390%)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Protokół zna DPI sesji tylko w zakresie 100..500% (ulDesktopScaleFactor), więc pomniejszenia nie da się zrobić tą drogą. Realizujemy je drugim mechanizmem: DPI zostaje na 100%, a klient żąda pulpitu WIĘKSZEGO niż panel (100/75 = 1.33×) i wciska go SmartSizingiem — więcej pulpitu, drobniejszy interfejs, kosztem lekkiego rozmycia (skalowanie w dół wygląda znacznie lepiej niż w górę). - RdpDisplay: MinScale 50, ProtocolMinScale 100, DesktopScaleFactor() przycina DPI do zakresu protokołu, ResolutionMultiplier()/ScaleResolution() liczą powiększoną rozdzielczość. Przy przekroczeniu limitu sesji (8192) przycinamy MNOŻNIK, nie pojedynczy wymiar — proporcje pulpitu muszą zostać jak okna. - RdpDynamicResolution: mnożnik nakładany na bazę (piksele panelu albo stałą rozdzielczość) w Display-Update, ApplyExact (pełny ekran) i przed Connect; SmartSizing wymuszony także dla skali < 100%. - Tryb „jak ekran lokalny" nadal nie schodzi poniżej 100% — pomniejszenie jest świadomym wyborem, nie skutkiem dziwnego odczytu DPI. - UI: pozycje 50/60/75/80/90%; etykieta bez „(DPI)", bo poniżej 100% to nie DPI. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01G2BeQc7J3e3NosuC3vvoGi --- README.md | 5 +- docs/ROADMAP.md | 3 +- src/RdpManager/Core/RdpDisplay.cs | 69 +++++++++++++++++++---- src/RdpManager/MainWindow.xaml | 5 ++ src/RdpManager/RdpDynamicResolution.cs | 56 ++++++++++-------- src/RdpManager/Themes/Strings.en.xaml | 4 +- src/RdpManager/Themes/Strings.pl.xaml | 4 +- tests/RdpManager.Tests/RdpDisplayTests.cs | 52 ++++++++++++++++- 8 files changed, 156 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 79dff6d..7a22c86 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,9 @@ it's a self-contained single file, no .NET install required (Windows 10/11 x64). - **RDP** — Microsoft's official ActiveX control (`mstscax`): dynamic resolution that follows the window/full-screen size (falls back to smart-sizing on older hosts) or a **fixed resolution** of your - choice, **remote desktop scaling (DPI)** so text stays readable on 4K/HiDPI screens - (*Settings → Connection*), clipboard/drive/printer/audio redirection, server-identity verification + choice, **remote desktop scaling** from 50% to 200% — larger, crisp text on 4K/HiDPI screens or a + smaller UI with more desktop area (*Settings → Connection*), + clipboard/drive/printer/audio redirection, server-identity verification level, RD Gateway / jump-host and admin session. - **SSH** — embedded **xterm.js** terminal: password and private-key auth (including **passphrase-protected keys**), host-key verification (trust-on-first-use with fingerprint prompt), diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 0ab629c..5c02c70 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -22,7 +22,8 @@ current priorities. window controls move onto the strip. Distinct from true full screen (F11). - ✅ **Full-screen** with auto-hiding, draggable, pinnable bar + "other connections" flyout. - ✅ **Dynamic resolution** — session = window/monitor physical pixels, DPI-safe; optionally a - fixed resolution, plus remote desktop scaling (DPI) for readable text on 4K/HiDPI screens. + fixed resolution, plus remote desktop scaling 50–200% (session DPI above 100%, oversized + desktop scaled into the window below it). - ✅ **Credentials in Windows Credential Manager** (DPAPI); prompt at connect; "connect as". - ✅ **Server tree** — groups, favorites (pinned), collapsible groups, one-click group rename, search, drag-and-drop reorder, background reachability dots. diff --git a/src/RdpManager/Core/RdpDisplay.cs b/src/RdpManager/Core/RdpDisplay.cs index 4cb2ef5..af90512 100644 --- a/src/RdpManager/Core/RdpDisplay.cs +++ b/src/RdpManager/Core/RdpDisplay.cs @@ -10,20 +10,30 @@ namespace RdpManager.Core /// Dwa niezależne pokrętła (Ustawienia → Połączenie → Domyślne RDP): /// - ROZDZIELCZOŚĆ: 0×0 = „dopasuj do okna" (domyślne — sesja trzyma natywne piksele panelu, obraz 1:1), /// wartość stała (np. 1600×900) = sesja negocjuje ten rozmiar, a SmartSizing rozciąga go do okna. - /// - SKALOWANIE (DPI): 0 = „jak ekran lokalny" (mnożnik DPI okna — tak robi mstsc), inaczej stałe procenty. - /// Trafia do ulDesktopScaleFactor (UpdateSessionDisplaySettings) i „DesktopScaleFactor" przed Connect, - /// więc zdalny pulpit RYSUJE interfejs większy — bez utraty ostrości (odwrotnie niż SmartSizing). - /// Na ekranie 4K/HiDPI sesja 1:1 przy 100% daje mikroskopijny tekst; to jest na to lekarstwo. + /// - SKALOWANIE: 0 = „jak ekran lokalny" (mnożnik DPI okna — tak robi mstsc), inaczej stałe procenty. + /// Dwa mechanizmy, bo protokół zna DPI tylko w zakresie 100..500%: + /// • >= 100% → ulDesktopScaleFactor (UpdateSessionDisplaySettings) i „DesktopScaleFactor" przed + /// Connect, więc zdalny pulpit RYSUJE interfejs większy — bez utraty ostrości. Na ekranie 4K/HiDPI + /// sesja 1:1 przy 100% daje mikroskopijny tekst; to jest na to lekarstwo. + /// • < 100% → DPI zostaje 100%, a żądamy PULPITU WIĘKSZEGO niż panel () + /// i zmniejszamy obraz SmartSizingiem: więcej pulpitu, mniejszy UI, kosztem lekkiego rozmycia. /// public static class RdpDisplay { // Zakresy z dokumentacji IMsRdpClient9::UpdateSessionDisplaySettings: ulDesktopScaleFactor 100..500, // ulDeviceScaleFactor TYLKO 100/140/180 (inna wartość => E_INVALIDARG, cała zmiana odrzucona). // Device trzymamy na 100 („bez dodatkowego skalowania urządzenia") — DPI sesji ustala desktopScaleFactor. - public const int MinScale = 100; public const int MaxScale = 500; public const uint DeviceScaleFactor = 100u; + /// Dół protokołu: DPI sesji nie zejdzie poniżej 100% (mniejsze wartości serwer IGNORUJE). + public const int ProtocolMinScale = 100; + + /// Dół naszego suwaka. Poniżej 100% DPI nie wchodzi w grę, więc pomniejszenie robimy drugą + /// drogą — patrz . Niżej niż 50% nie ma sensu (rozdzielczość rośnie + /// dwukrotnie, tekst i tak nieczytelny). + public const int MinScale = 50; + /// Czy ustawienie opisuje STAŁĄ rozdzielczość (a nie „dopasuj do okna"). public static bool IsFixed(int width, int height) => width >= RdpUtils.MinDim && height >= RdpUtils.MinDim; @@ -69,18 +79,57 @@ public static int ParseScale(string tag) } /// - /// Skala DPI wysyłana serwerowi. > 0 = wybór użytkownika (przycięty do - /// [100..500]); 0 = „jak ekran lokalny", czyli mnożnik DPI okna (1.5 → 150%). Zaokrąglamy do 5%, bo + /// Skala wybrana przez użytkownika. > 0 = jego wartość (przycięta do + /// [50..500]); 0 = „jak ekran lokalny", czyli mnożnik DPI okna (1.5 → 150%). Zaokrąglamy do 5%, bo /// mnożnik z VisualTreeHelper.GetDpi bywa niecałkowity (np. 1.4999), a serwer i tak operuje - /// na okrągłych krokach DPI. + /// na okrągłych krokach DPI. W trybie „auto" nie schodzimy poniżej 100% — ekran lokalny poniżej 100% + /// to zwykle artefakt pomiaru, a nie życzenie użytkownika (pomniejszenie wybiera się świadomie). /// public static int EffectiveScale(int configured, double localDpiScale) { if (configured > 0) return Math.Clamp(configured, MinScale, MaxScale); - if (double.IsNaN(localDpiScale) || double.IsInfinity(localDpiScale) || localDpiScale <= 0) return MinScale; + if (double.IsNaN(localDpiScale) || double.IsInfinity(localDpiScale) || localDpiScale <= 0) return ProtocolMinScale; int pct = (int)Math.Round(localDpiScale * 100.0 / 5.0, MidpointRounding.AwayFromZero) * 5; - return Math.Clamp(pct, MinScale, MaxScale); + return Math.Clamp(pct, ProtocolMinScale, MaxScale); + } + + /// + /// Wartość do ulDesktopScaleFactor: dla skali >= 100% to wprost skala (serwer rysuje UI + /// większe, obraz zostaje ostry). Poniżej 100% protokół nie pozwala zejść, więc DPI zostaje na 100%, + /// a pomniejszenie realizuje . + /// + public static uint DesktopScaleFactor(int effectiveScale) + => (uint)Math.Clamp(effectiveScale, ProtocolMinScale, MaxScale); + + /// + /// Mnożnik ROZDZIELCZOŚCI dla skali poniżej 100%: żądamy pulpitu większego niż panel (100/75 = 1.33×) + /// i zmniejszamy obraz SmartSizingiem. Skutek jak DPI < 100% — więcej pulpitu, mniejszy interfejs — + /// kosztem lekkiego rozmycia (skalowanie w dół wygląda znacznie lepiej niż w górę). Dla >= 100% + /// zwraca 1.0, bo tam pracuje już desktopScaleFactor i obraz ma zostać 1:1. + /// + public static double ResolutionMultiplier(int effectiveScale) + => effectiveScale >= ProtocolMinScale || effectiveScale <= 0 + ? 1.0 + : ProtocolMinScale / (double)Math.Max(effectiveScale, MinScale); + + /// + /// Nakłada na wymiary bazowe (piksele panelu albo stała + /// rozdzielczość) i normalizuje wynik. Gdy po przemnożeniu któryś wymiar wychodzi za limit sesji + /// (8192), przycinamy MNOŻNIK, nie pojedynczy wymiar — inaczej pulpit dostałby inne proporcje + /// niż okno i obraz byłby rozciągnięty. + /// + public static (int Width, int Height) ScaleResolution(int width, int height, int effectiveScale) + { + if (width < RdpUtils.MinDim || height < RdpUtils.MinDim) return (0, 0); + + double mult = ResolutionMultiplier(effectiveScale); + if (mult > 1.0) + mult = Math.Min(mult, Math.Min(RdpUtils.MaxDim / (double)width, RdpUtils.MaxDim / (double)height)); + if (mult <= 1.0) return (RdpUtils.NormalizeDim(width), RdpUtils.NormalizeDim(height)); + + return (RdpUtils.NormalizeDim((int)Math.Round(width * mult)), + RdpUtils.NormalizeDim((int)Math.Round(height * mult))); } } } diff --git a/src/RdpManager/MainWindow.xaml b/src/RdpManager/MainWindow.xaml index 53cb0b8..4ee706c 100644 --- a/src/RdpManager/MainWindow.xaml +++ b/src/RdpManager/MainWindow.xaml @@ -851,6 +851,11 @@ + + + + + diff --git a/src/RdpManager/RdpDynamicResolution.cs b/src/RdpManager/RdpDynamicResolution.cs index 2af6d2f..99950f3 100644 --- a/src/RdpManager/RdpDynamicResolution.cs +++ b/src/RdpManager/RdpDynamicResolution.cs @@ -26,8 +26,10 @@ namespace RdpManager /// przeliczane przez ) modyfikują oba wymiary tego zachowania: /// - stała rozdzielczość → nie mierzymy panelu, wysyłamy wybrany rozmiar i włączamy SmartSizing /// (pulpit i tak wypełnia okno, kosztem skalowania); - /// - skala DPI → jedzie w ulDesktopScaleFactor, więc zdalny pulpit rysuje UI większe zamiast - /// upychać je w natywnych pikselach 4K (to ratunek na „nic nie da się odczytać"). + /// - skala >= 100% → jedzie w ulDesktopScaleFactor, więc zdalny pulpit rysuje UI większe zamiast + /// upychać je w natywnych pikselach 4K (to ratunek na „nic nie da się odczytać"); + /// - skala < 100% → DPI zostaje 100%, ale żądamy pulpitu WIĘKSZEGO niż panel i wciskamy go + /// SmartSizingiem (pomniejszenie — protokół nie zna DPI poniżej 100%). /// public sealed class RdpDynamicResolution : IDisposable { @@ -80,16 +82,27 @@ public RdpDynamicResolution(Session session, WindowsFormsHost host, AppSettings return (w, h); } - /// Skala DPI dla serwera: wybór użytkownika, a przy „auto" mnożnik DPI okna (jak mstsc). - private uint EffectiveScale() + /// Skala wybrana przez użytkownika, a przy „auto" mnożnik DPI okna (jak mstsc). Wartości + /// poniżej 100% nie są DPI — realizuje je mnożnik rozdzielczości (patrz ). + private int EffectiveScale() { double dpi = TryGetDpiScale(out var sx, out _) ? sx : 1.0; - return (uint)RdpDisplay.EffectiveScale(_settings?.RdpScalePercent ?? 0, dpi); + return RdpDisplay.EffectiveScale(_settings?.RdpScalePercent ?? 0, dpi); } - /// Przy stałej rozdzielczości SmartSizing musi zostać włączony niezależnie od trybu podziału — - /// inaczej wybrany pulpit siedziałby w letterboxie zamiast wypełniać okno. - private bool WantSmartSizing(bool hasFixedRes) => _fit || hasFixedRes; + /// Wymiary do wysłania: baza (piksele panelu albo stała rozdzielczość) po nałożeniu mnożnika + /// dla skali poniżej 100%. + private (int W, int H) TargetRes(int baseW, int baseH) + { + var (w, h) = RdpDisplay.ScaleResolution(baseW, baseH, EffectiveScale()); + return (w, h); + } + + /// SmartSizing musi być włączony niezależnie od trybu podziału, gdy: (a) rozdzielczość jest + /// stała — inaczej pulpit siedziałby w letterboxie zamiast wypełniać okno; (b) skala jest poniżej 100% + /// — tam pomniejszenie POLEGA na wciśnięciu większego pulpitu w panel. + private bool WantSmartSizing(bool hasFixedRes) + => _fit || hasFixedRes || EffectiveScale() < RdpDisplay.ProtocolMinScale; private bool _fit; /// „Dopasuj do okna": host skaluje pulpit do swojego rozmiaru (SmartSizing), więc pulpit zawsze @@ -130,13 +143,14 @@ public void ApplyPreConnect() { // Tylko dla stałej rozdzielczości. W trybie „dopasuj do okna" zostawiamy domyślny rozmiar // kontrolki i renegocjujemy po zalogowaniu (ApplyInitial) — tak działało to do tej pory. - try { rdp.DesktopWidth = fx.W; rdp.DesktopHeight = fx.H; } + var target = TargetRes(fx.W, fx.H); + try { rdp.DesktopWidth = target.W; rdp.DesktopHeight = target.H; } catch (COMException) { } catch (InvalidComObjectException) { } } TrySetSmartSizing(rdp, WantSmartSizing(fx.W > 0)); - TrySetPreConnectScale(rdp, EffectiveScale()); + TrySetPreConnectScale(rdp, RdpDisplay.DesktopScaleFactor(EffectiveScale())); } /// @@ -166,9 +180,9 @@ public void ApplyExact(int physW, int physH) var fx = FixedRes(); if (fx.W > 0) { physW = fx.W; physH = fx.H; } - physW = RdpUtils.NormalizeDim(physW); - physH = RdpUtils.NormalizeDim(physH); - if (physW < MinDim || physH < MinDim) return; + var target = TargetRes(physW, physH); // skala < 100% => pulpit większy niż monitor + if (target.W < MinDim || target.H < MinDim) return; + physW = target.W; physH = target.H; // TargetRes już normalizuje (parzyste, [200..8192]) var rdp = _session.Rdp; if (rdp == null) return; @@ -216,18 +230,16 @@ private void OnDebounceTick(object sender, EventArgs e) catch (COMException) { return; } if (!live) return; - int w, h; + int baseW, baseH; var fx = FixedRes(); if (fx.W > 0) { - w = fx.W; h = fx.H; // stała rozdzielczość: nie mierzymy panelu, tylko wysyłamy wybraną - } - else - { - if (!TryGetPhysicalPixels(out w, out h)) return; - w = RdpUtils.NormalizeDim(w); - h = RdpUtils.NormalizeDim(h); + baseW = fx.W; baseH = fx.H; // stała rozdzielczość: nie mierzymy panelu, tylko wysyłamy wybraną } + else if (!TryGetPhysicalPixels(out baseW, out baseH)) return; + + // Skala < 100% => pulpit większy niż baza, wciśnięty w panel SmartSizingiem (pomniejszenie). + var (w, h) = TargetRes(baseW, baseH); if (w < MinDim || h < MinDim) return; if (w == _lastW && h == _lastH) return; @@ -269,7 +281,7 @@ private void OnDebounceTick(object sender, EventArgs e) /// private void UpdateDisplay(AxMsRdpClient11NotSafeForScripting rdp, int w, int h) { - uint scale = EffectiveScale(); + uint scale = RdpDisplay.DesktopScaleFactor(EffectiveScale()); if (scale == 100u) { rdp.UpdateSessionDisplaySettings((uint)w, (uint)h, 0u, 0u, 0u, 100u, RdpDisplay.DeviceScaleFactor); diff --git a/src/RdpManager/Themes/Strings.en.xaml b/src/RdpManager/Themes/Strings.en.xaml index 692cae0..95d6a67 100644 --- a/src/RdpManager/Themes/Strings.en.xaml +++ b/src/RdpManager/Themes/Strings.en.xaml @@ -110,8 +110,8 @@ RDP session resolution “Fit to window” keeps the resolution equal to the window (crisp 1:1 image). A fixed value is stretched to the window. Fit to window - Remote desktop scaling (DPI) - Enlarges text and icons inside the session without blurring. Helps on 4K/HiDPI displays; requires Windows 10 / Server 2016 or newer. + Remote desktop scaling + Above 100% the server draws text and icons larger, without blurring (session DPI; requires Windows 10 / Server 2016 or newer). Below 100% the client requests a higher resolution and scales the image down — more desktop, smaller interface, slightly softer. Match local display Automatically resume dropped connections Behavior diff --git a/src/RdpManager/Themes/Strings.pl.xaml b/src/RdpManager/Themes/Strings.pl.xaml index 5f7b6a2..b543567 100644 --- a/src/RdpManager/Themes/Strings.pl.xaml +++ b/src/RdpManager/Themes/Strings.pl.xaml @@ -110,8 +110,8 @@ Rozdzielczość sesji RDP „Dopasuj do okna" trzyma rozdzielczość równą oknu (ostry obraz 1:1). Stała wartość jest rozciągana do okna. Dopasuj do okna - Skalowanie pulpitu zdalnego (DPI) - Powiększa tekst i ikony w sesji bez rozmycia. Pomaga na ekranach 4K/HiDPI; wymaga serwera Windows 10 / Server 2016 lub nowszego. + Skalowanie pulpitu zdalnego + Powyżej 100% serwer rysuje tekst i ikony większe, bez rozmycia (DPI sesji; wymaga Windows 10 / Server 2016 lub nowszego). Poniżej 100% klient żąda wyższej rozdzielczości i zmniejsza obraz — więcej pulpitu, drobniejszy interfejs, nieco mniej ostro. Jak ekran lokalny Automatycznie wznawiaj zerwane połączenie Zachowanie diff --git a/tests/RdpManager.Tests/RdpDisplayTests.cs b/tests/RdpManager.Tests/RdpDisplayTests.cs index bbf9ab9..704b8c4 100644 --- a/tests/RdpManager.Tests/RdpDisplayTests.cs +++ b/tests/RdpManager.Tests/RdpDisplayTests.cs @@ -66,15 +66,17 @@ public void FormatResolution_RoundTripsWithParse() [InlineData("150", 150)] [InlineData("175%", 175)] // tolerujemy „%" z etykiety [InlineData("9000", 500)] // powyżej MaxScale - [InlineData("50", 100)] // poniżej MinScale + [InlineData("75", 75)] // pomniejszenie — poniżej 100% jest dozwolone + [InlineData("10", 50)] // poniżej MinScale public void ParseScale_ReadsPercentOrAuto(string tag, int expected) => Assert.Equal(expected, RdpDisplay.ParseScale(tag)); [Theory] [InlineData(125, 125)] [InlineData(200, 200)] + [InlineData(75, 75)] [InlineData(600, 500)] // przycięcie do MaxScale - [InlineData(10, 100)] // przycięcie do MinScale + [InlineData(10, 50)] // przycięcie do MinScale public void EffectiveScale_ConfiguredWins(int configured, int expected) => Assert.Equal(expected, RdpDisplay.EffectiveScale(configured, 1.0)); @@ -85,7 +87,7 @@ public void EffectiveScale_ConfiguredWins(int configured, int expected) [InlineData(1.4999, 150)] // mnożnik z GetDpi bywa niecałkowity → zaokrąglenie do 5% [InlineData(1.75, 175)] [InlineData(2.0, 200)] - [InlineData(0.5, 100)] // ekran „poniżej 100%" — serwer i tak nie przyjmie < 100 + [InlineData(0.5, 100)] // „auto" nie schodzi pod 100% — pomniejszenie wybiera się świadomie [InlineData(9.0, 500)] public void EffectiveScale_AutoFollowsLocalDpi(double dpiScale, int expected) => Assert.Equal(expected, RdpDisplay.EffectiveScale(0, dpiScale)); @@ -98,6 +100,50 @@ public void EffectiveScale_AutoFollowsLocalDpi(double dpiScale, int expected) public void EffectiveScale_AutoWithBogusDpiFallsBackTo100(double dpiScale) => Assert.Equal(100, RdpDisplay.EffectiveScale(0, dpiScale)); + // ---------- pomniejszenie (skala < 100%) ---------- + + [Theory] + [InlineData(50, 100u)] // DPI nie zejdzie poniżej 100% — pomniejsza mnożnik rozdzielczości + [InlineData(75, 100u)] + [InlineData(100, 100u)] + [InlineData(150, 150u)] + [InlineData(500, 500u)] + public void DesktopScaleFactor_ClampedToProtocolRange(int scale, uint expected) + => Assert.Equal(expected, RdpDisplay.DesktopScaleFactor(scale)); + + [Theory] + [InlineData(100, 1.0)] + [InlineData(150, 1.0)] // powyżej 100% pracuje DPI, rozdzielczość zostaje 1:1 + [InlineData(0, 1.0)] + [InlineData(50, 2.0)] + [InlineData(75, 100.0 / 75.0)] + [InlineData(80, 1.25)] + public void ResolutionMultiplier_OnlyGrowsBelow100(int scale, double expected) + => Assert.Equal(expected, RdpDisplay.ResolutionMultiplier(scale), 6); + + [Theory] + [InlineData(1920, 1080, 100, 1920, 1080)] + [InlineData(1920, 1080, 150, 1920, 1080)] // DPI, nie rozdzielczość + [InlineData(1920, 1080, 75, 2560, 1440)] + [InlineData(1920, 1080, 50, 3840, 2160)] + [InlineData(1280, 720, 80, 1600, 900)] + public void ScaleResolution_GrowsDesktopBelow100(int w, int h, int scale, int expW, int expH) + => Assert.Equal((expW, expH), RdpDisplay.ScaleResolution(w, h, scale)); + + [Fact] + public void ScaleResolution_KeepsAspectWhenHittingSessionLimit() + { + // 6000×3000 przy 50% chciałoby 12000×6000 — limit to 8192, więc przycinamy MNOŻNIK + // (oba wymiary o tyle samo), a nie pojedynczy wymiar, żeby nie zmienić proporcji. + var (w, h) = RdpDisplay.ScaleResolution(6000, 3000, 50); + Assert.Equal(RdpUtils.MaxDim, w); + Assert.Equal(RdpUtils.MaxDim / 2, h); + } + + [Fact] + public void ScaleResolution_RejectsTooSmallBase() + => Assert.Equal((0, 0), RdpDisplay.ScaleResolution(100, 100, 75)); + [Fact] public void DeviceScaleFactor_StaysAtAllowedValue() {