cranpose-render-wgpu 0.9.21

GPU renderer for Cranpose scenes through wgpu
Documentation
use cranpose_render_common::{
    graph::{
        DrawCommandId, DrawPrimitiveNode, DrawRunNode, LayerNode, PrimitiveEntry, PrimitiveNode,
        PrimitivePhase, RenderGraph, RenderNode,
    },
    layer_transform::layer_transform_to_parent,
    style_shared::DrawPlacement,
};
use cranpose_ui_graphics::{
    Brush, Color, CommandRecording, CornerRadii, DrawPrimitive, DrawScope, DrawScopeDefault,
    GraphicsLayer, Point, Rect, Size, TileMode,
};

use crate::{shared_test_support, support};

const SIDE: u32 = 256;
const TOGGLE: &str = "CRANPOSE_SHAPE_VARIANTS";
const MAX_DIVERGING_BYTES: usize = 16;
const PAGE: Color = Color(0.02, 0.02, 0.05, 1.0);
const BAR: Color = Color(1.0, 1.0, 1.0, 0.45);
const RAMPS: [(Color, Color); 4] = [
    (Color(0.0, 0.0, 0.0, 1.0), Color(1.0, 1.0, 1.0, 1.0)),
    (Color(0.9, 0.3, 0.1, 1.0), Color(0.1, 0.2, 0.8, 1.0)),
    (Color(1.0, 1.0, 1.0, 0.9), Color(0.2, 0.9, 0.4, 0.35)),
    (Color(0.05, 0.05, 0.1, 1.0), Color(0.3, 0.25, 0.9, 1.0)),
];

fn full(side: f32) -> Rect {
    Rect {
        x: 0.0,
        y: 0.0,
        width: side,
        height: side,
    }
}

fn ramp(index: usize, rect: Rect) -> Brush {
    let (from, to) = RAMPS[index % RAMPS.len()];
    let colors = vec![from, to];
    match index % 6 {
        0 => Brush::vertical_gradient(colors, 0.0, rect.height),
        1 => Brush::horizontal_gradient(colors, 0.0, rect.width),
        2 => Brush::linear_gradient(colors),
        3 => Brush::vertical_gradient(colors, rect.height, 0.0),
        4 => Brush::vertical_gradient_stops(
            vec![(0.2, from), (0.8, to)],
            -rect.height * 0.5,
            rect.height * 1.5,
            TileMode::Clamp,
        ),
        _ => Brush::vertical_gradient_tiled(colors, -3.0, rect.height + 5.0, TileMode::Mirror),
    }
}

fn scene(side: f32, columns: usize, rows: usize) -> Vec<DrawPrimitive> {
    let mut scope = DrawScopeDefault::new(Size::new(side, side));
    scope.draw_rect_at(full(side), Brush::solid(PAGE));
    let (cell_width, cell_height) = (side / columns as f32, side / rows as f32);
    for index in 0..columns * rows {
        let rect = Rect {
            x: (index % columns) as f32 * cell_width + 1.37,
            y: (index / columns) as f32 * cell_height + 2.61,
            width: cell_width - 3.1,
            height: cell_height - 3.7,
        };
        if index % 2 == 0 {
            scope.draw_rect_at(rect, ramp(index, rect));
        } else {
            let radius = rect.width.min(rect.height) * 0.2;
            scope.draw_round_rect_at(rect, ramp(index, rect), CornerRadii::uniform(radius));
        }
        if index % 3 == 0 {
            scope.draw_rect_at(
                Rect {
                    x: rect.x + rect.width * 0.3,
                    y: rect.y + rect.height * 0.4,
                    width: rect.width * 0.2,
                    height: rect.height * 0.5,
                },
                Brush::solid(BAR),
            );
        }
    }
    let primitives = scope.into_primitives();
    let recording = CommandRecording::from_primitives(primitives.clone());
    let shaded = (0..recording.shapes().len())
        .filter_map(|index| recording.shapes().get(index))
        .filter(cranpose_ui_graphics::ShapeRecord::is_vertex_gradient)
        .count();
    assert_eq!(
        shaded,
        columns * rows,
        "every ramp of the scene must be one its vertices shade"
    );
    primitives
}

fn layer(node_id: Option<cranpose_core::NodeId>, children: Vec<RenderNode>) -> RenderGraph {
    RenderGraph::new(LayerNode {
        node_id,
        local_bounds: full(SIDE as f32),
        children,
        translated_content_context: true,
        ..LayerNode::default()
    })
}

fn arena_graph(primitives: Vec<DrawPrimitive>) -> RenderGraph {
    layer(
        None,
        vec![RenderNode::DrawRun(DrawRunNode::new(
            PrimitivePhase::BeforeChildren,
            primitives,
        ))],
    )
}

fn clipped_graph(primitives: Vec<DrawPrimitive>, clip: Rect) -> RenderGraph {
    let children = primitives
        .into_iter()
        .map(|primitive| {
            RenderNode::Primitive(PrimitiveEntry {
                phase: PrimitivePhase::BeforeChildren,
                node: PrimitiveNode::Draw(Box::new(DrawPrimitiveNode {
                    primitive,
                    clip: Some(clip),
                })),
            })
        })
        .collect();
    layer(None, children)
}

fn stored_graph(primitives: Vec<DrawPrimitive>) -> RenderGraph {
    layer(
        Some(support::STORED_RUN_NODE),
        vec![RenderNode::DrawRun(DrawRunNode::for_command(
            PrimitivePhase::BeforeChildren,
            Some(DrawCommandId {
                node_id: support::STORED_RUN_NODE,
                command_index: 0,
                placement: DrawPlacement::Behind,
            }),
            primitives,
        ))],
    )
}

fn turned_graph(primitives: Vec<DrawPrimitive>, degrees: f32) -> RenderGraph {
    let bounds = Rect {
        x: 0.0,
        y: 0.0,
        width: 160.0,
        height: 160.0,
    };
    let turn = GraphicsLayer {
        rotation_z: degrees,
        ..GraphicsLayer::default()
    };
    let turned = shared_test_support::layer_node(
        bounds,
        layer_transform_to_parent(bounds, Point::new(48.0, 48.0), &turn),
        turn,
        vec![RenderNode::DrawRun(DrawRunNode::new(
            PrimitivePhase::BeforeChildren,
            primitives,
        ))],
    );
    let mut page = DrawScopeDefault::new(Size::new(SIDE as f32, SIDE as f32));
    page.draw_rect_at(full(SIDE as f32), Brush::solid(PAGE));
    layer(
        None,
        vec![
            RenderNode::DrawRun(DrawRunNode::new(
                PrimitivePhase::BeforeChildren,
                page.into_primitives(),
            )),
            RenderNode::Layer(Box::new(turned)),
        ],
    )
}

fn vertex_and_fragment_frames(
    graph: &RenderGraph,
    scale: f32,
    in_place: bool,
) -> Option<(Vec<u8>, Vec<u8>)> {
    let mut renderer = match support::headless_renderer() {
        Ok(renderer) => renderer,
        Err(err) => {
            eprintln!("skipping vertex gradient parity: headless WGPU init failed: {err}");
            return None;
        }
    };
    let mut frame = |toggle: Option<&str>| {
        cranpose_render_wgpu::set_debug_toggle(TOGGLE, toggle);
        support::stable_settled_capture(&mut renderer, |renderer| {
            let captured =
                support::capture_graph_with_scale(renderer, graph.clone(), SIDE, SIDE, scale);
            let stats = renderer.last_frame_stats().expect("frame statistics");
            assert!(
                !in_place || stats.isolated_layer_renders == 0,
                "the turned layer must draw in place: {stats:?}"
            );
            captured
        })
    };
    let vertex = frame(None);
    let fragment = frame(Some("0"));
    cranpose_render_wgpu::set_debug_toggle(TOGGLE, None);
    Some((vertex, fragment))
}

fn assert_shades_as_the_fragment_gradient(
    name: &str,
    graph: RenderGraph,
    scale: f32,
    (in_place, max_bytes): (bool, usize),
) {
    let Some((vertex, fragment)) = vertex_and_fragment_frames(&graph, scale, in_place) else {
        return;
    };
    assert!(
        support::distinct_colors(&vertex) > 256,
        "{name}: the ramps must draw"
    );
    let bytes = vertex.iter().zip(&fragment).filter(|(a, b)| a != b).count();
    let worst = support::max_channel_delta(&vertex, &fragment);
    let differing = support::differing_pixels(SIDE, &vertex, &fragment);
    let account = if differing.is_empty() {
        String::new()
    } else {
        support::describe_differing(&differing)
    };
    eprintln!(
        "{name}: vertex-vs-fragment gradient differing {bytes} bytes worst {worst} {account}"
    );
    assert!(
        bytes <= max_bytes && worst <= 1,
        "{name}: {bytes} bytes diverged (worst {worst}), over a bound of {max_bytes} bytes \
         and one level; a ramp its vertices shade must land on the bytes the fragment \
         stage samples: {account}"
    );
}

#[test]
fn vertex_gradients_over_rects_and_rounded_rects_shade_as_the_fragment_gradient() {
    assert_shades_as_the_fragment_gradient(
        "rects",
        arena_graph(scene(SIDE as f32, 4, 3)),
        1.0,
        (false, MAX_DIVERGING_BYTES),
    );
}

#[test]
fn clipped_vertex_gradients_shade_as_the_fragment_gradient() {
    let clip = Rect {
        x: 20.5,
        y: 30.25,
        width: 180.0,
        height: 170.5,
    };
    assert_shades_as_the_fragment_gradient(
        "clipped",
        clipped_graph(scene(SIDE as f32, 4, 3), clip),
        1.0,
        (false, MAX_DIVERGING_BYTES),
    );
}

#[test]
fn scaled_vertex_gradients_shade_as_the_fragment_gradient() {
    assert_shades_as_the_fragment_gradient(
        "scaled",
        arena_graph(scene(SIDE as f32, 4, 3)),
        1.75,
        (false, MAX_DIVERGING_BYTES),
    );
}

#[test]
fn unaligned_gradient_geometry_keeps_fragment_sampling() {
    let mut graph = arena_graph(scene(SIDE as f32, 4, 3));
    graph.root.translated_content_context = false;
    assert_shades_as_the_fragment_gradient("unaligned", graph, 1.0, (false, 0));
}

#[test]
fn stored_vertex_gradients_shade_as_the_fragment_gradient() {
    assert_shades_as_the_fragment_gradient(
        "stored",
        stored_graph(scene(SIDE as f32, 8, 9)),
        1.0,
        (false, MAX_DIVERGING_BYTES),
    );
}

#[test]
fn turned_vertex_gradients_shade_as_the_fragment_gradient() {
    assert_shades_as_the_fragment_gradient(
        "turned",
        turned_graph(scene(160.0, 3, 3), 23.0),
        1.0,
        (true, 0),
    );
}