#![cfg(not(target_arch = "wasm32"))]
use std::sync::{Mutex, MutexGuard, PoisonError};
use frust_engine::{EngineRenderer, EngineTarget, OutputAlpha, ShaderQuadPass};
use frust_gpu::{HeadlessTarget, TierCaps};
use frust_scene::{Scene, SceneBuilder, ShaderProgram};
use kurbo::{Affine, Rect};
use peniko::Color;
use peniko::color::palette::css::{BLACK, BLUE};
const SIZE: u32 = 64;
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const DEST: Rect = Rect::new(8.0, 8.0, 40.0, 40.0);
const DEST_EXTENT: f32 = 32.0;
const SHADER_GREEN: [u8; 4] = [0, 255, 0, 255];
const SHADER_RED: [u8; 4] = [255, 0, 0, 255];
static RENDER_LOCK: Mutex<()> = Mutex::new(());
fn render_lock() -> MutexGuard<'static, ()> {
RENDER_LOCK.lock().unwrap_or_else(PoisonError::into_inner)
}
fn solid(r: f32, g: f32, b: f32, a: f32) -> String {
format!(
"@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> {{ \
return vec4<f32>({r:?}, {g:?}, {b:?}, {a:?}); }}"
)
}
fn reports_resolution(expected: f32) -> String {
format!(
"@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> {{ \
let want = vec2<f32>({expected:?}, {expected:?}); \
if (all(frust_u.resolution == want)) {{ return vec4<f32>(0.0, 1.0, 0.0, 1.0); }} \
return vec4<f32>(1.0, 0.0, 0.0, 1.0); }}"
)
}
fn reports_resolution_as_color() -> String {
"@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> { \
return vec4<f32>(frust_u.resolution.x / 255.0, frust_u.resolution.y / 255.0, 0.0, 1.0); }"
.to_string()
}
fn reports_time_as_color() -> String {
"@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> { \
return vec4<f32>(frust_u.time, 0.0, 0.0, 1.0); }"
.to_string()
}
fn reports_aspect(expected: f32) -> String {
format!(
"@fragment fn fs_main(in: FrustVsOut) -> @location(0) vec4<f32> {{ \
let aspect = frust_u.resolution.x / frust_u.resolution.y; \
if (abs(aspect - {expected:?}) < 0.05) {{ return vec4<f32>(0.0, 1.0, 0.0, 1.0); }} \
return vec4<f32>(1.0, 0.0, 0.0, 1.0); }}"
)
}
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 shader quad adapter: {:?}", adapter.get_info());
let caps = TierCaps::probe(&adapter);
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("frust-engine shader quad 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)
})
}
struct Harness {
device: wgpu::Device,
queue: wgpu::Queue,
renderer: EngineRenderer,
shader_quads: ShaderQuadPass,
target: HeadlessTarget,
}
impl Harness {
fn new() -> Self {
let (device, queue, caps) = gpu();
let target = HeadlessTarget::new(&device, SIZE, SIZE, FORMAT);
let renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
Self {
device,
queue,
renderer,
shader_quads: ShaderQuadPass::new(None),
target,
}
}
fn render_over(&mut self, scene: &Scene, base: Color) -> Vec<u8> {
let scope = self.device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine shader quad frame"),
});
self.shader_quads.prepare(
&self.device,
&self.queue,
&mut encoder,
scene,
Affine::IDENTITY,
&mut self.renderer,
);
self.encode_into(&mut encoder, scene, base);
self.queue.submit([encoder.finish()]);
self.renderer.end_frame(&self.queue);
let error = drain_error_scope(&self.device, scope);
assert!(error.is_none(), "the frame raised {error:?}");
self.target.read_back(&self.device, &self.queue)
}
fn render_without_prepass(&mut self, scene: &Scene) -> Vec<u8> {
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine shader quad frame, no pre-pass"),
});
self.encode_into(&mut encoder, scene, BLACK);
self.queue.submit([encoder.finish()]);
self.renderer.end_frame(&self.queue);
self.target.read_back(&self.device, &self.queue)
}
fn encode_into(&mut self, encoder: &mut wgpu::CommandEncoder, scene: &Scene, base: Color) {
self.renderer
.encode(
&self.device,
&self.queue,
encoder,
scene,
EngineTarget {
view: self.target.view(),
format: FORMAT,
width: SIZE,
height: SIZE,
depth: None,
output: OutputAlpha::Premultiplied,
},
base,
Affine::IDENTITY,
)
.expect("a well-formed frame encodes");
}
fn age(&mut self, scene: &Scene, count: usize) {
for _ in 0..count {
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine shader quad aging"),
});
self.shader_quads.prepare(
&self.device,
&self.queue,
&mut encoder,
scene,
Affine::IDENTITY,
&mut self.renderer,
);
self.queue.submit([encoder.finish()]);
}
}
}
fn scene_of(record: impl FnOnce(&mut SceneBuilder<'_>)) -> Scene {
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
record(&mut builder);
scene
}
fn scene_drawing(program: &ShaderProgram, dest: Rect) -> Scene {
scene_of(|builder| builder.draw_shader(program, dest, 0.0))
}
fn pixel(frame: &[u8], x: u32, y: u32) -> [u8; 4] {
let start = ((y * SIZE + x) * 4) as usize;
frame[start..start + 4]
.try_into()
.expect("a read-back pixel is four bytes")
}
fn rgba(color: Color) -> [u8; 4] {
color.to_rgba8().to_u8_array()
}
fn assert_dest_is(frame: &[u8], expected: [u8; 4]) {
for probe in [(12, 12), (20, 20), (36, 36), (12, 36), (36, 12)] {
assert_eq!(
pixel(frame, probe.0, probe.1),
expected,
"the destination rectangle at {probe:?}"
);
}
}
fn assert_only_dest_painted(frame: &[u8], base: Color) {
for probe in [(1, 1), (SIZE - 2, 1), (1, SIZE - 2), (SIZE - 2, SIZE - 2)] {
assert_eq!(
pixel(frame, probe.0, probe.1),
rgba(base),
"the frame outside the destination rectangle is the base colour at {probe:?}"
);
}
}
fn assert_frame_is_base(frame: &[u8], base: Color) {
assert!(
frame
.as_chunks::<4>()
.0
.iter()
.all(|texel| *texel == rgba(base)),
"the frame is the base colour everywhere"
);
}
fn assert_close(actual: [u8; 4], expected: [u8; 4], tolerance: u8, at: &str) {
let close = actual
.iter()
.zip(expected.iter())
.all(|(a, e)| a.abs_diff(*e) <= tolerance);
assert!(close, "{at}: got {actual:?}, expected ~{expected:?}");
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_solid_program_fills_its_destination_rectangle() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(solid(0.0, 1.0, 0.0, 1.0));
let frame = harness.render_over(&scene_drawing(&program, DEST), BLACK);
assert_dest_is(&frame, SHADER_GREEN);
assert_only_dest_painted(&frame, BLACK);
assert_eq!(
harness.shader_quads.registered_len(),
1,
"the program's target stays registered for the next frame"
);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_targets_resolution_uniform_is_the_quads_device_extent() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(reports_resolution(DEST_EXTENT));
let frame = harness.render_over(&scene_drawing(&program, DEST), BLACK);
assert_dest_is(&frame, SHADER_GREEN);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_half_alpha_program_composites_premultiplied_over_the_base() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(solid(0.5, 0.0, 0.0, 0.5));
let frame = harness.render_over(&scene_drawing(&program, DEST), BLUE);
for probe in [(12, 12), (20, 20), (36, 36)] {
assert_close(
pixel(&frame, probe.0, probe.1),
[128, 0, 127, 255],
3,
&format!("the composited quad at {probe:?}"),
);
}
assert_only_dest_painted(&frame, BLUE);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_fully_transparent_program_leaves_the_frame_alone() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(solid(0.0, 0.0, 0.0, 0.0));
let frame = harness.render_over(&scene_drawing(&program, DEST), BLUE);
assert_frame_is_base(&frame, BLUE);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_quad_whose_program_never_rendered_draws_nothing() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(solid(0.0, 1.0, 0.0, 1.0));
let frame = harness.render_without_prepass(&scene_drawing(&program, DEST));
assert_frame_is_base(&frame, BLACK);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_program_that_fails_to_compile_draws_nothing() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new("this is not a shader");
let scene = scene_drawing(&program, DEST);
let frame = harness.render_over(&scene, BLACK);
assert_frame_is_base(&frame, BLACK);
assert_eq!(
harness.shader_quads.registered_len(),
0,
"a program with no pipeline registers no target"
);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn two_programs_in_one_frame_each_draw_their_own_pixels() {
let _guard = render_lock();
let mut harness = Harness::new();
let first = ShaderProgram::new(solid(1.0, 0.0, 0.0, 1.0));
let second = ShaderProgram::new(solid(0.0, 1.0, 0.0, 1.0));
let left = Rect::new(0.0, 16.0, 32.0, 48.0);
let right = Rect::new(32.0, 16.0, 64.0, 48.0);
let scene = scene_of(|builder| {
builder.draw_shader(&first, left, 0.0);
builder.draw_shader(&second, right, 0.0);
});
let frame = harness.render_over(&scene, BLACK);
assert_eq!(harness.shader_quads.registered_len(), 2);
assert_eq!(
pixel(&frame, 8, 32),
SHADER_RED,
"the left rectangle draws the first program"
);
assert_eq!(
pixel(&frame, 40, 32),
SHADER_GREEN,
"the right rectangle draws the second program"
);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn one_program_drawn_twice_in_a_frame_renders_once_and_draws_both_quads() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(solid(0.0, 1.0, 0.0, 1.0));
let left = Rect::new(0.0, 16.0, 32.0, 48.0);
let right = Rect::new(32.0, 16.0, 64.0, 48.0);
let scene = scene_of(|builder| {
builder.draw_shader(&program, left, 0.0);
builder.draw_shader(&program, right, 0.0);
});
let frame = harness.render_over(&scene, BLACK);
assert_eq!(
harness.shader_quads.registered_len(),
1,
"both quads share one target"
);
assert_eq!(pixel(&frame, 8, 32), SHADER_GREEN);
assert_eq!(pixel(&frame, 40, 32), SHADER_GREEN);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_program_the_scene_stops_drawing_is_reaped_and_unregistered() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(solid(0.0, 1.0, 0.0, 1.0));
let frame = harness.render_over(&scene_drawing(&program, DEST), BLACK);
assert_dest_is(&frame, SHADER_GREEN);
assert_eq!(harness.shader_quads.registered_len(), 1);
harness.age(
&Scene::new(),
frust_gpu::effects::MAX_UNSEEN_FRAMES as usize + 1,
);
assert_eq!(
harness.shader_quads.registered_len(),
0,
"a reaped program's registration is withdrawn with its target"
);
assert_eq!(
harness.renderer.bound_texture_count(),
0,
"the renderer holds no view for a reaped program"
);
let after = harness.render_without_prepass(&scene_drawing(&program, DEST));
assert_frame_is_base(&after, BLACK);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_target_reaped_after_idling_past_its_own_window_is_rebuilt_not_frozen() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(reports_time_as_color());
let before_scene = scene_of(|builder| builder.draw_shader(&program, DEST, 0.0));
let before = harness.render_over(&before_scene, BLUE);
assert_dest_is(&before, [0, 0, 0, 255]);
assert_eq!(harness.shader_quads.registered_len(), 1);
harness.age(
&Scene::new(),
frust_gpu::effects::MAX_UNSEEN_TARGET_FRAMES as usize + 1,
);
let after_scene = scene_of(|builder| builder.draw_shader(&program, DEST, 1.0));
let after = harness.render_over(&after_scene, BLUE);
assert_dest_is(&after, [255, 0, 0, 255]);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_resized_quad_reads_back_the_correct_exact_size_from_the_shared_quantized_target() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(reports_resolution_as_color());
let first = harness.render_over(&scene_drawing(&program, DEST), BLACK);
let before = pixel(&first, 20, 20);
assert_eq!(before[0], 32, "the shader reads back the exact 32px width");
assert_eq!(before[1], 32, "the shader reads back the exact 32px height");
assert_eq!(harness.shader_quads.registered_len(), 1);
let resized_dest = Rect::new(8.0, 8.0, 56.0, 56.0);
let second = harness.render_over(&scene_drawing(&program, resized_dest), BLACK);
let after = pixel(&second, 30, 30);
assert_eq!(
after[0], 48,
"after the resize the shader reads back 48px width"
);
assert_eq!(
after[1], 48,
"after the resize the shader reads back 48px height"
);
assert_eq!(
harness.shader_quads.registered_len(),
1,
"still one registration: the same program, a new exact demanded size"
);
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test shader_quad -- --ignored`"]
fn a_rotated_quad_sizes_its_target_from_dest_not_the_rotated_bbox() {
let _guard = render_lock();
let mut harness = Harness::new();
let program = ShaderProgram::new(reports_aspect(2.0));
let local_dest = Rect::new(-20.0, -10.0, 20.0, 10.0);
let scene = scene_of(|builder| {
builder.push_transform(
Affine::translate((32.0, 32.0)) * Affine::rotate(std::f64::consts::FRAC_PI_4),
);
builder.draw_shader(&program, local_dest, 0.0);
builder.pop_transform();
});
let frame = harness.render_over(&scene, BLACK);
assert_eq!(pixel(&frame, 32, 32), SHADER_GREEN);
}