molgfx-render 0.3.4

The render graph, passes and the engine that drives a frame.
Documentation
use super::{IllustrationStyle, TemporalOptions, TemporalState};
use crate::{BackdropStyle, DisplayTransform};
use molgfx_math::{Camera, Mat4, Projection, Vec3};

fn camera() -> Camera {
    Camera {
        eye: Vec3::new(0.0, 0.0, 8.0),
        target: Vec3::ZERO,
        up: Vec3::Y,
        projection: Projection::Perspective {
            fov_y: 0.8,
            aspect: 1.0,
            near: 0.1,
            far: 100.0,
        },
    }
}

fn options(quality: bool) -> TemporalOptions {
    TemporalOptions {
        extent: [800, 800],
        reset: false,
        quality,
        publication: false,
        illustration: IllustrationStyle::default(),
        depth_cue: [0.0; 4],
        optics: [8.0, 0.0, 0.0, 0.0],
        motion_blur: [0.0; 4],
        atmosphere: crate::engine::backdrop::pack(
            BackdropStyle::default(),
            DisplayTransform::default(),
            false,
            [0.0; 4],
        ),
        lighting: crate::LightingEnvironment::default().packed(),
        shadow_view: Mat4::IDENTITY,
        shadow_projection: Mat4::IDENTITY,
        shadow_view_proj: Mat4::IDENTITY,
    }
}

#[test]
fn the_first_sample_rejects_history_and_the_second_reprojects_it() {
    let mut state = TemporalState::default();
    let first = state.prepare(&camera(), &options(false));
    let second = state.prepare(&camera(), &options(false));
    assert_eq!(first.temporal[0].to_bits(), 0.0f32.to_bits());
    assert_eq!(second.temporal[0].to_bits(), 1.0f32.to_bits());
    assert_ne!(first.proj, second.proj);
    assert_eq!(state.write_index(), 1);
}

#[test]
fn a_camera_cut_discards_the_previous_history() {
    let mut state = TemporalState::default();
    let _ = state.prepare(&camera(), &options(false));
    let mut cut = camera();
    cut.eye = Vec3::new(20.0, 0.0, 8.0);
    let after_cut = state.prepare(&cut, &options(false));
    assert_eq!(after_cut.temporal[0].to_bits(), 0.0f32.to_bits());
    assert_eq!(after_cut.temporal[3].to_bits(), 0.0f32.to_bits());
}

#[test]
fn any_camera_motion_blocks_quality_until_a_stable_frame() {
    let mut state = TemporalState::default();
    assert!(state.camera_changed(&camera()));
    let _ = state.prepare(&camera(), &options(true));
    assert!(!state.camera_changed(&camera()));
    let mut moved = camera();
    moved.target.x = 0.01;
    assert!(state.camera_changed(&moved));
}

#[test]
fn camera_motion_uses_an_unjittered_projection() {
    let mut state = TemporalState::default();
    let _ = state.prepare(&camera(), &options(false));
    let mut moved = camera();
    moved.eye.x += 0.01;
    let frame = state.prepare(&moved, &options(false));
    assert_eq!(frame.proj, moved.projection.matrix());
}

#[test]
fn caller_scheduling_stops_after_each_mode_reaches_its_sample_budget() {
    let mut realtime = TemporalState::default();
    for _ in 0..8 {
        let _ = realtime.prepare(&camera(), &options(false));
    }
    assert!(!realtime.needs_another_frame(8));

    let mut quality = TemporalState::default();
    for _ in 0..63 {
        let _ = quality.prepare(&camera(), &options(true));
    }
    assert!(quality.needs_another_frame(64));
    let _ = quality.prepare(&camera(), &options(true));
    assert!(!quality.needs_another_frame(64));
}

#[test]
fn a_small_camera_move_restarts_refinement_without_discarding_reprojectable_history() {
    let mut state = TemporalState::default();
    for _ in 0..8 {
        state.prepare(&camera(), &options(false));
    }
    let mut moved = camera();
    moved.eye.x += 0.01;
    let first = state.prepare(&moved, &options(false));
    assert_ne!(first.temporal[0].to_bits(), 0.0_f32.to_bits());
    assert!(state.needs_another_frame(8));
    for _ in 0..7 {
        state.prepare(&moved, &options(false));
    }
    assert!(!state.needs_another_frame(8));
    state.invalidate_convergence();
    assert!(state.needs_another_frame(8));
}

#[test]
fn convergence_is_a_four_sample_budget_reached_in_order_and_restarted_by_any_change() {
    // The convergence contract, stated once: a clean frame starts from zero
    // history, four samples converge, and any change that invalidates history
    // restarts the count rather than letting a stale prefix satisfy it.
    const BUDGET: u8 = 4;

    let mut state = TemporalState::default();
    let mut samples = 0_u8;
    while state.needs_another_frame(BUDGET) {
        let _ = state.prepare(&camera(), &options(false));
        samples = samples.saturating_add(1);
        assert!(samples <= BUDGET, "the budget bounds the sample count");
    }
    assert_eq!(samples, BUDGET, "four samples converge the image");

    // A scene edit clears history, so the next frame must not consider itself
    // converged.
    state.invalidate_convergence();
    assert!(state.needs_another_frame(BUDGET));

    // So does camera motion, without discarding reprojectable history.
    for _ in 0..BUDGET {
        let _ = state.prepare(&camera(), &options(false));
    }
    assert!(!state.needs_another_frame(BUDGET));
    let mut moved = camera();
    moved.eye.x += 0.05;
    let _ = state.prepare(&moved, &options(false));
    assert!(state.needs_another_frame(BUDGET));
}