cranpose-render-wgpu 0.1.164

WGPU renderer backend for Cranpose
Documentation
use std::sync::atomic::Ordering;

use cranpose_ui_graphics::{
    GRADIENT_BLUR_WGSL, GRADIENT_CUT_MASK_WGSL, GRADIENT_FADE_DST_OUT_WGSL, LIQUID_GLASS_WGSL,
    ROUNDED_ALPHA_MASK_WGSL, RuntimeShader,
};
use web_time::{Duration, Instant};

use super::{
    RuntimeShaderPipelineMode, ShaderDrawVariant, ShaderPipelineCache, ShaderPipelineFit,
    validate_runtime_shader_source,
};
use crate::{
    effect_renderer::EffectRenderer, pipeline::GPU_TEXT_BRUSH_EFFECT_SHADER,
    pipeline_compiler::PipelineCompiler,
};

const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
const SETTLE: Duration = Duration::from_secs(30);

fn cache(device: &wgpu::Device, compiler: PipelineCompiler) -> ShaderPipelineCache {
    let renderer = EffectRenderer::new(
        device,
        PipelineCompiler::inactive(),
        None,
        FORMAT,
        device.adapter_info().backend,
    );
    ShaderPipelineCache::new(
        device,
        compiler,
        None,
        device.adapter_info().backend,
        FORMAT,
        &renderer.effect_texture_bind_group_layout,
        &renderer.effect_uniform_bind_group_layout,
    )
}

fn split_shader() -> RuntimeShader {
    let mut shader = RuntimeShader::new(&format!(
        "{}\noverride RED: bool = false;\noverride SPLIT: i32 = 0;",
        valid_shader()
    ));
    shader.set_override("RED", 0.0);
    shader.set_draw_split(Some("SPLIT"));
    shader.set_specialization_exact(true);
    shader
}

fn builds(cache: &ShaderPipelineCache) -> (usize, usize) {
    (
        cache.factory.counters.modules.load(Ordering::Relaxed),
        cache.factory.counters.pipelines.load(Ordering::Relaxed),
    )
}

fn settle(cache: &mut ShaderPipelineCache, shader: &RuntimeShader) {
    let deadline = Instant::now() + SETTLE;
    loop {
        let pending = [
            ShaderDrawVariant::Whole,
            ShaderDrawVariant::Interior,
            ShaderDrawVariant::Rim,
        ]
        .into_iter()
        .any(|variant| {
            let (_, fit) = cache
                .get_or_create(shader, RuntimeShaderPipelineMode::Replace, variant)
                .expect("valid shader");
            fit == ShaderPipelineFit::Fallback
        });
        if !pending {
            return;
        }
        assert!(Instant::now() < deadline, "specializations never landed");
        std::thread::sleep(Duration::from_millis(5));
    }
}

#[test]
fn warm_pipeline_lookups_do_not_rebuild_constants() {
    let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
    let mut cache = cache(&device, PipelineCompiler::inactive());
    let shader = split_shader();
    for forced in [false, true, false] {
        cache.set_forced_flags(forced.then_some("RED").into_iter());
        for _ in 0..12 {
            for variant in [
                ShaderDrawVariant::Whole,
                ShaderDrawVariant::Interior,
                ShaderDrawVariant::Rim,
            ] {
                let (_, fit) = cache
                    .get_or_create(&shader, RuntimeShaderPipelineMode::Replace, variant)
                    .expect("valid shader");
                assert_eq!(fit, ShaderPipelineFit::Specialized);
            }
        }
    }
    assert_eq!(cache.pipelines.len(), 6);
    assert_eq!(builds(&cache), (1, 6), "one module, one build per variant");
}

#[test]
fn a_specialization_draws_with_the_general_pipeline_until_it_lands() {
    let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
    let mut cache = cache(&device, PipelineCompiler::spawn());
    let shader = split_shader();
    let mode = RuntimeShaderPipelineMode::Replace;
    let (general, fit) = cache
        .get_or_create(&shader, mode, ShaderDrawVariant::Interior)
        .expect("valid shader");
    assert_eq!(fit, ShaderPipelineFit::Fallback);
    let general = general.clone();
    let (again, fit) = cache
        .get_or_create(&shader, mode, ShaderDrawVariant::Rim)
        .expect("valid shader");
    assert_eq!(fit, ShaderPipelineFit::Fallback);
    assert!(*again == general, "both draws share the general pipeline");
    settle(&mut cache, &shader);
    for variant in [ShaderDrawVariant::Interior, ShaderDrawVariant::Rim] {
        let (specialized, fit) = cache
            .get_or_create(&shader, mode, variant)
            .expect("valid shader");
        assert_eq!(fit, ShaderPipelineFit::Specialized);
        assert!(*specialized != general, "{variant:?} has its own pipeline");
    }
    let (whole, fit) = cache
        .get_or_create(&shader, mode, ShaderDrawVariant::Whole)
        .expect("valid shader");
    assert_eq!(
        fit,
        ShaderPipelineFit::Specialized,
        "an override set is a specialization"
    );
    assert!(*whole != general);
    assert_eq!(
        builds(&cache),
        (1, 4),
        "the general pipeline plus three variants"
    );
}

#[test]
fn an_override_that_is_not_a_fold_compiles_inside_the_frame() {
    let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
    let mut cache = cache(&device, PipelineCompiler::spawn());
    let mut shader = split_shader();
    shader.set_specialization_exact(false);
    let (_, fit) = cache
        .get_or_create(
            &shader,
            RuntimeShaderPipelineMode::Replace,
            ShaderDrawVariant::Interior,
        )
        .expect("valid shader");
    assert_eq!(fit, ShaderPipelineFit::Specialized);
    assert_eq!(
        builds(&cache),
        (1, 1),
        "the requested variant itself was built, and nothing else"
    );
}

#[test]
fn a_warmed_general_pipeline_is_ready_before_its_first_draw() {
    let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
    let mut cache = cache(&device, PipelineCompiler::spawn());
    let shader = RuntimeShader::new(&valid_shader());
    let mode = RuntimeShaderPipelineMode::PremultipliedSrcOver;
    cache.warm(&shader, mode);
    cache.warm(&shader, mode);
    let deadline = Instant::now() + SETTLE;
    while !cache.ready(cache.key(&shader, mode, ShaderDrawVariant::Whole)) {
        assert!(Instant::now() < deadline, "the warm-up never finished");
        std::thread::sleep(Duration::from_millis(5));
    }
    assert_eq!(builds(&cache), (1, 1), "one warm-up per general pipeline");
    let (_, fit) = cache
        .get_or_create(&shader, mode, ShaderDrawVariant::Whole)
        .expect("valid shader");
    assert_eq!(fit, ShaderPipelineFit::General);
    assert_eq!(
        builds(&cache),
        (1, 1),
        "the draw found the warm-up's pipeline"
    );
}

#[test]
fn an_invalid_shader_disables_every_variant_once() {
    let (_lock, device, _queue) = crate::frame_graph::upload_test_device();
    let mut cache = cache(&device, PipelineCompiler::spawn());
    let mut shader = RuntimeShader::new("this is not wgsl");
    shader.set_override("RED", 1.0);
    let mode = RuntimeShaderPipelineMode::Replace;
    assert!(
        cache
            .get_or_create(&shader, mode, ShaderDrawVariant::Whole)
            .is_none()
    );
    assert!(
        cache
            .get_or_create(&shader, mode, ShaderDrawVariant::Whole)
            .is_none()
    );
    assert_eq!(
        builds(&cache),
        (1, 0),
        "validation ran once and built nothing"
    );
}

fn valid_shader() -> String {
    format!(
        "{}\n{}",
        cranpose_ui_graphics::RUNTIME_SHADER_PRELUDE_WGSL,
        r"@fragment
fn effect_fs(input: VertexOutput) -> @location(0) vec4<f32> {
return textureSample(input_texture, input_sampler, input.uv);
}
"
    )
}

#[test]
fn validator_accepts_valid_runtime_shader() {
    assert!(validate_runtime_shader_source(&valid_shader(), wgpu::Backend::Vulkan).is_ok());
}

#[test]
fn validator_rejects_invalid_wgsl() {
    let invalid = "this is not wgsl";
    assert!(validate_runtime_shader_source(invalid, wgpu::Backend::Vulkan).is_err());
}

#[test]
fn validator_rejects_missing_required_entry_points() {
    let missing_effect_fs = r"
@vertex
fn fullscreen_vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
let x = f32(i32(i & 1u) * 2 - 1);
let y = f32(i32(i >> 1u) * 2 - 1);
return vec4<f32>(x, y, 0.0, 1.0);
}
";
    assert!(validate_runtime_shader_source(missing_effect_fs, wgpu::Backend::Vulkan).is_err());
}

#[test]
fn validator_accepts_gl_portable_builtin_runtime_shaders() {
    for (name, source) in [
        ("gradient_blur", GRADIENT_BLUR_WGSL),
        ("gradient_cut_mask", GRADIENT_CUT_MASK_WGSL),
        ("rounded_alpha_mask", ROUNDED_ALPHA_MASK_WGSL),
        ("gradient_fade_dst_out", GRADIENT_FADE_DST_OUT_WGSL),
        ("liquid_glass", LIQUID_GLASS_WGSL),
        ("gpu_text_brush_effect", GPU_TEXT_BRUSH_EFFECT_SHADER),
    ] {
        let result = validate_runtime_shader_source(source, wgpu::Backend::Gl);
        assert!(
            result.is_ok(),
            "{name} should remain GL-portable: {}",
            result.err().unwrap_or_default()
        );
    }
}