Skip to content
Open
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
83 changes: 70 additions & 13 deletions src/capture/scale.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,28 +170,63 @@ fn copy_scaled_visible_rect(
source_required
);
}
if source_stride < source.width as usize * 4 {
bail!(
"source stride {} too small for width {}",
source_stride,
source.width
);
}

let source_w = u64::from(source.width);
let source_h = u64::from(source.height);
let visible_w = u64::from(visible.width);
let visible_h = u64::from(visible.height);

// Area-average box filter: every output pixel averages the source region
// it covers, so downscaling anti-aliases instead of dropping pixels the
// way nearest sampling does (visible as jagged text on 4K -> window-size
// presentations). For upscaling the box degenerates to a single source
// pixel, matching the previous behavior.
for py in visible.y..visible.bottom() {
let rel_y = py - visible.y;
let sy = ((u64::from(rel_y) * u64::from(source.height)) / u64::from(visible.height))
.min(u64::from(source.height - 1)) as usize;
let rel_y = u64::from(py - visible.y);
let sy0 = (rel_y * source_h) / visible_h;
let sy1 = (((rel_y + 1) * source_h) / visible_h)
.max(sy0 + 1)
.min(source_h);
let (sy0, sy1) = (sy0 as usize, sy1 as usize);
for px in visible.x..visible.right() {
let rel_x = px - visible.x;
let sx = ((u64::from(rel_x) * u64::from(source.width)) / u64::from(visible.width))
.min(u64::from(source.width - 1)) as usize;

let source_offset = sy
.checked_mul(source_stride)
.and_then(|row| row.checked_add(sx.checked_mul(4)?))
.context("source pixel offset overflow")?;
let rel_x = u64::from(px - visible.x);
let sx0 = (rel_x * source_w) / visible_w;
let sx1 = (((rel_x + 1) * source_w) / visible_w)
.max(sx0 + 1)
.min(source_w);
let (sx0, sx1) = (sx0 as usize, sx1 as usize);

let mut sum = [0u64; 4];
for sy in sy0..sy1 {
let row = sy * source_stride;
for sx in sx0..sx1 {
let s = row + sx * 4;
sum[0] += u64::from(source_data[s]);
sum[1] += u64::from(source_data[s + 1]);
sum[2] += u64::from(source_data[s + 2]);
sum[3] += u64::from(source_data[s + 3]);
}
}
let count = ((sy1 - sy0) * (sx1 - sx0)) as u64;

let output_offset = usize::try_from(py)
.ok()
.and_then(|row| row.checked_mul(output_stride))
.and_then(|row| row.checked_add(usize::try_from(px).ok()?.checked_mul(4)?))
.context("output pixel offset overflow")?;

output[output_offset..output_offset + 4]
.copy_from_slice(&source_data[source_offset..source_offset + 4]);
let half = count / 2;
output[output_offset] = ((sum[0] + half) / count) as u8;
output[output_offset + 1] = ((sum[1] + half) / count) as u8;
output[output_offset + 2] = ((sum[2] + half) / count) as u8;
output[output_offset + 3] = ((sum[3] + half) / count) as u8;
}
}

Expand Down Expand Up @@ -278,6 +313,28 @@ mod tests {
assert_eq!(&prepared.data[48..52], &[0, 0, 0, 255]);
}

#[test]
fn capture_scale_downscale_averages_covered_source_pixels() {
// 2x1 black+white halves -> 1x1: the area filter must yield mid-gray;
// nearest sampling would return one of the extremes.
let mut source = vec![0u8; 8];
source[0..4].copy_from_slice(&[0, 0, 0, 255]);
source[4..8].copy_from_slice(&[255, 255, 255, 255]);

let prepared = prepare_presentation_frame(
&source,
2,
1,
8,
PixelFormat::BgrA32,
&[(0, 0, 2, 1)],
&snapshot((2, 1), (1, 1)),
)
.expect("scaled frame prepares");

assert_eq!(&prepared.data[0..4], &[128, 128, 128, 255]);
}

#[test]
fn capture_scale_maps_damage_before_frame_processor() {
let source = frame(4, 2, 16);
Expand Down