#![cfg(not(target_arch = "wasm32"))]
use std::sync::{Mutex, MutexGuard, PoisonError};
use frust_engine::cache::images::{AtlasBudget, is_mobile_tier};
use frust_engine::{EngineRenderer, EngineTarget, OutputAlpha};
use frust_gpu::{HeadlessTarget, TierCaps};
use frust_scene::{Scene, SceneBuilder};
use kurbo::{Affine, BezPath, Point, Rect};
use peniko::color::palette::css::{BLACK, BLUE, GREEN, RED, WHITE};
use peniko::{Brush, Color};
const SIZE: u32 = 256;
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const SECOND_PASS_CLEAR: wgpu::Color = wgpu::Color {
r: 0.0,
g: 0.0,
b: 1.0,
a: 1.0,
};
const FILL_RECT: Rect = Rect::new(32.0, 32.0, 96.0, 96.0);
static RENDER_LOCK: Mutex<()> = Mutex::new(());
fn render_lock() -> MutexGuard<'static, ()> {
RENDER_LOCK.lock().unwrap_or_else(PoisonError::into_inner)
}
fn block_on<F: std::future::Future>(future: F) -> F::Output {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(future);
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(value) => return value,
Poll::Pending => std::thread::yield_now(),
}
}
}
fn drain_error_scope(device: &wgpu::Device, scope: wgpu::ErrorScopeGuard) -> Option<wgpu::Error> {
use std::task::{Context, Poll, Waker};
let waker = Waker::noop();
let mut cx = Context::from_waker(waker);
let mut future = std::pin::pin!(scope.pop());
loop {
match future.as_mut().poll(&mut cx) {
Poll::Ready(error) => return error,
Poll::Pending => {
let _ = device.poll(wgpu::PollType::wait_indefinitely());
}
}
}
}
fn gpu() -> (wgpu::Device, wgpu::Queue, TierCaps) {
block_on(async {
let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle_from_env());
let adapter = wgpu::util::initialize_adapter_from_env_or_default(&instance, None)
.await
.expect("no compatible GPU adapter");
println!(
"frust-engine encode contract adapter: {:?}",
adapter.get_info()
);
let caps = TierCaps::probe(&adapter);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-engine encode contract device"),
required_features: wgpu::Features::empty(),
required_limits: frust_gpu::test_device_limits(&adapter, &caps),
..Default::default()
})
.await
.expect("failed to create the device");
(device, queue, caps)
})
}
fn diamond() -> BezPath {
let mut path = BezPath::new();
path.move_to((176.0, 116.0));
path.line_to((236.0, 176.0));
path.line_to((176.0, 236.0));
path.line_to((116.0, 176.0));
path.close_path();
path
}
fn scene() -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(FILL_RECT, Brush::Solid(RED));
builder.fill_path(diamond(), Brush::Solid(BLUE));
builder.stroke_line(
Point::new(16.0, 200.0),
Point::new(96.0, 200.0),
6.0,
Brush::Solid(GREEN),
);
scene
}
fn warm_up_ran(renderer: &EngineRenderer) {
assert!(
renderer.compiled_pipelines() > 0,
"the frames above compiled no pipeline at all"
);
}
fn pixel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
let index = ((y * SIZE + x) * 4) as usize;
pixels[index..index + 4]
.try_into()
.expect("a read-back row holds four bytes per pixel")
}
fn rendered(scene: &Scene, base_color: Color) -> Vec<u8> {
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine round frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
base_color,
Affine::IDENTITY,
)
.expect("a well-formed frame encodes");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the frame raised {error:?}");
warm_up_ran(&renderer);
target.read_back(&device, &queue)
}
const LAYER_RECT: Rect = Rect::new(64.0, 64.0, 192.0, 192.0);
fn layer_scene() -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(
Rect::new(0.0, 0.0, f64::from(SIZE), f64::from(SIZE)),
Brush::Solid(BLACK),
);
builder.push_layer(LAYER_RECT, 0.5);
builder.fill_rect(LAYER_RECT, Brush::Solid(WHITE));
builder.pop_layer();
scene
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_two_round_layer_frame_composites_its_page_with_the_layers_own_alpha() {
let _serialized = render_lock();
let pixels = rendered(&layer_scene(), Color::TRANSPARENT);
let inside = pixel(&pixels, 128, 128);
assert_eq!(inside[3], 255, "the backdrop keeps the surface opaque");
for channel in 0..3 {
let value = i32::from(inside[channel]);
assert!(
(value - 128).abs() <= 1,
"a 0.5 layer over black must composite to about 128, got {inside:?}"
);
}
assert_eq!(pixel(&pixels, 8, 8), [0, 0, 0, 255]);
assert_eq!(pixel(&pixels, 248, 248), [0, 0, 0, 255]);
}
const PUNCH_EDGE: f64 = 129.0;
fn punch_scene() -> Scene {
let edge = f64::from(SIZE);
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(Rect::new(0.0, 0.0, edge, edge), Brush::Solid(RED));
builder.push_layer(Rect::new(0.0, 0.0, edge, edge), 1.0);
builder.clear_rect(Rect::new(0.0, 0.0, PUNCH_EDGE, edge));
builder.pop_layer();
scene
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_punch_erases_to_full_transparency_pixel_exact_at_a_tile_unaligned_edge() {
let _serialized = render_lock();
let pixels = rendered(&punch_scene(), Color::TRANSPARENT);
assert_eq!(
pixel(&pixels, 64, 128),
[0, 0, 0, 0],
"the punch centre must reach full transparency, colour and alpha alike"
);
assert_eq!(
pixel(&pixels, 128, 128),
[0, 0, 0, 0],
"the last pixel inside the unaligned edge must be erased"
);
assert_eq!(
pixel(&pixels, 129, 128),
[255, 0, 0, 255],
"the pixel just past the edge shares its tile and must survive intact"
);
assert_eq!(
pixel(&pixels, 140, 128),
[255, 0, 0, 255],
"and so must the rest of the tile the edge falls in"
);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_punch_is_dropped_whole_on_a_target_that_disregards_alpha() {
let _serialized = render_lock();
let pixels = rendered(&punch_scene(), BLUE);
assert_eq!(
pixel(&pixels, 64, 128),
[255, 0, 0, 255],
"an opaque presentation reads exactly as it would without the clear"
);
assert_eq!(pixel(&pixels, 140, 128), [255, 0, 0, 255]);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn encode_records_into_the_callers_encoder_without_submitting_or_leaving_a_pass_open() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let unrelated = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let scene = scene();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine encode contract"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("a well-formed frame encodes");
drop(encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("unrelated pass after encode"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: unrelated.view(),
depth_slice: None,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(SECOND_PASS_CLEAR),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
}));
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "encoding raised {error:?}");
let pixels = target.read_back(&device, &queue);
assert_eq!(pixels.len(), (SIZE * SIZE * 4) as usize);
assert!(
pixels.iter().any(|byte| *byte != 0),
"the frame drew nothing at all"
);
assert_eq!(pixel(&pixels, 64, 64), [255, 0, 0, 255]);
assert_eq!(pixel(&pixels, 176, 176), [0, 0, 255, 255]);
assert_eq!(pixel(&pixels, 56, 200), [0, 128, 0, 255]);
assert_eq!(pixel(&pixels, 4, 4), [0, 0, 0, 0]);
assert_eq!(pixel(&pixels, 250, 8), [0, 0, 0, 0]);
let unrelated_pixels = unrelated.read_back(&device, &queue);
assert_eq!(pixel(&unrelated_pixels, 128, 128), [0, 0, 255, 255]);
warm_up_ran(&renderer);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_renderer_encodes_frame_after_frame_and_survives_a_resize() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let scene = scene();
for extent in [SIZE, SIZE, SIZE, SIZE / 2] {
if extent != SIZE {
renderer.resize(&device, extent, extent);
}
let target = HeadlessTarget::new(&device, extent, extent, FORMAT);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine repeat frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: extent,
height: extent,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("a well-formed frame encodes");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let pixels = target.read_back(&device, &queue);
let index = (((extent / 4) * extent + extent / 4) * 4) as usize;
assert_eq!(
&pixels[index..index + 4],
[255, 0, 0, 255],
"the rectangle is drawn at every extent (extent {extent})"
);
}
warm_up_ran(&renderer);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "repeat encoding raised {error:?}");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_caller_supplied_depth_attachment_is_used_instead_of_an_engine_owned_one() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let depth = device.create_texture(&frust_engine::gpu::depth::depth_texture_descriptor(
SIZE, SIZE,
));
let depth_view = depth.create_view(&wgpu::TextureViewDescriptor::default());
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let scene = scene();
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine caller depth"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: Some(&depth_view),
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("a frame with a caller-supplied depth attachment encodes");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let pixels = target.read_back(&device, &queue);
assert_eq!(pixel(&pixels, 64, 64), [255, 0, 0, 255]);
warm_up_ran(&renderer);
let error = drain_error_scope(&device, scope);
assert!(
error.is_none(),
"encoding against a caller depth raised {error:?}"
);
}
const GRADIENT_RECT: Rect = Rect::new(32.0, 32.0, 224.0, 96.0);
fn gradient_scene() -> Scene {
let gradient = peniko::Gradient::new_linear(
(GRADIENT_RECT.x0, GRADIENT_RECT.y0),
(GRADIENT_RECT.x1, GRADIENT_RECT.y0),
)
.with_stops([RED, BLUE]);
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(GRADIENT_RECT, Brush::Gradient(gradient));
scene
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_gradient_rect_renders_a_ramp_rather_than_one_flat_colour() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine gradient frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&gradient_scene(),
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("a gradient frame encodes");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the gradient frame raised {error:?}");
let pixels = target.read_back(&device, &queue);
let y = 64;
let near = pixel(&pixels, 40, y);
let middle = pixel(&pixels, 128, y);
let far = pixel(&pixels, 216, y);
for (label, sample) in [("near", near), ("middle", middle), ("far", far)] {
assert_eq!(
sample[3], 255,
"the {label} sample is inside an opaque rectangle, got {sample:?}"
);
}
assert_ne!(
near, far,
"the two ends of the gradient axis must not match"
);
assert!(
near[0] > far[0] && near[2] < far[2],
"red should fall and blue should rise along the axis: {near:?} -> {middle:?} -> {far:?}"
);
assert!(
middle[0] < near[0] && middle[0] > far[0],
"the middle sample should sit between the two ends: {near:?} -> {middle:?} -> {far:?}"
);
assert_eq!(pixel(&pixels, 4, 4), [0, 0, 0, 0]);
assert_eq!(pixel(&pixels, 128, 240), [0, 0, 0, 0]);
warm_up_ran(&renderer);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_solid_frame_after_a_gradient_frame_is_unaffected_by_it() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let mut rendered = Vec::new();
for scene in [gradient_scene(), scene()] {
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine mixed paint frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("both frames encode");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
rendered.push(target.read_back(&device, &queue));
}
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the frame pair raised {error:?}");
let solid = &rendered[1];
assert_eq!(pixel(solid, 64, 64), [255, 0, 0, 255]);
assert_eq!(pixel(solid, 176, 176), [0, 0, 255, 255]);
assert_eq!(pixel(solid, 56, 200), [0, 128, 0, 255]);
assert_eq!(pixel(solid, 4, 4), [0, 0, 0, 0]);
warm_up_ran(&renderer);
}
const IMAGE_RECT: Rect = Rect::new(32.0, 32.0, 96.0, 96.0);
fn solid_image(width: u32, height: u32, rgba: [u8; 4]) -> peniko::ImageData {
let mut data = Vec::with_capacity((width as usize) * (height as usize) * 4);
for _ in 0..(width as usize) * (height as usize) {
data.extend_from_slice(&rgba);
}
peniko::ImageData {
data: peniko::Blob::new(std::sync::Arc::new(data)),
format: peniko::ImageFormat::Rgba8,
alpha_type: peniko::ImageAlphaType::Alpha,
width,
height,
}
}
fn image_scene() -> Scene {
let image = solid_image(64, 64, [0, 255, 0, 255]);
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.draw_image(&image, IMAGE_RECT);
scene
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn an_image_draw_samples_the_real_atlas_rather_than_leaving_its_draw_skipped() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine image frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&image_scene(),
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("an image frame encodes");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the image frame raised {error:?}");
let pixels = target.read_back(&device, &queue);
assert_eq!(
pixel(&pixels, 64, 64),
[0, 255, 0, 255],
"the atlas-resident image reached the target rather than being skipped"
);
assert_eq!(pixel(&pixels, 4, 4), [0, 0, 0, 0]);
assert_eq!(pixel(&pixels, 250, 250), [0, 0, 0, 0]);
warm_up_ran(&renderer);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn an_image_survives_a_second_frame_with_no_new_upload() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let scene = image_scene();
let mut last = None;
for _ in 0..2 {
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine repeat image frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("both image frames encode");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
last = Some(target.read_back(&device, &queue));
}
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the repeated image frame raised {error:?}");
let pixels = last.expect("two frames ran");
assert_eq!(pixel(&pixels, 64, 64), [0, 255, 0, 255]);
warm_up_ran(&renderer);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_renderer_takes_the_atlas_budget_its_own_adapter_calls_for() {
let _serialized = render_lock();
let (device, _queue, caps) = gpu();
let renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
assert_eq!(
renderer.atlas_budget(),
AtlasBudget::for_caps(&caps),
"the renderer's budget must be the one its adapter calls for"
);
if !is_mobile_tier(&caps) {
assert_eq!(
renderer.atlas_budget().atlas_size,
AtlasBudget::DESKTOP.atlas_size,
"an immediate-mode adapter takes the desktop layer extent"
);
}
assert!(renderer.atlas_budget().total_bytes() <= AtlasBudget::DESKTOP.total_bytes());
}
const GROWTH_BUDGET: AtlasBudget = AtlasBudget {
atlas_size: (64, 64),
max_atlases: 4,
};
const RESIDENT_RECT: Rect = Rect::new(0.0, 0.0, 64.0, 48.0);
const LAYER_ZERO_RECT: Rect = Rect::new(0.0, 64.0, 32.0, 80.0);
const GROWTH_RECT: Rect = Rect::new(0.0, 100.0, 64.0, 148.0);
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_frame_that_grows_the_atlas_keeps_the_uploads_it_made_into_layer_zero() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
renderer.set_atlas_budget(GROWTH_BUDGET);
let resident = solid_image(64, 48, [0, 255, 0, 255]);
let into_layer_zero = solid_image(32, 16, [0, 0, 255, 255]);
let forces_growth = solid_image(64, 48, [255, 0, 0, 255]);
let first = {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.draw_image(&resident, RESIDENT_RECT);
scene
};
let second = {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.draw_image(&resident, RESIDENT_RECT);
builder.draw_image(&into_layer_zero, LAYER_ZERO_RECT);
builder.draw_image(&forces_growth, GROWTH_RECT);
scene
};
let mut last = None;
for scene in [&first, &second] {
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine atlas growth frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("both image frames encode");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
last = Some(target.read_back(&device, &queue));
}
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the growth frame raised {error:?}");
let pixels = last.expect("two frames ran");
assert_eq!(
pixel(&pixels, 16, 72),
[0, 0, 255, 255],
"the image uploaded into layer 0 was overwritten by the growth copy"
);
assert_eq!(pixel(&pixels, 32, 124), [255, 0, 0, 255]);
assert_eq!(pixel(&pixels, 32, 24), [0, 255, 0, 255]);
assert_eq!(
renderer.refused_atlas_regions(),
0,
"no region of a sound frame is ever declined"
);
assert!(
renderer.atlas_budget().max_atlases >= 2,
"the budget under test must admit the second layer"
);
warm_up_ran(&renderer);
}
const PUNCH_HALF: Rect = Rect::new(0.0, 0.0, 128.0, 256.0);
const TRANSLUCENT_LAYER: Rect = Rect::new(64.0, 64.0, 192.0, 192.0);
fn punch_under_translucent_layer_scene() -> Scene {
let edge = f64::from(SIZE);
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.fill_rect(Rect::new(0.0, 0.0, edge, edge), Brush::Solid(RED));
builder.clear_rect(PUNCH_HALF);
builder.push_layer(TRANSLUCENT_LAYER, 0.5);
builder.fill_rect(TRANSLUCENT_LAYER, Brush::Solid(WHITE));
builder.pop_layer();
scene
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_translucent_composite_recorded_after_a_clear_rect_lands_on_top_of_the_punch() {
let _serialized = render_lock();
let pixels = rendered(&punch_under_translucent_layer_scene(), Color::TRANSPARENT);
let over_the_hole = pixel(&pixels, 96, 128);
for channel in 0..4 {
assert!(
(i32::from(over_the_hole[channel]) - 128).abs() <= 2,
"inside both the layer and the punch, the composite lands on the erase at about \
half alpha, got {over_the_hole:?}"
);
}
assert_eq!(
pixel(&pixels, 32, 32),
[0, 0, 0, 0],
"and the rest of the punched half — which nothing was drawn over — is erased as it \
always was"
);
let composited = pixel(&pixels, 160, 128);
assert_eq!(composited[3], 255, "the backdrop keeps the surface opaque");
assert!(
composited[0] > 200,
"red survives under a half-opacity white: {composited:?}"
);
for channel in 1..3 {
let value = i32::from(composited[channel]);
assert!(
(value - 128).abs() <= 2,
"a 0.5 white layer over red must composite to about 128, got {composited:?}"
);
}
assert_eq!(pixel(&pixels, 200, 32), [255, 0, 0, 255]);
}
const BLUR_RECT: Rect = Rect::new(64.0, 56.0, 192.0, 168.0);
fn blurred_rect_scene() -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.draw_blurred_rounded_rect(BLUR_RECT, 16.0, 8.0, RED);
scene
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test encode_contract -- --ignored`"]
fn a_blurred_rounded_rect_draw_paints_a_falloff_rather_than_leaving_its_draw_skipped() {
let _serialized = render_lock();
let (device, queue, caps) = gpu();
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine blurred-rect frame"),
});
renderer
.encode(
&device,
&queue,
&mut encoder,
&blurred_rect_scene(),
EngineTarget {
view: target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
Color::TRANSPARENT,
Affine::IDENTITY,
)
.expect("a blurred-rect frame encodes");
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the blurred-rect frame raised {error:?}");
let pixels = target.read_back(&device, &queue);
let center = pixel(&pixels, 128, 112);
assert!(
center[0] > 200 && center[3] > 200,
"the shadow's centre should read as strongly opaque red, got {center:?}"
);
assert_eq!(center[1], 0);
assert_eq!(center[2], 0);
assert_eq!(pixel(&pixels, 4, 4), [0, 0, 0, 0]);
assert_eq!(pixel(&pixels, 250, 250), [0, 0, 0, 0]);
warm_up_ran(&renderer);
}