molgfx-render 0.1.1

The render graph, passes and the engine that drives a frame.
Documentation
use super::tests::{camera, engine, structure};
use molgfx_core::{
    AtomProperty, AtomPropertyMeaning, AtomSelection, ColorScheme, PropertyAppearance,
    RepresentationKind, ScalarFieldSemantics, ScalarRamp, Scene, VisualProgramBuilder, VisualStyle,
};
use std::sync::Arc;

fn property(
    owner: molgfx_core::StructureHandle,
    name: &'static str,
    values: [f32; 3],
) -> AtomProperty {
    AtomProperty::new(
        owner,
        name,
        Arc::from(values),
        AtomPropertyMeaning::Confidence,
        ScalarFieldSemantics::UncalibratedRank,
    )
    .unwrap_or_else(|error| panic!("{error}"))
}

#[test]
fn uncertainty_appearance_routes_atoms_through_transparency_and_tracks_its_property() {
    let source = structure();
    let mut scene = Scene::from_structure(&source).unwrap_or_else(|error| panic!("{error}"));
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture structure exists")
    };
    let selected = scene.add_selection(AtomSelection::All);
    let representation = scene
        .represent(selected, RepresentationKind::Spacefill)
        .unwrap_or_else(|error| panic!("{error}"));
    let confidence = scene
        .add_atom_property(property(owner, "confidence", [0.1, 0.5, 0.9]))
        .unwrap_or_else(|error| panic!("{error}"));
    let appearance = PropertyAppearance::confidence(confidence, [0.0, 1.0])
        .unwrap_or_else(|error| panic!("{error}"));
    let Some(representation) = scene.representation_mut(representation) else {
        panic!("representation resolves")
    };
    representation.appearance = Some(appearance);

    let mut engine = engine();
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    assert!(engine.scene_gpu.has_translucency());
    assert_eq!(engine.scene_gpu.atom_draws(false).count(), 0);
    assert_eq!(engine.scene_gpu.atom_draws(true).count(), 1);

    let before = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());
    scene
        .replace_atom_property(confidence, property(owner, "confidence", [0.9, 0.5, 0.1]))
        .unwrap_or_else(|error| panic!("{error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let after = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());
    assert!(after > before + 1, "dependent appearance records repack");
}

#[test]
fn replacing_one_property_recolors_only_its_dependent_slot() {
    let source = structure();
    let mut scene = Scene::from_structure(&source).unwrap_or_else(|error| panic!("{error}"));
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture structure exists")
    };
    let selected = scene.add_selection(AtomSelection::All);
    let representation = scene
        .represent(selected, RepresentationKind::Spacefill)
        .unwrap_or_else(|error| panic!("{error}"));
    let confidence = scene
        .add_atom_property(property(owner, "confidence", [0.1, 0.5, 0.9]))
        .unwrap_or_else(|error| panic!("{error}"));
    let Some(value) = scene.atom_property(confidence) else {
        panic!("confidence resolves")
    };
    let scheme = ColorScheme::property(confidence, value);
    let Some(representation) = scene.representation_mut(representation) else {
        panic!("representation resolves")
    };
    representation.color = scheme;

    let mut engine = engine();
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let before = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());

    scene
        .replace_atom_property(confidence, property(owner, "confidence", [0.9, 0.5, 0.1]))
        .unwrap_or_else(|error| panic!("{error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let after_recolor = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());
    assert!(after_recolor > before + 1, "dependent records are repacked");

    scene
        .add_atom_property(property(owner, "unrelated", [1.0, 2.0, 3.0]))
        .unwrap_or_else(|error| panic!("{error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let writes = engine
        .device
        .log
        .writes
        .lock()
        .unwrap_or_else(|error| panic!("{error}"));
    assert_eq!(
        writes.len() - after_recolor,
        1,
        "only frame uniforms change"
    );
}

#[test]
fn visual_properties_upload_once_for_all_dependent_slots() {
    let source = structure();
    let mut scene = Scene::from_structure(&source).unwrap_or_else(|error| panic!("{error}"));
    let Some((owner, _)) = scene.structures().next() else {
        panic!("fixture structure exists")
    };
    let selected = scene.add_selection(AtomSelection::All);
    let first = scene
        .represent(selected, RepresentationKind::Spacefill)
        .unwrap_or_else(|error| panic!("{error}"));
    let second = scene
        .represent(selected, RepresentationKind::Points)
        .unwrap_or_else(|error| panic!("{error}"));
    let confidence = scene
        .add_atom_property(property(owner, "confidence", [0.1, 0.5, 0.9]))
        .unwrap_or_else(|error| panic!("{error}"));
    let style = VisualStyle::color_by_property(confidence, ScalarRamp::sequential([0.0, 1.0]))
        .unwrap_or_else(|error| panic!("{error}"));
    for handle in [first, second] {
        let Some(representation) = scene.representation_mut(handle) else {
            panic!("representation resolves")
        };
        representation.visual = Some(style.clone());
    }
    let source_pointer = scene
        .atom_property(confidence)
        .map_or(0, |column| column.values().as_ptr() as usize);

    let mut engine = engine();
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let direct_writes = engine.device.log.writes.lock().map_or_else(
        |error| panic!("{error}"),
        |writes| {
            writes
                .iter()
                .filter(|(_, _, bytes, pointer)| *bytes == 12 && *pointer == source_pointer)
                .count()
        },
    );
    assert_eq!(direct_writes, 1, "one shared property upload is expected");

    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let steady_state_writes = engine.device.log.writes.lock().map_or_else(
        |error| panic!("{error}"),
        |writes| {
            writes
                .iter()
                .filter(|(_, _, bytes, pointer)| *bytes == 12 && *pointer == source_pointer)
                .count()
        },
    );
    assert_eq!(steady_state_writes, 1, "unchanged frames do not re-upload");
}

#[test]
fn visual_time_updates_only_the_presentation_uniform() {
    let source = structure();
    let mut scene = Scene::from_structure(&source).unwrap_or_else(|error| panic!("{error}"));
    let selected = scene.add_selection(AtomSelection::All);
    let handle = scene
        .represent(selected, RepresentationKind::Spacefill)
        .unwrap_or_else(|error| panic!("{error}"));
    let mut builder = VisualProgramBuilder::new();
    let time = builder
        .time()
        .unwrap_or_else(|error| panic!("time input should build: {error}"));
    builder
        .set_opacity(time)
        .unwrap_or_else(|error| panic!("opacity should build: {error}"));
    let style = VisualStyle::new(
        builder
            .finish()
            .unwrap_or_else(|error| panic!("program should build: {error}")),
    );
    let Some(representation) = scene.representation_mut(handle) else {
        panic!("representation resolves")
    };
    representation.visual = Some(style);

    let mut engine = engine();
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let before = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());
    scene
        .set_presentation_time(0.5)
        .unwrap_or_else(|error| panic!("{error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let after = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());
    assert_eq!(after - before, 2, "frame and visual uniforms change");
}

#[test]
fn uniform_visual_parameter_updates_only_the_resolved_config_without_rebinding() {
    let source = structure();
    let mut scene = Scene::from_structure(&source).unwrap_or_else(|error| panic!("{error}"));
    let selected = scene.add_selection(AtomSelection::All);
    let mut builder = VisualProgramBuilder::new();
    let (_, opacity) = builder
        .scalar_parameter(0.25)
        .unwrap_or_else(|error| panic!("parameter should build: {error}"));
    builder
        .set_opacity(opacity)
        .unwrap_or_else(|error| panic!("opacity should build: {error}"));
    let style = VisualStyle::new(
        builder
            .finish()
            .unwrap_or_else(|error| panic!("program should build: {error}")),
    );
    let handle = scene
        .represent(
            selected,
            molgfx_core::Representation::spacefill().visual(style),
        )
        .unwrap_or_else(|error| panic!("representation should build: {error}"));
    let mut engine = engine();
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));
    let before = engine
        .device
        .log
        .writes
        .lock()
        .map_or_else(|error| panic!("{error}"), |writes| writes.len());

    scene
        .set_visual_scalar_parameter(handle, 0, 0.75)
        .unwrap_or_else(|error| panic!("parameter should update: {error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("{error}"));

    let writes = engine
        .device
        .log
        .writes
        .lock()
        .unwrap_or_else(|error| panic!("{error}"));
    let changed = &writes[before..];
    assert_eq!(
        changed.len(),
        2,
        "only frame and the once-resolved uniform config change: {changed:?}"
    );
    assert_eq!(
        changed
            .iter()
            .filter(|(_, _, bytes, _)| *bytes == 16)
            .count(),
        0,
        "a uniform program has no per-entity or fragment parameter upload"
    );
}