Idiomatic, thin Rust bindings over the funnelcake SIMD YUV scaler and HDR
tone-mapper. No bindgen/libclang — the FFI structs are #[repr(C)], so the
compiler lays them out per the platform C ABI to match funnelcake.h
automatically. [Frame]/[HdrFrame] allocate input planes with the 32-byte
alignment the SIMD kernels need (via std::alloc), the scaler types wrap the
create/run/free lifecycle with Drop, and output views borrow the scaler so
the "valid until next run" rule is enforced by the borrow checker.
- A stable Rust toolchain (
cargo/rustc). Tested on 1.96. - The core static library, built by
make lib.
make lib # builds libfunnelcake.a at the repo root
make bindings-rust # cargo build
make test-rust # cargo testmake bindings-rust/make test-rust are opt-in — a plain make never touches
Rust. build.rs statically links the in-tree libfunnelcake.a (which contains
both the scaler API and the binding helpers); there are no crates.io
dependencies, so the build works offline.
The trial links
../../libfunnelcake.aby path from the crate. Publishing a standalone crate (with the native library bundled or discovered) is future work.
The SIMD kernels require 32-byte-aligned planes and 32-byte-aligned strides.
Frame::new(w, h) / HdrFrame::new(w, h, fmt) allocate exactly that, and the
scaler constructors derive matching source strides from the width — so a frame
and a scaler created with the same dimensions always line up. If a step ever
runs unaligned it silently drops to the scalar path (visible as
output.fallback == true).
use funnelcake::{Frame, Scaler, ScalerConfig, SCALE_2X};
let mut frame = Frame::new(1920, 1080); // aligned I420 input
frame.y_mut().copy_from_slice(&src_y);
frame.u_mut().copy_from_slice(&src_u);
frame.v_mut().copy_from_slice(&src_v);
let mut scaler = Scaler::new(&ScalerConfig::new(1920, 1080, SCALE_2X))?;
scaler.run(&frame);
if let Some(out) = scaler.output(SCALE_2X) { // 960x540
let _y: &[u8] = out.y(); // borrows `scaler`; valid until next run/drop
use_planes(out.y(), out.u(), out.v(), out.y_stride);
}
# Ok::<(), funnelcake::Error>(())A single scaler can request several steps from one family
(SCALE_2X | SCALE_4X | ... or SCALE_1_5X | SCALE_3X | ...) and an upscale
cascade (upscale_flags, a contiguous prefix such as UPSCALE_2X | UPSCALE_4X,
optionally with upscale_tail_1_5x). All requested outputs are produced in one
run.
Because Output borrows &self and run takes &mut self, this fails to
compile — you cannot hold a view across a re-run:
let out = scaler.output(SCALE_2X).unwrap();
scaler.run(&frame); // error: cannot borrow `scaler` as mutable
let _ = out.y(); // ...while `out` still borrows ituse funnelcake::*;
let mut frame = HdrFrame::new(3840, 2160, PixelFormat::I010);
// fill frame.y_mut(), frame.u_mut(), frame.v_mut() (&mut [u16]) ...
let cfg = HdrConfig {
src_width: 3840,
src_height: 2160,
format: PixelFormat::I010,
transfer: Transfer::Pq,
flags: SCALE_2X,
hdr_flags: SCALE_2X, // produce a 10-bit downscaled copy
sdr_flags: SCALE_2X, // and a tone-mapped 8-bit copy
tonemap_1x: true, // plus a 1:1 tone-mapped SDR copy
tonemap: TonemapConfig { curve: TonemapCurve::Bt2390, ..Default::default() },
..Default::default()
};
let mut scaler = HdrScaler::new(&cfg)?;
scaler.run(&frame);
let hdr = scaler.hdr_output(SCALE_2X); // Option<HdrOutput> (&[u16] planes)
let sdr = scaler.sdr_output(SCALE_2X); // Option<Output> (tone-mapped &[u8])
let one = scaler.tonemap_1x_output(); // Option<Output> (8-bit, source res)
# Ok::<(), funnelcake::Error>(())For semi-planar input (PixelFormat::P010/P210), fill u_mut() with the
interleaved UV plane; v_mut() returns None. 4:2:2 formats (I210/P210)
are accepted and decimated to 4:2:0 internally. A default TonemapConfig is the
Hable curve, 1000-nit peak, 100-nit target, and limited range — matching the C
defaults. Set curve: TonemapCurve::Custom with a 1024-byte custom_lut to
supply your own.
All items are at the crate root. Strides are in bytes (even for 10-bit planes, where a sample is 2 bytes).
pub fn simd_available() -> bool;True if the vectorized kernels will run here; when false, every fallback is
true and a scalar warning is expected.
Bitmask u32 consts (combine with |):
- Downscale:
SCALE_1_5X SCALE_3X SCALE_6X SCALE_12X(thirds),SCALE_2X SCALE_4X SCALE_8X SCALE_16X(pow2),SCALE_THIRDS_MASK,SCALE_POW2_MASK. One family per scaler. - Upscale:
UPSCALE_2X UPSCALE_4X UPSCALE_8X UPSCALE_16X UPSCALE_32X(contiguous prefix only). - Options:
OPT_NO_CROP,OPT_NO_FALLBACK.
Enums (all #[repr(i32)], Default):
PixelFormat { I010, P010, I210, P210 }, Transfer { Pq, Hlg },
Range { Limited, Full }, TonemapCurve { Hable, Reinhard, Bt2390, Custom }.
pub fn Frame::new(width: i32, height: i32) -> Frame; // panics if <= 0
pub fn width(&self) -> i32; pub fn height(&self) -> i32;
pub fn y_stride(&self) -> i32; pub fn uv_stride(&self) -> i32;
pub fn y_mut(&mut self) -> &mut [u8]; // fill these before run
pub fn u_mut(&mut self) -> &mut [u8];
pub fn v_mut(&mut self) -> &mut [u8];
// dropping the Frame frees the planespub fn HdrFrame::new(width: i32, height: i32, format: PixelFormat) -> HdrFrame;
pub fn width/height/format/y_stride/uv_stride(&self) -> ...;
pub fn y_mut(&mut self) -> &mut [u16];
pub fn u_mut(&mut self) -> &mut [u16]; // interleaved UV for P010/P210
pub fn v_mut(&mut self) -> Option<&mut [u16]>; // None for P010/P210pub struct ScalerConfig { // derives Default
pub src_width: i32, pub src_height: i32,
pub flags: u32, pub upscale_flags: u32,
pub upscale_tail_1_5x: bool, pub options: u32,
}
pub fn ScalerConfig::new(src_width: i32, src_height: i32, flags: u32) -> ScalerConfig;
pub fn Scaler::new(cfg: &ScalerConfig) -> Result<Scaler, Error>;
pub fn warnings(&self) -> Warnings;
pub fn run(&mut self, frame: &Frame);
pub fn effective_width(&self) -> i32; pub fn effective_height(&self) -> i32;
pub fn achieved_flags(&self) -> u32;
pub fn output(&self, flag: u32) -> Option<Output<'_>>;
pub fn upscale_output(&self, flag: u32) -> Option<Output<'_>>;
pub fn upscale_tail(&self) -> Option<Output<'_>>;
// dropping the Scaler frees its output bufferspub struct TonemapConfig { // derives Default (Hable, default nits, limited)
pub curve: TonemapCurve, pub peak_nits: i32, pub target_nits: i32,
pub src_range: Range, pub dst_range: Range, pub custom_lut: Option<Vec<u8>>,
}
pub struct HdrConfig { // derives Default
pub src_width: i32, pub src_height: i32,
pub format: PixelFormat, pub transfer: Transfer,
pub flags: u32, pub hdr_flags: u32, pub sdr_flags: u32, pub options: u32,
pub tonemap_1x: bool, pub tonemap: TonemapConfig,
pub upscale_flags: u32, pub upscale_tail_1_5x: bool,
pub upscale_sdr_flags: u32, pub upscale_sdr_tail_1_5x: bool,
}
pub fn HdrScaler::new(cfg: &HdrConfig) -> Result<HdrScaler, Error>;
pub fn warnings(&self) -> Warnings;
pub fn run(&mut self, frame: &HdrFrame);
pub fn effective_width/height(&self) -> i32;
pub fn hdr_output(&self, flag: u32) -> Option<HdrOutput<'_>>; // 10-bit
pub fn sdr_output(&self, flag: u32) -> Option<Output<'_>>; // tone-mapped 8-bit
pub fn tonemap_1x_output(&self) -> Option<Output<'_>>; // 8-bit, source res
pub fn upscale_hdr_output(&self, flag: u32) -> Option<HdrOutput<'_>>;
pub fn upscale_sdr_output(&self, flag: u32) -> Option<Output<'_>>;pub struct Output<'a> { // 8-bit
pub width: i32, pub height: i32, pub y_stride: i32, pub uv_stride: i32,
pub fallback: bool, // scalar kernel was used for this step
}
pub fn Output::y(&self) -> &'a [u8]; // also u(), v()
pub struct HdrOutput<'a> { // 10-bit; samples are u16 (little-endian)
pub width: i32, pub height: i32, pub y_stride: i32, pub uv_stride: i32,
pub fallback: bool,
}
pub fn HdrOutput::y(&self) -> &'a [u16]; // also u(), v()The 'a lifetime ties each view to the producing scaler: the slices cannot
outlive it, and you cannot call run (needs &mut) while a view is held.
pub enum Error { InvalidFlags, NoSteps, BadDimensions, BadAlignment, Unknown(i32) }
// implements std::error::Error + Display
pub struct Warnings(pub u32);
pub fn Warnings::scalar(&self) -> bool; // a step fell back to scalar
pub fn Warnings::partial(&self) -> bool; // a requested step was rejected
pub fn Warnings::cropped(&self) -> bool; // source was cropped
pub fn Warnings::perfect(&self) -> bool; // none of the aboveError is returned by the constructors for hard failures; Warnings (on
scaler.warnings()) reports non-fatal conditions and is never an error.
- Frame must match the scaler.
runpanics if the frame's dimensions (and, for HDR, the format) differ from the scaler's configuration, preventing an out-of-bounds read in the native kernels. - Custom LUT size. A
TonemapConfig.custom_lutmust be exactly 1024 bytes; otherwiseHdrScaler::newreturnsErr(Error::CustomLutLength). - Output lifetimes are compile-time enforced. Because each view borrows
&scaler, the borrow checker already prevents use-after-free and prevents arunwhile a view is held. To keep data past the scaler, copy it (out.y().to_vec()).
Each Scaler/HdrScaler (and Frame/HdrFrame) owns raw pointers into
library-managed memory, so the types are not Send/Sync and the compiler
will stop you from moving or sharing one across threads. Per-context work is
independent, so the intended pattern is to construct and use a scaler on the
thread that needs it. The binding forces the library's one-time CPU probe on
first use, so concurrent first constructions on different threads are safe.