Visual parity between Windows displays of different pixel densities.
If you put a 27" 1440p monitor next to a 27" 1080p monitor, everything on the 1080p one is 33% bigger. Windows cannot fix this, because it has no display scale factor below 100%. Multi-Deck corrects what can actually be corrected — natively, without resampling anything.
Two displays of the same physical width but different resolutions have different pixel densities:
| Display | Resolution | Diagonal | Density |
|---|---|---|---|
| 27" 1440p | 2560 × 1440 | 27" | ≈ 108.9 PPI |
| 27" 1080p | 1920 × 1080 | 27" | ≈ 81.7 PPI |
To make a UI element look the same physical size on both, the 1080p display
needs a scale factor of 81.7 / 108.9 = 0.75 — exactly 75%, which is just
1080 / 1440.
Windows will not do that. This is not a UI limitation, it is enforced in the composition engine:
- Microsoft's OEM documentation:
"All display scale factors in this mode are constrained to be one of these
four values: 100%, 125%, 150%, 200%", and
LogPixelsis "limited to the range (100%-500%)". - Microsoft support, on this exact scenario: "Unfortunately, the system limit is only 100%, which is by design."
- Measured directly:
DisplayConfigGetDeviceInfowith the undocumentedGET_DPI_SCALErequest reportsminScaleRel = 0— no headroom below the recommended value. Writing a lower value throughSET_DPI_SCALEreturnsERROR_SUCCESSand is stored, butGetDpiForMonitorkeeps reporting 96 DPI. The value is accepted and then ignored.
The same clamping applies to the mouse pointer: CursorBaseSize accepts a value
below 32, stores it, and SM_CXCURSOR still reports 32 with a 32 × 32 bitmap.
So the honest conclusion is: you cannot shrink the Windows UI itself. You can only make individual applications render smaller — which, unlike a GPU downscale, is genuinely sharp because the application rasterises at the final size.
| Feature | Mechanism | Result |
|---|---|---|
| Window rescaling | SetWinEventHook on move/size events, then SetWindowPos |
A window dragged to a lower-density display keeps the same physical footprint |
| Drag preview | Layered click-through overlay | Outlines where and at what size the window will land, while you are still positioning it |
| Application zoom | Sends the app's own zoom shortcut via SendInput |
Content re-rendered natively at the corrected size — sharp, not resampled |
| Cursor scaling | SetSystemCursor with a scaled cursor set |
Pointer keeps the same apparent size across displays |
| Taskbar | MMTaskbarEnabled, with backup and restore |
Optionally hides the taskbar on secondary displays |
Zoom and geometry are independent: a maximized window keeps whatever size the display gives it, but its content is still zoomed. That is the common case of a maximized chat or browser window moved to the other screen.
The window itself cannot be resized mid-drag — Windows runs a modal move loop
that reapplies the size captured when the drag began, so any SetWindowPos
issued during a drag is silently overwritten. The overlay exists precisely
because of that.
The correction ratio is derived from the panels themselves: Multi-Deck reads each display's EDID from the driver store, extracts its true physical dimensions, and computes the density ratio. No monitor model is hard-coded. Reinstall Windows, plug in different screens, run the setup wizard again.
- The taskbar, Explorer, dialog boxes, context menus, the notification area and system title bars cannot be rendered below 100%. Nothing in user space can change that.
- Applications with no zoom command of their own are not affected.
- Fullscreen games follow the resolution, not the DPI.
- No GPU downscaling (AMD VSR / NVIDIA DSR). It would correct everything, but it resamples the whole desktop and destroys ClearType subpixel rendering. If you want that, enable it in your driver — it is not this tool's job.
Requires the .NET 8 SDK and the MSVC toolchain (Visual Studio "Desktop development with C++") to build. The resulting executable needs neither.
git clone <this-repo>
cd Multi-Deck
.\build.ps1 -Install -RunOn first launch a setup wizard detects your displays, shows the computed ratios and asks which corrections you want.
MultiDeck.exe Run the agent (setup wizard on first launch)
MultiDeck.exe --setup Re-run the setup wizard
MultiDeck.exe --install Register autostart for the current user
MultiDeck.exe --uninstall Remove autostart, restore everything it changed
MultiDeck.exe --uninstall --purge ... and delete config and logs
MultiDeck.exe --restore Restore cursors and taskbar, keep config
MultiDeck.exe --status Print detected displays and current settings
MultiDeck.exe --diagnose Write a full diagnostics file for a bug report
--install copies the executable to %LOCALAPPDATA%\Multi-Deck\bin and points
autostart there, so the build directory can be moved or deleted afterwards.
Note that MultiDeck.exe is a GUI-subsystem binary: a shell does not block
on it. Scripts must use Start-Process -Wait around --install.
Everything else is on the notification-area icon: toggles for each correction, autostart, reload configuration, open the log, restore cursors, and exit.
config.json is watched, so hand edits apply within about a second — the
explicit Reload configuration menu item is only a fallback.
Emergency stop: Ctrl+Alt+Shift+D disables every correction and restores the
system cursors immediately. Configurable via panicHotkey.
%LOCALAPPDATA%\Multi-Deck\config.json — hand-editable, then Reload
configuration from disk in the tray menu.
Monitor keys are <EDID hardware id>#<serial>, which survive reboots, dock
cycles and display-index reshuffles.
Each profile declares the zoom factors an application can reach by pressing its step-down shortcut n times after a reset. Multi-Deck picks the entry closest to the display's ratio, so one profile works on any hardware combination.
{
"name": "Chromium and Electron",
"enabled": true,
"processes": ["chrome", "msedge", "spotify", "vesktop", "chatgpt"],
"resetKey": "ctrl+0",
"stepDownKey": "ctrl+minus",
"ladder": [1.0, 0.90, 0.80, 0.75, 0.67, 0.50]
}Chromium and Electron have a native 75% stop, so on a 1440p/1080p pair the match
is exact. Built-in profiles cover Chromium/Electron (about fifty applications),
VS Code (which zooms by powers of 1.2 and resets on Ctrl+NumPad0), Windows
Terminal (1pt font steps) and Notepad (10% steps).
Firefox ships disabled ("enabled": false), deliberately. Its zoom has no
75% stop, it only scales page content — tabs and the address bar stay at 100% —
and it is stored per site and persists, so driving it from here would quietly
rewrite the zoom level of every site you visit on the secondary display. Enable
it if you accept that.
For a whole-UI match, set layout.css.devPixelsPerPx to 0.75 in
about:config instead. That is global to Firefox, so it only makes sense if
Firefox lives on the lower-density display.
- No driver, no DLL injection, no kernel hook, no service, no elevation.
- No network access of any kind. No telemetry.
- The only registry values ever written are the autostart entry and — if you ask
for it — the multi-monitor taskbar setting, whose previous state is recorded in
state.jsonbefore the first change. - System cursors are restored on exit, on panic, on unhandled exception and on process exit.
--uninstallputs everything back.
WindowScaler measures rather than assumes. It records the window rectangle
before a move and reads it again after: Windows may already have rescaled it
(per-monitor-aware applications under mixed DPI) or left it untouched
(everything else). Dividing the desired ratio by the observed one handles both
cases without having to guess the application's DPI awareness mode.
'code' PL2745Q -> EK271 G: desired=0.750 observed=1.000 applied=0.750 880x640 -> 660x480
Display topology is rebuilt on WM_DISPLAYCHANGE, WM_DEVICECHANGE, resume
from sleep, session unlock and Explorer restart, always after a settle delay so
a burst of hot-plug events is coalesced.
- .NET 8 SDK
- MSVC toolchain with
link.exe(NativeAOT links natively) - Windows 10 1809+ or Windows 11
linux/ contains a Python port for KDE Plasma 6 on Wayland. The problem is
different there: KWin already scales each output independently, natively
and sharply, for every Wayland application — it just never derives the scale
from the panels. That derivation is what this port provides.
| Windows feature | Linux/KDE port |
|---|---|
| Window rescaling | Per-output compositor scale via kscreen-doctor, derived from EDID (scaling.mode: off / once / watch) |
| X11 (XWayland) leftovers | Opt-in geometry correction in a KWin script (windowScaling.x11CorrectionProcesses), same measure-don't-assume rule |
| Drag preview | Mostly unnecessary — KWin rescales windows live while you drag, so what you see is the final result; the Windows preview frame only existed because Windows cannot do that. workspace.showOutline() still covers the opt-in X11 correction |
| Application zoom | Same profiles, keys sent by ydotool. Auto-suppressed while per-output scales are in effect (the compositor already equalizes content; zooming would double-correct). Escape hatch: zoom.evenWhenScaled |
| Cursor scaling | Not ported — KWin scales the pointer per output natively |
| Taskbar | Not ported — Plasma panels follow their screen |
| Tray, wizard, panic hotkey | StatusNotifierItem, kdialog, KWin script shortcut |
Never resized by the script: native Wayland windows — they keep their logical size across outputs, so with the right per-output scales their physical size is already correct. Correcting them again would double-apply.
Layout integrity: a scale change alters an output's logical size, so
every apply also repositions the outputs in the same atomic
kscreen-doctor call. Without that, the logical rectangles overlap and
KWin duplicates the shared region on both screens.
Known limitation: X11 overlay windows that position themselves in absolute coordinates (screen indicators like OpenWhispr's recording dot, some widgets) keep their stale position after a layout change. Restart the affected app once; it re-anchors to the new geometry. Normal application windows are unaffected.
cd linux
./install.sh # = python3 -m multideck --install
python3 -m multideck --status # displays, densities, ratios, targets
python3 -m multideck --setup # first-run wizard (runs on first start too)
python3 -m multideck # the resident agent- Config:
~/.config/Multi-Deck/config.json(watched; edits apply live) - State and logs:
~/.local/share/Multi-Deck/(state.jsonholds the pre-Multi-Deck scales;--restorereapplies them) - Panic shortcut:
Ctrl+Alt+Shift+D, rebindable in System Settings like any KWin script shortcut - Remove:
python3 -m multideck --uninstall(or--purgeto also delete config, state and logs)
Requires only what a Plasma desktop already has: Python 3 with
python-gobject, kscreen-doctor, kpackagetool6, kdialog, kdotool, and
ydotool + ydotoold for the zoom feature (the agent runs fine without
them; zoom just stays off).
MIT. See LICENSE.
{ "referenceMonitor": "IVM669B#SERIAL1234", // the display everything matches "monitors": { "ACR0DF0#SERIAL5678": { "name": "EK271 G", "manage": true, "scaleOverride": null // null = derive from EDID density } }, "windowScaling": { "enabled": true, "excludedProcesses": ["steam", "cs2"] }, "zoom": { "enabled": true }, "cursor": { "enabled": true, "baseSize": 32 }, "taskbar": { "hideOnSecondaryDisplays": false }, "panicHotkey": "ctrl+alt+shift+d" }