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.
powershell -NoProfile -ExecutionPolicy Bypass -File .\dpiscale.ps1Real 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.
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.
| 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.
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. |
-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
.\dpiscale.ps1 -Json > report.json
.\dpiscale.ps1 -FromJson report.jsonEvery 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.
- 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
-Compareand 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.
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 caughtActual 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.
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.
- 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.
MIT. See LICENSE.