Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs/PERFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ Screen capture APIs have inherent limitations:

### Windows

- **Expected**: 20-30 FPS
- **Bottleneck**: GDI GetPixel API for grid sampling
- **Note**: Not yet tested in production
- **Current**: 15-20 FPS (optimized with BitBlt)
- **Previous**: < 1 FPS (~5+ seconds per frame with GetPixel)
- **Bottleneck**: GDI BitBlt screen capture speed
- **Optimization**: Uses single BitBlt call for entire grid instead of 81 individual GetPixel calls
- **Performance gain**: ~100x improvement over naive GetPixel approach

### Linux (Wayland)

Expand Down
4 changes: 2 additions & 2 deletions docs/PIXEL_SAMPLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ A standalone Rust binary that handles pixel sampling:

**Platform Implementations:**

- **macOS**: Uses Core Graphics for direct pixel access
- **Windows**: Uses GDI GetPixel API
- **macOS**: Uses Core Graphics CGWindowListCreateImage for optimized batch capture
- **Windows**: Uses GDI BitBlt for grid capture, GetPixel for single pixels
- **Linux (Wayland)**: Uses `grim` for screenshots, caches for performance
- **Linux (X11)**: Uses ImageMagick or scrot for screenshots

Expand Down
168 changes: 164 additions & 4 deletions electron-app/magnifier/rust-sampler/src/sampler/windows.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
use crate::types::{Color, PixelSampler, Point};
use std::mem;
use windows::Win32::Foundation::POINT;
use windows::Win32::Graphics::Gdi::{GetDC, GetPixel, ReleaseDC, HDC, CLR_INVALID};
use windows::Win32::UI::WindowsAndMessaging::GetCursorPos;
use windows::Win32::Graphics::Gdi::{
BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC,
GetDIBits, GetPixel, ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, BI_RGB,
CLR_INVALID, DIB_RGB_COLORS, HDC, SRCCOPY,
};
use windows::Win32::UI::WindowsAndMessaging::{GetCursorPos, GetSystemMetrics, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN};

pub struct WindowsSampler {
hdc: HDC,
screen_width: i32,
screen_height: i32,
}

impl WindowsSampler {
Expand All @@ -16,9 +23,17 @@ impl WindowsSampler {
return Err("Failed to get device context".to_string());
}

eprintln!("Windows sampler initialized");
// Get virtual screen dimensions (supports multi-monitor)
let screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
let screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);

Ok(WindowsSampler { hdc })
eprintln!("Windows sampler initialized ({}x{})", screen_width, screen_height);

Ok(WindowsSampler {
hdc,
screen_width,
screen_height,
})
}
}
}
Expand Down Expand Up @@ -67,4 +82,149 @@ impl PixelSampler for WindowsSampler {
})
}
}

// Optimized grid sampling using BitBlt for batch capture
// This is ~100x faster than calling GetPixel 81 times (for 9x9 grid)
fn sample_grid(&mut self, center_x: i32, center_y: i32, grid_size: usize, _scale_factor: f64) -> Result<Vec<Vec<Color>>, String> {
unsafe {
let half_size = (grid_size / 2) as i32;

// Calculate capture region
let x_start = center_x - half_size;
let y_start = center_y - half_size;
let width = grid_size as i32;
let height = grid_size as i32;

// Create memory DC compatible with screen DC
let mem_dc = CreateCompatibleDC(self.hdc);
if mem_dc.is_invalid() {
return Err("Failed to create compatible DC".to_string());
}

// Create compatible bitmap
let bitmap = CreateCompatibleBitmap(self.hdc, width, height);
if bitmap.is_invalid() {
let _ = DeleteDC(mem_dc);
return Err("Failed to create compatible bitmap".to_string());
}

// Select bitmap into memory DC
let old_bitmap = SelectObject(mem_dc, bitmap);

// Copy screen region to memory bitmap using BitBlt
// This is the key optimization - ONE API call instead of grid_size^2 calls
if let Err(_) = BitBlt(
mem_dc,
0,
0,
width,
height,
self.hdc,
x_start,
y_start,
SRCCOPY,
) {
// BitBlt failed - clean up and fall back to default implementation
SelectObject(mem_dc, old_bitmap);
let _ = DeleteObject(bitmap);
let _ = DeleteDC(mem_dc);

eprintln!("BitBlt failed, falling back to pixel-by-pixel sampling");
return self.sample_grid_fallback(center_x, center_y, grid_size);
}

// Prepare bitmap info for GetDIBits
let mut bmi = BITMAPINFO {
bmiHeader: BITMAPINFOHEADER {
biSize: mem::size_of::<BITMAPINFOHEADER>() as u32,
biWidth: width,
biHeight: -height, // Negative for top-down DIB
biPlanes: 1,
biBitCount: 32, // 32-bit BGRA
biCompression: BI_RGB.0 as u32,
biSizeImage: 0,
biXPelsPerMeter: 0,
biYPelsPerMeter: 0,
biClrUsed: 0,
biClrImportant: 0,
},
bmiColors: [Default::default(); 1],
};

// Allocate buffer for pixel data (4 bytes per pixel: BGRA)
let buffer_size = (width * height * 4) as usize;
let mut buffer: Vec<u8> = vec![0; buffer_size];
Comment on lines +155 to +156
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for calls to sample_grid to verify grid_size bounds checking
rg -nP --type=rust -C3 'sample_grid\s*\(' -g '!**/target/**'

Repository: shipshapecode/swach

Length of output: 32979


🏁 Script executed:

# Get context around the sample_grid call in main.rs
sed -n '140,180p' electron-app/magnifier/rust-sampler/src/main.rs

Repository: shipshapecode/swach

Length of output: 1566


🏁 Script executed:

# Search for current_grid_size initialization in main.rs
rg -n 'current_grid_size' electron-app/magnifier/rust-sampler/src/main.rs -B3 -A3

Repository: shipshapecode/swach

Length of output: 989


🏁 Script executed:

# Find initial_grid_size and Command definition
rg -n 'initial_grid_size\|enum Command' electron-app/magnifier/rust-sampler/src/main.rs -B2 -A2

Repository: shipshapecode/swach

Length of output: 45


🏁 Script executed:

# Search for initial_grid_size and Command in all Rust files
rg -n 'initial_grid_size' electron-app/magnifier/rust-sampler/src/ --type=rust

Repository: shipshapecode/swach

Length of output: 247


🏁 Script executed:

# Find the function/struct that contains initial_grid_size
sed -n '95,105p' electron-app/magnifier/rust-sampler/src/main.rs

Repository: shipshapecode/swach

Length of output: 305


🏁 Script executed:

# Find callers of run_sampling_loop
rg -n 'run_sampling_loop' electron-app/magnifier/rust-sampler/src/main.rs -B2 -A2

Repository: shipshapecode/swach

Length of output: 549


🏁 Script executed:

# Find Command enum definition and validation
rg -n 'enum Command|Command::' electron-app/magnifier/rust-sampler/src/main.rs -B1 -A1

Repository: shipshapecode/swach

Length of output: 1274


🏁 Script executed:

# Find Command enum definition
rg -n 'enum Command\s*{' electron-app/magnifier/rust-sampler/src/ -A5

Repository: shipshapecode/swach

Length of output: 158


🏁 Script executed:

# Find Command enum definition
rg -n 'enum Command' electron-app/magnifier/rust-sampler/src/ -A8

Repository: shipshapecode/swach

Length of output: 731


🏁 Script executed:

# Find where Command is parsed/deserialized from input
rg -n 'serde_json::from_str\|from_reader' electron-app/magnifier/rust-sampler/src/main.rs -B3 -A3

Repository: shipshapecode/swach

Length of output: 45


🏁 Script executed:

# Look at the stdin thread that reads commands
sed -n '35,65p' electron-app/magnifier/rust-sampler/src/main.rs

Repository: shipshapecode/swach

Length of output: 1243


Add bounds validation for grid_size parameter.

The buffer allocation (width * height * 4) bytes receives grid_size directly from user input (via JSON commands at src/main.rs:51) with no validation. A caller could send an arbitrarily large grid_size (e.g., 10000+) causing hundreds of megabytes of allocation per frame. Validate that callers enforce reasonable bounds (tests use up to 21×21; suggest enforcing a maximum like 25×25 or similar).


// Get bitmap bits
let scan_lines = GetDIBits(
mem_dc,
bitmap,
0,
height as u32,
Some(buffer.as_mut_ptr() as *mut _),
&mut bmi,
DIB_RGB_COLORS,
);

// Clean up GDI resources
SelectObject(mem_dc, old_bitmap);
let _ = DeleteObject(bitmap);
let _ = DeleteDC(mem_dc);

if scan_lines == 0 {
eprintln!("GetDIBits failed, falling back to pixel-by-pixel sampling");
return self.sample_grid_fallback(center_x, center_y, grid_size);
}

// Parse buffer and build grid
let mut grid = Vec::with_capacity(grid_size);

for row in 0..grid_size {
let mut row_pixels = Vec::with_capacity(grid_size);
for col in 0..grid_size {
// Calculate offset in buffer (BGRA format, 4 bytes per pixel)
let offset = ((row * grid_size + col) * 4) as usize;

if offset + 3 < buffer.len() {
// Windows DIB format is BGRA
let b = buffer[offset];
let g = buffer[offset + 1];
let r = buffer[offset + 2];
// Alpha channel at offset + 3 is ignored

row_pixels.push(Color::new(r, g, b));
} else {
// Fallback for out-of-bounds
row_pixels.push(Color::new(128, 128, 128));
}
}
grid.push(row_pixels);
}

Ok(grid)
}
}
}

impl WindowsSampler {
// Fallback to default pixel-by-pixel sampling if BitBlt fails
fn sample_grid_fallback(&mut self, center_x: i32, center_y: i32, grid_size: usize) -> Result<Vec<Vec<Color>>, String> {
let half_size = (grid_size / 2) as i32;
let mut grid = Vec::with_capacity(grid_size);

for row in 0..grid_size {
let mut row_pixels = Vec::with_capacity(grid_size);
for col in 0..grid_size {
let x = center_x + (col as i32 - half_size);
let y = center_y + (row as i32 - half_size);

let color = self.sample_pixel(x, y)
.unwrap_or(Color::new(128, 128, 128)); // Gray fallback
row_pixels.push(color);
}
grid.push(row_pixels);
}

Ok(grid)
}
}
Loading