molgfx-render 0.3.3

The render graph, passes and the engine that drives a frame.
Documentation
use super::tests::camera;
use super::{DerivedCacheBudget, Engine, EngineConfig};
use crate::testing::MockDevice;
use molgfx_core::{
    AnalyticSphere, AnalyticTemplate, AttributeColumn, AttributeValues, InstanceBatch,
    PlaybackMode, PointBatch, PointGlyph, PointStyle, Relation, RelationBatch, RelationStyle,
    RigidInstance, RowDomain, RowEntityRef, Scene, SourceNamespace, SourceRows, SpatialAnchor,
    TemplatePartRef, TimeWarp, Timeline, VisualDescriptor, VisualProgramBuilder, VisualStyle,
};
use molgfx_math::{Quat, Vec3};
use std::sync::Arc;

fn template(namespace: u64) -> Arc<AnalyticTemplate> {
    Arc::new(
        AnalyticTemplate::new(
            Arc::from([AnalyticSphere {
                center: [0.5, 0.0, 0.0],
                radius: 0.25,
            }]),
            Arc::from([]),
            SourceRows::ordered(SourceNamespace(namespace), 1),
        )
        .unwrap_or_else(|error| panic!("template validates: {error}")),
    )
}

fn transforms(count: u16, y: f32) -> Arc<[RigidInstance]> {
    (0..count)
        .map(|row| {
            RigidInstance::new(Vec3::new(f32::from(row), y, 0.0), Quat::IDENTITY, 1.0)
                .unwrap_or_else(|error| panic!("transform validates: {error}"))
        })
        .collect::<Vec<_>>()
        .into()
}

fn warp() -> TimeWarp {
    TimeWarp::new(0.0, 0.0, 1.0, [0.0, 1.0], PlaybackMode::Clamp)
        .unwrap_or_else(|error| panic!("warp validates: {error}"))
}

#[test]
fn zero_budget_relations_sample_instance_frames_without_a_derived_allocation() {
    let mut scene = Scene::new();
    let instances = InstanceBatch::new(
        template(740),
        transforms(2, 0.0),
        SourceRows::ordered(SourceNamespace(741), 2),
    )
    .unwrap_or_else(|error| panic!("instances validate: {error}"));
    let instances = scene.add_instance_batch(instances);
    scene
        .add_relation_batch(
            RelationBatch::new(
                Arc::from([Relation {
                    start: SpatialAnchor::world(Vec3::ZERO)
                        .unwrap_or_else(|error| panic!("anchor validates: {error}")),
                    end: SpatialAnchor::template_part(TemplatePartRef::new(instances, 0, 0)),
                }]),
                SourceRows::ordered(SourceNamespace(742), 1),
                RelationStyle::default(),
            )
            .unwrap_or_else(|error| panic!("relations validate: {error}")),
        )
        .unwrap_or_else(|error| panic!("relations attach: {error}"));
    let mut timeline = Timeline::new();
    timeline
        .bind_instances(
            &mut scene,
            instances,
            transforms(2, 0.0),
            transforms(2, 2.0),
            warp(),
        )
        .unwrap_or_else(|error| panic!("timeline binds: {error}"));
    timeline
        .apply(&mut scene, 0.25)
        .unwrap_or_else(|error| panic!("timeline samples: {error}"));

    let config = EngineConfig {
        derived_cache: DerivedCacheBudget {
            cpu_bytes: 0,
            gpu_bytes: 0,
        },
        ..EngineConfig::default()
    };
    let mut engine = Engine::<MockDevice>::new(&config, None)
        .unwrap_or_else(|error| panic!("engine opens: {error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("first frame renders: {error}"));
    timeline
        .apply(&mut scene, 0.75)
        .unwrap_or_else(|error| panic!("timeline resamples: {error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("second frame renders: {error}"));

    assert_eq!(engine.derived_cache_usage().gpu_bytes, 0);
    let buffers = engine
        .device
        .log
        .buffers
        .lock()
        .unwrap_or_else(|error| panic!("buffer log locks: {error}"));
    assert!(
        !buffers
            .iter()
            .any(|(_, label, _)| { *label == "materialized generic instance timeline" })
    );
    drop(buffers);
    let dispatches = engine
        .device
        .log
        .dispatches
        .lock()
        .unwrap_or_else(|error| panic!("dispatch log locks: {error}"));
    assert_eq!(
        dispatches
            .iter()
            .filter(|groups| **groups == (1, 1, 1))
            .count(),
        10,
        "the rigid relation resolves again after the GPU timeline sample changes"
    );
}

#[test]
fn deformable_points_and_their_relations_share_one_gpu_timeline() {
    let mut scene = Scene::new();
    let start: Arc<[[f32; 3]]> = Arc::from([[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]]);
    let end: Arc<[[f32; 3]]> = Arc::from([[0.0, 0.2, 0.0], [1.0, -0.2, 0.0]]);
    let points = scene.add_point_batch(
        PointBatch::new(
            Arc::clone(&start),
            SourceRows::ordered(SourceNamespace(748), 2),
            PointGlyph::Sphere,
            PointStyle::default(),
        )
        .unwrap_or_else(|error| panic!("points validate: {error}")),
    );
    let anchor = |row| {
        SpatialAnchor::entity(RowEntityRef::new(RowDomain::Points(points), row))
            .unwrap_or_else(|error| panic!("anchor validates: {error}"))
    };
    scene
        .add_relation_batch(
            RelationBatch::new(
                Arc::from([Relation {
                    start: anchor(0),
                    end: anchor(1),
                }]),
                SourceRows::ordered(SourceNamespace(749), 1),
                RelationStyle::default(),
            )
            .unwrap_or_else(|error| panic!("relations validate: {error}")),
        )
        .unwrap_or_else(|error| panic!("relations attach: {error}"));
    let mut timeline = Timeline::new();
    timeline
        .bind_points(&mut scene, points, start, end, warp())
        .unwrap_or_else(|error| panic!("timeline binds: {error}"));
    timeline
        .apply(&mut scene, 0.25)
        .unwrap_or_else(|error| panic!("timeline samples: {error}"));

    let direct_config = EngineConfig {
        derived_cache: DerivedCacheBudget {
            cpu_bytes: 0,
            gpu_bytes: 0,
        },
        ..EngineConfig::default()
    };
    let mut direct = Engine::<MockDevice>::new(&direct_config, None)
        .unwrap_or_else(|error| panic!("direct engine opens: {error}"));
    direct
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("direct frame renders: {error}"));
    assert_eq!(direct.derived_cache_usage().gpu_bytes, 0);
    assert!(!has_buffer(&direct, "materialized generic point timeline"));
    assert_eq!(latest_point_alpha(&direct).to_bits(), 0.25f32.to_bits());

    let mut cached = Engine::<MockDevice>::new(&EngineConfig::default(), None)
        .unwrap_or_else(|error| panic!("cached engine opens: {error}"));
    cached
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("cached frame renders: {error}"));
    assert!(has_buffer(&cached, "materialized generic point timeline"));
    assert_eq!(cached.derived_cache_usage().gpu_bytes, 24);
    assert_eq!(latest_point_alpha(&cached).to_bits(), 0.0f32.to_bits());
}

fn has_buffer(engine: &Engine<MockDevice>, label: &'static str) -> bool {
    engine
        .device
        .log
        .buffers
        .lock()
        .unwrap_or_else(|error| panic!("buffer log locks: {error}"))
        .iter()
        .any(|(_, candidate, _)| *candidate == label)
}

fn latest_point_alpha(engine: &Engine<MockDevice>) -> f32 {
    let id = engine
        .device
        .log
        .buffers
        .lock()
        .unwrap_or_else(|error| panic!("buffer log locks: {error}"))
        .iter()
        .find(|(_, label, _)| *label == "generic point configuration")
        .map_or_else(|| panic!("point config exists"), |(id, _, _)| *id);
    let index = engine.device.log.writes.lock().ok().and_then(|writes| {
        writes
            .iter()
            .enumerate()
            .rev()
            .find_map(|(index, (buffer, _, _, _))| (*buffer == id).then_some(index))
    });
    let bytes = index.and_then(|index| {
        engine
            .device
            .log
            .write_payloads
            .lock()
            .ok()
            .and_then(|payloads| payloads.get(index).cloned())
    });
    let Some(bytes) = bytes else {
        panic!("point config is written")
    };
    f32::from_ne_bytes(
        bytes[32..36]
            .try_into()
            .unwrap_or_else(|error| panic!("alpha bytes: {error}")),
    )
}

#[test]
fn one_budget_is_shared_by_attribute_and_instance_materializations() {
    let mut scene = Scene::new();
    let points = scene.add_point_batch(
        PointBatch::new(
            Arc::from(vec![[0.0, 0.0, 0.0]; 4]),
            SourceRows::ordered(SourceNamespace(743), 4),
            PointGlyph::Sphere,
            PointStyle::default(),
        )
        .unwrap_or_else(|error| panic!("points validate: {error}")),
    );
    let domain = RowDomain::Points(points);
    let attribute = scene
        .add_attribute(
            AttributeColumn::new(
                domain,
                "animated",
                AttributeValues::Scalar(Arc::from([1.0; 4])),
            )
            .unwrap_or_else(|error| panic!("attribute validates: {error}")),
        )
        .unwrap_or_else(|error| panic!("attribute attaches: {error}"));
    let mut builder = VisualProgramBuilder::new();
    let value = builder
        .scalar_attribute(attribute)
        .unwrap_or_else(|error| panic!("attribute input builds: {error}"));
    builder
        .set_radius_scale(value)
        .unwrap_or_else(|error| panic!("radius output builds: {error}"));
    builder
        .set_opacity(value)
        .unwrap_or_else(|error| panic!("opacity output builds: {error}"));
    scene
        .set_domain_visual(
            domain,
            VisualDescriptor::new(VisualStyle::new(
                builder
                    .finish()
                    .unwrap_or_else(|error| panic!("program builds: {error}")),
            )),
        )
        .unwrap_or_else(|error| panic!("visual attaches: {error}"));

    let instances = scene.add_instance_batch(
        InstanceBatch::new(
            template(744),
            transforms(4, 0.0),
            SourceRows::ordered(SourceNamespace(745), 4),
        )
        .unwrap_or_else(|error| panic!("instances validate: {error}")),
    );
    let mut timeline = Timeline::new();
    timeline
        .bind_attribute(
            &mut scene,
            attribute,
            AttributeValues::Scalar(Arc::from([0.5; 4])),
            AttributeValues::Scalar(Arc::from([1.5; 4])),
            warp(),
        )
        .unwrap_or_else(|error| panic!("attribute timeline binds: {error}"));
    timeline
        .bind_instances(
            &mut scene,
            instances,
            transforms(4, 0.0),
            transforms(4, 1.0),
            warp(),
        )
        .unwrap_or_else(|error| panic!("instance timeline binds: {error}"));
    timeline
        .apply(&mut scene, 0.5)
        .unwrap_or_else(|error| panic!("timelines sample: {error}"));

    let config = EngineConfig {
        derived_cache: DerivedCacheBudget {
            cpu_bytes: 0,
            gpu_bytes: 128,
        },
        ..EngineConfig::default()
    };
    let mut engine = Engine::<MockDevice>::new(&config, None)
        .unwrap_or_else(|error| panic!("engine opens: {error}"));
    engine
        .render(&scene, &camera())
        .unwrap_or_else(|error| panic!("scene renders: {error}"));

    let usage = engine.derived_cache_usage();
    assert_eq!(usage.gpu_bytes, 16, "the first deterministic plan wins");
    assert!(usage.peak_gpu_bytes <= config.derived_cache.gpu_bytes);
    let buffers = engine
        .device
        .log
        .buffers
        .lock()
        .unwrap_or_else(|error| panic!("buffer log locks: {error}"));
    assert!(
        !buffers
            .iter()
            .any(|(_, label, _)| { *label == "materialized generic instance timeline" })
    );
}