From 065f3d2e5715836403d0abba9640d5db9f2b5d93 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 5 Aug 2026 10:30:56 +0200 Subject: [PATCH] fix(cursor): make the Windows cursor sampler DPI-aware cursor-sampler.exe shipped with no dpiAware manifest and never called SetProcessDpiAwareness*, so the process was DPI-unaware and Win32 handed it *virtualized* coordinates: GetCursorInfo().ptScreenPos and GetWindowRect both come back divided by the primary display's scale factor. b31bb71f assumed the opposite ("reports raw x/y in physical screen pixels") and converted the Electron display bounds to physical before normalizing. Both sides then lived in different spaces, so on a scaled display normalizeSample produced cx = (x/s)/W instead of x/W: the preview cursor sits at 1/s of its real offset from the top-left, an error that grows with the distance to the display origin (at 150% on a 2560px-wide screen, ~850px short at the right edge). Opt the helper into per-monitor-v2 awareness rather than walking the normalization back to DIPs: the numbers really do become physical, which is what every consumer already assumes, and unlike DIP normalization it also holds on mixed-DPI multi-monitor setups (virtualization always uses the primary display's scale, whatever monitor the cursor is actually on). Window captures were never affected: there the sampler supplies its own GetWindowRect bounds, so numerator and denominator were virtualized together and the ratio came out right either way. payload.x/y being physical now, the asset's display lookup needs screenToDipPoint -- screen.getDisplayNearestPoint works in DIPs and would otherwise pick the wrong monitor. macOS and Linux are unaffected: the SCK helper reports its capture frame in points, the same space as screen.getCursorScreenPoint(), and the PipeWire helper normalizes against the stream's own pixel dimensions. Verified: GetProcessDpiAwareness on the rebuilt binary reports PER_MONITOR_AWARE (was UNAWARE), and the reported position is unchanged at 100% scaling. Fixes #272 --- .../cursor/recording/windowsNativeRecordingSession.ts | 5 ++++- electron/native/wgc-capture/src/cursor-sampler.cpp | 8 ++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts b/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts index c7a057fea3..d2d2f0cc86 100644 --- a/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts +++ b/electron/native-bridge/cursor/recording/windowsNativeRecordingSession.ts @@ -186,7 +186,10 @@ export class WindowsNativeRecordingSession implements CursorRecordingSession { } if (payload.asset?.id && !this.assets.has(payload.asset.id)) { - const assetDisplay = screen.getDisplayNearestPoint({ x: payload.x, y: payload.y }); + // payload.x/y are physical screen pixels; `screen` works in DIPs. + const assetDisplay = screen.getDisplayNearestPoint( + screen.screenToDipPoint({ x: payload.x, y: payload.y }), + ); this.assets.set(payload.asset.id, { id: payload.asset.id, platform: "win32", diff --git a/electron/native/wgc-capture/src/cursor-sampler.cpp b/electron/native/wgc-capture/src/cursor-sampler.cpp index 21558c79a4..fc68ebf283 100644 --- a/electron/native/wgc-capture/src/cursor-sampler.cpp +++ b/electron/native/wgc-capture/src/cursor-sampler.cpp @@ -410,6 +410,14 @@ static void runSamplingLoop(int intervalMs, HWND targetWindow, const CLSID& pngC // main // ───────────────────────────────────────────────────────────────────────────── int main(int argc, char* argv[]) { + // Without this the process is DPI-unaware and Win32 virtualises every + // coordinate it hands back — GetCursorInfo().ptScreenPos and GetWindowRect + // come out divided by the primary display's scale factor — while the WGC + // capture and the consumer both work in physical pixels. On any scaled + // display the cursor then lands short of its real position, by more the + // further it is from the origin (getopenscreen/openscreen#272). + SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if (argc < 2) { std::cerr << "Usage: cursor-sampler [windowHandle]" << std::endl; return 1;