NetDesk 1.5.0 — source release

This commit is contained in:
NetDesk committed 2026-10-18 02:02:46 +00:00
commit 40318aafdd
227 files changed
+80798

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
[package]
name = "netdesk-vpx"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
description = "VP8/VP9 (libvpx) encode/decode for NetDesk — the constrained-link codec. Developed by Akronlogic."
links = "vpx"
build = "build.rs"
[dependencies]
anyhow = { workspace = true }
[build-dependencies]
bindgen = "=0.65.1"
+62
View File
@@ -0,0 +1,62 @@
//! Link the vendored static libvpx (UCRT MinGW ABI) + generate FFI bindings
//! from the vendored vpx headers via bindgen (libclang). RustDesk does the same
//! (its vpx.rs include!s an OUT_DIR/vpx_ffi.rs). We vendor a UCRT-ABI libvpx.a
//! so there is no vcpkg/MSYS2 or runtime DLL dependency.
use std::path::PathBuf;
fn main() {
let manifest = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
let tp = manifest.join("..").join("third_party").join("ucrt64");
let lib = tp.join("lib");
let inc = tp.join("include");
println!("cargo:rustc-link-search=native={}", lib.display());
println!("cargo:rustc-link-lib=static=vpx");
println!("cargo:rustc-link-lib=pthread");
println!("cargo:rerun-if-changed=wrapper.h");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", lib.join("libvpx.a").display());
// bindgen needs libclang; point at the installed LLVM if the env is unset.
if std::env::var("LIBCLANG_PATH").is_err() {
for p in ["C:/Program Files/LLVM/bin", "C:/Program Files (x86)/LLVM/bin"] {
if PathBuf::from(p).join("libclang.dll").exists() {
std::env::set_var("LIBCLANG_PATH", p);
break;
}
}
}
// Based on RustDesk's proven bindgen config (libs/scrap/build.rs): a single
// `^[vV].*` allowlist for types/vars/functions, rustified enums.
//
// vpx_codec_enc_cfg and vpx_codec_dec_cfg are forward-declared (by pointer)
// in vpx_codec.h's iface struct BEFORE vpx_encoder.h/vpx_decoder.h define
// them. libclang 22 then reports them to bindgen as incomplete, so bindgen
// emits them as opaque `_address: u8` blobs with no fields — making g_w /
// rc_target_bitrate / threads unsettable. (bindgen version, trust_clang_
// mangling, rustified_enum, and a by-value use all failed to change this.)
// So we blocklist both here and hand-write their exact #[repr(C)] layouts
// in src/lib.rs (ffi module) from the vendored headers. Everything else
// (vpx_image, cx_pkt, ctx, enums, functions) still comes from bindgen.
let regex = "^[vV].*";
let bindings = bindgen::Builder::default()
.header("wrapper.h")
.clang_arg(format!("-I{}", inc.display()))
.allowlist_type(regex)
.allowlist_var(regex)
.allowlist_function(regex)
.rustified_enum(regex)
.blocklist_type("vpx_codec_enc_cfg")
.blocklist_type("vpx_codec_enc_cfg_t")
.blocklist_type("vpx_codec_dec_cfg")
.blocklist_type("vpx_codec_dec_cfg_t")
.trust_clang_mangling(false)
.layout_tests(false)
.generate_comments(false)
.generate()
.expect("bindgen libvpx");
bindings
.write_to_file(PathBuf::from(std::env::var("OUT_DIR").unwrap()).join("vpx_ffi.rs"))
.expect("write vpx_ffi.rs");
}
+742
View File
@@ -0,0 +1,742 @@
//! VP8/VP9 software codec over the vendored static libvpx (UCRT MinGW).
//!
//! The raw FFI is generated by bindgen from the vendored headers (build.rs),
//! EXCEPT for `vpx_codec_enc_cfg` / `vpx_codec_dec_cfg`: those are forward-
//! declared by pointer in vpx_codec.h before their definitions, which libclang
//! reports to bindgen as opaque (`_address: u8`) blobs with no settable fields.
//! They are blocklisted in build.rs and hand-written below with the exact
//! `#[repr(C)]` layout from the vendored headers (ABI-identical: C `enum` and
//! `unsigned int` are both 4 bytes, `struct vpx_rational` is two i32, and
//! `vpx_fixed_buf_t` is `{void*, size_t}`).
//!
//! The safe Encoder/Decoder are ported from RustDesk's libs/scrap vpxcodec.rs
//! (VP9 CBR realtime config, cpu-used 7, row-mt, 4 tile columns). Color
//! conversions (BGRA<->I420, BT.601) are pure-Rust so there is no libyuv dep.
#![allow(non_upper_case_globals)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(dead_code)]
use anyhow::{anyhow, bail, Result};
use std::os::raw::{c_int, c_uint};
pub mod ffi {
#![allow(
non_camel_case_types,
non_snake_case,
non_upper_case_globals,
dead_code,
improper_ctypes
)]
use std::os::raw::{c_int, c_uint, c_void};
// ---- hand-written config structs (blocklisted from bindgen) ----
// Distinct helper names (Nd*) so they never collide with bindgen's own
// vpx_rational / vpx_fixed_buf while staying ABI-identical.
#[repr(C)]
#[derive(Copy, Clone)]
pub struct NdVpxRational {
pub num: c_int,
pub den: c_int,
}
#[repr(C)]
#[derive(Copy, Clone)]
pub struct NdVpxFixedBuf {
pub buf: *mut c_void,
pub sz: usize,
}
/// Mirror of `vpx_codec_enc_cfg_t` (vpx_encoder.h) — 61 fields, in order.
#[repr(C)]
#[derive(Copy, Clone)]
pub struct vpx_codec_enc_cfg {
pub g_usage: c_uint,
pub g_threads: c_uint,
pub g_profile: c_uint,
pub g_w: c_uint,
pub g_h: c_uint,
pub g_bit_depth: c_uint, // enum vpx_bit_depth
pub g_input_bit_depth: c_uint,
pub g_timebase: NdVpxRational,
pub g_error_resilient: c_uint, // vpx_codec_er_flags_t
pub g_pass: c_uint, // enum vpx_enc_pass
pub g_lag_in_frames: c_uint,
pub rc_dropframe_thresh: c_uint,
pub rc_resize_allowed: c_uint,
pub rc_scaled_width: c_uint,
pub rc_scaled_height: c_uint,
pub rc_resize_up_thresh: c_uint,
pub rc_resize_down_thresh: c_uint,
pub rc_end_usage: c_uint, // enum vpx_rc_mode
pub rc_twopass_stats_in: NdVpxFixedBuf,
pub rc_firstpass_mb_stats_in: NdVpxFixedBuf,
pub rc_target_bitrate: c_uint,
pub rc_min_quantizer: c_uint,
pub rc_max_quantizer: c_uint,
pub rc_undershoot_pct: c_uint,
pub rc_overshoot_pct: c_uint,
pub rc_buf_sz: c_uint,
pub rc_buf_initial_sz: c_uint,
pub rc_buf_optimal_sz: c_uint,
pub rc_2pass_vbr_bias_pct: c_uint,
pub rc_2pass_vbr_minsection_pct: c_uint,
pub rc_2pass_vbr_maxsection_pct: c_uint,
pub rc_2pass_vbr_corpus_complexity: c_uint,
pub kf_mode: c_uint, // enum vpx_kf_mode
pub kf_min_dist: c_uint,
pub kf_max_dist: c_uint,
pub ss_number_layers: c_uint,
pub ss_enable_auto_alt_ref: [c_int; 5], // VPX_SS_MAX_LAYERS
pub ss_target_bitrate: [c_uint; 5],
pub ts_number_layers: c_uint,
pub ts_target_bitrate: [c_uint; 5], // VPX_TS_MAX_LAYERS
pub ts_rate_decimator: [c_uint; 5],
pub ts_periodicity: c_uint,
pub ts_layer_id: [c_uint; 16], // VPX_TS_MAX_PERIODICITY
pub layer_target_bitrate: [c_uint; 12], // VPX_MAX_LAYERS
pub temporal_layering_mode: c_int,
pub use_vizier_rc_params: c_int,
pub active_wq_factor: NdVpxRational,
pub err_per_mb_factor: NdVpxRational,
pub sr_default_decay_limit: NdVpxRational,
pub sr_diff_factor: NdVpxRational,
pub kf_err_per_mb_factor: NdVpxRational,
pub kf_frame_min_boost_factor: NdVpxRational,
pub kf_frame_max_boost_first_factor: NdVpxRational,
pub kf_frame_max_boost_subs_factor: NdVpxRational,
pub kf_max_total_boost_factor: NdVpxRational,
pub gf_max_total_boost_factor: NdVpxRational,
pub gf_frame_max_boost_factor: NdVpxRational,
pub zm_factor: NdVpxRational,
pub rd_mult_inter_qp_fac: NdVpxRational,
pub rd_mult_arf_qp_fac: NdVpxRational,
pub rd_mult_key_qp_fac: NdVpxRational,
}
pub type vpx_codec_enc_cfg_t = vpx_codec_enc_cfg;
/// Mirror of `vpx_codec_dec_cfg_t` (vpx_decoder.h).
#[repr(C)]
#[derive(Copy, Clone)]
pub struct vpx_codec_dec_cfg {
pub threads: c_uint,
pub w: c_uint,
pub h: c_uint,
}
pub type vpx_codec_dec_cfg_t = vpx_codec_dec_cfg;
include!(concat!(env!("OUT_DIR"), "/vpx_ffi.rs"));
}
use std::ffi::CStr;
/// libvpx version string, e.g. "v1.17.0".
pub fn version() -> String {
unsafe {
let p = ffi::vpx_codec_version_str();
if p.is_null() {
return String::new();
}
CStr::from_ptr(p).to_string_lossy().into_owned()
}
}
/// Which libvpx codec to use. VP9 is the constrained-link default (far better
/// per-bitrate than H.264); VP8 is a fallback for very old decoders.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum VpxCodec {
Vp8,
Vp9,
}
/// One compressed output packet.
pub struct EncodedFrame {
pub data: Vec<u8>,
pub key: bool,
}
/// One decoded frame, converted to packed BGRA (8-bit, straight alpha = 255).
pub struct DecodedFrame {
pub width: u32,
pub height: u32,
pub bgra: Vec<u8>,
}
#[inline]
fn ck(code: i32, what: &str) -> Result<()> {
if code == 0 {
Ok(())
} else {
Err(anyhow!("{what}: vpx error {code}"))
}
}
fn thread_num() -> c_uint {
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(4)
.min(16) as c_uint
}
/// VP8/VP9 encoder (software, libvpx). Input is packed BGRA; dimensions must be
/// even. One keyframe is emitted on the first frame (or on demand).
pub struct Encoder {
ctx: ffi::vpx_codec_ctx_t,
width: u32,
height: u32,
i420: Vec<u8>, // reusable packed plane scratch (I420 Y|U|V, or I444 Y|U|V)
i444: bool, // true => VP9 profile-1 4:4:4 chroma (crisp text); else I420
}
impl Encoder {
/// `bitrate_kbps` is the CBR target in kilobits/sec. I420 (4:2:0) chroma.
pub fn new(codec: VpxCodec, width: u32, height: u32, fps: u32, bitrate_kbps: u32) -> Result<Self> {
Self::new_i444(codec, width, height, fps, bitrate_kbps, false)
}
/// Like [`Encoder::new`], but `i444` selects VP9 profile-1 4:4:4 chroma
/// (full-resolution U/V) for crisp text and saturated 1px edges that 4:2:0
/// blurs. VP8 has no such profile, so it is ignored there (stays I420).
pub fn new_i444(
codec: VpxCodec,
width: u32,
height: u32,
fps: u32,
bitrate_kbps: u32,
i444: bool,
) -> Result<Self> {
if width == 0 || height == 0 || width % 2 != 0 || height % 2 != 0 {
bail!("vpx encoder needs non-zero even dimensions, got {width}x{height}");
}
let _ = fps; // reserved for future rc tuning; ms timebase is fps-independent
unsafe {
let iface = match codec {
VpxCodec::Vp8 => ffi::vpx_codec_vp8_cx(),
VpxCodec::Vp9 => ffi::vpx_codec_vp9_cx(),
};
if iface.is_null() {
bail!("vpx codec iface null");
}
let mut cfg: ffi::vpx_codec_enc_cfg_t = std::mem::zeroed();
ck(
ffi::vpx_codec_enc_config_default(iface, &mut cfg, 0) as i32,
"enc_config_default",
)?;
// Match RustDesk's VP9 realtime CBR config.
cfg.g_w = width;
cfg.g_h = height;
cfg.g_timebase.num = 1;
cfg.g_timebase.den = 1000; // ms timestamps
cfg.rc_undershoot_pct = 95;
cfg.rc_dropframe_thresh = 25;
cfg.g_threads = thread_num();
cfg.g_lag_in_frames = 0; // realtime: no lookahead, emit every frame immediately
cfg.g_error_resilient = 1; // VPX_ERROR_RESILIENT_DEFAULT
cfg.rc_end_usage = 1; // VPX_CBR
cfg.kf_mode = 0; // VPX_KF_DISABLED (we force keyframes on demand)
cfg.rc_min_quantizer = 0;
cfg.rc_max_quantizer = 63;
cfg.rc_target_bitrate = bitrate_kbps.max(50);
// VP9 profile 1 carries 8-bit 4:2:2/4:4:4; VP8 has no such profile.
let use_i444 = i444 && codec == VpxCodec::Vp9;
cfg.g_profile = if use_i444 { 1 } else { 0 };
let mut ctx: ffi::vpx_codec_ctx_t = std::mem::zeroed();
ck(
ffi::vpx_codec_enc_init_ver(
&mut ctx,
iface,
&cfg,
0,
ffi::VPX_ENCODER_ABI_VERSION as c_int,
) as i32,
"enc_init",
)?;
if codec == VpxCodec::Vp9 {
ck(
ffi::vpx_codec_control_(
&mut ctx,
ffi::vp8e_enc_control_id::VP8E_SET_CPUUSED as c_int,
7 as c_int,
) as i32,
"set_cpuused",
)?;
ck(
ffi::vpx_codec_control_(
&mut ctx,
ffi::vp8e_enc_control_id::VP9E_SET_ROW_MT as c_int,
1 as c_int,
) as i32,
"set_row_mt",
)?;
ck(
ffi::vpx_codec_control_(
&mut ctx,
ffi::vp8e_enc_control_id::VP9E_SET_TILE_COLUMNS as c_int,
4 as c_int,
) as i32,
"set_tile_columns",
)?;
} else {
ck(
ffi::vpx_codec_control_(
&mut ctx,
ffi::vp8e_enc_control_id::VP8E_SET_CPUUSED as c_int,
12 as c_int,
) as i32,
"set_cpuused_vp8",
)?;
}
Ok(Self {
ctx,
width,
height,
i420: Vec::new(),
i444: use_i444,
})
}
}
/// Encode one packed-BGRA frame at `pts_ms`. Returns any compressed packets
/// the encoder produced (usually one). `force_key` requests a keyframe.
pub fn encode_bgra(&mut self, bgra: &[u8], pts_ms: i64, force_key: bool) -> Result<Vec<EncodedFrame>> {
let (w, h) = (self.width as usize, self.height as usize);
if bgra.len() != w * h * 4 {
bail!("bgra len {} != {}", bgra.len(), w * h * 4);
}
let fmt = if self.i444 {
bgra_to_i444(bgra, w, h, &mut self.i420);
ffi::vpx_img_fmt::VPX_IMG_FMT_I444
} else {
bgra_to_i420(bgra, w, h, &mut self.i420);
ffi::vpx_img_fmt::VPX_IMG_FMT_I420
};
unsafe {
let mut img: ffi::vpx_image_t = std::mem::zeroed();
let wrapped = ffi::vpx_img_wrap(
&mut img,
fmt,
self.width as c_uint,
self.height as c_uint,
1,
self.i420.as_ptr() as *mut u8,
);
if wrapped.is_null() {
bail!("vpx_img_wrap failed");
}
let flags: ffi::vpx_enc_frame_flags_t = if force_key {
ffi::VPX_EFLAG_FORCE_KF as ffi::vpx_enc_frame_flags_t
} else {
0
};
ck(
ffi::vpx_codec_encode(
&mut self.ctx,
&img,
pts_ms as ffi::vpx_codec_pts_t,
1,
flags,
ffi::VPX_DL_REALTIME as _,
) as i32,
"encode",
)?;
Ok(self.drain())
}
}
unsafe fn drain(&mut self) -> Vec<EncodedFrame> {
let mut iter: ffi::vpx_codec_iter_t = std::ptr::null();
let mut out = Vec::new();
loop {
let pkt = ffi::vpx_codec_get_cx_data(&mut self.ctx, &mut iter);
if pkt.is_null() {
break;
}
if (*pkt).kind == ffi::vpx_codec_cx_pkt_kind::VPX_CODEC_CX_FRAME_PKT {
let f = &(*pkt).data.frame;
let data = std::slice::from_raw_parts(f.buf as *const u8, f.sz as usize).to_vec();
let key = (f.flags & ffi::VPX_FRAME_IS_KEY) != 0;
out.push(EncodedFrame { data, key });
}
}
out
}
}
impl Drop for Encoder {
fn drop(&mut self) {
unsafe {
ffi::vpx_codec_destroy(&mut self.ctx);
}
}
}
// A libvpx context holds raw pointers, so it is not auto-Send. It is never
// shared (no Sync), but a single owner may move it between threads — the host
// capture/encode thread owns the Encoder, the viewer network thread owns the
// Decoder. This matches RustDesk's VpxEncoder/VpxDecoder.
unsafe impl Send for Encoder {}
unsafe impl Send for Decoder {}
/// VP8/VP9 decoder (software, libvpx). Produces packed BGRA frames.
pub struct Decoder {
ctx: ffi::vpx_codec_ctx_t,
}
impl Decoder {
pub fn new(codec: VpxCodec) -> Result<Self> {
unsafe {
let iface = match codec {
VpxCodec::Vp8 => ffi::vpx_codec_vp8_dx(),
VpxCodec::Vp9 => ffi::vpx_codec_vp9_dx(),
};
if iface.is_null() {
bail!("vpx decoder iface null");
}
let cfg = ffi::vpx_codec_dec_cfg_t {
threads: thread_num(),
w: 0,
h: 0,
};
let mut ctx: ffi::vpx_codec_ctx_t = std::mem::zeroed();
ck(
ffi::vpx_codec_dec_init_ver(
&mut ctx,
iface,
&cfg,
0,
ffi::VPX_DECODER_ABI_VERSION as c_int,
) as i32,
"dec_init",
)?;
Ok(Self { ctx })
}
}
/// Decode one compressed packet into zero or more BGRA frames.
pub fn decode_to_bgra(&mut self, data: &[u8]) -> Result<Vec<DecodedFrame>> {
unsafe {
ck(
ffi::vpx_codec_decode(
&mut self.ctx,
data.as_ptr(),
data.len() as c_uint,
std::ptr::null_mut(),
0,
) as i32,
"decode",
)?;
let mut iter: ffi::vpx_codec_iter_t = std::ptr::null();
let mut out = Vec::new();
loop {
let img = ffi::vpx_codec_get_frame(&mut self.ctx, &mut iter);
if img.is_null() {
break;
}
let im = &*img;
let w = im.d_w as usize;
let h = im.d_h as usize;
// The VP9 decoder adapts to the stream's profile: profile-1
// frames arrive as I444 (full-res chroma), profile-0 as I420.
// One Decoder therefore handles either without reconfiguration.
let bgra = if im.fmt == ffi::vpx_img_fmt::VPX_IMG_FMT_I444 {
i444_to_bgra(
im.planes[0] as *const u8,
im.planes[1] as *const u8,
im.planes[2] as *const u8,
im.stride[0] as usize,
im.stride[1] as usize,
w,
h,
)
} else {
i420_to_bgra(
im.planes[0] as *const u8,
im.planes[1] as *const u8,
im.planes[2] as *const u8,
im.stride[0] as usize,
im.stride[1] as usize,
w,
h,
)
};
out.push(DecodedFrame {
width: im.d_w,
height: im.d_h,
bgra,
});
}
Ok(out)
}
}
}
impl Drop for Decoder {
fn drop(&mut self) {
unsafe {
ffi::vpx_codec_destroy(&mut self.ctx);
}
}
}
// ---- pure-Rust BT.601 color conversion (no libyuv) ----
/// Packed BGRA -> packed I420 (Y plane w*h, then U, then V at cw*ch). `w`,`h`
/// must be even. Output is laid out exactly as vpx_img_wrap(I420, align=1)
/// expects for even dimensions.
fn bgra_to_i420(bgra: &[u8], w: usize, h: usize, out: &mut Vec<u8>) {
let cw = w / 2;
let ch = h / 2;
let y_size = w * h;
let c_size = cw * ch;
out.clear();
out.resize(y_size + 2 * c_size, 0);
let (y_plane, rest) = out.split_at_mut(y_size);
let (u_plane, v_plane) = rest.split_at_mut(c_size);
for j in 0..h {
for i in 0..w {
let p = (j * w + i) * 4;
let b = bgra[p] as f32;
let g = bgra[p + 1] as f32;
let r = bgra[p + 2] as f32;
let y = 0.257 * r + 0.504 * g + 0.098 * b + 16.0;
y_plane[j * w + i] = clamp8(y);
}
}
for cj in 0..ch {
for ci in 0..cw {
let mut rs = 0.0f32;
let mut gs = 0.0f32;
let mut bs = 0.0f32;
for dy in 0..2 {
for dx in 0..2 {
let p = ((cj * 2 + dy) * w + (ci * 2 + dx)) * 4;
bs += bgra[p] as f32;
gs += bgra[p + 1] as f32;
rs += bgra[p + 2] as f32;
}
}
let (r, g, b) = (rs / 4.0, gs / 4.0, bs / 4.0);
let u = -0.148 * r - 0.291 * g + 0.439 * b + 128.0;
let v = 0.439 * r - 0.368 * g - 0.071 * b + 128.0;
u_plane[cj * cw + ci] = clamp8(u);
v_plane[cj * cw + ci] = clamp8(v);
}
}
}
/// I420 planes (with strides) -> packed BGRA.
unsafe fn i420_to_bgra(
y: *const u8,
u: *const u8,
v: *const u8,
ys: usize,
cs: usize,
w: usize,
h: usize,
) -> Vec<u8> {
let mut out = vec![0u8; w * h * 4];
for j in 0..h {
for i in 0..w {
let yv = *y.add(j * ys + i) as f32;
let uu = *u.add((j / 2) * cs + i / 2) as f32;
let vv = *v.add((j / 2) * cs + i / 2) as f32;
let c = yv - 16.0;
let d = uu - 128.0;
let e = vv - 128.0;
let r = 1.164 * c + 1.596 * e;
let g = 1.164 * c - 0.391 * d - 0.813 * e;
let b = 1.164 * c + 2.018 * d;
let p = (j * w + i) * 4;
out[p] = clamp8(b);
out[p + 1] = clamp8(g);
out[p + 2] = clamp8(r);
out[p + 3] = 255;
}
}
out
}
/// Packed BGRA -> packed I444 (Y|U|V, each w*h at full resolution). Unlike
/// I420 the chroma planes keep every pixel's own U/V, so 4:4:4 preserves 1px
/// colored text and hard edges that 4:2:0 averages away. Layout matches
/// vpx_img_wrap(I444, align=1).
fn bgra_to_i444(bgra: &[u8], w: usize, h: usize, out: &mut Vec<u8>) {
let n = w * h;
out.clear();
out.resize(3 * n, 0);
let (y_plane, rest) = out.split_at_mut(n);
let (u_plane, v_plane) = rest.split_at_mut(n);
for idx in 0..n {
let p = idx * 4;
let b = bgra[p] as f32;
let g = bgra[p + 1] as f32;
let r = bgra[p + 2] as f32;
y_plane[idx] = clamp8(0.257 * r + 0.504 * g + 0.098 * b + 16.0);
u_plane[idx] = clamp8(-0.148 * r - 0.291 * g + 0.439 * b + 128.0);
v_plane[idx] = clamp8(0.439 * r - 0.368 * g - 0.071 * b + 128.0);
}
}
/// I444 planes (full-res chroma, with strides) -> packed BGRA.
unsafe fn i444_to_bgra(
y: *const u8,
u: *const u8,
v: *const u8,
ys: usize,
cs: usize,
w: usize,
h: usize,
) -> Vec<u8> {
let mut out = vec![0u8; w * h * 4];
for j in 0..h {
for i in 0..w {
let yv = *y.add(j * ys + i) as f32;
let uu = *u.add(j * cs + i) as f32;
let vv = *v.add(j * cs + i) as f32;
let c = yv - 16.0;
let d = uu - 128.0;
let e = vv - 128.0;
let p = (j * w + i) * 4;
out[p] = clamp8(1.164 * c + 2.018 * d);
out[p + 1] = clamp8(1.164 * c - 0.391 * d - 0.813 * e);
out[p + 2] = clamp8(1.164 * c + 1.596 * e);
out[p + 3] = 255;
}
}
out
}
#[inline]
fn clamp8(v: f32) -> u8 {
v.round().clamp(0.0, 255.0) as u8
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bindgen_links_libvpx() {
let v = version();
assert!(v.starts_with('v'), "libvpx version via bindgen FFI: {v}");
let iface = unsafe { ffi::vpx_codec_vp9_cx() };
assert!(!iface.is_null(), "vp9 encoder iface");
println!("VPX_BINDGEN_OK {v}");
}
#[test]
fn vp9_roundtrip_bgra() {
let (w, h) = (64usize, 64usize);
let mut bgra = vec![0u8; w * h * 4];
// four solid quadrants (BGRA order)
for j in 0..h {
for i in 0..w {
let p = (j * w + i) * 4;
let (b, g, r) = if j < h / 2 && i < w / 2 {
(30u8, 30, 200) // red
} else if j < h / 2 {
(30, 200, 30) // green
} else if i < w / 2 {
(200, 30, 30) // blue
} else {
(200, 200, 200) // gray
};
bgra[p] = b;
bgra[p + 1] = g;
bgra[p + 2] = r;
bgra[p + 3] = 255;
}
}
let mut enc = Encoder::new(VpxCodec::Vp9, w as u32, h as u32, 30, 2000).unwrap();
let frames = enc.encode_bgra(&bgra, 0, true).unwrap();
assert!(!frames.is_empty(), "encoder produced no packets");
assert!(frames[0].key, "first frame must be a keyframe");
let mut dec = Decoder::new(VpxCodec::Vp9).unwrap();
let mut got = Vec::new();
for f in &frames {
got.extend(dec.decode_to_bgra(&f.data).unwrap());
}
assert_eq!(got.len(), 1, "expected exactly one decoded frame");
let img = &got[0];
assert_eq!((img.width, img.height), (w as u32, h as u32));
let sample = |x: usize, y: usize| {
let p = (y * w + x) * 4;
(img.bgra[p], img.bgra[p + 1], img.bgra[p + 2])
};
let approx = |a: u8, b: u8| (a as i32 - b as i32).abs() <= 48;
let (b, g, r) = sample(w / 4, h / 4); // red quadrant center
assert!(
approx(r, 200) && approx(g, 30) && approx(b, 30),
"red quad decoded as b{b} g{g} r{r}"
);
let (b, g, r) = sample(3 * w / 4, 3 * h / 4); // gray quadrant center
assert!(
approx(r, 200) && approx(g, 200) && approx(b, 200),
"gray quad decoded as b{b} g{g} r{r}"
);
println!(
"VPX_ROUNDTRIP_OK vp9 {w}x{h} key_bytes={}",
frames[0].data.len()
);
}
#[test]
fn vp9_i444_beats_i420_on_1px_chroma() {
// 1px-wide vertical stripes of two DIFFERENT-chroma colors (magenta /
// green). 4:2:0 stores one chroma sample per 2x2 block, so it CANNOT
// represent per-column chroma no matter the bitrate; 4:4:4 keeps it.
let (w, h) = (64usize, 64usize);
let mut bgra = vec![0u8; w * h * 4];
for j in 0..h {
for i in 0..w {
let p = (j * w + i) * 4;
let (b, g, r) = if i % 2 == 0 {
(200u8, 0, 200) // magenta
} else {
(0, 170, 0) // green
};
bgra[p] = b;
bgra[p + 1] = g;
bgra[p + 2] = r;
bgra[p + 3] = 255;
}
}
let mae = |i444: bool| -> f64 {
let mut enc =
Encoder::new_i444(VpxCodec::Vp9, w as u32, h as u32, 30, 4000, i444).unwrap();
let frames = enc.encode_bgra(&bgra, 0, true).unwrap();
let mut dec = Decoder::new(VpxCodec::Vp9).unwrap();
let mut got = Vec::new();
for f in &frames {
got.extend(dec.decode_to_bgra(&f.data).unwrap());
}
assert_eq!(got.len(), 1);
let img = &got[0];
let mut e = 0f64;
for idx in 0..w * h {
let p = idx * 4;
e += (img.bgra[p] as i32 - bgra[p] as i32).abs() as f64;
e += (img.bgra[p + 1] as i32 - bgra[p + 1] as i32).abs() as f64;
e += (img.bgra[p + 2] as i32 - bgra[p + 2] as i32).abs() as f64;
}
e / (3 * w * h) as f64
};
let e420 = mae(false);
let e444 = mae(true);
println!("VPX_I444_OK e420={e420:.2} e444={e444:.2}");
assert!(
e444 + 1.0 < e420,
"i444 MAE {e444:.2} should clearly beat i420 {e420:.2} on 1px chroma"
);
}
}
+9
View File
@@ -0,0 +1,9 @@
#include <vpx/vp8.h>
#include <vpx/vp8cx.h>
#include <vpx/vp8dx.h>
#include <vpx/vpx_codec.h>
#include <vpx/vpx_decoder.h>
#include <vpx/vpx_encoder.h>
#include <vpx/vpx_frame_buffer.h>
#include <vpx/vpx_image.h>
#include <vpx/vpx_integer.h>