use frust_render::{HeadlessOptions, HeadlessRenderer, HeadlessSpec};
use frust_scene::{Scene, SceneBuilder};
use peniko::Brush;
use peniko::color::palette::css::{BLACK, BLUE, GREEN, RED};
const SIZE: u32 = 64;
async fn headless() -> HeadlessRenderer {
let renderer = HeadlessRenderer::new(HeadlessOptions::default())
.await
.expect("failed to create the headless renderer");
println!("frust gpu_smoke: {}", renderer.meta());
renderer
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-render -- --ignored`"]
fn red_fill_rect_produces_non_zero_pixels() {
pollster::block_on(run());
}
async fn run() {
let mut headless = headless().await;
let mut fk_scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut fk_scene);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64),
Brush::Solid(RED),
);
}
let image = headless
.render(&fk_scene, &HeadlessSpec::new(SIZE, SIZE))
.await
.expect("headless render failed");
let non_zero = image.rgba8.iter().any(|&b| b != 0);
assert!(non_zero, "expected non-zero pixels from a red FillRect");
let [r, g, b, _] = image.pixel(SIZE / 2, SIZE / 2);
assert!(
r > g && r > b,
"centre pixel should be red-dominant, got rgb=({r},{g},{b})"
);
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-render -- --ignored`"]
fn clear_rect_punches_pixel_exact_through_a_layer_group() {
pollster::block_on(run_clear_probe());
}
async fn run_clear_probe() {
let mut headless = headless().await;
let punch_edge = (SIZE / 2 + 1) as f64;
let mut fk_scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut fk_scene);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64),
Brush::Solid(RED),
);
builder.push_layer(kurbo::Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64), 1.0);
builder.clear_rect(kurbo::Rect::new(0.0, 0.0, punch_edge, SIZE as f64));
builder.pop_layer();
}
let image = headless
.render(
&fk_scene,
&HeadlessSpec {
base_color: peniko::color::palette::css::TRANSPARENT,
..HeadlessSpec::new(SIZE, SIZE)
},
)
.await
.expect("headless render failed");
let px = |x: u32| {
let [r, g, b, a] = image.pixel(x, SIZE / 2);
(r, g, b, a)
};
assert_eq!(px(SIZE / 4), (0, 0, 0, 0), "punch centre must be erased");
assert_eq!(
px(SIZE / 2),
(0, 0, 0, 0),
"last px inside the unaligned edge must be erased"
);
for x in [SIZE / 2 + 1, SIZE / 2 + 4, SIZE / 2 + 14] {
let (r, _, _, a) = px(x);
assert!(
r > 200 && a > 200,
"backdrop at x={x} (same tile as the punch edge) must stay opaque red, got {:?}",
px(x)
);
}
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-render -- --ignored`"]
fn snapshot_bracket_inline_emulation_matches_the_composite_arithmetic() {
pollster::block_on(run_snapshot());
}
const SNAPSHOT_TOLERANCE: u8 = 3;
async fn run_snapshot() {
const ALPHA: f32 = 0.75;
let bracket = kurbo::Affine::translate((8.0, 8.0)) * kurbo::Affine::scale(2.0);
let rect = kurbo::Rect::new(0.0, 0.0, 20.0, 20.0);
let overlay = peniko::Color::from_rgba8(255, 255, 0, 255);
let mut fk_scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut fk_scene);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64),
Brush::Solid(BLACK),
);
builder.fill_rect(kurbo::Rect::new(52.0, 52.0, 60.0, 60.0), Brush::Solid(BLUE));
builder.push_transform(bracket);
builder.push_snapshot(1, rect, ALPHA, 1.0);
builder.fill_rect(kurbo::Rect::new(0.0, 0.0, 20.0, 10.0), Brush::Solid(RED));
builder.fill_rect(
kurbo::Rect::new(0.0, 10.0, 20.0, 15.0),
Brush::Solid(GREEN.with_alpha(0.5)),
);
builder.fill_rect(kurbo::Rect::new(5.0, 15.0, 15.0, 20.0), Brush::Solid(BLACK));
builder.pop_snapshot();
builder.pop_transform();
builder.fill_rect(
kurbo::Rect::new(20.0, 20.0, 30.0, 30.0),
Brush::Solid(overlay),
);
}
let mut headless = headless().await;
let image = headless
.render(&fk_scene, &HeadlessSpec::new(SIZE, SIZE))
.await
.expect("headless render failed");
let red = image.pixel(40, 12);
assert!(
red[0].abs_diff(191) <= SNAPSHOT_TOLERANCE,
"the bracket's opaque block must composite at its alpha, got {red:?}"
);
let green = image.pixel(40, 32);
assert!(
green[1].abs_diff(48) <= SNAPSHOT_TOLERANCE,
"the bracket's half-alpha band must composite at 128 * 0.5 * 0.75, got {green:?}"
);
assert_eq!(
image.pixel(24, 24),
[255, 255, 0, 255],
"a command recorded after the bracket must land on top of it"
);
let marker = image.pixel(56, 56);
assert!(
marker[2] > 200,
"content recorded before the bracket must survive, got {marker:?}"
);
}
#[test]
#[ignore = "requires a GPU; run locally with `cargo test -p frust-render -- --ignored`"]
fn unaligned_width_reads_back_without_row_padding() {
pollster::block_on(run_row_padding());
}
async fn run_row_padding() {
const PAD_WIDTH: u32 = 97;
const PAD_HEIGHT: u32 = 33;
const EDGE: u32 = 48;
let mut headless = headless().await;
let mut fk_scene = Scene::new();
{
let mut builder = SceneBuilder::new(&mut fk_scene);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, EDGE as f64, PAD_HEIGHT as f64),
Brush::Solid(RED),
);
}
let image = headless
.render(&fk_scene, &HeadlessSpec::new(PAD_WIDTH, PAD_HEIGHT))
.await
.expect("headless render failed");
assert_eq!(
image.rgba8.len(),
(PAD_WIDTH * PAD_HEIGHT * 4) as usize,
"the image must carry tightly packed rows, with no padding"
);
for y in 0..PAD_HEIGHT {
let inside = image.pixel(EDGE - 1, y);
assert!(
inside[0] > 200 && inside[1] < 64 && inside[2] < 64,
"row {y} must still be red up to the block's edge, got {inside:?}"
);
assert_eq!(
image.pixel(EDGE, y),
[0, 0, 0, 255],
"row {y} must be the base color just past the block's edge"
);
assert_eq!(
image.pixel(PAD_WIDTH - 1, y),
[0, 0, 0, 255],
"row {y}'s last pixel must be the base color, not a neighbouring row's"
);
}
let mut square = Scene::new();
{
let mut builder = SceneBuilder::new(&mut square);
builder.fill_rect(
kurbo::Rect::new(0.0, 0.0, SIZE as f64, SIZE as f64),
Brush::Solid(RED),
);
}
let reused = headless
.render(&square, &HeadlessSpec::new(SIZE, SIZE))
.await
.expect("headless render failed on a reused renderer");
let [r, g, b, _] = reused.pixel(SIZE / 2, SIZE / 2);
assert!(
r > 200 && g < 64 && b < 64,
"a reused renderer must still render, got rgb=({r},{g},{b})"
);
}