Skip to content

Commit faff20e

Browse files
Make windows sampler DPI aware (#2493)
* Make windows sampler DPI aware * Update windows_sampler_tests.rs
1 parent 89937e3 commit faff20e

File tree

4 files changed

+71
-81
lines changed

4 files changed

+71
-81
lines changed

electron-app/magnifier/rust-sampler/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ windows = { version = "0.58", features = [
3333
"Win32_Foundation",
3434
"Win32_Graphics_Gdi",
3535
"Win32_UI_WindowsAndMessaging",
36+
"Win32_UI_HiDpi",
3637
] }
3738

3839
[target.'cfg(target_os = "linux")'.dependencies]

electron-app/magnifier/rust-sampler/src/main.rs

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -67,36 +67,12 @@ fn run() -> Result<(), String> {
6767
}
6868
});
6969

70-
// Get DPI scale - Windows needs it, others use 1.0
71-
let dpi_scale = get_dpi_scale();
72-
73-
fn get_dpi_scale() -> f64 {
74-
#[cfg(target_os = "windows")]
75-
{
76-
// On Windows, get DPI scale directly from system
77-
use windows::Win32::Graphics::Gdi::{GetDC, GetDeviceCaps, LOGPIXELSX, ReleaseDC};
78-
unsafe {
79-
let hdc = GetDC(None);
80-
if !hdc.is_invalid() {
81-
let dpi = GetDeviceCaps(hdc, LOGPIXELSX);
82-
let _ = ReleaseDC(None, hdc);
83-
return dpi as f64 / 96.0;
84-
}
85-
}
86-
1.0 // Fallback
87-
}
88-
#[cfg(not(target_os = "windows"))]
89-
{
90-
1.0
91-
}
92-
}
93-
9470
// Main loop - wait for commands from channel
9571
loop {
9672
match cmd_rx.recv() {
9773
Ok(Command::Start { grid_size, sample_rate }) => {
9874
eprintln!("Starting sampling: grid_size={}, sample_rate={}", grid_size, sample_rate);
99-
if let Err(e) = run_sampling_loop(&mut *sampler, grid_size, sample_rate, dpi_scale, &cmd_rx) {
75+
if let Err(e) = run_sampling_loop(&mut *sampler, grid_size, sample_rate, &cmd_rx) {
10076
eprintln!("Sampling loop error: {}", e);
10177
send_error(&e);
10278
}
@@ -123,7 +99,6 @@ fn run_sampling_loop(
12399
sampler: &mut dyn PixelSampler,
124100
initial_grid_size: usize,
125101
sample_rate: u64,
126-
dpi_scale: f64,
127102
cmd_rx: &std::sync::mpsc::Receiver<Command>,
128103
) -> Result<(), String> {
129104
use std::sync::mpsc::TryRecvError;
@@ -159,8 +134,8 @@ fn run_sampling_loop(
159134

160135
let loop_start = std::time::Instant::now();
161136

162-
// Get cursor position (returns physical coordinates for Electron window positioning)
163-
let physical_cursor = match sampler.get_cursor_position() {
137+
// Get cursor position (returns logical coordinates, DPI-aware)
138+
let cursor_pos = match sampler.get_cursor_position() {
164139
Ok(pos) => pos,
165140
Err(_e) => {
166141
// On Wayland/some platforms, we can't get cursor position directly
@@ -171,24 +146,18 @@ fn run_sampling_loop(
171146

172147
// Sample every frame regardless of cursor movement for smooth updates
173148
// This ensures the UI is responsive even if cursor position can't be tracked
174-
last_cursor = physical_cursor.clone();
175-
176-
// Convert physical coordinates back to virtual for sampling
177-
// We know dpi_scale is available here since it's declared at function scope
178-
let virtual_cursor = Point {
179-
x: (physical_cursor.x as f64 / dpi_scale) as i32,
180-
y: (physical_cursor.y as f64 / dpi_scale) as i32,
181-
};
149+
last_cursor = cursor_pos.clone();
182150

151+
// Samplers handle DPI internally (like macOS), so pass coordinates directly
183152
// Sample center pixel
184-
let center_color = sampler.sample_pixel(virtual_cursor.x, virtual_cursor.y)
153+
let center_color = sampler.sample_pixel(cursor_pos.x, cursor_pos.y)
185154
.unwrap_or_else(|e| {
186155
eprintln!("Failed to sample center pixel: {}", e);
187156
Color::new(128, 128, 128)
188157
});
189158

190159
// Sample grid
191-
let grid = sampler.sample_grid(virtual_cursor.x, virtual_cursor.y, current_grid_size, 1.0)
160+
let grid = sampler.sample_grid(cursor_pos.x, cursor_pos.y, current_grid_size, 1.0)
192161
.unwrap_or_else(|e| {
193162
eprintln!("Failed to sample grid: {}", e);
194163
vec![vec![Color::new(128, 128, 128); current_grid_size]; current_grid_size]
@@ -201,7 +170,7 @@ fn run_sampling_loop(
201170
.collect();
202171

203172
let pixel_data = PixelData {
204-
cursor: physical_cursor.clone(),
173+
cursor: cursor_pos.clone(),
205174
center: center_color.into(),
206175
grid: grid_data,
207176
timestamp: SystemTime::now()

electron-app/magnifier/rust-sampler/src/sampler/windows.rs

Lines changed: 39 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use windows::Win32::Graphics::Gdi::{
66
GetDeviceCaps, GetDIBits, GetPixel, LOGPIXELSX, ReleaseDC, SelectObject, BITMAPINFO,
77
BITMAPINFOHEADER, BI_RGB, CLR_INVALID, DIB_RGB_COLORS, HDC, SRCCOPY,
88
};
9+
use windows::Win32::UI::HiDpi::{SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2};
910
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
1011

1112
pub struct WindowsSampler {
@@ -16,6 +17,11 @@ pub struct WindowsSampler {
1617
impl WindowsSampler {
1718
pub fn new() -> Result<Self, String> {
1819
unsafe {
20+
// Set DPI awareness to per-monitor v2 so we can access physical pixels
21+
// This must be done before any GDI calls
22+
// Ignore errors - if it fails, we'll fall back to system DPI awareness
23+
let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
24+
1925
let hdc = GetDC(None);
2026

2127
if hdc.is_invalid() {
@@ -27,6 +33,8 @@ impl WindowsSampler {
2733
// Standard DPI is 96, so scale = actual_dpi / 96
2834
let dpi = GetDeviceCaps(hdc, LOGPIXELSX);
2935
let dpi_scale = dpi as f64 / 96.0;
36+
37+
eprintln!("[WindowsSampler] DPI scale factor: {}", dpi_scale);
3038

3139
Ok(WindowsSampler {
3240
hdc,
@@ -47,12 +55,13 @@ impl Drop for WindowsSampler {
4755
impl PixelSampler for WindowsSampler {
4856
fn sample_pixel(&mut self, x: i32, y: i32) -> Result<Color, String> {
4957
unsafe {
50-
// On Windows, for a DPI-unaware process (which this Rust subprocess is):
51-
// - GetCursorPos returns VIRTUALIZED coordinates (e.g., 0-2559 at 200% on 5120 wide screen)
52-
// - GetDC(None) returns a VIRTUALIZED DC that also uses virtual coordinates
53-
// - GetPixel on that DC expects the SAME virtualized coordinates
54-
// NO conversion needed - both APIs work in the same virtualized space
55-
let color_ref = GetPixel(self.hdc, x, y);
58+
// With DPI awareness enabled, follow the macOS pattern:
59+
// - x, y are logical coordinates (like CGWindowListCreateImage on macOS)
60+
// - Convert to physical coordinates internally for GDI
61+
let physical_x = (x as f64 * self.dpi_scale) as i32;
62+
let physical_y = (y as f64 * self.dpi_scale) as i32;
63+
64+
let color_ref = GetPixel(self.hdc, physical_x, physical_y);
5665

5766
// Check for error (CLR_INVALID is returned on error)
5867
// COLORREF is a newtype wrapper around u32
@@ -79,14 +88,16 @@ impl PixelSampler for WindowsSampler {
7988
GetCursorPos(&mut point)
8089
.map_err(|e| format!("Failed to get cursor position: {}", e))?;
8190

82-
// Convert from virtual coordinates (returned by GetCursorPos) to physical coordinates
83-
// Electron (per-monitor DPI aware) expects physical coordinates for window positioning
84-
let physical_x = (point.x as f64 * self.dpi_scale) as i32;
85-
let physical_y = (point.y as f64 * self.dpi_scale) as i32;
86-
91+
// With DPI awareness enabled, follow the macOS pattern:
92+
// - GetCursorPos returns physical coordinates
93+
// - Convert to logical coordinates (like macOS CGEventGetLocation)
94+
// - This matches Electron's coordinate system and main.rs expectations
95+
let logical_x = (point.x as f64 / self.dpi_scale) as i32;
96+
let logical_y = (point.y as f64 / self.dpi_scale) as i32;
97+
8798
Ok(Point {
88-
x: physical_x,
89-
y: physical_y,
99+
x: logical_x,
100+
y: logical_y,
90101
})
91102
}
92103
}
@@ -97,10 +108,14 @@ impl PixelSampler for WindowsSampler {
97108
unsafe {
98109
let half_size = (grid_size / 2) as i32;
99110

100-
// For a DPI-unaware process, all GDI operations use virtualized coordinates
101-
// No conversion needed
102-
let x_start = center_x - half_size;
103-
let y_start = center_y - half_size;
111+
// With DPI awareness enabled, follow the macOS pattern:
112+
// - center_x, center_y are logical coordinates
113+
// - Convert to physical coordinates for GDI operations
114+
let physical_center_x = (center_x as f64 * self.dpi_scale) as i32;
115+
let physical_center_y = (center_y as f64 * self.dpi_scale) as i32;
116+
117+
let x_start = physical_center_x - half_size;
118+
let y_start = physical_center_y - half_size;
104119
let width = grid_size as i32;
105120
let height = grid_size as i32;
106121

@@ -220,13 +235,16 @@ impl WindowsSampler {
220235
let half_size = (grid_size / 2) as i32;
221236
let mut grid = Vec::with_capacity(grid_size);
222237

223-
// For DPI-unaware process, use coordinates directly
238+
// center_x, center_y are logical coordinates - convert to physical
239+
let physical_center_x = (center_x as f64 * self.dpi_scale) as i32;
240+
let physical_center_y = (center_y as f64 * self.dpi_scale) as i32;
241+
224242
for row in 0..grid_size {
225243
let mut row_pixels = Vec::with_capacity(grid_size);
226244
for col in 0..grid_size {
227-
// Calculate pixel coordinates (no conversion needed)
228-
let x = center_x + (col as i32 - half_size);
229-
let y = center_y + (row as i32 - half_size);
245+
// Calculate physical pixel coordinates
246+
let x = physical_center_x + (col as i32 - half_size);
247+
let y = physical_center_y + (row as i32 - half_size);
230248

231249
let color_ref = GetPixel(self.hdc, x, y);
232250

electron-app/magnifier/rust-sampler/tests/windows_sampler_tests.rs

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,9 @@ impl MockWindowsSampler {
3232

3333
impl PixelSampler for MockWindowsSampler {
3434
fn sample_pixel(&mut self, x: i32, y: i32) -> Result<Color, String> {
35-
// Simulate DPI coordinate conversion (virtual -> physical)
36-
// In DPI-aware Electron: GetCursorPos gives virtual, GetPixel expects physical
35+
// With DPI awareness enabled, follow the macOS pattern:
36+
// - x, y are logical coordinates
37+
// - Convert to physical coordinates internally for sampling
3738
let physical_x = (x as f64 * self.dpi_scale) as i32;
3839
let physical_y = (y as f64 * self.dpi_scale) as i32;
3940

@@ -52,32 +53,32 @@ impl PixelSampler for MockWindowsSampler {
5253
}
5354

5455
fn get_cursor_position(&self) -> Result<Point, String> {
55-
// Simulate Windows sampler behavior: return physical coordinates
56-
// (virtual coordinates converted to physical for Electron compatibility)
57-
let virtual_x = 100;
58-
let virtual_y = 100;
59-
let physical_x = (virtual_x as f64 * self.dpi_scale) as i32;
60-
let physical_y = (virtual_y as f64 * self.dpi_scale) as i32;
61-
Ok(Point { x: physical_x, y: physical_y })
56+
// With DPI awareness, GetCursorPos returns physical coordinates
57+
// But we return logical coordinates (physical / dpi_scale) for Electron
58+
let physical_x = 200; // Simulated physical cursor position
59+
let physical_y = 200;
60+
let logical_x = (physical_x as f64 / self.dpi_scale) as i32;
61+
let logical_y = (physical_y as f64 / self.dpi_scale) as i32;
62+
Ok(Point { x: logical_x, y: logical_y })
6263
}
6364

64-
// Override sample_grid to simulate production behavior (virtual coordinates)
65+
// Override sample_grid to simulate production behavior (logical coordinates)
6566
fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result<Vec<Vec<Color>>, String> {
6667
let half_size = (grid_size / 2) as i32;
6768
let mut grid = Vec::with_capacity(grid_size);
6869

69-
// Production sample_grid operates in virtual coordinates (no DPI scaling)
70+
// Production sample_grid operates in logical coordinates like macOS
7071
for row in 0..grid_size {
7172
let mut row_pixels = Vec::with_capacity(grid_size);
7273
for col in 0..grid_size {
73-
// Calculate virtual pixel coordinates (matches production behavior)
74-
let virtual_x = center_x + (col as i32 - half_size);
75-
let virtual_y = center_y + (row as i32 - half_size);
74+
// Calculate logical pixel coordinates (matches production behavior)
75+
let logical_x = center_x + (col as i32 - half_size);
76+
let logical_y = center_y + (row as i32 - half_size);
7677

77-
// Convert virtual to physical for bounds checking and color calculation
78+
// Convert logical to physical for bounds checking and color calculation
7879
// (since screen_width/screen_height are physical dimensions)
79-
let physical_x = (virtual_x as f64 * self.dpi_scale) as i32;
80-
let physical_y = (virtual_y as f64 * self.dpi_scale) as i32;
80+
let physical_x = (logical_x as f64 * self.dpi_scale) as i32;
81+
let physical_y = (logical_y as f64 * self.dpi_scale) as i32;
8182

8283
// Sample in physical space
8384
if physical_x < 0 || physical_y < 0 || physical_x >= self.screen_width || physical_y >= self.screen_height {
@@ -128,8 +129,9 @@ fn test_windows_sampler_cursor_position() {
128129
let sampler = MockWindowsSampler::new(1920, 1080);
129130

130131
let cursor = sampler.get_cursor_position().unwrap();
131-
assert_eq!(cursor.x, 100);
132-
assert_eq!(cursor.y, 100);
132+
// With DPI awareness, physical 200 / scale 1.0 = logical 200
133+
assert_eq!(cursor.x, 200);
134+
assert_eq!(cursor.y, 200);
133135
}
134136

135137
#[test]
@@ -496,10 +498,10 @@ fn test_windows_sampler_dpi_150_percent() {
496498
#[test]
497499
fn test_windows_sampler_dpi_200_percent() {
498500
// Test 200% DPI scaling (2x) - the reported issue
499-
// Physical screen: 5120x2880, Virtual screen: 2560x1440
501+
// Physical screen: 5120x2880, Logical screen: 2560x1440
500502
let mut sampler = MockWindowsSampler::new_with_dpi(5120, 2880, 2.0);
501503

502-
// Virtual coordinate 1000 should map to physical 2000 (1000 * 2.0)
504+
// Logical coordinate 1000 should map to physical 2000 (1000 * 2.0) internally
503505
let color = sampler.sample_pixel(1000, 500).unwrap();
504506

505507
// Color should be based on physical coordinates (2000, 1000)

0 commit comments

Comments
 (0)