molgfx-render 0.3.0

The render graph, passes and the engine that drives a frame.
Documentation
use super::{
    all_stages, atom_cull_entries, bond_cull_entries, quality_entries, relation_cull_entries,
    relation_resolve_entries, representation_entries, storage_counts, validate_storage_limit,
    visual_cull_entries,
};
use molgfx_gpu::{GpuError, ShaderStages};

const PORTABLE_COMPUTE_LIMIT: u32 = 8;
const PORTABLE_FRAGMENT_LIMIT: u32 = 8;

#[test]
fn representation_storage_bindings_fit_every_portable_stage() {
    let counts = storage_counts(&representation_entries());

    assert_eq!(counts.fragment, PORTABLE_FRAGMENT_LIMIT);
    assert!(counts.vertex <= PORTABLE_FRAGMENT_LIMIT);
    assert!(counts.fragment <= PORTABLE_FRAGMENT_LIMIT);
    assert!(counts.compute <= PORTABLE_FRAGMENT_LIMIT);
}

#[test]
fn surface_motion_reads_previous_coordinates_in_the_fragment_stage_without_fragment_bonds() {
    let entries = representation_entries();
    assert!(entries.iter().any(|entry| {
        entry.binding == 13
            && entry.visibility == ShaderStages::VERTEX.union(ShaderStages::FRAGMENT)
    }));
    assert!(
        entries
            .iter()
            .any(|entry| { entry.binding == 3 && entry.visibility == ShaderStages::VERTEX })
    );
}

#[test]
fn quality_storage_bindings_fit_the_portable_fragment_limit() {
    let counts = storage_counts(&quality_entries());

    assert_eq!(counts.fragment, PORTABLE_FRAGMENT_LIMIT);
    assert!(counts.fragment <= PORTABLE_FRAGMENT_LIMIT);
    assert_eq!(counts.vertex, 0);
    assert_eq!(counts.compute, 0);
}

#[test]
fn compute_culling_uses_but_does_not_exceed_the_portable_limit() {
    for counts in [
        storage_counts(&atom_cull_entries()),
        storage_counts(&bond_cull_entries()),
        storage_counts(&visual_cull_entries()),
    ] {
        assert!(counts.compute <= PORTABLE_COMPUTE_LIMIT);
        assert_eq!(counts.fragment, 0);
        assert_eq!(counts.vertex, 0);
    }
}

#[test]
fn dynamic_relation_resolution_uses_only_its_homogeneous_sources() {
    let counts = storage_counts(&relation_resolve_entries());

    assert_eq!(
        counts.compute, 6,
        "the maximal rigid/rigid stream needs two timeline sources per endpoint"
    );
    assert!(counts.compute <= PORTABLE_COMPUTE_LIMIT);
    assert_eq!(counts.vertex, 0);
    assert_eq!(counts.fragment, 0);
}

#[test]
fn relation_culling_stays_well_below_the_portable_storage_limit() {
    let counts = storage_counts(&relation_cull_entries());

    assert_eq!(counts.compute, 8);
    assert!(counts.compute <= PORTABLE_COMPUTE_LIMIT);
    assert_eq!(counts.vertex, 0);
    assert_eq!(counts.fragment, 0);
}

#[test]
fn layout_contract_returns_a_typed_error_before_backend_validation() {
    let error = validate_storage_limit("test representation", &representation_entries(), 7);

    assert!(matches!(
        error,
        Err(GpuError::LimitExceeded {
            resource: "test representation",
            limit: 7
        })
    ));
}

#[test]
fn every_representation_binding_is_declared_in_both_layouts() {
    // A slot builds one group2 and binds it to whichever pipeline a draw
    // selects, so a binding present in only one layout is a validation failure
    // the moment that draw is recorded in that mode.
    let representation: Vec<u32> = representation_entries()
        .iter()
        .map(|entry| entry.binding)
        .collect();
    assert_eq!(
        representation,
        vec![
            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19, 20
        ],
        "the representation layout's bindings are fixed by the group2 a slot builds"
    );
    // The quality layout reuses the same group2, so it must declare every
    // binding that group carries. Its only addition is the quality hierarchy,
    // which no raster draw reads.
    for entry in quality_entries() {
        // A quality frame binds a smaller group2 than a raster frame, so it
        // declares a subset. Its one addition is the shared quality hierarchy,
        // which no raster draw reads.
        assert!(
            entry.binding == 21 || representation.contains(&entry.binding),
            "the quality layout binds {} which the representation layout does not declare; \
             a slot builds one group2 and binds it in both modes",
            entry.binding
        );
    }
}

#[test]
fn the_uniform_only_bindings_are_uniform_and_not_counted_as_storage() {
    // The visual program and the colour scheme block are a uniform and a small
    // palette; neither belongs in the storage-buffer budget, which is fully
    // spent on the buffers that need it.
    let entries = representation_entries();
    let fragment_program = entries.iter().find(|entry| entry.binding == 17);
    assert!(
        fragment_program.is_some_and(|entry| {
            entry.visibility == ShaderStages::FRAGMENT
                && matches!(entry.ty, molgfx_gpu::BindingType::Uniform)
        }),
        "the visual program is a fragment uniform: only that stage interprets one"
    );
    let color = entries.iter().find(|entry| entry.binding == 18);
    assert!(
        color.is_some_and(|entry| {
            // The vertex stage resolves an atom's colour, because it writes the
            // per-instance payload the fragment stage shades.
            entry.visibility == all_stages() && matches!(entry.ty, molgfx_gpu::BindingType::Uniform)
        }),
        "the colour scheme block is a uniform every drawing stage can read"
    );
}