cranpose-render-wgpu 0.9.21

GPU renderer for Cranpose scenes through wgpu
Documentation
use cranpose_render_common::{
    Renderer,
    graph::{
        DrawPrimitiveNode, PrimitiveEntry, PrimitiveNode, PrimitivePhase, ProjectiveTransform,
        RenderGraph, RenderNode,
    },
};
use cranpose_ui_graphics::{
    BlendMode, Brush, Color, DrawPrimitive, GraphicsLayer, LayerShape, Rect, RenderEffect,
    RoundedCornerShape, ShadowPrimitive,
};
use support::{
    glass_page::{
        BLUR_RADIUS, FRAME_HEIGHT, FRAME_WIDTH, GLASS_HEIGHT, GLASS_LEFT, GLASS_PITCH,
        GLASS_RADIUS, GLASS_TOP, GLASS_WIDTH, glass_shader,
    },
    solid_rect,
};

use crate::{shared_test_support, support};

const STALE_SHADOWS: usize = 9;
const BAND: Color = Color(0.85, 0.15, 0.10, 1.0);

fn rect(x: f32, y: f32, width: f32, height: f32) -> Rect {
    Rect {
        x,
        y,
        width,
        height,
    }
}

fn glass_layer(index: usize) -> RenderNode {
    glass_layer_at(index, GLASS_TOP)
}

fn glass_layer_at(index: usize, top: f32) -> RenderNode {
    RenderNode::Layer(Box::new(shared_test_support::layer_node(
        rect(0.0, 0.0, GLASS_WIDTH, GLASS_HEIGHT),
        ProjectiveTransform::translation(GLASS_LEFT + index as f32 * GLASS_PITCH, top),
        GraphicsLayer {
            backdrop_effect: Some(RenderEffect::blur(BLUR_RADIUS).then(glass_shader())),
            clip: true,
            shape: LayerShape::Rounded(RoundedCornerShape::uniform(GLASS_RADIUS)),
            ..GraphicsLayer::default()
        },
        vec![solid_rect(
            rect(14.0, 14.0, GLASS_WIDTH - 28.0, GLASS_HEIGHT - 28.0),
            Color::from_rgba_u8(255, 255, 255, 40),
        )],
    )))
}

fn drop_shadow(caster: Rect, blur_radius: f32) -> RenderNode {
    RenderNode::Primitive(PrimitiveEntry {
        phase: PrimitivePhase::BeforeChildren,
        node: PrimitiveNode::Draw(Box::new(DrawPrimitiveNode {
            primitive: DrawPrimitive::Shadow(ShadowPrimitive::Drop {
                shape: Box::new(DrawPrimitive::Rect {
                    rect: caster,
                    brush: Brush::solid(Color::BLACK),
                    stroke: None,
                }),
                cutout: None,
                blur_radius,
                blend_mode: BlendMode::SrcOver,
            }),
            clip: None,
        })),
    })
}

fn stale_page() -> RenderGraph {
    let mut children = support::striped_page(FRAME_WIDTH, FRAME_HEIGHT);
    for index in 0..STALE_SHADOWS {
        let width = 10.0 + index as f32 * 7.0;
        let height = 8.0 + index as f32 * 5.0;
        children.push(drop_shadow(
            rect(4.0 + index as f32 * 20.0, 20.0, width, height),
            4.0 + index as f32,
        ));
    }
    support::page_graph(FRAME_WIDTH, FRAME_HEIGHT, children)
}

fn glass_page_over(band: Option<Color>) -> RenderGraph {
    let mut children = support::striped_page(FRAME_WIDTH, FRAME_HEIGHT);
    if let Some(color) = band {
        children.push(solid_rect(
            rect(
                0.0,
                GLASS_TOP - 8.0,
                FRAME_WIDTH as f32,
                GLASS_HEIGHT + 16.0,
            ),
            color,
        ));
    }
    for index in 0..3 {
        children.push(glass_layer(index));
    }
    support::page_graph(FRAME_WIDTH, FRAME_HEIGHT, children)
}

fn glass_page() -> RenderGraph {
    glass_page_over(None)
}

#[test]
fn retained_texture_stats_count_the_offscreen_pool_once() {
    let Ok(mut renderer) = support::headless_renderer() else {
        eprintln!("skipping (headless WGPU init failed)");
        return;
    };
    support::capture_graph(&mut renderer, glass_page(), FRAME_WIDTH, FRAME_HEIGHT);
    let stats = renderer.last_frame_stats().expect("frame stats");

    assert!(
        stats.offscreen_pool_bytes > 0,
        "the glass frame uses pooled textures"
    );
    assert_eq!(
        stats.retained_texture_bytes,
        stats.offscreen_pool_bytes + stats.layer_cache_bytes,
        "retained texture totals include pooled textures and retained layer entries once"
    );
}

/// The glasses scrolled up by `offset`, so the frame's top edge clips their
/// captures more the further they go.
fn scrolled_glass_page(offset: f32) -> RenderGraph {
    let mut children = support::striped_page(FRAME_WIDTH, FRAME_HEIGHT);
    for index in 0..3 {
        children.push(glass_layer_at(index, GLASS_TOP - offset));
    }
    support::page_graph(FRAME_WIDTH, FRAME_HEIGHT, children)
}

const SCROLL_OFFSETS: [f32; 8] = [0.0, 7.0, 14.0, 21.0, 28.0, 35.0, 42.0, 49.0];

#[test]
fn a_repeated_frame_of_glasses_creates_no_transient_textures() {
    let Ok(mut renderer) = support::headless_renderer() else {
        eprintln!("skipping (headless WGPU init failed)");
        return;
    };
    renderer.scene_mut().graph = Some(stale_page());
    renderer
        .render_current_scene_to_texture(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("render should succeed");
    renderer.scene_mut().graph = Some(glass_page());
    let first = renderer
        .render_current_scene_to_texture(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("render should succeed");
    assert_eq!(
        first.offscreen_news, 3,
        "three blurred glasses of one stage resolve through one atlas, one scratch and one \
         result on their first frame, yet {} textures were created",
        first.offscreen_news
    );
    let second = renderer
        .render_current_scene_to_texture(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("render should succeed");
    assert_eq!(
        second.offscreen_news, 0,
        "the same page of glasses created {} transient textures on its second frame \
         (acquired {}), instead of reusing the first frame's",
        second.offscreen_news, second.offscreen_acquires
    );
}

#[test]
fn a_frame_through_reused_transients_matches_a_renderer_that_never_pooled() {
    let Ok(mut renderer) = support::headless_renderer() else {
        eprintln!("skipping (headless WGPU init failed)");
        return;
    };
    renderer.scene_mut().graph = Some(stale_page());
    renderer
        .render_current_scene_to_texture(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("render should succeed");
    support::capture_graph(&mut renderer, glass_page(), FRAME_WIDTH, FRAME_HEIGHT);
    let reused = support::capture_graph(
        &mut renderer,
        glass_page_over(Some(BAND)),
        FRAME_WIDTH,
        FRAME_HEIGHT,
    );
    let stats = renderer.last_frame_stats().expect("stats");
    assert_eq!(
        stats.offscreen_news, 0,
        "the band frame must resolve through reused transients for this to test reuse: {stats:?}"
    );
    let mut fresh = support::headless_renderer_beside_locked().expect("second headless renderer");
    fresh.scene_mut().graph = Some(glass_page_over(Some(BAND)));
    let reference = fresh
        .capture_frame(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("capture should succeed");
    support::assert_same_bytes(
        "glasses resolved through reused transients",
        FRAME_WIDTH,
        &reused.pixels,
        &reference.pixels,
    );
}

#[test]
fn glasses_scrolling_back_and_forth_under_the_top_edge_create_no_textures() {
    let Ok(mut renderer) = support::headless_renderer() else {
        eprintln!("skipping (headless WGPU init failed)");
        return;
    };
    renderer.scene_mut().graph = Some(stale_page());
    renderer
        .render_current_scene_to_texture(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("render should succeed");
    let mut render = |offset: f32| {
        renderer.scene_mut().graph = Some(scrolled_glass_page(offset));
        renderer
            .render_current_scene_to_texture(FRAME_WIDTH, FRAME_HEIGHT)
            .expect("render should succeed")
    };
    let first_sweep: u32 = SCROLL_OFFSETS
        .iter()
        .map(|offset| render(*offset).offscreen_news)
        .sum();
    assert!(
        first_sweep <= 7,
        "the first sweep created {first_sweep} textures, exceeding its allocation budget"
    );
    for offset in SCROLL_OFFSETS.iter().rev().chain(SCROLL_OFFSETS.iter()) {
        let stats = render(*offset);
        assert_eq!(
            stats.offscreen_news, 0,
            "scrolled {offset} px after a sweep, the captures created {} textures (acquired {}) \
             instead of reusing the sweep's",
            stats.offscreen_news, stats.offscreen_acquires
        );
    }
}

#[test]
fn a_new_scrolled_size_matches_fresh_then_reuses_its_exact_textures() {
    let Ok(mut renderer) = support::headless_renderer() else {
        eprintln!("skipping (headless WGPU init failed)");
        return;
    };
    support::capture_graph(
        &mut renderer,
        scrolled_glass_page(0.0),
        FRAME_WIDTH,
        FRAME_HEIGHT,
    );
    let offset = SCROLL_OFFSETS[5];
    let reused = support::capture_graph(
        &mut renderer,
        scrolled_glass_page(offset),
        FRAME_WIDTH,
        FRAME_HEIGHT,
    );
    let stats = renderer.last_frame_stats().expect("stats");
    assert!(
        stats.offscreen_news <= 1,
        "the first clipped frame exceeded its allocation budget: {stats:?}"
    );
    let mut fresh = support::headless_renderer_beside_locked().expect("second headless renderer");
    fresh.scene_mut().graph = Some(scrolled_glass_page(offset));
    let reference = fresh
        .capture_frame(FRAME_WIDTH, FRAME_HEIGHT)
        .expect("capture should succeed");
    support::assert_same_bytes(
        "first clipped frame versus a fresh renderer",
        FRAME_WIDTH,
        &reused.pixels,
        &reference.pixels,
    );

    let repeated = support::capture_graph(
        &mut renderer,
        scrolled_glass_page(offset),
        FRAME_WIDTH,
        FRAME_HEIGHT,
    );
    let repeated_stats = renderer.last_frame_stats().expect("repeated frame stats");
    assert_eq!(
        repeated_stats.offscreen_news, 0,
        "the repeated clipped frame should reuse exact-size textures: {repeated_stats:?}"
    );
    support::assert_same_bytes(
        "repeated clipped frame versus the fresh renderer",
        FRAME_WIDTH,
        &repeated.pixels,
        &reference.pixels,
    );
}