Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

6 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Multi-Deck

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.

Download


The problem, stated precisely

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 LogPixels is "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: DisplayConfigGetDeviceInfo with the undocumented GET_DPI_SCALE request reports minScaleRel = 0 — no headroom below the recommended value. Writing a lower value through SET_DPI_SCALE returns ERROR_SUCCESS and is stored, but GetDpiForMonitor keeps 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.

What Multi-Deck does

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.

What it deliberately does not do

  • 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.

Install

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 -Run

On first launch a setup wizard detects your displays, shows the computed ratios and asks which corrections you want.

Usage

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.

Configuration

%LOCALAPPDATA%\Multi-Deck\config.json — hand-editable, then Reload configuration from disk in the tray menu.

{
  "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"
}

Monitor keys are <EDID hardware id>#<serial>, which survive reboots, dock cycles and display-index reshuffles.

Zoom profiles

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.

Safety

  • 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.json before the first change.
  • System cursors are restored on exit, on panic, on unhandled exception and on process exit.
  • --uninstall puts everything back.

How it works

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.

Build requirements

  • .NET 8 SDK
  • MSVC toolchain with link.exe (NativeAOT links natively)
  • Windows 10 1809+ or Windows 11

Licence

MIT. See LICENSE.