cranpose-render-wgpu 0.9.21

GPU renderer for Cranpose scenes through wgpu
Documentation
use cranpose_ui_graphics::{
    BRUSH_KIND_LINEAR, BlendMode, FRAGMENT_KIND_FILL, RecordLane, RecordSegment,
};

use super::{RunTier, SegmentClip, ShapeDepth, ShapePipelineKey, ShapeTurns, ShapeVariant};
use crate::ablation::ShapeAblation;

fn fill_segment(gradient: bool, vertex_gradient: bool, interiors: bool) -> RecordSegment {
    RecordSegment {
        lane: RecordLane::Shapes,
        start: 0,
        count: 1,
        blend: BlendMode::SrcOver,
        gradient,
        vertex_gradient,
        brushes: if gradient || vertex_gradient {
            1 << BRUSH_KIND_LINEAR
        } else {
            1
        },
        kinds: 1 << FRAGMENT_KIND_FILL,
        band_class: 0,
        interiors,
        occluders: false,
        bare_interiors: interiors,
    }
}

fn variant(segment: RecordSegment, clipped: bool) -> ShapeVariant {
    let clip = if clipped {
        SegmentClip::Tested
    } else {
        SegmentClip::Untested
    };
    ShapeVariant::of_segment(
        &segment,
        clip,
        ShapeAblation::default(),
        false,
        (true, true),
    )
}

fn key(segment: RecordSegment) -> ShapePipelineKey {
    ShapePipelineKey {
        blend_mode: BlendMode::SrcOver,
        tier: RunTier::Arena,
        variant: variant(segment, false),
        turns: ShapeTurns::None,
        depth: ShapeDepth::Off,
    }
}

#[test]
fn vertex_gradients_draw_on_the_solid_fill_entries_with_the_dither() {
    let dithered = variant(fill_segment(false, true, false), false);
    assert_eq!(
        dithered.entries(true),
        ("vs_record_dithered_fill", "fs_dithered_fill")
    );
    assert_eq!(
        dithered.entries(false),
        ("vs_record_solid", "fs_solid"),
        "turned records read their position and dither from the solid vectors"
    );
    assert_eq!(
        variant(fill_segment(false, true, false), true).entries(true),
        ("vs_record_solid", "fs_solid"),
        "a clipped batch dithers on the solid vectors too"
    );
    assert_eq!(
        variant(fill_segment(false, false, false), false).entries(true),
        ("vs_record_plain_fill", "fs_plain_fill"),
        "a batch without them keeps the plain fill"
    );
    assert_eq!(
        variant(fill_segment(true, false, false), false).entries(true),
        ("vs_record_gradient_fill", "fs_gradient_fill"),
        "a gradient sampled per fragment keeps its own batch"
    );
}

#[test]
fn a_joined_key_adds_the_dither_to_a_solid_batch_and_the_interior_test_to_a_gradient_one() {
    let solid = key(fill_segment(false, false, false));
    let dithered = key(fill_segment(false, true, false));
    assert_ne!(solid, dithered);
    assert_eq!(solid.joined(), dithered);
    assert_eq!(dithered.joined(), dithered);
    let untested = key(fill_segment(true, false, false));
    let tested = key(fill_segment(true, false, true));
    assert_ne!(untested, tested);
    assert_eq!(untested.joined(), tested);
    assert_eq!(tested.joined(), tested);
}

#[test]
fn vertex_shading_requires_an_aligned_placement_and_viewport_without_turns() {
    use cranpose_ui_graphics::Point;

    use crate::{
        geometry::SegmentTransform,
        render::{GpuRenderer, ViewportUniformParams},
        scene::{Placement, SnapAnchor},
    };

    let segment = fill_segment(false, true, false);
    for snapped in [false, true] {
        for offset in [[0.0, 0.0], [3.0, -2.0], [0.13, 0.0]] {
            for turns in [ShapeTurns::None, ShapeTurns::All, ShapeTurns::Mixed] {
                let placement = Placement::at(
                    Point::default(),
                    snapped.then(|| SnapAnchor::rigid(Point::default())),
                    None,
                );
                let viewport = ViewportUniformParams {
                    width: 256,
                    height: 256,
                    offset,
                    transform: SegmentTransform::IDENTITY,
                    origin: [0.0; 2],
                    depth_base: 0.0,
                };
                let key = GpuRenderer::run_pipeline_key(
                    &segment,
                    &placement,
                    RunTier::Arena,
                    ShapeAblation::default(),
                    turns,
                    (false, SegmentClip::Untested),
                    viewport,
                );
                let vertex = snapped && offset[0].fract() == 0.0 && turns == ShapeTurns::None;
                assert_eq!(key.variant.solid, vertex, "{snapped} {offset:?} {turns:?}");
                assert_eq!(key.variant.dither, vertex);
                if !vertex {
                    assert_eq!(
                        key.variant.entries(true),
                        ("vs_record_gradient_fill", "fs_gradient_fill")
                    );
                }
            }
        }
    }
}

/// The GL backend lowers each program with its constants applied and the
/// branches they decide removed. Every program a segment can select must
/// still lower to WebGL's GLSL ES 300 that way.
#[test]
fn every_shape_program_lowers_to_webgl_with_its_decided_branches_removed() {
    use cranpose_ui_graphics::{
        FRAGMENT_KIND_ARC, FRAGMENT_KIND_LINE, FRAGMENT_KIND_STROKE, FRAGMENT_KIND_TRAPEZOID,
    };

    let shader = crate::gles_lowering::ParsedShader::new(crate::shaders::SHADER)
        .unwrap_or_else(|error| panic!("shape.wgsl: {error}"));
    let webgl = crate::run_store::RunBufferMode {
        storage: false,
        trig_fill: false,
    };
    let fill = 1 << FRAGMENT_KIND_FILL;
    let kind_sets = [
        fill,
        1 << FRAGMENT_KIND_STROKE,
        1 << FRAGMENT_KIND_ARC,
        1 << FRAGMENT_KIND_LINE,
        1 << FRAGMENT_KIND_TRAPEZOID,
        fill | (1 << FRAGMENT_KIND_STROKE) | (1 << FRAGMENT_KIND_ARC),
        fill | (1 << FRAGMENT_KIND_LINE) | (1 << FRAGMENT_KIND_TRAPEZOID),
    ];
    let mut keys = Vec::new();
    for kinds in kind_sets {
        for (gradient, vertex_gradient) in [(false, false), (false, true), (true, false)] {
            for clip in [
                SegmentClip::Untested,
                SegmentClip::Tested,
                SegmentClip::Rounded,
            ] {
                for turns in [ShapeTurns::None, ShapeTurns::All, ShapeTurns::Mixed] {
                    let segment = RecordSegment {
                        kinds,
                        ..fill_segment(gradient, vertex_gradient, true)
                    };
                    let variant = ShapeVariant::of_segment(
                        &segment,
                        clip,
                        ShapeAblation::default(),
                        false,
                        (turns == ShapeTurns::None, true),
                    );
                    keys.push(ShapePipelineKey {
                        variant,
                        turns,
                        ..key(segment)
                    });
                }
            }
        }
    }
    for tier in [RunTier::Store, RunTier::Arena, RunTier::Either] {
        for depth in [ShapeDepth::Off, ShapeDepth::Tested, ShapeDepth::Interior] {
            keys.push(ShapePipelineKey {
                tier,
                depth,
                ..key(fill_segment(false, false, true))
            });
        }
    }
    let mut lowered = std::collections::HashSet::new();
    for key in keys {
        let program = key.program(webgl);
        let constants = naga::back::PipelineConstants::from_iter(
            program
                .constants
                .iter()
                .map(|&(name, value)| (name.to_string(), value)),
        );
        for (entry, stage) in [
            (program.vertex_entry, naga::ShaderStage::Vertex),
            (program.fragment_entry, naga::ShaderStage::Fragment),
        ] {
            let bits = program.constants.map(|(_, value)| value.to_bits());
            if lowered.insert((entry, bits)) {
                shader
                    .lower_to_gles(entry, stage, &constants)
                    .unwrap_or_else(|error| panic!("{entry} for {key:?}: {error}"));
            }
        }
    }
}