cranpose-render-wgpu 0.1.164

WGPU renderer backend for Cranpose
Documentation
use std::rc::Rc;

use cranpose_render_common::{
    graph::{DrawCommandId, DrawRunNode, LayerNode, PrimitivePhase, RenderGraph, RenderNode},
    style_shared::DrawPlacement,
};
use cranpose_ui_graphics::{
    ArcRecordArgs, BlendMode, Brush, Color, CommandRecorder, CommandRecording, DrawScope,
    DrawScopeDefault, Point, Rect, Size, normalized_band,
};

use crate::support;

const SIDE: u32 = 256;
const COLUMNS: usize = 16;
const RECORDS: usize = COLUMNS * COLUMNS;

fn rotating_arcs(angle: f32, color: Color) -> RenderGraph {
    let mut scope = DrawScopeDefault::new(Size::new(SIDE as f32, SIDE as f32));
    let cell = SIDE as f32 / COLUMNS as f32;
    for index in 0..RECORDS {
        scope.draw_annular_sector(
            Brush::solid(color),
            Point::new(
                (index % COLUMNS) as f32 * cell + cell * 0.5,
                (index / COLUMNS) as f32 * cell + cell * 0.5,
            ),
            cell * 0.2,
            cell * 0.4,
            angle,
            1.5,
        );
    }
    recording_graph(scope.finish(), SIDE)
}

fn recording_graph(recording: CommandRecording, side: u32) -> RenderGraph {
    let recording = Rc::new(recording);
    let segments = recording.all_segments();
    RenderGraph::new(LayerNode {
        node_id: Some(support::STORED_RUN_NODE),
        local_bounds: Rect {
            x: 0.0,
            y: 0.0,
            width: side as f32,
            height: side as f32,
        },
        children: vec![RenderNode::DrawRun(DrawRunNode::for_command_shared(
            PrimitivePhase::BeforeChildren,
            Some(DrawCommandId {
                node_id: support::STORED_RUN_NODE,
                command_index: 0,
                placement: DrawPlacement::Behind,
            }),
            recording,
            segments,
        ))],
        ..LayerNode::default()
    })
}

fn large_arc_run(count: usize, phase: usize, scope_recorded: bool) -> RenderGraph {
    const COLUMNS: usize = 104;
    const CELL: f32 = 5.0;
    let mut scope = DrawScopeDefault::new(Size::new(520.0, 520.0));
    let mut serial = CommandRecorder::default();
    for index in 0..count {
        let brush = Brush::solid(if (index + phase).is_multiple_of(7) {
            Color(0.1, 0.7, 0.3, 1.0)
        } else {
            Color(0.8, 0.3, 0.1, 1.0)
        });
        let center = Point::new(
            (index % COLUMNS) as f32 * CELL + CELL * 0.5,
            (index / COLUMNS) as f32 * CELL + CELL * 0.5,
        );
        let start = phase as f32 * 0.375 + index as f32 * 0.125;
        let sweep = if (index + phase).is_multiple_of(29) {
            0.0
        } else {
            1.5
        };
        if scope_recorded {
            scope.draw_annular_sector(brush, center, 1.0, 2.0, start, sweep);
        } else {
            let args = ArcRecordArgs {
                brush: &brush,
                center,
                radius: 2.0,
                start_angle: start,
                sweep_angle: sweep,
                stroke: None,
                inner_radius: 1.0,
                blend_mode: BlendMode::SrcOver,
            };
            let geometry = normalized_band(&args);
            if !geometry.is_degenerate() {
                serial.push_scope_arc(&args, &geometry);
            }
        }
    }
    recording_graph(
        if scope_recorded {
            scope.finish()
        } else {
            serial.finish()
        },
        520,
    )
}

#[test]
fn large_rotating_runs_preserve_pixels_when_lengths_change() {
    let mut renderer = support::headless_renderer().expect("headless renderer");
    let frames = [(5_003, 0), (10_007, 1), (4_201, 2), (123, 3), (5_017, 4)];
    let actual: Vec<_> = frames
        .iter()
        .map(|&(count, phase)| {
            support::present_and_read(&mut renderer, 520, 520, large_arc_run(count, phase, true))
        })
        .collect();
    drop(renderer);
    for ((count, phase), pixels) in frames.into_iter().zip(actual) {
        let mut fresh = support::headless_renderer().expect("fresh renderer");
        let expected =
            support::present_and_read(&mut fresh, 520, 520, large_arc_run(count, phase, false));
        assert!(
            pixels == expected,
            "draw-scope frame {phase} with {count} inputs differs from fresh serial pixels"
        );
    }
}

#[test]
fn rotating_arcs_upload_motion_and_preserve_changed_pixels() {
    let mut renderer = support::headless_renderer().expect("headless renderer");
    let color = Color(0.8, 0.3, 0.1, 1.0);
    let before = support::present_and_read(&mut renderer, SIDE, SIDE, rotating_arcs(0.0, color));
    let after = support::present_and_read(&mut renderer, SIDE, SIDE, rotating_arcs(0.8, color));
    let stats = renderer.last_frame_stats().expect("frame stats");
    assert!(
        before != after,
        "a changed arc angle must change the picture"
    );
    assert!(
        stats.upload_bytes <= (RECORDS * 32 + 4096) as u64,
        "rotation must upload motion without resending unchanged geometry and paint: {} bytes",
        stats.upload_bytes,
    );
    drop(renderer);

    let mut fresh = support::headless_renderer().expect("fresh headless renderer");
    let expected = support::present_and_read(&mut fresh, SIDE, SIDE, rotating_arcs(0.8, color));
    assert!(
        after == expected,
        "the updated table must match a fresh render"
    );
    let recolored = support::present_and_read(
        &mut fresh,
        SIDE,
        SIDE,
        rotating_arcs(0.8, Color(0.1, 0.7, 0.3, 1.0)),
    );
    assert!(
        expected != recolored,
        "paint changes must invalidate the geometry and paint column"
    );
}