Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust SDK authoring

Minimal plugin type

#![allow(unused)]
fn main() {
use pixelflow_plugin_sdk::{
    ExecutorRequest, FilterCompatibility, FilterPlan, FilterRegistration, Frame, FrameExecutor,
    FrameRequest, MetadataKind, MetadataRegistration, PlanRequest, Plugin,
    RegistrationContext, Result, pixelflow_plugin,
};

fn plan_identity(request: PlanRequest<'_>) -> Result<FilterPlan> {
    let input = request.input_media()[0].clone();
    Ok(FilterPlan::new(input, FilterCompatibility::Preserve))
}

struct IdentityExecutor;

impl FrameExecutor for IdentityExecutor {
    fn prepare(&self, request: FrameRequest<'_>) -> Result<Frame> {
        request.input_frame(0, request.frame_number())
    }
}

fn create_identity_executor(
    _request: ExecutorRequest<'_>,
) -> Result<Box<dyn FrameExecutor>> {
    Ok(Box::new(IdentityExecutor))
}

#[derive(Default)]
pub struct SamplePlugin;

impl Plugin for SamplePlugin {
    fn name(&self) -> &'static str {
        "pixelflow-sample-plugin"
    }

    fn register(&self, registry: &mut RegistrationContext<'_>) -> Result<()> {
        registry.register_metadata(MetadataRegistration::new(
            "pixelflow/sample:enabled",
            MetadataKind::Bool,
        ))?;
        registry.register_filter(
            FilterRegistration::new("sample.identity", "pixelflow", "sample")
                .with_planner(plan_identity)
                .with_executor(create_identity_executor),
        )
    }
}

pixelflow_plugin!(SamplePlugin);
}

What register() does

register() declares filters and metadata keys through RegistrationContext.

In current SDK surface:

  • register_filter(FilterRegistration::new(...)) declares one filter name plus publisher and plugin namespaces.
  • PlanRequest exposes input media, including clip-level metadata constants, graph-node options, and the metadata schema.
  • Planner callbacks return FilterPlan, including output media, clip-level metadata constants, compatibility, dependency pattern, and concurrency class.
  • ExecutorRequest exposes input media, output media, graph-node options, metadata schema, and output_frame_builder() for fixed outputs.
  • FrameRequest exposes the output frame number and input_frame(input_index, frame_number) for upstream requests.
  • Frame allocation and plane access use the re-exported core frame APIs such as FrameBuilder, typed planes, RawPlane, and RawPlaneMut.
  • register_metadata(MetadataRegistration::new(...)) declares one metadata key and its MetadataKind.
  • Registration is synchronous. Host accepts or rejects each declaration during plugin load.

Private helper filters and graph expansion

Rust plugins can register private filters for helper stages that should be scheduled and cached by PixelFlow but not called directly from scripts. Mark helpers with FilterRegistration::private().

A public filter can register an expander with FilterRegistration::with_expander(expand_filter). During script graph construction, the expander receives the public call inputs and options through ExpansionRequest, adds private helper filters with ExpansionRequest::add_private_filter, and returns the output Clip. Use this for multi-stage filters whose intermediate frames should be visible to the scheduler.

Private filters are still normal graph nodes once an expander adds them. They need planners and executors just like public filters, and their media must be valid after source indexing and graph replanning.

Scheduling declarations

Filters default to same-frame, stateless scheduling. Temporal filters should return a FilterPlan with explicit scheduling metadata by calling with_schedule(dependencies, concurrency).

Use DependencyPattern::window(...), DependencyPattern::frame_map(...), or DependencyPattern::dynamic(...) to declare which input frames may be requested. Use ConcurrencyClass::frame_group_serial(group_size) for cycle-based filters where several adjacent output frames scan the same input cycle and should not run that cycle work concurrently. For example, a 5-in/4-out decimator can use frame_group_serial(4) to serialize output frames 0..3, 4..7, and so on.

By default, grouped serial scheduling also limits a node to two distinct active output groups at a time. This prevents speculative cycle leaders from overwhelming ordered single-lane sources. Use ConcurrencyClass::frame_group_serial_unbounded(group_size) only when the filter genuinely benefits from unbounded cross-group speculation and its upstream access pattern is safe for random concurrent requests.

Grouped scheduling only controls core scheduler concurrency. If a filter computes expensive cycle decisions, cache those decisions inside the executor until PixelFlow has a batch/shared-cycle executor API.

Clip-level metadata constants

Planner callbacks can publish clip invariants in their planned output media. Downstream planners read those values from PlanRequest::input_media() without rendering frame 0.

Register plugin metadata keys before writing constants, then validate writes with the request schema:

#![allow(unused)]
fn main() {
use pixelflow_plugin_sdk::{
    ErrorCategory, ErrorCode, FilterCompatibility, FilterPlan, MetadataKind,
    MetadataRegistration, MetadataValue, PixelFlowError, PlanRequest, RegistrationContext, Result,
};

const SUPER_PEL: &str = "zoomvtools/super:pel";

fn plan_super(request: PlanRequest<'_>) -> Result<FilterPlan> {
    let output = request.input_media()[0]
        .clone()
        .with_metadata(request.metadata_schema(), SUPER_PEL, MetadataValue::Int(2))?;
    Ok(FilterPlan::new(output, FilterCompatibility::Preserve))
}

fn plan_finest(request: PlanRequest<'_>) -> Result<FilterPlan> {
    let input = &request.input_media()[0];
    let Some(MetadataValue::Int(pel)) = input.metadata(SUPER_PEL) else {
        return Err(PixelFlowError::new(
            ErrorCategory::Format,
            ErrorCode::new("filter.missing_clip_metadata"),
            "finest requires upstream super metadata",
        ));
    };

    let _pel = pel;
    Ok(FilterPlan::new(input.clone(), FilterCompatibility::Preserve))
}

fn register_metadata(registry: &mut RegistrationContext<'_>) -> Result<()> {
    registry.register_metadata(MetadataRegistration::new(SUPER_PEL, MetadataKind::Int))
}
}

Use clip-level constants only for values that are invariant for the whole clip. Per-frame properties still belong on Frame metadata during execution.

Keep register() focused on capability declaration. Do not hand-write ABI tables or exported symbols.

Entry point export

pixelflow_plugin! exports pixelflow_plugin_entry_v2 and is the supported path.

Macro requires plugin type to implement:

  • Plugin
  • Default
  • 'static

SDK wraps entry-table export, planner callbacks, executor factory callbacks, and executor calls with panic guards so plugin panics do not unwind across FFI boundary.

Source of truth

Start with examples/sample-rust-plugin/src/lib.rs for smallest working plugin. Read crates/pixelflow-plugin-sdk/src/lib.rs for Plugin, pixelflow_plugin!, ABI versioning, and entry-point behavior.

Also useful while authoring:

  • crates/pixelflow-plugin-sdk/src/builders.rs for FilterRegistration and MetadataRegistration
  • crates/pixelflow-plugin-sdk/src/registration.rs for RegistrationContext
  • crates/pixelflow-plugin-sdk/tests/plugin_contracts.rs for SDK behavior and failure contracts

Practical authoring notes

  • Keep plugin name() stable. Host uses it for diagnostics.
  • Register plugin metadata keys before filters or scripts rely on them.
  • Keep planner callbacks deterministic and side-effect free. Sources are placeholder media during script evaluation and are replanned after reachable sources are indexed, so a planner may run more than once for the same graph node.
  • Use committed sample plugin as smallest working template, not handwritten FFI.
  • Return PixelFlowError for expected failures. Structured plugin errors cross the ABI boundary without unwinding.
  • If host rejects registration callback, SDK surfaces structured plugin error instead of silent success.