#![cfg(feature = "gpu-render")]
use std::sync::Arc;
use zpdf::display_list::{
BlendMode, Color, DisplayList, FillRule, Paint, Path, RenderCommand, SoftMask, SoftMaskKind,
};
use zpdf::{Rect, RenderBackend};
const SCALE: f32 = 2.0;
const THRESHOLD: u8 = 16;
const MAX_DIFF_PCT: f64 = 1.0;
fn page() -> Rect {
Rect {
x0: 0.0,
y0: 0.0,
x1: 200.0,
y1: 200.0,
}
}
fn rect_path(x0: f64, y0: f64, x1: f64, y1: f64) -> Path {
let mut p = Path::new();
p.rect(Rect { x0, y0, x1, y1 });
p
}
fn fill(x0: f64, y0: f64, x1: f64, y1: f64, c: Color, alpha: f32) -> RenderCommand {
RenderCommand::FillPath {
path: rect_path(x0, y0, x1, y1),
rule: FillRule::NonZero,
paint: Paint::Solid(c),
alpha,
overprint: None,
}
}
fn masked_group(content: RenderCommand, mask: SoftMask) -> Vec<RenderCommand> {
vec![
RenderCommand::PushBlendGroup {
blend_mode: BlendMode::Normal,
isolated: true,
knockout: false,
bounds: page(),
alpha: 1.0,
mask: Some(mask),
},
content,
RenderCommand::PopBlendGroup,
]
}
fn page_with(background: Color, group: Vec<RenderCommand>) -> DisplayList {
let mut dl = DisplayList::new(page());
dl.push(fill(0.0, 0.0, 200.0, 200.0, background, 1.0));
for c in group {
dl.push(c);
}
dl
}
type BackendDiff = (f64, (u32, u32), (u32, u32));
fn compare(dl: &DisplayList) -> Option<BackendDiff> {
let gpu = match zpdf::gpu::WgpuRenderer::new().render_display_list(dl, SCALE) {
Ok(t) => t,
Err(e) => {
eprintln!("skipping GPU soft-mask acceptance (no adapter?): {e}");
return None;
}
};
let cpu = zpdf::cpu::CpuRenderer::new()
.render_display_list(dl, SCALE)
.expect("cpu render");
if (cpu.width, cpu.height) != (gpu.width, gpu.height) {
return Some((100.0, (cpu.width, cpu.height), (gpu.width, gpu.height)));
}
let total = (cpu.width * cpu.height) as u64;
let mut diff = 0u64;
for i in 0..total as usize {
let b = i * 4;
let dr = (gpu.data[b] as i32 - cpu.data[b] as i32).unsigned_abs();
let dg = (gpu.data[b + 1] as i32 - cpu.data[b + 1] as i32).unsigned_abs();
let db = (gpu.data[b + 2] as i32 - cpu.data[b + 2] as i32).unsigned_abs();
if dr.max(dg).max(db) > THRESHOLD as u32 {
diff += 1;
}
}
Some((
diff as f64 / total as f64 * 100.0,
(cpu.width, cpu.height),
(gpu.width, gpu.height),
))
}
fn assert_match(name: &str, dl: &DisplayList) {
match compare(dl) {
None => eprintln!("GPU soft-mask `{name}` skipped (no adapter)."),
Some((pct, cdim, gdim)) => {
assert_eq!(
cdim, gdim,
"{name}: dimension mismatch {cdim:?} vs {gdim:?}"
);
println!(" softmask {name}: {pct:.3}% differing");
assert!(
pct < MAX_DIFF_PCT,
"{name}: GPU vs CPU {pct:.3}% exceeds {MAX_DIFF_PCT}%"
);
}
}
}
#[test]
fn gpu_matches_cpu_softmask_transfer() {
let mask_dl = {
let mut dl = DisplayList::new(page());
dl.push(fill(
40.0,
40.0,
160.0,
160.0,
Color::rgb(0.25, 0.25, 0.25),
1.0,
));
dl
};
let mut lut = [0u8; 256];
for (i, v) in lut.iter_mut().enumerate() {
*v = 255 - i as u8;
}
let mask = SoftMask {
kind: SoftMaskKind::Luminosity,
commands: Arc::new(mask_dl),
offset: (0.0, 0.0),
backdrop_luma: 0.0,
transfer: Some(Arc::new(lut)),
};
let group = masked_group(
fill(40.0, 40.0, 160.0, 160.0, Color::rgb(0.0, 0.0, 1.0), 1.0),
mask,
);
assert_match("transfer", &page_with(Color::rgb(1.0, 1.0, 0.0), group));
}
#[test]
fn gpu_matches_cpu_softmask_offset() {
let mask_dl = {
let mut dl = DisplayList::new(page());
dl.push(fill(20.0, 20.0, 100.0, 100.0, Color::white(), 1.0));
dl
};
let mask = SoftMask {
kind: SoftMaskKind::Luminosity,
commands: Arc::new(mask_dl),
offset: (40.0, 40.0),
backdrop_luma: 0.0,
transfer: None,
};
let group = masked_group(
fill(20.0, 20.0, 180.0, 180.0, Color::rgb(0.0, 0.0, 1.0), 1.0),
mask,
);
assert_match("offset", &page_with(Color::rgb(1.0, 1.0, 0.0), group));
}
#[test]
fn gpu_matches_cpu_softmask_nested_group() {
let mask_dl = {
let mut dl = DisplayList::new(page());
dl.push(RenderCommand::PushBlendGroup {
blend_mode: BlendMode::Normal,
isolated: true,
knockout: false,
bounds: page(),
alpha: 0.6,
mask: None,
});
dl.push(fill(30.0, 30.0, 170.0, 170.0, Color::white(), 1.0));
dl.push(RenderCommand::PopBlendGroup);
dl
};
let mask = SoftMask {
kind: SoftMaskKind::Luminosity,
commands: Arc::new(mask_dl),
offset: (0.0, 0.0),
backdrop_luma: 0.0,
transfer: None,
};
let group = masked_group(
fill(30.0, 30.0, 170.0, 170.0, Color::rgb(1.0, 0.0, 0.0), 1.0),
mask,
);
assert_match("nested_group", &page_with(Color::rgb(1.0, 1.0, 0.0), group));
}
#[test]
fn gpu_matches_cpu_softmask_alpha_transfer() {
let mask_dl = {
let mut dl = DisplayList::new(page());
dl.push(fill(50.0, 50.0, 150.0, 150.0, Color::white(), 0.5));
dl
};
let mut lut = [0u8; 256];
for (i, v) in lut.iter_mut().enumerate() {
let x = i as f32 / 255.0;
*v = (x * x * 255.0).round() as u8;
}
let mask = SoftMask {
kind: SoftMaskKind::Alpha,
commands: Arc::new(mask_dl),
offset: (0.0, 0.0),
backdrop_luma: 0.0,
transfer: Some(Arc::new(lut)),
};
let group = masked_group(
fill(50.0, 50.0, 150.0, 150.0, Color::rgb(0.0, 0.0, 1.0), 1.0),
mask,
);
assert_match(
"alpha_transfer",
&page_with(Color::rgb(1.0, 1.0, 0.0), group),
);
}
#[test]
fn gpu_matches_cpu_softmask_colored_luminosity() {
let mask_dl = {
let mut dl = DisplayList::new(page());
dl.push(fill(
40.0,
40.0,
160.0,
160.0,
Color::rgb(0.0, 0.0, 1.0),
1.0,
));
dl
};
let mut lut = [0u8; 256];
for (i, v) in lut.iter_mut().enumerate() {
*v = if i >= 29 { 255 } else { 0 };
}
let mask = SoftMask {
kind: SoftMaskKind::Luminosity,
commands: Arc::new(mask_dl),
offset: (0.0, 0.0),
backdrop_luma: 0.0,
transfer: Some(Arc::new(lut)),
};
let group = masked_group(
fill(40.0, 40.0, 160.0, 160.0, Color::rgb(1.0, 0.0, 0.0), 1.0),
mask,
);
assert_match(
"colored_luminosity",
&page_with(Color::rgb(1.0, 1.0, 0.0), group),
);
}
#[test]
fn gpu_matches_cpu_softmask_mask_in_mask() {
let inner_dl = {
let mut dl = DisplayList::new(page());
dl.push(fill(30.0, 30.0, 100.0, 100.0, Color::white(), 1.0));
dl
};
let inner = SoftMask {
kind: SoftMaskKind::Luminosity,
commands: Arc::new(inner_dl),
offset: (0.0, 0.0),
backdrop_luma: 0.0,
transfer: None,
};
let outer_dl = {
let mut dl = DisplayList::new(page());
dl.push(RenderCommand::PushBlendGroup {
blend_mode: BlendMode::Normal,
isolated: true,
knockout: false,
bounds: page(),
alpha: 1.0,
mask: Some(inner),
});
dl.push(fill(30.0, 30.0, 170.0, 170.0, Color::white(), 1.0));
dl.push(RenderCommand::PopBlendGroup);
dl
};
let outer = SoftMask {
kind: SoftMaskKind::Luminosity,
commands: Arc::new(outer_dl),
offset: (0.0, 0.0),
backdrop_luma: 0.0,
transfer: None,
};
let group = masked_group(
fill(30.0, 30.0, 170.0, 170.0, Color::rgb(1.0, 0.0, 0.0), 1.0),
outer,
);
assert_match("mask_in_mask", &page_with(Color::rgb(1.0, 1.0, 0.0), group));
}