#![cfg_attr(not(test), warn(clippy::print_stderr))]
mod adapter_choice;
mod assets;
mod audio;
mod capture;
mod cli;
mod config;
#[cfg(all(target_os = "windows", feature = "hwencode"))]
mod d3d12va_probe;
mod decode;
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
mod dx12_adapters;
mod effect_request;
mod encode;
mod export_decode;
mod failure;
mod frames;
#[cfg(all(target_os = "windows", feature = "hwencode"))]
mod gpu_encode_probe;
#[cfg(feature = "hwencode")]
mod gpu_export;
mod gpu_output;
mod heartbeat;
mod host;
mod media;
mod memory;
mod metrics;
mod packet_copy;
mod protocol;
mod providers;
mod recovery;
mod render_cache;
mod tag;
mod teardown;
mod termination;
#[cfg(test)]
mod tests;
mod trace;
#[cfg(feature = "hwencode")]
mod yuv_transcode;
use std::collections::HashSet;
use std::env;
use std::io::{BufRead, Write};
use std::process::ExitCode;
use std::time::Duration;
use crate::cli::{conform_cli, export_cli, usage};
use crate::config::VideoDecodePolicy;
use crate::host::Host;
use crate::protocol::{
HostError, MAX_REQUEST_LINE_BYTES, operation_from_line, parse_request, request_id_from_line,
};
use alpha2_gpu::{GpuContext, GpuDiagnostic};
use alpha2_native::{codec_capabilities, ffmpeg_runtime_identity};
use alpha2_render::Renderer;
use serde_json::{Value, json};
const PROTOCOL_VERSION: u32 = 1;
const GPU_INIT_EXIT_CODE: u8 = 70;
const DEFAULT_EXPORT_IN_FLIGHT_BYTES: u64 = 256 * 1024 * 1024;
const DEFAULT_IN_FLIGHT_VIDEO_FRAMES: usize = 2;
const MAX_IN_FLIGHT_VIDEO_FRAMES: usize = 3;
const MAX_PENDING_MEDIA_EVENTS: usize = 8;
const MIN_ENCODE_CHANNEL_DEPTH: usize = 2;
const MAX_ENCODE_CHANNEL_DEPTH: usize = 8;
const AUDIO_INTERPOLATION_MARGIN: i64 = 2;
const DEFAULT_AUDIO_WINDOW_BYTES: u64 = 256 * 1024 * 1024;
const DEFAULT_DECODE_QUEUE_BYTES: u64 = 256 * 1024 * 1024;
const DEFAULT_PROVIDER_SOURCE_BYTES: u64 = 512 * 1024 * 1024;
const DEFAULT_DECODE_LOOKAHEAD: usize = 2;
const MAX_DECODE_LOOKAHEAD: usize = 3;
const DEFAULT_DECODE_CONCURRENCY: usize = 2;
const MAX_DECODE_CONCURRENCY: usize = 4;
const DEFAULT_DECODER_SESSIONS: usize = 8;
const MAX_DECODER_SESSIONS: usize = 32;
const DEFAULT_REVERSE_CACHE_BYTES: u64 = 256 * 1024 * 1024;
const DEFAULT_REVERSE_HARDWARE_SURFACES: usize = 8;
#[cfg_attr(
not(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
)),
allow(dead_code)
)]
const DEFAULT_DECODE_SURFACES: u32 = 32;
#[cfg_attr(
not(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwdecode"
)),
allow(dead_code)
)]
const DEFAULT_DECODE_SURFACE_BYTES: u64 = 512 * 1024 * 1024;
const MAX_REVERSE_HARDWARE_SURFACES: usize = 16;
const WORKER_RETRY_INTERVAL: Duration = Duration::from_millis(1);
fn main() -> ExitCode {
let mut host = None;
let result = run(&mut host);
ExitCode::from(conclude(result, host))
}
fn conclude(result: Result<(), HostError>, host: Option<Host>) -> u8 {
let code = match result {
Ok(()) => 0,
Err(error) => {
alpha2_os::diagnostic!("alpha2-host: {}", error.message);
if let Some(details) = &error.details {
alpha2_os::diagnostic!("{details}");
}
if error.code == "GPU_INIT_FAILED" {
GPU_INIT_EXIT_CODE
} else {
1
}
}
};
let code = termination::requested().map_or(code, alpha2_os::Termination::exit_code);
exit_if_worker_threads_abandoned(code);
teardown::release(host, code);
code
}
fn exit_if_worker_threads_abandoned(code: u8) {
if !heartbeat::worker_threads_abandoned() {
return;
}
exit_now(code);
}
fn exit_if_gpu_lost(host: &Host) {
let Some(reason) = gpu_unusable(host) else {
return;
};
alpha2_os::diagnostic!(
"alpha2-host: {reason}; exiting so that the next request gets a new process"
);
exit_now(1);
}
fn gpu_unusable(host: &Host) -> Option<String> {
if let Some(lost) = host.renderer.gpu_lost() {
return Some(lost.to_string());
}
host.gpu_log
.device_lost()
.then(|| "the GPU device was lost".to_owned())
}
fn exit_if_unusable(host: Option<&Host>) {
exit_if_worker_threads_abandoned(
termination::requested().map_or(1, alpha2_os::Termination::exit_code),
);
if let Some(host) = host {
exit_if_gpu_lost(host);
}
}
fn exit_now(code: u8) -> ! {
let _ = std::io::stdout().flush();
exit_unflushed(code)
}
fn exit_unflushed(code: u8) -> ! {
let _ = std::io::stderr().flush();
#[cfg(feature = "hwdevice")]
alpha2_ffmpeg_bridge::exit_without_handlers(code);
#[cfg(not(feature = "hwdevice"))]
std::process::exit(i32::from(code));
}
fn run(host: &mut Option<Host>) -> Result<(), HostError> {
let mut args = utf8_arguments(env::args_os().skip(1))?.into_iter();
match args.next().as_deref() {
Some("serve") => {
if args.next().is_some() {
return Err(HostError::invalid_input(usage()));
}
serve(host)
}
Some("export") => export_cli(args.collect(), host),
Some("conform") => conform_cli(args.collect()),
#[cfg(all(target_os = "linux", feature = "hwencode"))]
Some("probe-gpu-encode") => probe_gpu_encode(args.collect()),
#[cfg(all(target_os = "macos", feature = "hwencode"))]
Some("probe-gpu-encode") => probe_metal_gpu_encode(args.collect()),
#[cfg(all(target_os = "windows", feature = "hwencode"))]
Some("probe-gpu-encode") => crate::gpu_encode_probe::probe(args.collect()),
#[cfg(all(target_os = "windows", feature = "hwencode"))]
Some(crate::d3d12va_probe::SUBCOMMAND) => {
crate::d3d12va_probe::child(&args.collect::<Vec<_>>())
}
_ => Err(HostError::invalid_input(usage())),
}
}
fn utf8_arguments(
arguments: impl Iterator<Item = std::ffi::OsString>,
) -> Result<Vec<String>, HostError> {
arguments
.map(|argument| {
argument.into_string().map_err(|argument| {
HostError::invalid_input(format!(
"argument {:?} is not valid UTF-8",
argument.to_string_lossy()
))
})
})
.collect()
}
#[cfg(all(target_os = "linux", feature = "hwencode"))]
fn probe_gpu_encode(args: Vec<String>) -> Result<(), HostError> {
let mut args = args.into_iter();
let width = args
.next()
.map_or(Ok(1_920), |value| value.parse::<u32>())
.map_err(|_| HostError::invalid_input("width must be a positive integer"))?;
let height = args
.next()
.map_or(Ok(1_080), |value| value.parse::<u32>())
.map_err(|_| HostError::invalid_input("height must be a positive integer"))?;
let slice_path = args.next();
if args.next().is_some() {
return Err(HostError::invalid_input(usage()));
}
let shared = alpha2_native_gpu::SharedVulkanContext::create()
.map_err(|error| HostError::new("GPU_ERROR", format!("shared Vulkan device: {error}")))?;
alpha2_os::diagnostic!(
"alpha2-host shared Vulkan: decode=[{}] encode=[{}]",
shared.decode_codecs().join(", "),
shared.encode_codecs().join(", ")
);
let probe = alpha2_native::probe_vulkan_encode(&shared.ffmpeg_device(), width, height)
.map_err(|error| HostError::new("ENCODE_ERROR", format!("{error}")))?;
let rendered = slice_path
.map(|path| {
render_gpu_encode_slice(
&shared.gpu(),
&alpha2_native::HardwareEncodeDevice::vulkan(shared.clone()),
width,
height,
&path,
)
})
.transpose()?;
write_stdout(&json!({
"ok": true,
"result": {
"encoder": probe.encoder,
"codedSize": [probe.coded_size.0, probe.coded_size.1],
"framesUsage": format!("0x{:04x}", probe.frames_usage),
"imagesPerFrame": probe.images_per_frame,
"initialLayout": probe.initial_layout,
"queueFamily": probe.queue_family,
"hasTimelineSemaphore": probe.has_timeline_semaphore,
"packets": probe.packets,
"packetBytes": probe.packet_bytes,
"rendered": rendered,
},
}))
}
#[cfg(all(
any(target_os = "linux", target_os = "macos", target_os = "windows"),
feature = "hwencode"
))]
fn render_gpu_encode_slice(
gpu: &GpuContext,
device: &alpha2_native::HardwareEncodeDevice,
width: u32,
height: u32,
path: &str,
) -> Result<Value, HostError> {
use alpha2_render::{Nv12PlaneTargets, RenderSize, Renderer};
let encode_error =
|error: alpha2_media::EncodeError| HostError::new("ENCODE_ERROR", format!("{error}"));
let renderer = Renderer::new(gpu.clone());
let size = RenderSize::new(width, height)
.map_err(|error| HostError::new("INVALID_INPUT", format!("{error:?}")))?;
let color = alpha2_render::Color::rgba(0.25, 0.5, 0.75, 1.0);
let target = renderer
.render_solid(size, color)
.map_err(|error| HostError::new("GPU_ERROR", format!("{error:?}")))?;
let rate = alpha2_time::FrameRate::new(30, 1)
.map_err(|error| HostError::new("INVALID_INPUT", format!("{error:?}")))?;
let codec = device
.preferred_capability()
.map_err(|error| HostError::new("ENCODE_ERROR", error))?
.codec;
let (mut session, mut input) = device
.open(&alpha2_native::HardwareEncodeRequest {
width,
height,
codec,
frame_rate: rate,
rate_control: alpha2_media::VideoRateControl {
bitrate: 12_000_000,
mode: alpha2_media::BitrateMode::Variable,
quantizer: None,
},
color: alpha2_media::ColorMetadata::EXPORT,
output: std::path::Path::new(path),
container: alpha2_media::Container::Mp4,
audio: None,
})
.map_err(|error| HostError::new("ENCODE_ERROR", error.to_string()))?;
let mut frames = 0_u32;
for index in 0..8_u32 {
let mut surface = input.acquire_surface().map_err(encode_error)?;
let frame = input.begin_frame(&surface).map_err(encode_error)?;
let (luma, chroma) = input.planes(&frame);
let mut encoder = gpu
.device()
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("alpha2 NV12 pack"),
});
let pack = renderer
.encode_nv12_pack(
&target,
Nv12PlaneTargets { luma, chroma },
alpha2_media::YuvEncoding::export(codec.model()),
&mut encoder,
)
.map_err(|error| HostError::new("GPU_ERROR", format!("{error:?}")))?;
gpu.submit([encoder.finish()]);
drop(pack);
input.submit(frame, &mut surface).map_err(encode_error)?;
let time = rate
.frame_start(i64::from(index))
.ok_or_else(|| HostError::new("INVALID_INPUT", "frame time overflowed"))?;
input.set_time(&mut surface, time);
session.encode(&surface).map_err(encode_error)?;
frames += 1;
}
session.finish().map_err(encode_error)?;
input.finish().map_err(encode_error)?;
Ok(json!({
"path": path,
"frames": frames,
"solidRgba": [0.25, 0.5, 0.75, 1.0],
}))
}
#[cfg(all(target_os = "macos", feature = "hwencode"))]
fn probe_metal_gpu_encode(args: Vec<String>) -> Result<(), HostError> {
let mut args = args.into_iter();
let width = args
.next()
.map_or(Ok(1_920), |value| value.parse::<u32>())
.map_err(|_| HostError::invalid_input("width must be a positive integer"))?;
let height = args
.next()
.map_or(Ok(1_080), |value| value.parse::<u32>())
.map_err(|_| HostError::invalid_input("height must be a positive integer"))?;
let slice_path = args.next();
if args.next().is_some() {
return Err(HostError::invalid_input(usage()));
}
let gpu = pollster::block_on(GpuContext::request_high_performance())
.map_err(|error| HostError::new("GPU_INIT_FAILED", error_message(error)))?;
let shared = alpha2_native_gpu::SharedMetalContext::from_gpu(gpu)
.map_err(|error| HostError::new("GPU_ERROR", format!("shared Metal device: {error}")))?;
let probe = alpha2_native::probe_videotoolbox_encode(&shared, width, height)
.map_err(|error| HostError::new("ENCODE_ERROR", format!("{error}")))?;
let rendered = slice_path
.map(|path| {
render_gpu_encode_slice(
&shared.gpu(),
&alpha2_native::HardwareEncodeDevice::metal(shared.clone()),
width,
height,
&path,
)
})
.transpose()?;
write_stdout(&json!({
"ok": true,
"result": {
"encoder": probe.encoder,
"planeSizes": probe.plane_sizes,
"packets": probe.packets,
"packetBytes": probe.packet_bytes,
"rendered": rendered,
},
}))
}
#[allow(clippy::too_many_lines)]
fn create_host(policy: VideoDecodePolicy) -> Result<Host, HostError> {
if policy.requires_hardware()
&& !cfg!(all(
any(
target_os = "linux",
target_os = "macos",
target_os = "windows"
),
feature = "hwdecode"
))
{
return Err(HostError::new(
"HARDWARE_DECODE_UNAVAILABLE",
"hardware decode is required, but this host was built without hardware decode",
));
}
let request_default = || {
pollster::block_on(GpuContext::request_high_performance())
.map_err(|error| HostError::new("GPU_INIT_FAILED", error_message(error)))
};
#[cfg(all(target_os = "linux", feature = "hwdevice"))]
let (shared_vulkan, unavailable) = shared_device_context(
policy,
cfg!(feature = "hwencode"),
backend_excluded(wgpu::Backends::from_env(), wgpu::Backends::VULKAN, "Vulkan"),
alpha2_native_gpu::SharedVulkanContext::create,
)?;
#[cfg(all(target_os = "linux", feature = "hwdevice"))]
let unavailable = video_session_decode(
policy,
shared_vulkan
.as_ref()
.and_then(alpha2_native_gpu::SharedVulkanContext::video_session_hazard),
)?
.or(unavailable);
#[cfg(all(target_os = "linux", feature = "hwdevice"))]
let gpu = if let Some(context) = &shared_vulkan {
context.gpu()
} else {
request_default()?
};
#[cfg(all(target_os = "windows", feature = "hwencode"))]
crate::d3d12va_probe::install();
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let adapter_policy = crate::adapter_choice::AdapterPolicy::from_env();
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let decode_on_gpu = cfg!(feature = "hwdecode") && policy.allows_hardware();
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let encode_on_gpu = gpu_encode_wanted();
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let (mut dx12, dx12_unavailable) = {
let scanned = pollster::block_on(crate::dx12_adapters::scan(
&adapter_policy,
decode_on_gpu,
encode_on_gpu,
));
if let Err(crate::dx12_adapters::ScanError::Pin(reason)) = &scanned {
return Err(HostError::invalid_input(reason.clone()));
}
decode_backend_gpu(
policy,
"DX12",
|| scanned,
|| request_default().map(crate::dx12_adapters::Dx12Choice::without_scan),
)?
};
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let gpu = dx12.gpu.clone();
#[cfg(not(any(
all(target_os = "linux", feature = "hwdevice"),
all(target_os = "windows", feature = "hwdevice")
)))]
let gpu = request_default()?;
#[cfg(all(target_os = "macos", feature = "hwdevice"))]
let (shared_metal, unavailable) =
shared_device_context(policy, cfg!(feature = "hwencode"), None, || {
alpha2_native_gpu::SharedMetalContext::from_gpu(gpu.clone())
})?;
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let (shared_d3d12, unavailable, no_encode_device) =
d3d12_devices(policy, dx12_unavailable, &mut dx12)?;
#[cfg(all(target_os = "windows", feature = "hwencode"))]
let test_encodes = start_test_encodes(shared_d3d12.as_ref(), encode_on_gpu);
let gpu_log = std::sync::Arc::new(alpha2_gpu::GpuDiagnosticLog::default());
let logged = std::sync::Arc::clone(&gpu_log);
gpu.set_diagnostic_handler(move |diagnostic: GpuDiagnostic| {
alpha2_os::diagnostic!("alpha2-host {diagnostic}");
logged.push(diagnostic);
});
let mut renderer = Renderer::new(gpu);
let wait_timeout = crate::config::configured_gpu_wait_timeout();
renderer.set_gpu_wait_timeout(wait_timeout);
#[cfg(feature = "hwdevice")]
alpha2_native_gpu::set_gpu_wait_timeout(wait_timeout);
let builtin_ids = pollster::block_on(renderer.register_builtins()).map_err(|diagnostics| {
HostError::new(
"EFFECT_ERROR",
format!("built-in effect registration failed: {diagnostics:?}"),
)
})?;
let adapter = renderer.diagnostics().adapter;
alpha2_os::diagnostic!(
"alpha2-host GPU: {}, {}, {} {}, tier {:?}",
adapter.name,
adapter.backend,
adapter.driver,
adapter.driver_info,
adapter.tier
);
let codecs = codec_capabilities();
let ffmpeg = ffmpeg_runtime_identity();
match ffmpeg.pinned_sdk_mismatch() {
None => alpha2_os::diagnostic!(
"alpha2-host FFmpeg: {}, pinned SDK {}",
ffmpeg.version,
ffmpeg.pinned_sdk_release
),
Some(reason) => alpha2_os::diagnostic!(
"alpha2-host FFmpeg: {}, WARNING: not the pinned SDK {} ({reason}); this build \
linked whatever FFmpeg pkg-config found, so its encoders and exports differ from \
a release.",
ffmpeg.version,
ffmpeg.pinned_sdk_release
),
}
alpha2_os::diagnostic!(
"alpha2-host MP4 encoders: video=[{}] audio=[{}]",
codecs
.video
.iter()
.map(|capability| format!(
"{}:{}{}",
capability.codec.as_str(),
capability.encoder_name,
if capability.hardware_accelerated {
" (hw)"
} else {
""
}
))
.collect::<Vec<_>>()
.join(", "),
codecs
.audio
.iter()
.map(|capability| format!("{}:{}", capability.codec.as_str(), capability.encoder_name))
.collect::<Vec<_>>()
.join(", ")
);
let builtins = builtin_ids
.into_iter()
.map(|id| (id.as_str().to_owned(), id.version()))
.collect::<HashSet<_>>();
#[cfg(all(target_os = "linux", feature = "hwdevice"))]
let mut host = Host::new_with_vulkan(renderer, builtins, shared_vulkan, policy);
#[cfg(not(any(
all(target_os = "linux", feature = "hwdevice"),
all(target_os = "macos", feature = "hwdevice"),
all(target_os = "windows", feature = "hwdevice")
)))]
let mut host = Host::new(renderer, builtins, policy);
#[cfg(all(target_os = "macos", feature = "hwdevice"))]
let mut host = Host::new_with_metal(renderer, builtins, shared_metal, policy);
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
let mut host = Host::new_with_d3d12(renderer, builtins, shared_d3d12, policy);
#[cfg(any(
all(target_os = "linux", feature = "hwdevice"),
all(target_os = "macos", feature = "hwdevice"),
all(target_os = "windows", feature = "hwdevice")
))]
{
host.hardware_decode_unavailable = unavailable;
}
#[cfg(all(target_os = "windows", feature = "hwencode"))]
{
host.encode_adapters = crate::adapter_choice::EncodeAdapterNotes {
no_device: no_encode_device,
elsewhere: dx12.chosen.map_or_else(Vec::new, |chosen| {
crate::adapter_choice::encode_elsewhere(&dx12.records, chosen, decode_on_gpu)
}),
};
}
#[cfg(all(target_os = "windows", feature = "hwencode"))]
if let Some(test_encodes) = test_encodes {
test_encodes.finish();
}
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
{
#[cfg(not(feature = "hwencode"))]
let _ = no_encode_device;
report_dx12_adapters(&host, &dx12, encode_on_gpu);
}
host.gpu_log = gpu_log;
if let Err(reason) = crate::host::host_binary_digest() {
alpha2_os::diagnostic!("alpha2-host render cache unavailable: {reason}");
}
Ok(host)
}
#[cfg(any(test, all(target_os = "windows", feature = "hwdevice")))]
fn decode_backend_gpu<T, E: std::fmt::Debug>(
policy: VideoDecodePolicy,
backend: &str,
preferred: impl FnOnce() -> Result<T, E>,
fallback: impl FnOnce() -> Result<T, HostError>,
) -> Result<(T, Option<String>), HostError> {
match preferred() {
Ok(gpu) => Ok((gpu, None)),
Err(error) => {
let reason = format!("{backend} unavailable: {error:?}");
if policy.requires_hardware() {
return Err(HostError::new("HARDWARE_DECODE_UNAVAILABLE", reason));
}
if !policy.allows_hardware() {
alpha2_os::diagnostic!("alpha2-host {reason}; rendering on the default backend");
return fallback().map(|gpu| (gpu, None));
}
alpha2_os::diagnostic!(
"alpha2-host hardware decode unavailable, using software: {reason}"
);
fallback().map(|gpu| (gpu, Some(reason)))
}
}
}
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
fn gpu_encode_wanted() -> bool {
#[cfg(feature = "hwencode")]
return crate::config::configured_gpu_encode_switch().is_ok();
#[cfg(not(feature = "hwencode"))]
false
}
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
type D3D12Devices = (
Option<alpha2_native_gpu::SharedD3D12Context>,
Option<String>,
Option<String>,
);
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
fn d3d12_devices(
policy: VideoDecodePolicy,
dx12_unavailable: Option<String>,
dx12: &mut crate::dx12_adapters::Dx12Choice,
) -> Result<D3D12Devices, HostError> {
if let Some(reason) = dx12_unavailable {
return Ok((None, Some(reason.clone()), Some(reason)));
}
let decode_unavailable = match dx12.decode_refused.take() {
None => None,
Some(reason) if policy.requires_hardware() => {
return Err(HostError::new("HARDWARE_DECODE_UNAVAILABLE", reason));
}
Some(reason) => {
alpha2_os::diagnostic!(
"alpha2-host hardware decode unavailable, using software: {reason}"
);
Some(reason)
}
};
let (shared, no_encode_device) = match dx12.shared.take() {
None => (None, None),
Some(Ok(context)) => (Some(context), None),
Some(Err(reason)) => {
let adapter = dx12.gpu.capabilities().adapter_name.clone();
if decode_unavailable.is_none() {
alpha2_os::diagnostic!(
"alpha2-host shared GPU device unavailable, exports take the readback path: \
{reason}"
);
}
(
None,
Some(format!(
"{adapter} has no D3D12 device shared with a hardware encoder: {reason}"
)),
)
}
};
Ok((shared, decode_unavailable, no_encode_device))
}
#[cfg(all(target_os = "windows", feature = "hwencode"))]
fn start_test_encodes(
shared: Option<&alpha2_native_gpu::SharedD3D12Context>,
encode_on_gpu: bool,
) -> Option<crate::d3d12va_probe::Prewarm> {
let shared = shared.filter(|_| encode_on_gpu)?;
#[cfg(test)]
if crate::d3d12va_probe::injected() {
crate::d3d12va_probe::prewarm_injected(
&shared.gpu().capabilities().adapter_name,
shared.adapter().map_or(0, |identity| identity.luid),
);
return None;
}
let codecs: Vec<_> = crate::adapter_choice::ENCODE_CODECS
.into_iter()
.filter(|codec| crate::d3d12va_probe::device_encodes(shared, *codec).is_ok())
.collect();
Some(crate::d3d12va_probe::start_prewarm(
shared,
&codecs,
crate::config::configured_gpu_wait_timeout(),
))
}
#[cfg(all(target_os = "windows", feature = "hwdevice"))]
#[cfg_attr(not(feature = "hwencode"), allow(unused_variables))]
fn report_dx12_adapters(host: &Host, dx12: &crate::dx12_adapters::Dx12Choice, encode_on_gpu: bool) {
#[cfg(feature = "hwencode")]
let encodes = match (&host.shared_d3d12, encode_on_gpu) {
(_, false) => "off (ALPHA2_GPU_ENCODE is off)".to_owned(),
(None, true) => "none".to_owned(),
(Some(_), true) => {
let taken: Vec<_> = host
.gpu_output_codecs()
.iter()
.map(|capability| capability.codec)
.collect();
if taken.is_empty() {
"none".to_owned()
} else {
crate::adapter_choice::codec_list(&taken)
}
}
};
#[cfg(not(feature = "hwencode"))]
let encodes = "not in this build".to_owned();
alpha2_os::diagnostic!(
"alpha2-host GPU: {} (renders, decodes: {}, encodes: {encodes})",
host.renderer.diagnostics().adapter.name,
if host.hardware_decode_ready() {
"yes"
} else {
"no"
}
);
for (at, record) in dx12.records.iter().enumerate() {
if Some(at) != dx12.chosen {
alpha2_os::diagnostic!("alpha2-host GPU passed over: {}", record.summary());
}
}
#[cfg(feature = "hwencode")]
let encode = gpu_encode_diagnostic(
&host.renderer.diagnostics().adapter.name,
&host.gpu_output_codecs(),
host.gpu_output_capability().err(),
);
#[cfg(not(feature = "hwencode"))]
let encode = "unavailable: this host was built without hardware encode".to_owned();
alpha2_os::diagnostic!("alpha2-host GPU encode: {encode}");
}
#[cfg(all(any(test, target_os = "windows"), feature = "hwencode"))]
fn gpu_encode_diagnostic(
adapter: &str,
codecs: &[alpha2_native::HardwareEncodeCapability],
unavailable: Option<String>,
) -> String {
match codecs.first() {
Some(first) => format!(
"{} {} on {adapter}",
first.backend.as_str(),
codecs
.iter()
.map(|capability| capability.encoder)
.collect::<Vec<_>>()
.join(", ")
),
None => format!(
"unavailable: {}",
unavailable.unwrap_or_else(|| "no encoder takes GPU frames here".to_owned())
),
}
}
#[cfg(any(test, all(target_os = "linux", feature = "hwdevice")))]
fn backend_excluded(
chosen: Option<wgpu::Backends>,
shared: wgpu::Backends,
name: &str,
) -> Option<String> {
chosen
.filter(|chosen| !chosen.contains(shared))
.map(|chosen| format!("WGPU_BACKEND selects {chosen:?}, not {name}"))
}
#[cfg(any(test, all(target_os = "linux", feature = "hwdevice")))]
fn video_session_decode(
policy: VideoDecodePolicy,
hazard: Option<String>,
) -> Result<Option<String>, HostError> {
let Some(hazard) = hazard.filter(|_| cfg!(feature = "hwdecode") && policy.allows_hardware())
else {
return Ok(None);
};
if policy.requires_hardware() {
return Err(HostError::new("HARDWARE_DECODE_UNAVAILABLE", hazard));
}
alpha2_os::diagnostic!("alpha2-host hardware decode unavailable, using software: {hazard}");
Ok(Some(hazard))
}
#[cfg(any(
test,
all(target_os = "linux", feature = "hwdevice"),
all(target_os = "macos", feature = "hwdevice")
))]
fn shared_device_context<T, E: std::error::Error + 'static>(
policy: VideoDecodePolicy,
encode: bool,
excluded: Option<String>,
create: impl FnOnce() -> Result<T, E>,
) -> Result<(Option<T>, Option<String>), HostError> {
let decode = cfg!(feature = "hwdecode") && policy.allows_hardware();
if !decode && !encode {
return Ok((None, None));
}
let created = match excluded {
Some(reason) => Err(reason),
None => create().map_err(error_message),
};
match created {
Ok(context) => Ok((Some(context), None)),
Err(reason) if !decode => {
alpha2_os::diagnostic!(
"alpha2-host shared GPU device unavailable, exports take the readback path: {reason}"
);
Ok((None, None))
}
Err(reason) if policy == VideoDecodePolicy::Auto => {
alpha2_os::diagnostic!(
"alpha2-host hardware decode unavailable, using software: {reason}"
);
Ok((None, Some(reason)))
}
Err(reason) => Err(HostError::new("HARDWARE_DECODE_UNAVAILABLE", reason)),
}
}
fn install_termination() {
if let Err(error) = termination::install() {
alpha2_os::diagnostic!(
"alpha2-host: termination requests will end the process at once: {error}"
);
}
}
fn serve(host: &mut Option<Host>) -> Result<(), HostError> {
install_termination();
let policy = match VideoDecodePolicy::from_env() {
Ok(policy) => policy,
Err(message) => {
return Err(write_fatal(
HostError::invalid_input(message),
&mut std::io::stdout().lock(),
)?);
}
};
if let Err(reason) = crate::host::host_binary_digest() {
alpha2_os::diagnostic!("alpha2-host render cache unavailable: {reason}");
}
let mut serving = Serving::new(policy);
let served = serve_requests(
&mut serving,
&mut std::io::stdin().lock(),
&mut std::io::stdout().lock(),
&mut exit_if_unusable,
);
*host = serving.host.take();
served
}
fn write_fatal(error: HostError, stdout: &mut impl Write) -> Result<HostError, HostError> {
let error = fatal_error(error);
let fatal = json!({
"protocolVersion": PROTOCOL_VERSION,
"type": "fatal",
"error": error,
});
serde_json::to_writer(&mut *stdout, &fatal).map_err(HostError::json)?;
stdout.write_all(b"\n").map_err(HostError::io)?;
stdout.flush().map_err(HostError::io)?;
Ok(error)
}
fn fatal_error(error: HostError) -> HostError {
let failure = crate::failure::startup_failure_json(error.code, &error.message, error.evidence);
error.with_failure(failure)
}
struct Serving {
policy: VideoDecodePolicy,
host: Option<Host>,
create: fn(VideoDecodePolicy) -> Result<Host, HostError>,
}
impl Serving {
const fn new(policy: VideoDecodePolicy) -> Self {
Self {
policy,
host: None,
create: create_host,
}
}
#[cfg(test)]
const fn started(host: Host) -> Self {
Self {
policy: host.decode_policy,
host: Some(host),
create: create_host,
}
}
fn host(&mut self) -> Result<&mut Host, HostError> {
match &mut self.host {
Some(host) => Ok(host),
slot => Ok(slot.insert((self.create)(self.policy)?)),
}
}
}
fn serve_requests(
serving: &mut Serving,
input: &mut impl BufRead,
stdout: &mut impl Write,
answered: &mut dyn FnMut(Option<&Host>),
) -> Result<(), HostError> {
let mut buffer = Vec::new();
loop {
let envelope = match read_request_line(input, MAX_REQUEST_LINE_BYTES, &mut buffer)
.map_err(HostError::io)?
{
RequestLine::End => break,
RequestLine::Line(line) if line.trim().is_empty() => continue,
RequestLine::Line(line) => {
let envelope = parse_request(line);
let request_id = match &envelope {
Ok(request) => request.request_id().to_owned(),
Err(_) => request_id_from_line(line),
};
let export = envelope.is_err()
&& operation_from_line(line).is_none_or(|operation| operation == "export");
(request_id, envelope, export)
}
RequestLine::NotUtf8 => (
"unknown".to_owned(),
Err(HostError::new(
"INVALID_REQUEST",
"a request line must be UTF-8 JSON",
)),
true,
),
RequestLine::TooLong(bytes) => (
"unknown".to_owned(),
Err(HostError::new(
"INVALID_REQUEST",
format!(
"the request line is {bytes} bytes; the maximum is {MAX_REQUEST_LINE_BYTES}"
),
)),
true,
),
};
let (request_id, envelope, refused_export) = envelope;
let needs_host = match &envelope {
Ok(request) => !matches!(request, crate::protocol::Request::Conform(_)),
Err(_) => refused_export,
};
if needs_host && let Err(error) = serving.host() {
return Err(write_fatal(error, stdout)?);
}
let _request = termination::terminations().begin_request();
let response = match envelope {
Ok(crate::protocol::Request::Conform(request)) => {
crate::host::conform_media(request, serving.policy)
}
Ok(request) => serving.host()?.handle(request, &mut |progress| {
let event = json!({
"protocolVersion": PROTOCOL_VERSION,
"type": "progress",
"requestId": request_id,
"phase": progress.phase.as_str(),
"completed": progress.completed,
"total": progress.total,
});
serde_json::to_writer(&mut *stdout, &event).map_err(HostError::json)?;
stdout.write_all(b"\n").map_err(HostError::io)?;
stdout.flush().map_err(HostError::io)
}),
Err(error) if refused_export => Err(serving.host()?.refuse_export(error)),
Err(error) => Err(error),
};
let body = match response {
Ok(value) => json!({
"protocolVersion": PROTOCOL_VERSION,
"requestId": request_id,
"ok": true,
"result": value,
}),
Err(error) => json!({
"protocolVersion": PROTOCOL_VERSION,
"requestId": request_id,
"ok": false,
"error": error,
}),
};
serde_json::to_writer(&mut *stdout, &body).map_err(HostError::json)?;
stdout.write_all(b"\n").map_err(HostError::io)?;
stdout.flush().map_err(HostError::io)?;
answered(serving.host.as_ref());
}
Ok(())
}
enum RequestLine<'a> {
Line(&'a str),
TooLong(usize),
NotUtf8,
End,
}
fn read_request_line<'a>(
input: &mut impl BufRead,
limit: usize,
buffer: &'a mut Vec<u8>,
) -> std::io::Result<RequestLine<'a>> {
buffer.clear();
let mut length = 0_usize;
let mut ended = false;
loop {
let available = match input.fill_buf() {
Ok(available) => available,
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => continue,
Err(error) => return Err(error),
};
if available.is_empty() {
break;
}
let (taken, newline) = available
.iter()
.position(|byte| *byte == b'\n')
.map_or((available.len(), false), |at| (at + 1, true));
let content = &available[..taken - usize::from(newline)];
length = length.saturating_add(content.len());
if length <= limit {
buffer.extend_from_slice(content);
}
input.consume(taken);
ended = true;
if newline {
break;
}
}
if !ended {
return Ok(RequestLine::End);
}
if length > limit {
buffer.clear();
return Ok(RequestLine::TooLong(length));
}
if buffer.last() == Some(&b'\r') {
buffer.pop();
}
Ok(std::str::from_utf8(buffer).map_or(RequestLine::NotUtf8, RequestLine::Line))
}
fn write_stdout(value: &Value) -> Result<(), HostError> {
let mut stdout = std::io::stdout().lock();
serde_json::to_writer(&mut stdout, value).map_err(HostError::json)?;
stdout.write_all(b"\n").map_err(HostError::io)?;
stdout.flush().map_err(HostError::io)
}
enum GpuCall {
Fault(alpha2_media::GpuFault),
#[cfg_attr(not(feature = "hwdevice"), allow(dead_code))]
Failed,
}
fn gpu_call(error: &(dyn std::error::Error + 'static)) -> Option<GpuCall> {
#[cfg(feature = "hwdevice")]
{
let mut cause = Some(error);
while let Some(current) = cause {
if let Some(alpha2_ffmpeg_bridge::BridgeError::NativeGpu(failure)) =
current.downcast_ref::<alpha2_ffmpeg_bridge::BridgeError>()
{
return Some(failure.fault.map_or(GpuCall::Failed, GpuCall::Fault));
}
cause = current.source();
}
}
alpha2_engine::gpu_fault_of(error).map(GpuCall::Fault)
}
fn error_message(error: impl std::error::Error + 'static) -> String {
use alpha2_media::GpuFault;
match gpu_call(&error) {
Some(GpuCall::Fault(GpuFault::Timeout)) => {
return format!("{}{}", protocol::gpu_timeout_tag(), error);
}
Some(GpuCall::Fault(GpuFault::DeviceLost)) => {
return protocol::code_tag("GPU_DEVICE_LOST", error.to_string());
}
Some(GpuCall::Fault(GpuFault::DeviceMemory)) => {
return failure::evidence_tag(
"OUT_OF_MEMORY",
protocol::code_tag("ALPHA2_ERROR", error.to_string()),
);
}
Some(GpuCall::Fault(GpuFault::HostMemory)) => {
return failure::evidence_tag(
"ALLOCATION_FAILED",
protocol::code_tag("ALPHA2_ERROR", error.to_string()),
);
}
Some(_) => return protocol::code_tag("ALPHA2_ERROR", error.to_string()),
None => {}
}
match alpha2_engine::gpu_error_of(&error) {
Some(alpha2_gpu::GpuErrorKind::OutOfMemory) => {
return failure::evidence_tag(
"OUT_OF_MEMORY",
protocol::code_tag("ALPHA2_ERROR", error.to_string()),
);
}
Some(alpha2_gpu::GpuErrorKind::Validation) => {
return failure::evidence_tag(
"VALIDATION_FAILED",
protocol::code_tag("ALPHA2_ERROR", error.to_string()),
);
}
Some(_) => return protocol::code_tag("ALPHA2_ERROR", error.to_string()),
None => {}
}
match (&error as &dyn std::error::Error).downcast_ref::<alpha2_media::EncodeError>() {
Some(alpha2_media::EncodeError::Output(_)) => {
return protocol::code_tag("IO_ERROR", error.to_string());
}
Some(alpha2_media::EncodeError::OutputInUse(_)) => {
return protocol::code_tag("OUTPUT_IN_USE", error.to_string());
}
_ => {}
}
match alpha2_engine::ErrorCode::of(&error) {
Some(code) => protocol::code_tag(code.as_str(), error.to_string()),
None => error.to_string(),
}
}