Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

dpiscale

Windows Settings shows one number for your display - Scale: 200% - and stops there. Behind that number sit four different numbers, and Windows hands out different ones to different applications.

That is why OBS display capture comes out the wrong size, why a game recorded at "4K" is really upscaled 1080p, and why a screenshot of one window is sharp and the next is soft. dpiscale reads every one of those numbers, from every monitor, and shows you which of your applications is being told what.

Single PowerShell script. No install, no dependencies, no administrator, and it changes nothing - it only reads.


Quick start

powershell -NoProfile -ExecutionPolicy Bypass -File .\dpiscale.ps1

Real output, captured on a Surface Book 3:

  dpiscale 1.0.0   2026-09-13 14:22:52
  --------------------------------------------------------------

  Monitor 1  \\.\DISPLAY1   (primary)
    LG Display 0554   15.1 in   1920 x 1080 in use   60 Hz
    best mode this display offers: 3240 x 2160   (94 modes available)

    Windows scale       100%      (this is the number Settings shows)
    effective DPI       96        the scale factor, as a DPI
    raw DPI             153 x 154 pixels per inch at this resolution
    angular DPI         133       adjusted for viewing distance
    panel size          12.6 x 8.3 in from the monitor EDID

    what each application is told this monitor is:
      DPI-unaware app     1920 x 1080
      system-aware app    1920 x 1080
      per-monitor aware   1920 x 1080

    BELOW_NATIVE
      This display is running at 1920x1080 but it supports
      3240x2160. Everything on screen is being stretched to fill the
      panel by the graphics driver before you ever see it, so
      captures and recordings are soft at the source. Scaling is at
      100%, so nothing is virtualised on top of that.

  --------------------------------------------------------------
  BELOW_NATIVE
    Nothing is virtualised by DPI scaling, but 1 of your 1
    displays is not running at the best resolution it offers,
    which softens everything before scaling is even considered.

  monitors 1

That is a real finding on a real machine. This laptop has a 3240x2160 panel and was driving it at 1920x1080. Windows Settings showed 100% and said nothing at all about the other 2.8 million pixels. Every capture taken on that desktop was soft before OBS ever touched it.


The part Settings will not tell you

Ask Windows the same question from three processes that differ only in the DPI awareness they declare, and you get three different answers. -Compare launches those three processes and puts the answers side by side:

powershell -NoProfile -ExecutionPolicy Bypass -File .\dpiscale.ps1 -Compare
  --------------------------------------------------------------
  measured, by asking Windows the same question from three
  processes that differ only in their declared DPI awareness:

    declared awareness    GetSystemMetrics  GetMonitorInfo  DESKTOPHORZRES
    unaware               1920 x 1080       1920 x 1080     1920
    system                1920 x 1080       1920 x 1080     1920
    permonitorv2          1920 x 1080       1920 x 1080     1920

    Same panel, same API calls, one moment in time. The only thing
    that changed is what each process declared about itself.
    DESKTOPHORZRES is the column that never lies.

Three identical rows is the good case: this desktop is at 100%, so nothing is being virtualised and every application is told the truth.

Turn scaling on and the first row moves. Windows divides what it reports by the scale factor for any process that has not declared itself aware:

unaware width  x  effective DPI   ==   physical width  x  96

At 200% on a 3240-pixel-wide panel that is 1620 x 192 == 3240 x 96. A DPI-unaware capture tool asks for the screen, is handed 1620x1080, records 1620x1080, and Windows stretches it back to 3240 on the way to your eyes. The recording really is a quarter of the pixels. DESKTOPHORZRES still says 3240, which is why dpiscale uses that and the display mode as its sources and never trusts the virtualised ones.

The identity above is checked against live hardware on every run of the test suite, and -Compare measures it rather than assuming it.


What each number means

Number Where it comes from What it tells you
native resolution EnumDisplaySettings The pixels the panel is actually being driven at. Never rewritten for the caller.
best mode full mode enumeration The best resolution the display will accept. If you are below it, everything is soft before scaling is considered.
Windows scale GetScaleFactorForMonitor The percentage Settings shows. Cross-checked against effective DPI.
effective DPI GetDpiForMonitor The scale factor as a DPI. 96 is 100%, 192 is 200%.
raw DPI GetDpiForMonitor The real pixel density at the current resolution.
angular DPI GetDpiForMonitor Density adjusted for normal viewing distance.
panel size root\wmi EDID The physical glass, straight from the monitor. Used as an independent check on raw DPI.

Anything Windows will not answer is reported as -, never as 0.


Verdicts

Each monitor gets one, and the desktop gets one for the whole set.

Verdict Meaning
BELOW_NATIVE Running below the best mode the display offers. Checked first, because it beats every other explanation for a soft image.
NATIVE At the best mode, unscaled. Nothing to fix.
TINY_UNSCALED A dense panel at 100%. Sharp, but everything will be very small.
SCALED_INTEGER Scaled by a whole multiple of 96 DPI. Unaware apps land on exact pixel boundaries.
SCALED_FRACTIONAL Scaled by something like 125% or 150%. Unaware windows land between pixels, which is a second, separate source of softness.
MIXED_SCALING Your monitors do not agree, so a window changes size when you drag it between them.
SCALED_BELOW_NATIVE Both problems at once.

Options

  -ListMonitors       one line per monitor, nothing else
  -Compare            launch unaware / system / per-monitor-v2 child
                      processes and print what each one is told
  -Json               emit the whole report as JSON
  -FromJson <path>    re-render a saved report, on any machine
  -Info               explain what every number means and where it
                      comes from
  -Quiet              print nothing; use the exit code only
  -CompareTimeoutMs   how long to wait for a child probe (default 20000)

-ListMonitors, exactly as captured:

* \\.\DISPLAY1    1920 x 1080   100%   LG Display 0554

JSON

.\dpiscale.ps1 -Json > report.json
.\dpiscale.ps1 -FromJson report.json

Every number in the text report is in the document, plus the cross-checks the tool ran on itself. Real output, trimmed to one monitor:

{
    "schema":  "dpiscale/1",
    "tool":  "dpiscale",
    "version":  "1.0.0",
    "generated":  "2026-09-13 14:23:10",
    "processAwareness":  2,
    "awarenessSetVia":  "context",
    "monitorCount":  1,
    "desktopVerdict":  "BELOW_NATIVE",
    "monitors":  [
                     {
                         "device":  "\\\\.\\DISPLAY1",
                         "primary":  true,
                         "name":  "LG Display 0554",
                         "manufacturer":  "LG Display",
                         "physicalWidth":  1920,
                         "physicalHeight":  1080,
                         "desktopHorzRes":  1920,
                         "logPixels":  96,
                         "effectiveDpiX":  96,
                         "rawDpiX":  153,
                         "scalePercent":  100,
                         "scaleApiPercent":  100,
                         "scaleAgrees":  true,
                         "unawareWidth":  1920,
                         "unawareHeight":  1080,
                         "panelWidthMm":  320,
                         "panelHeightMm":  210,
                         "diagonalInches":  15.069022999216251,
                         "edidDpiX":  152.39999999999998,
                         "rawDpiDelta":  0.60000000000002274,
                         "desktopResAgrees":  true,
                         "monitorRectAgrees":  true,
                         "readingsVirtualized":  false,
                         "maxModeWidth":  3240,
                         "maxModeHeight":  2160,
                         "modeCount":  94,
                         "atMaxMode":  false,
                         "verdict":  "BELOW_NATIVE"
                     }
                 ]
}

edidDpiX 152.4 and rawDpiX 153 are two completely independent answers to the same physical question - one derived from the panel's own EDID, one from the DPI API. They agree to within 0.6 DPI. If they ever disagree badly on your machine, one of the two is wrong and the report says so.


Why you should care

  • OBS display capture is the wrong size. OBS is per-monitor aware; a game or a source that is not sees a different desktop. Run -Compare and the mismatch is right there in the first column.
  • Your "4K" recording is not 4K. If the verdict is BELOW_NATIVE, the panel is being fed fewer pixels than it has, and nothing downstream can put them back.
  • Windows only ever shows you one of the four numbers. The scale percent. It will not tell you the native resolution is higher than what you are running, it will not tell you the panel is 259 DPI, and it will not tell you that half your applications are being handed a smaller desktop.
  • Dragging a window between monitors resizes it. That is MIXED_SCALING, and it is a per-monitor setting, not a global one.

How it is tested

Three suites, all in the repo, all runnable on your own machine:

.\selftest.ps1     # 184 hermetic tests, synthetic readings, no hardware
.\realcheck.ps1    # 93 checks against this machine's real displays
.\mutate.ps1       # 27 deliberate bugs, each one must be caught

Actual results on the committed code:

selftest: 184 passed, 0 failed
realcheck: 93 passed, 0 failed, 0 skipped
mutate: 27 killed, 0 survived, 0 harness errors
mutate: dpiscale.ps1 restored byte for byte

realcheck.ps1 does not check the tool against itself. It checks it against five independent sources that must all agree: GetScaleFactorForMonitor against effective DPI, DESKTOPHORZRES against the display mode, GDI HORZRES and LOGPIXELSX against both, the panel's EDID size against the raw DPI API, and a real unelevated child process against the elevated parent. It also launches genuinely DPI-unaware and per-monitor-aware child processes and verifies that the identity above holds between them.

mutate.ps1 plants 27 specific, realistic bugs in the tool one at a time - dividing where it should multiply, reading MDT_RAW where it should read MDT_EFFECTIVE, treating the EDID centimetres as millimetres, reading the first display mode instead of the current one, looking up the panel by the raw interface name instead of the normalised PnP id - and requires the test suites to catch every single one. It restores the original file byte for byte and verifies the restore before it exits.


Requirements

Windows 8.1 or later, Windows PowerShell 5.1. No administrator. No modules, no downloads, no Add-Type beyond the interop declarations in the script itself.

Tested on Windows 11 26200 with PowerShell 5.1.26100.9444.


See also

  • obs-4k60-recorder - the OBS settings that actually produce a clean 4K60 recording.
  • truehz - the refresh rate Windows claims versus the one your monitor is really receiving.
  • framecheck - find the dropped and duplicated frames in a finished recording.
  • gpucheck - what your GPU is actually doing while you record.

License

MIT. See LICENSE.

About

Windows Settings says '200%' and stops there. dpiscale shows what your apps actually see: the SAME Windows API returns 1620x1080 to a DPI-unaware app and 3240x2160 to an aware one, for one physical panel. Reports native vs virtualized resolution, effective/angular/raw DPI, and why OBS display capture comes out the wrong size. No admin.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages