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.PlanRequestexposes 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. ExecutorRequestexposes input media, output media, graph-node options, metadata schema, andoutput_frame_builder()for fixed outputs.FrameRequestexposes the output frame number andinput_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, andRawPlaneMut. register_metadata(MetadataRegistration::new(...))declares one metadata key and itsMetadataKind.- 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:
PluginDefault'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.rsforFilterRegistrationandMetadataRegistrationcrates/pixelflow-plugin-sdk/src/registration.rsforRegistrationContextcrates/pixelflow-plugin-sdk/tests/plugin_contracts.rsfor 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
PixelFlowErrorfor 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.