use std::env;
use alpha2_media::{AudioCodec, BitrateMode, VideoCodec, VideoRateControl};
use alpha2_model::{MediaKind, Project};
use alpha2_time::{FrameRate, TimeRange};
use crate::protocol::{AudioQualityRequest, VideoQualityRequest};
use crate::{
DEFAULT_DECODE_CONCURRENCY, DEFAULT_DECODE_LOOKAHEAD, DEFAULT_DECODER_SESSIONS,
DEFAULT_IN_FLIGHT_VIDEO_FRAMES, DEFAULT_REVERSE_HARDWARE_SURFACES, MAX_DECODE_CONCURRENCY,
MAX_DECODE_LOOKAHEAD, MAX_DECODER_SESSIONS, MAX_ENCODE_CHANNEL_DEPTH,
MAX_IN_FLIGHT_VIDEO_FRAMES, MAX_REVERSE_HARDWARE_SURFACES,
};
pub fn configured_gpu_wait_timeout() -> std::time::Duration {
env::var("ALPHA2_GPU_TIMEOUT_MS")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| *value > 0)
.map_or(
alpha2_render::DEFAULT_GPU_WAIT_TIMEOUT,
std::time::Duration::from_millis,
)
}
pub fn configured_stall_timeout() -> std::time::Duration {
alpha2_native::DECODE_WORK_LIMIT.saturating_add(configured_gpu_wait_timeout())
}
#[cfg(all(target_os = "linux", feature = "hwencode"))]
pub fn configured_gpu_encode(video_session_hazard: Option<String>) -> Result<(), String> {
gpu_encode_gate(
env::var("ALPHA2_GPU_ENCODE").ok().as_deref(),
video_session_hazard,
)
}
#[cfg(all(target_os = "linux", feature = "hwencode"))]
fn gpu_encode_gate(
setting: Option<&str>,
video_session_hazard: Option<String>,
) -> Result<(), String> {
match setting {
Some("0" | "false" | "off") => Err("ALPHA2_GPU_ENCODE is off".to_owned()),
Some("1" | "true" | "on") => Ok(()),
_ => video_session_hazard.map_or(Ok(()), |hazard| {
Err(format!(
"{hazard}; use a 1.4 layer or set ALPHA2_GPU_ENCODE=1"
))
}),
}
}
#[cfg(all(not(target_os = "linux"), feature = "hwencode"))]
pub fn configured_gpu_encode_switch() -> Result<(), String> {
match env::var("ALPHA2_GPU_ENCODE").ok().as_deref() {
Some("0" | "false" | "off") => Err("ALPHA2_GPU_ENCODE is off".to_owned()),
_ => Ok(()),
}
}
#[cfg(feature = "hwencode")]
pub fn configured_yuv_transcode() -> Result<(), String> {
match env::var("ALPHA2_YUV_TRANSCODE").ok().as_deref() {
Some("0" | "false" | "off") => Err("ALPHA2_YUV_TRANSCODE is off".to_owned()),
_ => Ok(()),
}
}
#[derive(Copy, Clone, Debug, Default, serde::Deserialize, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum VideoInputPolicy {
#[default]
Auto,
GpuRequired,
CpuReadback,
}
impl VideoInputPolicy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::GpuRequired => "gpu-required",
Self::CpuReadback => "cpu-readback",
}
}
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"auto" => Ok(Self::Auto),
"gpu-required" => Ok(Self::GpuRequired),
"cpu-readback" => Ok(Self::CpuReadback),
other => Err(format!(
"input policy must be auto, gpu-required, or cpu-readback, not {other}"
)),
}
}
}
#[derive(Copy, Clone, Debug, Default, serde::Deserialize, Eq, PartialEq)]
#[serde(rename_all = "kebab-case")]
pub enum ExportStrategy {
#[default]
Render,
PreferCopy,
RequireCopy,
}
impl ExportStrategy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Render => "render",
Self::PreferCopy => "prefer-copy",
Self::RequireCopy => "require-copy",
}
}
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"render" => Ok(Self::Render),
"prefer-copy" => Ok(Self::PreferCopy),
"require-copy" => Ok(Self::RequireCopy),
other => Err(format!(
"strategy must be render, prefer-copy, or require-copy, not {other}"
)),
}
}
}
pub fn configured_cancel_at_frame() -> Option<u64> {
env::var("ALPHA2_EXPORT_CANCEL_AT_FRAME")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|frame| *frame > 0)
}
pub fn configured_fail_readback_at_frame() -> Option<u64> {
let frame = env::var("ALPHA2_EXPORT_FAIL_READBACK_AT_FRAME")
.ok()
.and_then(|value| value.parse::<u64>().ok())?;
match env::var_os("ALPHA2_EXPORT_FAIL_ONCE_MARKER") {
None => Some(frame),
Some(marker) => std::fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(marker)
.ok()
.map(|_| frame),
}
}
pub fn configured_frame_capture() -> Option<std::path::PathBuf> {
env::var_os("ALPHA2_EXPORT_FRAMES_Y4M")
.filter(|path| !path.is_empty())
.map(std::path::PathBuf::from)
}
pub fn configured_video_bit_rate() -> Option<u64> {
env::var("ALPHA2_EXPORT_VIDEO_BITRATE")
.ok()
.and_then(|value| value.parse::<u64>().ok())
.filter(|value| *value > 0)
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum RateSource {
Request,
EnvOverride,
}
impl RateSource {
pub const fn as_str(self) -> &'static str {
match self {
Self::Request => "request",
Self::EnvOverride => "env-override",
}
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ResolvedVideoRate {
pub control: VideoRateControl,
pub source: RateSource,
pub requested: VideoQualityRequest,
}
impl ResolvedVideoRate {
pub fn to_json(
self,
selection: &alpha2_native::NativeVideoEncoderSelection,
) -> serde_json::Value {
serde_json::json!({
"bitrate": self.control.bitrate,
"mode": self.control.mode.as_str(),
"quantizer": self.control.quantizer,
"source": self.source.as_str(),
"level": match self.requested {
VideoQualityRequest::Level(level) => Some(level.as_str()),
VideoQualityRequest::Explicit(_) => None,
},
"applied": selection.rate_control.as_str(),
"quantizerRefused": selection.quantizer_refused,
})
}
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct ResolvedAudioRate {
pub bitrate: u32,
pub requested: AudioQualityRequest,
}
impl ResolvedAudioRate {
pub const fn resolve(requested: AudioQualityRequest, codec: AudioCodec) -> Self {
let bitrate = match requested {
AudioQualityRequest::Explicit(bitrate) => bitrate,
AudioQualityRequest::Level(level) => alpha2_media::resolve_audio_bitrate(codec, level),
};
Self { bitrate, requested }
}
pub fn to_json(self) -> serde_json::Value {
serde_json::json!({
"bitrate": self.bitrate,
"source": RateSource::Request.as_str(),
"level": match self.requested {
AudioQualityRequest::Level(level) => Some(level.as_str()),
AudioQualityRequest::Explicit(_) => None,
},
})
}
}
pub fn resolve_video_rate_control(
requested: VideoQualityRequest,
codec: VideoCodec,
width: u32,
height: u32,
frame_rate: FrameRate,
) -> Result<ResolvedVideoRate, String> {
if let Some(bitrate) = configured_video_bit_rate() {
return Ok(ResolvedVideoRate {
control: VideoRateControl {
bitrate,
mode: BitrateMode::Variable,
quantizer: None,
},
source: RateSource::EnvOverride,
requested,
});
}
let control = match requested {
VideoQualityRequest::Explicit(rate) => rate,
VideoQualityRequest::Level(level) => {
alpha2_media::resolve_video_rate_control(codec, width, height, frame_rate, level)
.map_err(|error| error.to_string())?
}
};
Ok(ResolvedVideoRate {
control,
source: RateSource::Request,
requested,
})
}
pub fn configured_decode_concurrency(
pipeline_enabled: bool,
hardware_decode: bool,
workload_parallelism: usize,
) -> usize {
if !pipeline_enabled {
return 1;
}
let (default, maximum) = decode_concurrency_policy(hardware_decode);
env::var("ALPHA2_DECODE_CONCURRENCY")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(default)
.clamp(1, maximum)
.min(workload_parallelism.max(1))
}
const fn decode_concurrency_policy(hardware_decode: bool) -> (usize, usize) {
#[cfg(target_os = "macos")]
if hardware_decode {
return (3, 3);
}
let _ = hardware_decode;
(DEFAULT_DECODE_CONCURRENCY, MAX_DECODE_CONCURRENCY)
}
pub fn visual_decode_parallelism(project: &Project, export_range: TimeRange) -> usize {
let decoded = project
.sources
.iter()
.filter(|source| source.kind == MediaKind::Video && source.provider.is_none())
.map(|source| &source.id)
.collect::<std::collections::HashSet<_>>();
let Ok(index) = alpha2_eval::ActiveIndex::build(project) else {
return 1;
};
index
.peak_source_reads(export_range, |source| decoded.contains(source))
.max(1)
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub enum VideoDecodePolicy {
Software,
Auto,
HardwareRequired,
}
impl Default for VideoDecodePolicy {
fn default() -> Self {
if cfg!(all(target_os = "linux", feature = "hwdecode")) {
Self::Auto
} else {
Self::Software
}
}
}
impl VideoDecodePolicy {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Software => "software",
Self::Auto => "auto",
Self::HardwareRequired => "hardware-required",
}
}
pub fn parse(value: &str) -> Result<Self, String> {
match value {
"software" => Ok(Self::Software),
"auto" => Ok(Self::Auto),
"hardware-required" => Ok(Self::HardwareRequired),
other => Err(format!(
"video decode mode must be software, auto, or hardware-required; got {other}"
)),
}
}
pub fn from_env() -> Result<Self, String> {
match env::var("ALPHA2_VIDEO_DECODE") {
Ok(value) => Self::parse(&value),
Err(_) => Ok(Self::default()),
}
}
#[cfg_attr(
not(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
)),
allow(dead_code)
)]
#[must_use]
pub const fn allows_hardware(self) -> bool {
matches!(self, Self::Auto | Self::HardwareRequired)
}
#[must_use]
pub const fn requires_hardware(self) -> bool {
matches!(self, Self::HardwareRequired)
}
}
pub fn configured_reverse_hardware_surfaces(policy: VideoDecodePolicy) -> usize {
let hardware_required = policy.requires_hardware();
let default = if hardware_required {
DEFAULT_REVERSE_HARDWARE_SURFACES
} else {
0
};
env::var("ALPHA2_REVERSE_DECODE_SURFACES")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(default)
.min(MAX_REVERSE_HARDWARE_SURFACES)
}
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
pub fn configured_decode_surface_limits() -> (u32, u64) {
decode_surface_limits(
env::var_os("ALPHA2_DECODE_SURFACES"),
env::var_os("ALPHA2_DECODE_SURFACE_BYTES"),
)
}
#[cfg(any(
test,
all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
)
))]
fn decode_surface_limits(
surfaces: Option<std::ffi::OsString>,
bytes: Option<std::ffi::OsString>,
) -> (u32, u64) {
(
positive_setting(
"ALPHA2_DECODE_SURFACES",
surfaces,
crate::DEFAULT_DECODE_SURFACES,
),
positive_setting(
"ALPHA2_DECODE_SURFACE_BYTES",
bytes,
crate::DEFAULT_DECODE_SURFACE_BYTES,
),
)
}
#[cfg(any(
test,
all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
)
))]
fn positive_setting<T>(variable: &str, setting: Option<std::ffi::OsString>, default: T) -> T
where
T: std::str::FromStr + PartialOrd + Default + std::fmt::Display,
{
let Some(setting) = setting else {
return default;
};
if let Some(value) = setting
.to_str()
.and_then(|text| text.parse::<T>().ok())
.filter(|value| *value > T::default())
{
return value;
}
alpha2_os::diagnostic!(
"alpha2-host: ignoring {variable}={setting:?}: not a positive integer; \
using the default {default}"
);
default
}
pub fn configured_decoder_sessions() -> usize {
env::var("ALPHA2_DECODER_SESSIONS")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(DEFAULT_DECODER_SESSIONS)
.clamp(1, MAX_DECODER_SESSIONS)
}
pub fn configured_frame_depth() -> usize {
let configured = env::var("ALPHA2_EXPORT_IN_FLIGHT_FRAMES")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(DEFAULT_IN_FLIGHT_VIDEO_FRAMES)
.clamp(1, MAX_IN_FLIGHT_VIDEO_FRAMES);
if std::thread::available_parallelism().is_ok_and(|cores| cores.get() == 1) {
1
} else {
configured
}
}
pub fn configured_encode_channel_depth() -> Option<usize> {
if std::thread::available_parallelism().is_ok_and(|cores| cores.get() == 1) {
return Some(0);
}
env::var("ALPHA2_ENCODE_QUEUE_DEPTH")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.map(|value| value.min(MAX_ENCODE_CHANNEL_DEPTH))
}
pub fn configured_pipeline_enabled() -> bool {
env::var("ALPHA2_EXPORT_PIPELINE").map_or(true, |value| {
!matches!(value.to_ascii_lowercase().as_str(), "0" | "false" | "off")
})
}
pub fn configured_decode_lookahead(pipeline_enabled: bool) -> usize {
if !pipeline_enabled {
return 1;
}
let configured = env::var("ALPHA2_DECODE_LOOKAHEAD")
.ok()
.and_then(|value| value.parse::<usize>().ok())
.unwrap_or(DEFAULT_DECODE_LOOKAHEAD)
.clamp(1, MAX_DECODE_LOOKAHEAD);
if std::thread::available_parallelism().is_ok_and(|cores| cores.get() == 1) {
1
} else {
configured
}
}
pub fn codec_thread_policy(video_encoder_hardware: bool) -> &'static str {
let setting = alpha2_native::configured_codec_thread_override();
if let Err(ignored) = &setting
&& !video_encoder_hardware
{
alpha2_os::diagnostic!(
"alpha2-host: ignoring ALPHA2_FFMPEG_THREADS={ignored:?}: not a positive worker count; \
using the default budget"
);
}
codec_thread_policy_for(video_encoder_hardware, &setting)
}
pub fn codec_thread_policy_for(
video_encoder_hardware: bool,
setting: &Result<Option<usize>, String>,
) -> &'static str {
if video_encoder_hardware {
"codec-default"
} else if matches!(setting, Ok(Some(_))) {
"explicit"
} else {
"output-weighted"
}
}
pub fn decoder_thread_policy(hardware_decode: bool, software_video_encoder: bool) -> &'static str {
if hardware_decode {
"hardware-even-share"
} else if software_video_encoder {
"source-area"
} else {
"even-share"
}
}
#[cfg(test)]
mod tests {
#[test]
fn an_ignored_thread_setting_is_not_reported_as_explicit() {
use std::ffi::OsStr;
let policy = |setting: Option<&str>| {
super::codec_thread_policy_for(
false,
&alpha2_native::codec_thread_override(setting.map(OsStr::new)),
)
};
assert_eq!(policy(Some("6")), "explicit");
assert_eq!(policy(None), "output-weighted");
for ignored in ["0", "abc", "-2", ""] {
assert_eq!(policy(Some(ignored)), "output-weighted", "{ignored:?}");
}
assert_eq!(
alpha2_native::codec_thread_override(Some(OsStr::new("500"))),
Ok(Some(64))
);
assert_eq!(
super::codec_thread_policy_for(true, &Ok(Some(6))),
"codec-default"
);
}
#[test]
fn a_decoder_surface_ceiling_of_zero_or_not_a_number_is_the_default() {
use std::ffi::OsString;
let defaults = (
crate::DEFAULT_DECODE_SURFACES,
crate::DEFAULT_DECODE_SURFACE_BYTES,
);
let limits = |surfaces: Option<&str>, bytes: Option<&str>| {
super::decode_surface_limits(surfaces.map(OsString::from), bytes.map(OsString::from))
};
assert_eq!(limits(None, None), defaults);
for ignored in ["0", "-1", "", "lots", "512MiB", "4294967296"] {
assert_eq!(
limits(Some(ignored), None).0,
defaults.0,
"ALPHA2_DECODE_SURFACES={ignored:?}"
);
}
for ignored in ["0", "-1", "", "lots", "512MiB", "18446744073709551616"] {
assert_eq!(
limits(None, Some(ignored)).1,
defaults.1,
"ALPHA2_DECODE_SURFACE_BYTES={ignored:?}"
);
}
assert_eq!(limits(Some("0"), Some("0")), defaults);
assert_eq!(limits(Some("4"), Some("50000000")), (4, 50_000_000));
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
))]
{
let (surfaces, bytes) = limits(Some("0"), Some("0"));
alpha2_native_gpu::SurfaceBudget::new(surfaces, bytes)
.try_reserve(3840 * 2160 * 3 / 2)
.expect("a 4K NV12 surface");
}
}
#[cfg(all(target_os = "linux", feature = "hwencode"))]
#[test]
fn only_a_video_session_hazard_keeps_exports_off_the_gpu() {
let hazard = || Some("the loaded Vulkan validation layer is 1.3.204".to_owned());
assert_eq!(super::gpu_encode_gate(None, None), Ok(()));
let refused = super::gpu_encode_gate(None, hazard()).expect_err("1.3.204");
assert!(refused.contains("1.3.204"), "{refused}");
assert!(refused.contains("ALPHA2_GPU_ENCODE=1"), "{refused}");
assert_eq!(super::gpu_encode_gate(Some("1"), hazard()), Ok(()));
assert_eq!(
super::gpu_encode_gate(Some("off"), None),
Err("ALPHA2_GPU_ENCODE is off".to_owned())
);
}
use super::visual_decode_parallelism;
use alpha2_time::{MediaTime, TimeRange};
fn range(start: i64, end: i64) -> TimeRange {
TimeRange::new(MediaTime::from_ticks(start), MediaTime::from_ticks(end)).unwrap()
}
#[test]
fn reverse_hardware_surfaces_default_to_software_unless_strict() {
use super::VideoDecodePolicy;
assert_eq!(
super::configured_reverse_hardware_surfaces(VideoDecodePolicy::Auto),
0
);
assert_eq!(
super::configured_reverse_hardware_surfaces(VideoDecodePolicy::HardwareRequired),
crate::DEFAULT_REVERSE_HARDWARE_SURFACES
);
}
#[test]
fn video_decode_modes_round_trip_and_reject_unknown_values() {
use super::VideoDecodePolicy;
for policy in [
VideoDecodePolicy::Software,
VideoDecodePolicy::Auto,
VideoDecodePolicy::HardwareRequired,
] {
assert_eq!(VideoDecodePolicy::parse(policy.as_str()), Ok(policy));
}
assert!(VideoDecodePolicy::parse("gpu").is_err());
assert!(!VideoDecodePolicy::Software.allows_hardware());
assert!(VideoDecodePolicy::Auto.allows_hardware());
assert!(!VideoDecodePolicy::Auto.requires_hardware());
assert!(VideoDecodePolicy::HardwareRequired.requires_hardware());
}
#[test]
fn the_default_mode_follows_the_platform() {
use super::VideoDecodePolicy;
let expected = if cfg!(all(target_os = "linux", feature = "hwdecode")) {
VideoDecodePolicy::Auto
} else {
VideoDecodePolicy::Software
};
assert_eq!(VideoDecodePolicy::default(), expected);
assert!(!VideoDecodePolicy::default().requires_hardware());
}
#[test]
fn decode_parallelism_counts_what_compositions_read() {
use serde_json::json;
let span = |start, end| json!({"start": start, "end": end});
let video = |id: &str| json!({"id": id, "kind": "video", "locator": format!("{id}.mp4")});
let media = |id: &str| json!({"id": id, "op": {"type": "media", "source_id": id}});
let quad = json!({"id": "quad", "nodes": [
media("a"), media("b"), media("c"), media("d"),
{"id": "mix", "op": {"type": "merge", "layers": [
{"input": "a"}, {"input": "b"}, {"input": "c"}, {"input": "d"}
]}}
], "output": "mix"});
let project = |clip: serde_json::Value| {
alpha2_model::Project::from_json(
&json!({
"schema_version": 1, "id": "p", "name": "p",
"settings": {"width": 64, "height": 36, "frame_rate": {"num": 30, "den": 1}, "sample_rate": 48_000},
"sources": [video("a"), video("b"), video("c"), video("d"),
{"id": "split", "kind": "composition", "composition": "quad"}],
"compositions": [quad],
"timeline": {"tracks": [{"id": "t", "name": "V", "kind": "video", "clips": [clip]}]}
})
.to_string(),
)
.expect("valid")
};
let composed = project(json!({
"id": "c", "source_id": "a", "composition": {"id": "quad"},
"timeline_range": span(0, 120_000), "source_range": span(0, 120_000)
}));
assert_eq!(visual_decode_parallelism(&composed, range(0, 120_000)), 4);
let source = project(json!({
"id": "c", "source_id": "split",
"timeline_range": span(0, 120_000), "source_range": span(0, 120_000)
}));
assert_eq!(visual_decode_parallelism(&source, range(0, 120_000)), 4);
}
}