#![cfg(not(target_arch = "wasm32"))]
use std::sync::{Mutex, MutexGuard, PoisonError};
use frust_engine::schedule::{MAX_LIVE_PAGES, PageConfig, PageParity, Round, Schedule};
use frust_engine::{
EngineError, EngineRenderer, EngineTarget, OutputAlpha, SceneCompiler, compile::CompiledFrame,
};
use frust_gpu::{ContextOptions, DownlevelProfile, HeadlessTarget, RenderContext, TierCaps};
use frust_scene::{Scene, SceneBuilder};
use kurbo::{Affine, Rect};
use peniko::{Brush, Color};
const SCRUB: (u32, u32) = (375, 667);
const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const TOLERANCE: u8 = 4;
const BACKDROP: Color = Color::from_rgba8(0, 0, 255, 255);
const PAGE: Color = Color::from_rgba8(28, 28, 30, 255);
const CHIP: Color = Color::from_rgba8(0, 200, 60, 255);
const TINT: Color = Color::from_rgba8(220, 40, 240, 255);
const NESTED: Color = Color::from_rgba8(255, 255, 255, 255);
const TINT_ALPHA: f32 = 0.35;
const NESTED_ALPHA: f32 = 0.12;
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 as TaskContext, Poll, Waker};
let waker = Waker::noop();
let mut cx = TaskContext::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 as TaskContext, Poll, Waker};
let waker = Waker::noop();
let mut cx = TaskContext::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() -> (RenderContext, wgpu::Device, wgpu::Queue, TierCaps) {
let mut context = RenderContext::with_options(ContextOptions {
device_label: "frust-engine scrub device".to_string(),
backends: None,
});
let handle = block_on(context.device()).expect("no compatible GPU adapter");
println!(
"frust-engine scrub adapter: {:?}",
handle.adapter.get_info()
);
let (device, queue, caps) = (
handle.device.clone(),
handle.queue.clone(),
handle.caps.clone(),
);
(context, device, queue, caps)
}
fn scrub_alphas() -> Vec<f32> {
let up = (0..25).map(|step| 0.04 + 0.86 * (step as f32) / 24.0);
let down = (0..15).map(|step| 0.9 - 0.8 * (step as f32) / 14.0);
up.chain(down).collect()
}
fn chip_rect(index: u32) -> Rect {
let x = 24.0 + 168.0 * f64::from(index);
Rect::new(x, 120.0, x + 152.0, 420.0)
}
fn nested_rect(index: u32) -> Rect {
let chip = chip_rect(index);
Rect::new(
chip.x0 + 32.0,
chip.y0 + 32.0,
chip.x0 + 120.0,
chip.y0 + 200.0,
)
}
fn chip(builder: &mut SceneBuilder<'_>, index: u32, nested: bool) {
let rect = chip_rect(index);
builder.fill_rect(rect, Brush::Solid(CHIP));
builder.push_layer(rect, TINT_ALPHA);
builder.fill_rect(rect, Brush::Solid(TINT));
if nested {
let inner = nested_rect(index);
builder.push_layer(inner, NESTED_ALPHA);
builder.fill_rect(inner, Brush::Solid(NESTED));
builder.pop_layer();
}
builder.pop_layer();
}
fn scrub_scene(alpha: f32) -> Scene {
let full = Rect::new(0.0, 0.0, f64::from(SCRUB.0), f64::from(SCRUB.1));
let mut scene = Scene::new();
let mut builder = SceneBuilder::new(&mut scene);
builder.push_layer(full, alpha);
builder.fill_rect(full, Brush::Solid(PAGE));
chip(&mut builder, 0, false);
chip(&mut builder, 1, true);
builder.pop_layer();
scene
}
fn compile(scene: &Scene) -> CompiledFrame {
let size = (
u16::try_from(SCRUB.0).expect("the scrub surface is inside the device grid"),
u16::try_from(SCRUB.1).expect("the scrub surface is inside the device grid"),
);
SceneCompiler::new(size.0, size.1)
.compile(scene, Affine::IDENTITY, size)
.expect("an in-range scene compiles")
}
fn rounds(frame: &CompiledFrame) -> Result<Vec<Round>, EngineError> {
Schedule::build(
&frame.recorder,
&TierCaps::fake(DownlevelProfile::Full),
&PageConfig::default(),
)
}
fn over(source: Color, dest: Color, alpha: f32) -> Color {
let (src, dst) = (source.components, dest.components);
Color::new([
alpha * src[0] + (1.0 - alpha) * dst[0],
alpha * src[1] + (1.0 - alpha) * dst[1],
alpha * src[2] + (1.0 - alpha) * dst[2],
1.0,
])
}
fn bytes(color: Color) -> [u8; 4] {
color
.components
.map(|channel| (channel * 255.0).round().clamp(0.0, 255.0) as u8)
}
fn pixel(pixels: &[u8], x: u32, y: u32) -> [u8; 4] {
let index = ((y * SCRUB.0 + x) * 4) as usize;
pixels[index..index + 4]
.try_into()
.expect("a readback row holds four bytes per pixel")
}
fn assert_pixel(pixels: &[u8], (x, y): (u32, u32), expected: Color, what: &str) {
let actual = pixel(pixels, x, y);
let expected = bytes(expected);
let off = (0..4).any(|channel| actual[channel].abs_diff(expected[channel]) > TOLERANCE);
assert!(
!off,
"{what}: pixel ({x}, {y}) is {actual:?}, expected {expected:?} +/- {TOLERANCE}"
);
}
fn point_in(rect: Rect, offset: (f64, f64)) -> (u32, u32) {
((rect.x0 + offset.0) as u32, (rect.y0 + offset.1) as u32)
}
fn samples(alpha: f32) -> Vec<((u32, u32), Color, &'static str)> {
let tinted = over(TINT, CHIP, TINT_ALPHA);
let nested = over(over(NESTED, TINT, NESTED_ALPHA), CHIP, TINT_ALPHA);
vec![
(
(40, 560),
over(PAGE, BACKDROP, alpha),
"the scrubbing page's own backdrop",
),
(
point_in(chip_rect(0), (16.0, 240.0)),
over(tinted, BACKDROP, alpha),
"the plain chip's tint over its body",
),
(
point_in(nested_rect(1), (24.0, 80.0)),
over(nested, BACKDROP, alpha),
"the nested chip inside the later chip",
),
]
}
#[test]
fn every_frame_of_the_scrub_schedules_without_refusing() {
let mut refused: Vec<String> = Vec::new();
let mut spilled = 0_usize;
for (frame, alpha) in scrub_alphas().iter().enumerate() {
let compiled = compile(&scrub_scene(*alpha));
match rounds(&compiled) {
Err(error) => refused.push(format!("frame {frame} at alpha {alpha:.3}: {error}")),
Ok(rounds) => {
assert!(
rounds
.iter()
.filter_map(Round::page)
.any(|page| page.parity == PageParity::Spill),
"frame {frame} at alpha {alpha:.3} is served without the spill page, so this \
recording no longer holds the shape the case is about: {rounds:?}"
);
spilled += 1;
}
}
}
assert!(
refused.is_empty(),
"{} of {} scrub frames were refused:\n{}",
refused.len(),
scrub_alphas().len(),
refused.join("\n")
);
assert_eq!(spilled, scrub_alphas().len());
}
#[test]
fn a_scrubbed_frame_never_holds_more_pages_live_than_the_bound() {
for alpha in scrub_alphas() {
let compiled = compile(&scrub_scene(alpha));
let rounds = rounds(&compiled).expect("a scrub frame is served");
for round in &rounds {
let live = usize::from(round.page().is_some()) + round.released.len();
assert!(
live <= MAX_LIVE_PAGES,
"alpha {alpha:.3} holds {live} pages live in {round:?}"
);
}
}
}
#[test]
#[ignore = "requires a GPU (Vulkan/Metal); run with `cargo test -p frust-engine --test scrub_served -- --ignored`"]
fn a_forty_frame_scrub_on_one_renderer_serves_every_frame_and_paints_the_analytic_answer() {
let _guard = render_lock();
let (_context, device, queue, caps) = gpu();
let (width, height) = SCRUB;
let scope = device.push_error_scope(wgpu::ErrorFilter::Validation);
let target = HeadlessTarget::new(&device, width, height, FORMAT);
let mut renderer = EngineRenderer::new(&device, &caps, FORMAT, None)
.expect("the engine builds on this device");
let alphas = scrub_alphas();
let mut refused: Vec<String> = Vec::new();
let mut served = 0_usize;
for (frame, alpha) in alphas.iter().enumerate() {
let scene = scrub_scene(*alpha);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("frust-engine scrub frame"),
});
let encoded = renderer.encode(
&device,
&queue,
&mut encoder,
&scene,
EngineTarget {
view: target.view(),
format: FORMAT,
width,
height,
depth: None,
output: OutputAlpha::Premultiplied,
},
BACKDROP,
Affine::IDENTITY,
);
queue.submit([encoder.finish()]);
renderer.end_frame(&queue);
if let Err(error) = encoded {
refused.push(format!("frame {frame} at alpha {alpha:.3}: {error}"));
continue;
}
served += 1;
if frame % 5 == 0 || frame + 1 == alphas.len() {
let pixels = target.read_back(&device, &queue);
for (at, expected, what) in samples(*alpha) {
assert_pixel(
&pixels,
at,
expected,
&format!("frame {frame} at alpha {alpha:.3}: {what}"),
);
}
}
}
let error = drain_error_scope(&device, scope);
assert!(error.is_none(), "the scrub raised {error:?}");
println!(
"frust-engine scrub: {served}/{} frames served",
alphas.len()
);
assert!(
refused.is_empty(),
"{} of {} scrub frames were refused, so the surface would have frozen for the whole \
gesture:\n{}",
refused.len(),
alphas.len(),
refused.join("\n")
);
assert_eq!(served, alphas.len());
}