cranpose-render-wgpu 0.1.164

WGPU renderer backend for Cranpose
Documentation
use std::{cell::RefCell, rc::Rc};

use cranpose_app_shell::AppShell;
use cranpose_core::{MutableState, location_key};
use cranpose_render_wgpu::{CapturedFrame, RenderStatsSnapshot, WgpuRenderer};
use cranpose_ui::{
    Alignment, Color, LinearArrangement, Modifier, TextStyle, composable,
    widgets::{Box, BoxSpec, Column, ColumnSpec, Row, RowSpec, Text},
};

use crate::support;

const FRAME_WIDTH: u32 = 360;
const FRAME_HEIGHT: u32 = 480;
const COLUMNS: usize = 3;
const ROWS: usize = 4;
const TILES: u32 = (COLUMNS * ROWS) as u32;
const WARMUP_FRAMES: usize = 4;
const MEASURED_FRAMES: usize = 12;
const SCALE_PERIOD_FRAMES: usize = 126;
const MAX_STEPS_PER_TILE: u32 = 6;

const PALETTE: [Color; 4] = [
    Color(0.85, 0.25, 0.30, 1.0),
    Color(0.20, 0.55, 0.85, 1.0),
    Color(0.25, 0.70, 0.40, 1.0),
    Color(0.80, 0.60, 0.15, 1.0),
];

#[composable]
fn Tile(index: usize, seconds: MutableState<f32>) {
    let phase = index as f32;
    Box(
        Modifier::empty()
            .weight(1.0)
            .fill_max_height()
            .graphics_layer_block(move |layer| {
                let t = seconds.get();
                layer.rotation_z = (t * 90.0 + phase * 13.0) % 360.0;
                let scale = 0.85 + 0.15 * (t * 3.0 + phase * 0.4).sin();
                layer.scale_x = scale;
                layer.scale_y = scale;
                layer.alpha = 0.65 + 0.35 * (0.5 + 0.5 * (t * 2.0 + phase * 0.7).sin());
            })
            .background(PALETTE[index % PALETTE.len()])
            .rounded_corners(12.0),
        BoxSpec::new().content_alignment(Alignment::CENTER),
        move || {
            Text(index.to_string(), Modifier::empty(), TextStyle::default());
        },
    );
}

#[composable]
fn Tiles(seconds: MutableState<f32>) {
    Column(
        Modifier::empty().fill_max_size().padding(4.0),
        ColumnSpec::new().vertical_arrangement(LinearArrangement::spaced_by(8.0)),
        move || {
            for row in 0..ROWS {
                Row(
                    Modifier::empty().fill_max_width().weight(1.0),
                    RowSpec::new().horizontal_arrangement(LinearArrangement::spaced_by(8.0)),
                    move || {
                        for column in 0..COLUMNS {
                            Tile(row * COLUMNS + column, seconds);
                        }
                    },
                );
            }
        },
    );
}

struct TileHarness {
    shell: AppShell<WgpuRenderer>,
    seconds: Rc<RefCell<Option<MutableState<f32>>>>,
}

impl TileHarness {
    fn new(renderer: WgpuRenderer) -> Self {
        let root_key = location_key(file!(), line!(), column!());
        let seconds: Rc<RefCell<Option<MutableState<f32>>>> = Rc::new(RefCell::new(None));
        let seconds_for_app = Rc::clone(&seconds);
        let mut shell = AppShell::new(renderer, root_key, move || {
            let state = cranpose_core::rememberMutableStateOf(|| 0.0f32);
            *seconds_for_app.borrow_mut() = Some(state);
            Tiles(state);
        });
        shell.set_viewport(FRAME_WIDTH as f32, FRAME_HEIGHT as f32);
        shell.set_buffer_size(FRAME_WIDTH, FRAME_HEIGHT);
        shell.update();
        Self { shell, seconds }
    }

    fn frame(&mut self, seconds: f32) -> (RenderStatsSnapshot, CapturedFrame) {
        let state = self
            .seconds
            .borrow()
            .as_ref()
            .copied()
            .expect("state captured");
        self.shell.debug_enter_app_context(|| state.set(seconds));
        support::update_and_capture(&mut self.shell, FRAME_WIDTH, FRAME_HEIGHT)
    }

    fn settled_frame(&mut self, seconds: f32) -> CapturedFrame {
        let deadline = std::time::Instant::now() + std::time::Duration::from_secs(30);
        loop {
            let (stats, frame) = self.frame(seconds);
            if support::pipelines_settled(&stats) {
                return frame;
            }
            assert!(
                std::time::Instant::now() < deadline,
                "specialization did not finish"
            );
            std::thread::sleep(std::time::Duration::from_millis(5));
        }
    }
}

fn frame_seconds(frame: usize) -> f32 {
    frame as f32 / 60.0
}

fn harness() -> Option<(std::sync::MutexGuard<'static, ()>, TileHarness)> {
    match support::headless_renderer_parts() {
        Ok((lock, renderer)) => Some((lock, TileHarness::new(renderer))),
        Err(err) => {
            eprintln!("skipping (headless WGPU init failed): {err}");
            None
        }
    }
}

fn fresh_harness() -> TileHarness {
    TileHarness::new(support::headless_renderer_beside_locked().expect("reference renderer"))
}

fn second_period_stats(harness: &mut TileHarness) -> Vec<RenderStatsSnapshot> {
    for frame in 0..SCALE_PERIOD_FRAMES {
        harness.frame(frame_seconds(frame));
    }
    (SCALE_PERIOD_FRAMES..SCALE_PERIOD_FRAMES + MEASURED_FRAMES)
        .map(|frame| harness.frame(frame_seconds(frame)).0)
        .collect()
}

#[test]
fn an_animated_layer_transform_allocates_no_texture_once_its_scales_were_seen() {
    let Some((_lock, mut harness)) = harness() else {
        return;
    };
    let stats = second_period_stats(&mut harness);
    let news: Vec<u32> = stats.iter().map(|stats| stats.offscreen_news).collect();
    assert!(
        news.iter().all(|news| *news == 0),
        "tiles whose rotation, scale and alpha keep changing over unchanged content must \
         reuse what an earlier period drew, not create textures every frame: {news:?}"
    );
}

#[test]
fn an_animated_layer_transform_redraws_almost_no_surface_and_bounds_the_cache() {
    let Some((_lock, mut harness)) = harness() else {
        return;
    };
    let stats = second_period_stats(&mut harness);
    let renders: u32 = stats.iter().map(|stats| stats.isolated_layer_renders).sum();
    assert!(
        renders <= TILES,
        "a period after every scale step was drawn, {MEASURED_FRAMES} frames of {TILES} \
         animated tiles redrew {renders} surfaces; before the fix they redrew every tile \
         every frame"
    );
    let sizes: Vec<u32> = stats.iter().map(|stats| stats.layer_cache_size).collect();
    assert!(
        sizes.iter().all(|size| *size <= TILES * MAX_STEPS_PER_TILE),
        "each tile keeps at most one raster per scale step it passes through: {sizes:?}"
    );
}

#[test]
fn a_layer_that_stops_scaling_draws_what_a_fresh_renderer_draws() {
    let Some((_lock, mut animated)) = harness() else {
        return;
    };
    for frame in 0..WARMUP_FRAMES {
        animated.frame(frame_seconds(frame));
    }
    support::wait_for_background_compiler_idle();
    let held = frame_seconds(WARMUP_FRAMES);
    let (_, moving) = animated.frame(held);
    let (stats, still) = animated.frame(held);
    assert!(
        support::pipelines_settled(&stats),
        "the still frame drew with stand-in pipelines: {stats:?}"
    );
    assert!(
        stats.isolated_layer_renders >= TILES,
        "the first frame a scale holds redraws every tile at its exact scale: {stats:?}"
    );
    let expected = fresh_harness().settled_frame(held);
    support::assert_same_bytes("still frame", FRAME_WIDTH, &expected.pixels, &still.pixels);
    assert_ne!(
        moving.pixels, still.pixels,
        "the moving frame must differ from the still one, or the still frame proves nothing"
    );
}

#[test]
fn an_animated_frame_draws_what_a_fresh_renderer_given_the_same_motion_draws() {
    let Some((_lock, mut animated)) = harness() else {
        return;
    };
    for frame in 0..WARMUP_FRAMES {
        animated.frame(frame_seconds(frame));
    }
    support::wait_for_background_compiler_idle();
    for frame in WARMUP_FRAMES..WARMUP_FRAMES + MEASURED_FRAMES {
        let (stats, actual) = animated.frame(frame_seconds(frame));
        if frame % 4 != 3 {
            continue;
        }
        assert!(
            support::pipelines_settled(&stats),
            "frame {frame} drew with stand-in pipelines: {stats:?}"
        );
        let mut fresh = fresh_harness();
        fresh.settled_frame(frame_seconds(frame - 1));
        let (fresh_stats, expected) = fresh.frame(frame_seconds(frame));
        assert!(
            support::pipelines_settled(&fresh_stats),
            "the reference frame {frame} drew with stand-in pipelines: {fresh_stats:?}"
        );
        support::assert_same_bytes(
            &format!("frame {frame}"),
            FRAME_WIDTH,
            &expected.pixels,
            &actual.pixels,
        );
    }
}