use std::sync::Arc;
use agg_gui::color::Color;
use agg_gui::draw_ctx::DrawCtx;
use agg_gui::text::Font;
use crate::WgpuGfxCtx;
const TEST_FONT: &[u8] = include_bytes!("../assets/fonts/NotoSans-Regular.ttf");
pub(crate) fn try_device() -> Option<(Arc<wgpu::Device>, Arc<wgpu::Queue>)> {
let mut desc = wgpu::InstanceDescriptor::new_without_display_handle();
desc.backends = wgpu::Backends::all();
let instance = wgpu::Instance::new(desc);
let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
compatible_surface: None,
force_fallback_adapter: false,
}))
.ok()?;
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
label: Some("readback-test"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::Performance,
experimental_features: wgpu::ExperimentalFeatures::default(),
trace: wgpu::Trace::Off,
}))
.ok()?;
Some((Arc::new(device), Arc::new(queue)))
}
pub(crate) struct Target {
device: Arc<wgpu::Device>,
queue: Arc<wgpu::Queue>,
texture: wgpu::Texture,
pub(crate) view: wgpu::TextureView,
w: u32,
h: u32,
}
impl Target {
pub(crate) fn new(device: Arc<wgpu::Device>, queue: Arc<wgpu::Queue>, w: u32, h: u32) -> Self {
assert_eq!(
(w * 4) % 256,
0,
"width must keep bytes_per_row 256-aligned"
);
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("readback-target"),
size: wgpu::Extent3d {
width: w,
height: h,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
Self {
device,
queue,
texture,
view,
w,
h,
}
}
pub(crate) fn read(&self) -> Vec<u8> {
let bpr = self.w * 4;
let buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
label: Some("readback-buf"),
size: (bpr * self.h) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let mut enc = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
enc.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &self.texture,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &buffer,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(bpr),
rows_per_image: Some(self.h),
},
},
wgpu::Extent3d {
width: self.w,
height: self.h,
depth_or_array_layers: 1,
},
);
self.queue.submit(std::iter::once(enc.finish()));
let slice = buffer.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |r| {
let _ = tx.send(r);
});
let _ = self.device.poll(wgpu::PollType::wait_indefinitely());
rx.recv().unwrap().unwrap();
let data = slice.get_mapped_range().to_vec();
buffer.unmap();
data
}
}
pub(crate) fn px(data: &[u8], w: u32, x: u32, y: u32) -> [u8; 4] {
let i = ((y * w + x) * 4) as usize;
[data[i], data[i + 1], data[i + 2], data[i + 3]]
}
fn font() -> Arc<Font> {
Arc::new(Font::from_slice(TEST_FONT).unwrap())
}
#[test]
fn lcd_text_in_alpha_layer_is_gray_not_washed_out() {
let Some((device, queue)) = try_device() else {
eprintln!("SKIP: no GPU adapter");
return;
};
let (w, h) = (64u32, 32u32);
let target = Target::new(Arc::clone(&device), Arc::clone(&queue), w, h);
let mut ctx = WgpuGfxCtx::new(
Arc::clone(&device),
Arc::clone(&queue),
wgpu::TextureFormat::Rgba8Unorm,
w as f32,
h as f32,
);
ctx.reset(w as f32, h as f32);
ctx.set_lcd_mode(true);
ctx.clear(Color::white());
ctx.set_font(font());
ctx.set_font_size(22.0);
ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, 1.0));
ctx.push_layer_with_alpha(w as f64, h as f64, 0.5);
ctx.fill_text("HHHH", 4.0, 10.0);
ctx.pop_layer();
ctx.flush_to_surface(&target.view);
let data = target.read();
let mut darkest = u16::MAX;
let mut darkest_px = [255u8; 4];
for y in 0..h {
for x in 0..w {
let p = px(&data, w, x, y);
let lum = p[0] as u16 + p[1] as u16 + p[2] as u16;
if lum < darkest {
darkest = lum;
darkest_px = p;
}
}
}
let [r, g, b, _a] = darkest_px;
assert!(
(90..=165).contains(&r),
"darkest text pixel should be mid-gray (~127); got {darkest_px:?}"
);
let spread = r.max(g).max(b) - r.min(g).min(b);
assert!(
spread <= 24,
"text inside a layer must be grayscale (no fringing); channel spread \
{spread} for {darkest_px:?}"
);
let corner = px(&data, w, w - 1, 0);
assert!(
corner[0] > 230 && corner[1] > 230 && corner[2] > 230,
"background corner must stay white; got {corner:?}"
);
}
#[test]
fn lcd_backbuffer_in_alpha_layer_writes_alpha() {
let Some((device, queue)) = try_device() else {
eprintln!("SKIP: no GPU adapter");
return;
};
let (w, h) = (64u32, 32u32);
let target = Target::new(Arc::clone(&device), Arc::clone(&queue), w, h);
let mut ctx = WgpuGfxCtx::new(
Arc::clone(&device),
Arc::clone(&queue),
wgpu::TextureFormat::Rgba8Unorm,
w as f32,
h as f32,
);
ctx.reset(w as f32, h as f32);
let (pw, ph) = (8u32, 8u32);
let mut color = Vec::with_capacity((pw * ph * 3) as usize);
let mut alpha = Vec::with_capacity((pw * ph * 3) as usize);
for _ in 0..(pw * ph) {
color.extend_from_slice(&[0, 0, 0]);
alpha.extend_from_slice(&[255, 255, 255]);
}
let color = Arc::new(color);
let alpha = Arc::new(alpha);
ctx.clear(Color::white());
ctx.push_layer_with_alpha(w as f64, h as f64, 0.5);
ctx.draw_lcd_backbuffer_arc(&color, &alpha, 0, pw, ph, 4.0, 4.0, pw as f64, ph as f64);
ctx.pop_layer();
ctx.flush_to_surface(&target.view);
let data = target.read();
let mut darkest = u16::MAX;
let mut darkest_px = [255u8; 4];
for y in 0..h {
for x in 0..w {
let p = px(&data, w, x, y);
let lum = p[0] as u16 + p[1] as u16 + p[2] as u16;
if lum < darkest {
darkest = lum;
darkest_px = p;
}
}
}
let [r, g, b, _a] = darkest_px;
assert!(
(90..=165).contains(&r),
"covered black backbuffer in a 0.5 layer must be mid-gray (~127), not \
washed out; got {darkest_px:?}"
);
let spread = r.max(g).max(b) - r.min(g).min(b);
assert!(
spread <= 12,
"flattened backbuffer must be gray; got {darkest_px:?}"
);
}
fn rec709(r: f64, g: f64, b: f64) -> f64 {
0.2126 * r + 0.7152 * g + 0.0722 * b
}
const PATCHES: [[u8; 3]; 2] = [[85, 200, 85], [50, 120, 230]];
#[test]
fn flattened_lcd_backbuffer_luminance_inside_layer() {
let Some((device, queue)) = try_device() else {
eprintln!("SKIP: no GPU adapter");
return;
};
let (w, h) = (64u32, 32u32);
let (pw, ph) = (8u32, 8u32);
let arms: [(&str, f64, Color); 2] = [
("dark-on-light", 0.1, Color::white()),
(
"light-on-dark",
0.9,
Color::rgba(20.0 / 255.0, 20.0 / 255.0, 20.0 / 255.0, 1.0),
),
];
for (arm, level, bg) in arms {
let target = Target::new(Arc::clone(&device), Arc::clone(&queue), w, h);
let mut ctx = WgpuGfxCtx::new(
Arc::clone(&device),
Arc::clone(&queue),
wgpu::TextureFormat::Rgba8Unorm,
w as f32,
h as f32,
);
ctx.reset(w as f32, h as f32);
let planes: Vec<(Arc<Vec<u8>>, Arc<Vec<u8>>)> = PATCHES
.iter()
.map(|cov| {
let premult: [u8; 3] = [
(cov[0] as f64 * level).round() as u8,
(cov[1] as f64 * level).round() as u8,
(cov[2] as f64 * level).round() as u8,
];
let mut color = Vec::with_capacity((pw * ph * 3) as usize);
let mut alpha = Vec::with_capacity((pw * ph * 3) as usize);
for _ in 0..(pw * ph) {
color.extend_from_slice(&premult);
alpha.extend_from_slice(cov);
}
(Arc::new(color), Arc::new(alpha))
})
.collect();
ctx.clear(bg);
ctx.push_layer_with_alpha(w as f64, h as f64, 1.0);
for (i, (color, alpha)) in planes.iter().enumerate() {
let x = 4.0 + (i as f64) * 24.0;
ctx.draw_lcd_backbuffer_arc(
color,
alpha,
(i + 1) as u64,
pw,
ph,
x,
8.0,
pw as f64,
ph as f64,
);
}
ctx.pop_layer();
ctx.flush_to_surface(&target.view);
let data = target.read();
let dst = (bg.r as f64 * 255.0).round();
for (i, cov) in PATCHES.iter().enumerate() {
let sx = 4 + (i as u32) * 24 + pw / 2;
let sy = h - 12;
let p = px(&data, w, sx, sy);
let mut expect = [0.0f64; 3];
for c in 0..3 {
let a = cov[c] as f64 / 255.0;
expect[c] = (cov[c] as f64 * level).round() + dst * (1.0 - a);
}
let exp_lum = rec709(expect[0], expect[1], expect[2]);
let act_lum = rec709(p[0] as f64, p[1] as f64, p[2] as f64);
assert!(
(act_lum - exp_lum).abs() <= 3.0,
"{arm} coverage {cov:?}: flattened luminance {act_lum:.2} != \
per-channel luminance {exp_lum:.2}; read back {p:?}, \
per-channel {expect:?}"
);
}
}
}
#[test]
fn pop_layer_composite_respects_parent_clip_wgpu() {
let Some((device, queue)) = try_device() else {
eprintln!("SKIP: no GPU adapter");
return;
};
let (w, h) = (64u32, 32u32);
let target = Target::new(Arc::clone(&device), Arc::clone(&queue), w, h);
let mut ctx = WgpuGfxCtx::new(
Arc::clone(&device),
Arc::clone(&queue),
wgpu::TextureFormat::Rgba8Unorm,
w as f32,
h as f32,
);
ctx.reset(w as f32, h as f32);
ctx.clear(Color::rgba(0.0, 0.0, 0.0, 1.0)); ctx.clip_rect(0.0, 0.0, (w / 2) as f64, h as f64); ctx.push_layer_with_alpha(w as f64, h as f64, 1.0);
ctx.set_fill_color(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, w as f64, h as f64);
ctx.fill();
ctx.pop_layer();
ctx.flush_to_surface(&target.view);
let data = target.read();
let left = px(&data, w, w / 4, h / 2);
assert!(
left[0] > 200 && left[1] < 60 && left[2] < 60,
"inside the parent clip must be red; got {left:?}"
);
let right = px(&data, w, 3 * w / 4, h / 2);
assert!(
right[0] < 40 && right[1] < 40 && right[2] < 40,
"outside the parent clip must stay black background; got {right:?}"
);
}
#[test]
fn lcd_text_in_opaque_backdrop_layer_stays_subpixel() {
let Some((device, queue)) = try_device() else {
eprintln!("SKIP: no GPU adapter");
return;
};
let (w, h) = (64u32, 32u32);
let target = Target::new(Arc::clone(&device), Arc::clone(&queue), w, h);
let mut ctx = WgpuGfxCtx::new(
Arc::clone(&device),
Arc::clone(&queue),
wgpu::TextureFormat::Rgba8Unorm,
w as f32,
h as f32,
);
ctx.reset(w as f32, h as f32);
ctx.set_lcd_mode(true);
ctx.clear(Color::white());
ctx.set_font(font());
ctx.set_font_size(22.0);
ctx.push_layer_with_alpha(w as f64, h as f64, 1.0);
ctx.set_fill_color(Color::white());
ctx.begin_path();
ctx.rect(0.0, 0.0, w as f64, h as f64);
ctx.fill();
ctx.set_layer_opaque_backdrop(true);
ctx.set_fill_color(Color::rgba(0.0, 0.0, 0.0, 1.0));
ctx.fill_text("HHHH", 4.0, 10.0);
ctx.pop_layer();
ctx.flush_to_surface(&target.view);
let data = target.read();
let mut max_spread = 0u8;
let mut darkest = u16::MAX;
for y in 0..h {
for x in 0..w {
let p = px(&data, w, x, y);
let lum = p[0] as u16 + p[1] as u16 + p[2] as u16;
darkest = darkest.min(lum);
let spread = p[0].max(p[1]).max(p[2]) - p[0].min(p[1]).min(p[2]);
max_spread = max_spread.max(spread);
}
}
assert!(
max_spread > 24,
"text over an opaque layer backdrop must render subpixel (per-channel \
coverage); max channel spread was only {max_spread}, i.e. grayscale"
);
assert!(
darkest < 200,
"text should still be dark over the opaque backdrop; darkest luminance \
sum was {darkest}"
);
let corner = px(&data, w, w - 1, 0);
assert!(
corner[0] > 230 && corner[1] > 230 && corner[2] > 230,
"background corner must stay white; got {corner:?}"
);
}