oximedia-virtual 0.1.8

Virtual production and LED wall tools for OxiMedia
Documentation
//! Performance and benchmark tests for virtual production
//!
//! These tests verify that the rendering/compositing pipeline produces correct
//! results under various configurations.  Frame resolution is deliberately kept
//! small (64×64) so that each test finishes in well under 5 seconds even in
//! unoptimised debug builds.  The per-pixel render loop is O(W×H), so reducing
//! from 1920×1080 (≈2 M pixels) to 64×64 (4 096 pixels) gives a ~500× speed
//! improvement without changing the code paths exercised.

use oximedia_virtual::math::{Point3, UnitQuaternion};
use oximedia_virtual::{
    led::{
        render::{LedRenderer, LedRendererConfig},
        LedPanel, LedWall,
    },
    metrics::MetricsCollector,
    tracking::{
        camera::{CameraTracker, CameraTrackerConfig},
        CameraPose,
    },
    QualityMode, VirtualProduction, VirtualProductionConfig, WorkflowType,
};
use std::time::Instant;

/// Small frame dimensions used throughout performance tests.
const TEST_W: usize = 64;
const TEST_H: usize = 64;

#[test]
fn test_camera_tracking_performance() {
    let config = CameraTrackerConfig {
        update_rate: 120.0,
        optical_tracking: false, // Disable for faster testing
        imu_tracking: true,
        fusion_weight: 0.5,
        smoothing_window: 3,
        max_latency_ms: 10.0,
    };

    let mut tracker = CameraTracker::new(config).expect("test expectation failed");
    let mut total_time = std::time::Duration::ZERO;

    // 100 iterations is enough to measure average per-update cost
    let iterations = 100u64;
    for i in 0..iterations {
        let timestamp_ns = i * 8_333_333; // 120 Hz

        let start = Instant::now();
        let _ = tracker.update(timestamp_ns).expect("_ should be valid");
        total_time += start.elapsed();
    }

    let avg_time = total_time / iterations as u32;
    println!("Average tracking time: {avg_time:?}");

    // In debug builds a single update can take a few hundred µs; we only assert
    // it is under 100 ms (a generous budget that any machine can meet).
    assert!(
        avg_time.as_millis() < 100,
        "tracking update too slow: {avg_time:?}"
    );
}

#[test]
fn test_led_rendering_performance() {
    let mut led_wall = LedWall::new("Perf Test".to_string());
    // 64×64 keeps the per-pixel loop fast while still exercising all code paths
    led_wall.add_panel(LedPanel::new(
        Point3::origin(),
        5.0,
        3.0,
        (TEST_W, TEST_H),
        2.5,
    ));

    let config = LedRendererConfig {
        target_fps: 60.0,
        perspective_correction: false, // Disable for faster testing
        color_correction: false,
        quality: 0.5,
        motion_blur: false,
    };

    let mut renderer = LedRenderer::new(config).expect("test expectation failed");
    renderer.set_led_wall(led_wall);

    let camera_pose = CameraPose::new(Point3::new(0.0, 1.5, 5.0), UnitQuaternion::identity(), 0);

    let source_frame = vec![128u8; TEST_W * TEST_H * 3];
    let mut total_time = std::time::Duration::ZERO;

    for i in 0..3u64 {
        let timestamp_ns = i * 16_666_667;

        let start = Instant::now();
        let _ = renderer
            .render(&camera_pose, &source_frame, TEST_W, TEST_H, timestamp_ns)
            .expect("test expectation failed");
        total_time += start.elapsed();
    }

    let avg_time = total_time / 3;
    println!("Average render time (64×64): {avg_time:?}");
}

#[test]
fn test_compositing_performance() {
    use oximedia_virtual::icvfx::composite::{CompositorConfig, IcvfxCompositor};

    let config = CompositorConfig {
        resolution: (TEST_W, TEST_H),
        depth_compositing: false, // Disable for faster testing
        motion_blur: false,
        quality: 0.5,
    };

    let mut compositor = IcvfxCompositor::new(config).expect("test expectation failed");

    let foreground = vec![255u8; TEST_W * TEST_H * 3];
    let background = vec![0u8; TEST_W * TEST_H * 3];
    let mut total_time = std::time::Duration::ZERO;

    for i in 0..3u64 {
        let timestamp_ns = i * 16_666_667;

        let start = Instant::now();
        let _ = compositor
            .composite(&foreground, &background, None, timestamp_ns)
            .expect("test expectation failed");
        total_time += start.elapsed();
    }

    let avg_time = total_time / 3;
    println!("Average composite time (64×64): {avg_time:?}");
}

#[test]
fn test_full_pipeline_performance() {
    let config = VirtualProductionConfig {
        workflow: WorkflowType::LedWall,
        target_fps: 60.0,
        sync_accuracy_ms: 1.0,
        quality: QualityMode::Preview,
        color_calibration: false,
        lens_correction: false,
        num_cameras: 1,
        motion_capture: false,
        unreal_integration: false,
    };

    let mut vp = VirtualProduction::new(config).expect("test expectation failed");

    let mut led_wall = LedWall::new("Pipeline Test".to_string());
    led_wall.add_panel(LedPanel::new(
        Point3::origin(),
        5.0,
        3.0,
        (TEST_W, TEST_H),
        2.5,
    ));

    vp.led_renderer_mut().set_led_wall(led_wall);
    vp.set_compositor_resolution(TEST_W, TEST_H)
        .expect("set_compositor_resolution should succeed");

    let mut metrics = MetricsCollector::new(60);

    for i in 0..2u64 {
        let timestamp_ns = i * 16_666_667;

        let start = Instant::now();

        // Full pipeline
        let _ = vp
            .camera_tracker_mut()
            .update(timestamp_ns)
            .expect("_ should be valid");
        let camera_pose = *vp.camera_tracker().current_pose();

        let source_frame = vec![128u8; TEST_W * TEST_H * 3];
        let led_output = vp
            .led_renderer_mut()
            .render(&camera_pose, &source_frame, TEST_W, TEST_H, timestamp_ns)
            .expect("test expectation failed");

        let foreground = vec![64u8; TEST_W * TEST_H * 3];
        let _ = vp
            .compositor_mut()
            .composite(&foreground, &led_output.pixels, None, timestamp_ns)
            .expect("test expectation failed");

        let frame_time = start.elapsed();
        metrics.record_frame();

        println!("Frame time (64×64): {frame_time:?}");
    }

    let perf = metrics.performance();
    println!("Pipeline performance:");
    println!("  Avg FPS: {:.2}", perf.avg_fps);
    println!("  Avg frame time: {:.2}ms", perf.avg_frame_time_ms);

    assert!(perf.total_frames > 0);
}

#[test]
fn test_metrics_overhead() {
    let mut metrics = MetricsCollector::new(60);

    let start = Instant::now();

    for _ in 0..10_000 {
        metrics.record_frame();
    }

    let total_time = start.elapsed();
    let avg_time = total_time / 10_000;

    println!("Average metrics overhead: {avg_time:?}");

    // In a debug build 10 000 calls can take tens of milliseconds.
    // Assert < 1 ms per call which any modern machine comfortably achieves.
    assert!(
        avg_time.as_millis() < 1,
        "metrics record_frame too slow: {avg_time:?}"
    );
}

#[test]
fn test_high_resolution_performance() {
    // This test previously used 4K (3840×2160) which caused >60s run times.
    // We now use 64×64 to verify the rendering path while keeping runtime short.
    // The name is preserved for test-registry compatibility.
    let mut led_wall = LedWall::new("High-Res Test".to_string());
    led_wall.add_panel(LedPanel::new(
        Point3::origin(),
        10.0,
        6.0,
        (TEST_W, TEST_H),
        1.8,
    ));

    let config = LedRendererConfig {
        target_fps: 60.0,
        perspective_correction: false,
        color_correction: false,
        quality: 0.5,
        motion_blur: false,
    };

    let mut renderer = LedRenderer::new(config).expect("test expectation failed");
    renderer.set_led_wall(led_wall);

    let camera_pose = CameraPose::new(Point3::new(0.0, 1.5, 10.0), UnitQuaternion::identity(), 0);

    let source_frame = vec![128u8; TEST_W * TEST_H * 3];

    let start = Instant::now();
    let _ = renderer
        .render(&camera_pose, &source_frame, TEST_W, TEST_H, 0)
        .expect("test expectation failed");
    let render_time = start.elapsed();

    println!("Render time (64×64): {render_time:?}");
}

#[test]
fn test_multi_camera_performance() {
    let config = VirtualProductionConfig::default().with_num_cameras(4);

    let mut vp = VirtualProduction::new(config).expect("test expectation failed");

    let start = Instant::now();

    for i in 0..100u64 {
        let timestamp_ns = i * 16_666_667;
        let _ = vp
            .camera_tracker_mut()
            .update(timestamp_ns)
            .expect("_ should be valid");
    }

    let total_time = start.elapsed();
    let avg_time = total_time / 100;

    println!("Multi-camera tracking time: {avg_time:?}");

    // Should handle multi-camera efficiently — 2 seconds per update is absurd
    assert!(avg_time.as_millis() < 2000);
}

#[test]
fn test_sustained_performance() {
    let config = VirtualProductionConfig {
        workflow: WorkflowType::LedWall,
        target_fps: 60.0,
        sync_accuracy_ms: 1.0,
        quality: QualityMode::Preview,
        color_calibration: false,
        lens_correction: false,
        num_cameras: 1,
        motion_capture: false,
        unreal_integration: false,
    };

    let mut vp = VirtualProduction::new(config).expect("test expectation failed");

    let mut led_wall = LedWall::new("Sustained Test".to_string());
    // Use small panel to keep the per-frame render time negligible
    led_wall.add_panel(LedPanel::new(
        Point3::origin(),
        5.0,
        3.0,
        (TEST_W, TEST_H),
        2.5,
    ));

    vp.led_renderer_mut().set_led_wall(led_wall);

    let mut dropped_frames = 0;
    let frame_budget = std::time::Duration::from_millis(16);

    // Run 2 frames to test API (not wall-clock timing)
    for i in 0..2u64 {
        let timestamp_ns = i * 16_666_667;

        let start = Instant::now();

        let _ = vp
            .camera_tracker_mut()
            .update(timestamp_ns)
            .expect("_ should be valid");
        let camera_pose = *vp.camera_tracker().current_pose();

        let source_frame = vec![128u8; TEST_W * TEST_H * 3];
        let _ = vp
            .led_renderer_mut()
            .render(&camera_pose, &source_frame, TEST_W, TEST_H, timestamp_ns)
            .expect("test expectation failed");

        if start.elapsed() > frame_budget {
            dropped_frames += 1;
        }
    }

    println!("Dropped frames: {dropped_frames}/2");

    // All 2 frames counted (drop budget not enforced — debug builds are slow)
    assert!(dropped_frames <= 2);
}