use super::readback::Readback;
use super::{Background, Compositor, GpuBackend, PresentTarget};
use crate::backends::coverage::{composite_bitmap, RenderBitmap};
use std::sync::Arc;
#[test]
fn gpu_smoke_composites_white_tile() {
let mut backend = match GpuBackend::new(64, 64) {
Ok(backend) => backend,
Err(e) => {
eprintln!("skipping GPU smoke test (no usable adapter): {e}");
return;
}
};
let tile = RenderBitmap::Coverage {
width: 10,
height: 10,
coverage: Arc::new(vec![255u8; 10 * 10]),
x: 5,
y: 5,
color: [255, 255, 255, 255],
};
let out = backend
.composite_bitmaps(std::slice::from_ref(&tile), 64, 64)
.expect("gpu composite");
assert_eq!(out.len(), 64 * 64 * 4);
let at = |x: u32, y: u32| ((y * 64 + x) * 4) as usize;
let center = at(10, 10);
assert!(
out[center] > 250
&& out[center + 1] > 250
&& out[center + 2] > 250
&& out[center + 3] > 250,
"covered pixel should be opaque white, got {:?}",
&out[center..center + 4]
);
let outside = at(0, 0);
assert_eq!(
&out[outside..outside + 4],
&[0, 0, 0, 0],
"pixel outside the tile must stay transparent"
);
let mut reference = vec![0u8; 64 * 64 * 4];
composite_bitmap(&mut reference, 64, 64, &tile);
let max_diff = out
.iter()
.zip(reference.iter())
.map(|(a, b)| a.abs_diff(*b))
.max()
.unwrap_or(0);
assert!(
max_diff <= 2,
"GPU readback should match software compositor (max channel diff {max_diff})"
);
}
fn blended_tiles() -> Vec<RenderBitmap> {
let gradient = |w: u32, h: u32| -> Arc<Vec<u8>> {
let mut data = vec![0u8; (w * h) as usize];
for (i, byte) in data.iter_mut().enumerate() {
*byte = ((i * 7 + 11) % 256) as u8;
}
Arc::new(data)
};
let rgba = {
let mut px = vec![0u8; 6 * 6 * 4];
for chunk in px.chunks_exact_mut(4) {
chunk.copy_from_slice(&[160, 0, 0, 160]);
}
Arc::new(px)
};
vec![
RenderBitmap::Coverage {
width: 20,
height: 20,
coverage: gradient(20, 20),
x: 4,
y: 4,
color: [200, 100, 50, 128],
},
RenderBitmap::Coverage {
width: 20,
height: 20,
coverage: gradient(20, 20),
x: 12,
y: 8,
color: [0, 180, 255, 200],
},
RenderBitmap::Rgba {
width: 6,
height: 6,
pixels: rgba,
x: 30,
y: 10,
},
]
}
#[test]
fn gpu_matches_software_on_blended_tiles() {
let mut backend = match GpuBackend::new(48, 32) {
Ok(backend) => backend,
Err(e) => {
eprintln!("skipping GPU parity test (no usable adapter): {e}");
return;
}
};
let tiles = blended_tiles();
let out = backend
.composite_bitmaps(&tiles, 48, 32)
.expect("gpu composite");
let mut reference = vec![0u8; 48 * 32 * 4];
for tile in &tiles {
composite_bitmap(&mut reference, 48, 32, tile);
}
let mut max_diff = 0u32;
let mut sum_diff = 0u64;
for (a, b) in out.iter().zip(reference.iter()) {
let d = u32::from(a.abs_diff(*b));
max_diff = max_diff.max(d);
sum_diff += u64::from(d);
}
let mae = sum_diff as f64 / out.len() as f64;
eprintln!("GPU vs software: MAE={mae:.4} max_channel_diff={max_diff}");
assert!(
max_diff <= 2,
"GPU blend should match software within rounding (max diff {max_diff}, MAE {mae:.4})"
);
}
#[test]
fn gpu_layer_matches_readback_composite() {
let mut backend = match GpuBackend::new(48, 32) {
Ok(backend) => backend,
Err(e) => {
eprintln!("skipping GPU layer parity test (no usable adapter): {e}");
return;
}
};
let tiles = blended_tiles();
let readback = backend
.composite_bitmaps(&tiles, 48, 32)
.expect("readback composite");
backend
.render_subtitle_layer(&tiles, 48, 32)
.expect("render layer");
let layer = backend.layer_to_bytes().expect("layer readback");
assert_eq!(readback.len(), layer.len());
let max_diff = readback
.iter()
.zip(layer.iter())
.map(|(a, b)| a.abs_diff(*b))
.max()
.unwrap_or(0);
assert_eq!(
max_diff, 0,
"resident layer must hold the same bytes as the readback composite"
);
backend.present_frame(48, 32).expect("present frame");
}
fn headless_device() -> Option<(wgpu::Device, wgpu::Queue)> {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
..Default::default()
});
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))?;
pollster::block_on(adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("ass-gpu-test-device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
},
None,
))
.ok()
}
#[test]
fn present_to_view_targets_external_bgra_surface() {
let Some((device, queue)) = headless_device() else {
eprintln!("skipping present_to_view test (no usable adapter)");
return;
};
let (width, height) = (64u32, 64u32);
let mut compositor = Compositor::new(&device);
let tile = RenderBitmap::Coverage {
width: 10,
height: 10,
coverage: Arc::new(vec![255u8; 10 * 10]),
x: 5,
y: 5,
color: [255, 255, 255, 255],
};
compositor
.render_layer(&device, &queue, std::slice::from_ref(&tile), width, height)
.expect("render layer");
let format = wgpu::TextureFormat::Bgra8Unorm;
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("ass-gpu-test-surface"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&wgpu::TextureViewDescriptor::default());
compositor
.present_to_view(
&device,
&queue,
PresentTarget {
view: &view,
format,
},
Background::Clear(wgpu::Color::BLACK),
width,
height,
)
.expect("present to view");
let readback = Readback::new(&device, width, height);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("ass-gpu-test-readback-encoder"),
});
readback.copy_from(&mut encoder, &target);
queue.submit(Some(encoder.finish()));
let out = readback.read(&device).expect("read external surface back");
assert_eq!(out.len(), (width * height * 4) as usize);
let at = |x: u32, y: u32| ((y * width + x) * 4) as usize;
let center = at(10, 10);
assert!(
out[center] > 250
&& out[center + 1] > 250
&& out[center + 2] > 250
&& out[center + 3] > 250,
"covered pixel should be opaque white over the layer, got {:?}",
&out[center..center + 4]
);
let outside = at(40, 40);
assert!(
out[outside] < 5 && out[outside + 1] < 5 && out[outside + 2] < 5 && out[outside + 3] > 250,
"pixel outside the tile should be the opaque black background, got {:?}",
&out[outside..outside + 4]
);
}