wavyte 0.2.1

Programmatic video composition and rendering engine in Rust (CPU backend, ffmpeg MP4 encoding)
Documentation
use super::*;
use crate::{
    assets::store::PreparedAssetStore,
    compile::{CompositeOp, PixelFormat},
    foundation::core::{Canvas, Rgba8Premul},
    foundation::error::WavyteResult,
};

#[derive(Default)]
struct MockBackend {
    calls: Vec<&'static str>,
}

impl PassBackend for MockBackend {
    fn ensure_surface(&mut self, _id: SurfaceId, _desc: &SurfaceDesc) -> WavyteResult<()> {
        self.calls.push("ensure_surface");
        Ok(())
    }

    fn exec_scene(&mut self, _pass: &ScenePass, _assets: &PreparedAssetStore) -> WavyteResult<()> {
        self.calls.push("exec_scene");
        Ok(())
    }

    fn exec_offscreen(
        &mut self,
        _pass: &OffscreenPass,
        _assets: &PreparedAssetStore,
    ) -> WavyteResult<()> {
        self.calls.push("exec_offscreen");
        Ok(())
    }

    fn exec_composite(
        &mut self,
        _pass: &CompositePass,
        _assets: &PreparedAssetStore,
    ) -> WavyteResult<()> {
        self.calls.push("exec_composite");
        Ok(())
    }

    fn readback_rgba8(
        &mut self,
        _surface: SurfaceId,
        plan: &RenderPlan,
        _assets: &PreparedAssetStore,
    ) -> WavyteResult<FrameRGBA> {
        self.calls.push("readback_rgba8");
        Ok(FrameRGBA {
            width: plan.canvas.width,
            height: plan.canvas.height,
            data: vec![0; (plan.canvas.width * plan.canvas.height * 4) as usize],
            premultiplied: true,
        })
    }
}

#[test]
fn execute_plan_calls_in_expected_order() {
    let plan = RenderPlan {
        canvas: Canvas {
            width: 4,
            height: 3,
        },
        surfaces: vec![
            SurfaceDesc {
                width: 4,
                height: 3,
                format: PixelFormat::Rgba8Premul,
            },
            SurfaceDesc {
                width: 4,
                height: 3,
                format: PixelFormat::Rgba8Premul,
            },
        ],
        passes: vec![
            Pass::Scene(ScenePass {
                target: SurfaceId(1),
                ops: vec![],
                clear_to_transparent: true,
            }),
            Pass::Offscreen(OffscreenPass {
                input: SurfaceId(1),
                output: SurfaceId(1),
                fx: crate::effects::fx::PassFx::Blur {
                    radius_px: 0,
                    sigma: 1.0,
                },
            }),
            Pass::Composite(CompositePass {
                target: SurfaceId(0),
                ops: vec![CompositeOp::Over {
                    src: SurfaceId(1),
                    opacity: 1.0,
                }],
            }),
        ],
        final_surface: SurfaceId(0),
    };

    let mut backend = MockBackend::default();
    let comp = crate::Composition {
        fps: crate::Fps::new(30, 1).unwrap(),
        canvas: crate::Canvas {
            width: 1,
            height: 1,
        },
        duration: crate::FrameIndex(1),
        assets: std::collections::BTreeMap::new(),
        tracks: vec![],
        seed: 0,
    };
    let store = PreparedAssetStore::prepare(&comp, ".").unwrap();
    let out = execute_plan(&mut backend, &plan, &store).unwrap();
    assert_eq!(out.width, 4);
    assert_eq!(out.height, 3);
    assert!(out.premultiplied);
    assert_eq!(
        backend.calls,
        vec![
            "ensure_surface",
            "ensure_surface",
            "exec_scene",
            "exec_offscreen",
            "exec_composite",
            "readback_rgba8",
        ]
    );
}

#[test]
fn execute_plan_returns_final_frame() {
    let plan = RenderPlan {
        canvas: Canvas {
            width: 2,
            height: 2,
        },
        surfaces: vec![SurfaceDesc {
            width: 2,
            height: 2,
            format: PixelFormat::Rgba8Premul,
        }],
        passes: vec![Pass::Scene(ScenePass {
            target: SurfaceId(0),
            ops: vec![crate::compile::DrawOp::FillPath {
                path: crate::foundation::core::BezPath::new(),
                transform: crate::foundation::core::Affine::IDENTITY,
                color: Rgba8Premul::from_straight_rgba(0, 0, 0, 0),
                opacity: 1.0,
                blend: crate::composition::model::BlendMode::Normal,
                z: 0,
            }],
            clear_to_transparent: true,
        })],
        final_surface: SurfaceId(0),
    };

    let mut backend = MockBackend::default();
    let comp = crate::Composition {
        fps: crate::Fps::new(30, 1).unwrap(),
        canvas: crate::Canvas {
            width: 1,
            height: 1,
        },
        duration: crate::FrameIndex(1),
        assets: std::collections::BTreeMap::new(),
        tracks: vec![],
        seed: 0,
    };
    let store = PreparedAssetStore::prepare(&comp, ".").unwrap();
    let out = execute_plan(&mut backend, &plan, &store).unwrap();
    assert_eq!(out.data.len(), 16);
}