|
|
|
|
@ -70,6 +70,9 @@ pub struct VideoExportState {
|
|
|
|
|
|
|
|
|
|
/// Zero-copy VAAPI video production: renders each frame to RGBA and hardware-encodes it
|
|
|
|
|
/// into a VAAPI surface, all on the encoder's own wgpu device (no readback / swscale).
|
|
|
|
|
/// VAAPI is Linux-only, so this and its machinery are `cfg`-gated; other platforms always
|
|
|
|
|
/// use the software encoder path.
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
struct ZeroCopyVideo {
|
|
|
|
|
encoder: gpu_video_encoder::encoder::ZeroCopyEncoder,
|
|
|
|
|
renderer: vello::Renderer,
|
|
|
|
|
@ -833,21 +836,131 @@ impl ExportOrchestrator {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Start a video export in the background (encoder thread)
|
|
|
|
|
/// Spawn the software video-encoder thread and build its UI-driven export state. Used by every
|
|
|
|
|
/// non-zero-copy path (non-H.264, VAAPI unavailable, or non-Linux platforms). The caller drives
|
|
|
|
|
/// frames into it via `render_next_video_frame()`.
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn spawn_software_video(
|
|
|
|
|
settings: VideoExportSettings,
|
|
|
|
|
output_path: PathBuf,
|
|
|
|
|
frame_rx: Receiver<VideoFrameMessage>,
|
|
|
|
|
frame_tx: Sender<VideoFrameMessage>,
|
|
|
|
|
progress_tx: Sender<ExportProgress>,
|
|
|
|
|
cancel_flag: Arc<AtomicBool>,
|
|
|
|
|
total_frames: usize,
|
|
|
|
|
start_time: f64,
|
|
|
|
|
end_time: f64,
|
|
|
|
|
framerate: f64,
|
|
|
|
|
width: u32,
|
|
|
|
|
height: u32,
|
|
|
|
|
) -> (std::thread::JoinHandle<()>, VideoExportState) {
|
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
|
|
|
Self::run_video_encoder(settings, output_path, frame_rx, progress_tx, cancel_flag, total_frames);
|
|
|
|
|
});
|
|
|
|
|
// GPU resources + readback pipeline init lazily on the first frame (needs device).
|
|
|
|
|
let state = VideoExportState {
|
|
|
|
|
current_frame: 0,
|
|
|
|
|
total_frames,
|
|
|
|
|
start_time,
|
|
|
|
|
end_time,
|
|
|
|
|
framerate,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
frame_tx: Some(frame_tx),
|
|
|
|
|
gpu_resources: None,
|
|
|
|
|
readback_pipeline: None,
|
|
|
|
|
cpu_yuv_converter: None,
|
|
|
|
|
frames_in_flight: 0,
|
|
|
|
|
next_frame_to_encode: 0,
|
|
|
|
|
perf_metrics: Some(perf_metrics::ExportMetrics::new()),
|
|
|
|
|
};
|
|
|
|
|
(handle, state)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Build a zero-copy VAAPI H.264 encoder targeting `output_path` (with its own vello
|
|
|
|
|
/// renderer + GPU resources + RGBA texture on the encoder's device), or `None` when the
|
|
|
|
|
/// codec isn't H.264 or VAAPI / the GPU device is unavailable — in which case the caller
|
|
|
|
|
/// falls back to the software encoder path. Used by both the video-only and video+audio
|
|
|
|
|
/// export entry points. VAAPI is Linux-only, so this whole path is `cfg`-gated.
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
fn try_build_zero_copy(
|
|
|
|
|
settings: &VideoExportSettings,
|
|
|
|
|
width: u32,
|
|
|
|
|
height: u32,
|
|
|
|
|
framerate: f64,
|
|
|
|
|
output_path: &std::path::Path,
|
|
|
|
|
) -> Option<ZeroCopyVideo> {
|
|
|
|
|
if !matches!(settings.codec, lightningbeam_core::export::VideoCodec::H264) {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
let encoder = match gpu_video_encoder::encoder::ZeroCopyEncoder::new(
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
framerate.round() as i32,
|
|
|
|
|
settings.quality.bitrate_kbps(),
|
|
|
|
|
output_path,
|
|
|
|
|
) {
|
|
|
|
|
Ok(e) => e,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("🎬 [EXPORT] zero-copy unavailable ({e}); software path");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let renderer = match vello::Renderer::new(
|
|
|
|
|
encoder.device(),
|
|
|
|
|
vello::RendererOptions {
|
|
|
|
|
use_cpu: false,
|
|
|
|
|
antialiasing_support: vello::AaSupport::all(),
|
|
|
|
|
num_init_threads: None,
|
|
|
|
|
pipeline_cache: None,
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
Ok(r) => r,
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("🎬 [EXPORT] zero-copy renderer init failed ({e}); software path");
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
let gpu_resources = video_exporter::ExportGpuResources::new(encoder.device(), width, height);
|
|
|
|
|
let rgba = encoder.device().create_texture(&wgpu::TextureDescriptor {
|
|
|
|
|
label: Some("zerocopy_export_rgba"),
|
|
|
|
|
size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
|
|
|
|
|
mip_level_count: 1,
|
|
|
|
|
sample_count: 1,
|
|
|
|
|
dimension: wgpu::TextureDimension::D2,
|
|
|
|
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
|
|
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
|
|
|
| wgpu::TextureUsages::TEXTURE_BINDING
|
|
|
|
|
| wgpu::TextureUsages::COPY_SRC,
|
|
|
|
|
view_formats: &[],
|
|
|
|
|
});
|
|
|
|
|
println!("🎬 [EXPORT] zero-copy VAAPI H.264 enabled");
|
|
|
|
|
Some(ZeroCopyVideo { encoder, renderer, gpu_resources, rgba })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Start a video export in the background.
|
|
|
|
|
///
|
|
|
|
|
/// Returns immediately after spawning encoder thread. Caller must call
|
|
|
|
|
/// `render_next_video_frame()` repeatedly from the main thread to feed frames.
|
|
|
|
|
/// For H.264 with VAAPI available this renders + hardware-encodes on a background thread
|
|
|
|
|
/// (writing `output_path` directly); otherwise it spawns the software encoder thread and the
|
|
|
|
|
/// caller drives frames via `render_next_video_frame()` from the main thread.
|
|
|
|
|
///
|
|
|
|
|
/// # Arguments
|
|
|
|
|
/// * `settings` - Video export settings
|
|
|
|
|
/// * `output_path` - Output file path
|
|
|
|
|
/// * `document`/`video_manager`/`raster_store`/`container_path` - scene data; the zero-copy
|
|
|
|
|
/// path snapshots the document and renders off-thread (the UI keeps the live one).
|
|
|
|
|
///
|
|
|
|
|
/// # Returns
|
|
|
|
|
/// Ok(()) on success, Err on failure
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
pub fn start_video_export(
|
|
|
|
|
&mut self,
|
|
|
|
|
settings: VideoExportSettings,
|
|
|
|
|
output_path: PathBuf,
|
|
|
|
|
document: &Document,
|
|
|
|
|
video_manager: Arc<std::sync::Mutex<VideoManager>>,
|
|
|
|
|
raster_store: lightningbeam_core::raster_store::RasterStore,
|
|
|
|
|
container_path: Option<PathBuf>,
|
|
|
|
|
) -> Result<(), String> {
|
|
|
|
|
println!("🎬 [VIDEO EXPORT] Starting video export");
|
|
|
|
|
|
|
|
|
|
@ -870,38 +983,43 @@ impl ExportOrchestrator {
|
|
|
|
|
self.cancel_flag.store(false, Ordering::Relaxed);
|
|
|
|
|
let cancel_flag = Arc::clone(&self.cancel_flag);
|
|
|
|
|
|
|
|
|
|
// Spawn encoder thread
|
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
|
|
|
Self::run_video_encoder(
|
|
|
|
|
settings,
|
|
|
|
|
output_path,
|
|
|
|
|
frame_rx,
|
|
|
|
|
progress_tx,
|
|
|
|
|
cancel_flag,
|
|
|
|
|
total_frames,
|
|
|
|
|
// Zero-copy VAAPI H.264 (Linux only) writes the final output directly on a background
|
|
|
|
|
// thread (no audio → no mux), reporting through the single-export progress channel. On
|
|
|
|
|
// success it returns here; otherwise we fall through to the software encoder thread.
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
{
|
|
|
|
|
if let Some(zc) = Self::try_build_zero_copy(&settings, width, height, framerate, &output_path) {
|
|
|
|
|
drop(frame_rx);
|
|
|
|
|
let document_snapshot = document.clone();
|
|
|
|
|
let mut image_cache = ImageCache::new();
|
|
|
|
|
image_cache.set_container_path(container_path);
|
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
|
|
|
Self::run_zerocopy_video_export(
|
|
|
|
|
zc, document_snapshot, image_cache, video_manager, raster_store,
|
|
|
|
|
total_frames, start_time, framerate, width, height,
|
|
|
|
|
output_path, progress_tx, cancel_flag,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
self.thread_handle = Some(handle);
|
|
|
|
|
self.video_state = None;
|
|
|
|
|
println!("🎬 [VIDEO EXPORT] zero-copy thread spawned");
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Zero-copy isn't used here; the scene-snapshot inputs are Linux-zero-copy-only.
|
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
|
|
|
let _ = (document, &video_manager, &raster_store, &container_path);
|
|
|
|
|
|
|
|
|
|
let (handle, video_state) = {
|
|
|
|
|
let (h, s) = Self::spawn_software_video(
|
|
|
|
|
settings, output_path, frame_rx, frame_tx, progress_tx, cancel_flag,
|
|
|
|
|
total_frames, start_time, end_time, framerate, width, height,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
(h, Some(s))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
self.thread_handle = Some(handle);
|
|
|
|
|
|
|
|
|
|
// Initialize video export state
|
|
|
|
|
// GPU resources and readback pipeline will be initialized lazily on first frame (needs device)
|
|
|
|
|
self.video_state = Some(VideoExportState {
|
|
|
|
|
current_frame: 0,
|
|
|
|
|
total_frames,
|
|
|
|
|
start_time,
|
|
|
|
|
end_time,
|
|
|
|
|
framerate,
|
|
|
|
|
width,
|
|
|
|
|
height,
|
|
|
|
|
frame_tx: Some(frame_tx),
|
|
|
|
|
gpu_resources: None,
|
|
|
|
|
readback_pipeline: None,
|
|
|
|
|
cpu_yuv_converter: None,
|
|
|
|
|
frames_in_flight: 0,
|
|
|
|
|
next_frame_to_encode: 0,
|
|
|
|
|
perf_metrics: Some(perf_metrics::ExportMetrics::new()),
|
|
|
|
|
});
|
|
|
|
|
self.video_state = video_state;
|
|
|
|
|
|
|
|
|
|
println!("🎬 [VIDEO EXPORT] Encoder thread spawned, ready for frames");
|
|
|
|
|
Ok(())
|
|
|
|
|
@ -994,65 +1112,14 @@ impl ExportOrchestrator {
|
|
|
|
|
let video_cancel_flag = Arc::clone(&self.cancel_flag);
|
|
|
|
|
let audio_cancel_flag = Arc::clone(&self.cancel_flag);
|
|
|
|
|
|
|
|
|
|
// Try the zero-copy VAAPI path for H.264: render + hardware-encode each frame inline
|
|
|
|
|
// on its own device, writing the temp .mp4 directly (no readback / swscale / encoder
|
|
|
|
|
// thread). Falls back to the software encoder thread when unavailable.
|
|
|
|
|
let zero_copy = if matches!(video_settings.codec, lightningbeam_core::export::VideoCodec::H264) {
|
|
|
|
|
match gpu_video_encoder::encoder::ZeroCopyEncoder::new(
|
|
|
|
|
video_width,
|
|
|
|
|
video_height,
|
|
|
|
|
video_framerate.round() as i32,
|
|
|
|
|
video_settings.quality.bitrate_kbps(),
|
|
|
|
|
&temp_video_path,
|
|
|
|
|
) {
|
|
|
|
|
Ok(encoder) => match vello::Renderer::new(
|
|
|
|
|
encoder.device(),
|
|
|
|
|
vello::RendererOptions {
|
|
|
|
|
use_cpu: false,
|
|
|
|
|
antialiasing_support: vello::AaSupport::all(),
|
|
|
|
|
num_init_threads: None,
|
|
|
|
|
pipeline_cache: None,
|
|
|
|
|
},
|
|
|
|
|
) {
|
|
|
|
|
Ok(renderer) => {
|
|
|
|
|
let gpu_resources = video_exporter::ExportGpuResources::new(
|
|
|
|
|
encoder.device(),
|
|
|
|
|
video_width,
|
|
|
|
|
video_height,
|
|
|
|
|
);
|
|
|
|
|
let rgba = encoder.device().create_texture(&wgpu::TextureDescriptor {
|
|
|
|
|
label: Some("zerocopy_export_rgba"),
|
|
|
|
|
size: wgpu::Extent3d { width: video_width, height: video_height, depth_or_array_layers: 1 },
|
|
|
|
|
mip_level_count: 1,
|
|
|
|
|
sample_count: 1,
|
|
|
|
|
dimension: wgpu::TextureDimension::D2,
|
|
|
|
|
format: wgpu::TextureFormat::Rgba8Unorm,
|
|
|
|
|
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
|
|
|
|
|
| wgpu::TextureUsages::TEXTURE_BINDING
|
|
|
|
|
| wgpu::TextureUsages::COPY_SRC,
|
|
|
|
|
view_formats: &[],
|
|
|
|
|
});
|
|
|
|
|
println!("🎬 [PARALLEL EXPORT] zero-copy VAAPI H.264 enabled");
|
|
|
|
|
Some(ZeroCopyVideo { encoder, renderer, gpu_resources, rgba })
|
|
|
|
|
}
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("🎬 [PARALLEL EXPORT] zero-copy renderer init failed ({e}); software path");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Err(e) => {
|
|
|
|
|
println!("🎬 [PARALLEL EXPORT] zero-copy unavailable ({e}); software path");
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Spawn the video thread: either the background zero-copy renderer/encoder (which owns a
|
|
|
|
|
// document snapshot + its own device, decoupled from the UI's vsync loop) or the software
|
|
|
|
|
// encoder thread fed by `render_next_video_frame` on the UI thread.
|
|
|
|
|
let (video_thread, video_state) = match zero_copy {
|
|
|
|
|
// Spawn the video thread: on Linux, try the background zero-copy renderer/encoder (its own
|
|
|
|
|
// device + a document snapshot, decoupled from the UI vsync loop, writing the temp .mp4
|
|
|
|
|
// directly); otherwise (non-H.264, no VAAPI, or non-Linux) the software encoder thread fed
|
|
|
|
|
// by `render_next_video_frame` on the UI thread.
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
let (video_thread, video_state) = match Self::try_build_zero_copy(
|
|
|
|
|
&video_settings, video_width, video_height, video_framerate, &temp_video_path,
|
|
|
|
|
) {
|
|
|
|
|
Some(zc) => {
|
|
|
|
|
drop(frame_rx); // the zero-copy path renders internally, no frame channel
|
|
|
|
|
let document_snapshot = document.clone();
|
|
|
|
|
@ -1063,56 +1130,34 @@ impl ExportOrchestrator {
|
|
|
|
|
let temp_video_path = temp_video_path.clone();
|
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
|
|
|
Self::run_zerocopy_video_export(
|
|
|
|
|
zc,
|
|
|
|
|
document_snapshot,
|
|
|
|
|
image_cache,
|
|
|
|
|
video_manager,
|
|
|
|
|
raster_store,
|
|
|
|
|
total_frames,
|
|
|
|
|
video_start_time,
|
|
|
|
|
video_framerate,
|
|
|
|
|
video_width,
|
|
|
|
|
video_height,
|
|
|
|
|
temp_video_path,
|
|
|
|
|
video_progress_tx,
|
|
|
|
|
video_cancel_flag,
|
|
|
|
|
zc, document_snapshot, image_cache, video_manager, raster_store,
|
|
|
|
|
total_frames, video_start_time, video_framerate, video_width, video_height,
|
|
|
|
|
temp_video_path, video_progress_tx, video_cancel_flag,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
// No UI-thread video state: rendering happens entirely on the background thread.
|
|
|
|
|
(Some(handle), None)
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
let video_settings_clone = video_settings.clone();
|
|
|
|
|
let temp_video_path_clone = temp_video_path.clone();
|
|
|
|
|
let handle = std::thread::spawn(move || {
|
|
|
|
|
Self::run_video_encoder(
|
|
|
|
|
video_settings_clone,
|
|
|
|
|
temp_video_path_clone,
|
|
|
|
|
frame_rx,
|
|
|
|
|
video_progress_tx,
|
|
|
|
|
video_cancel_flag,
|
|
|
|
|
total_frames,
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
let state = VideoExportState {
|
|
|
|
|
current_frame: 0,
|
|
|
|
|
total_frames,
|
|
|
|
|
start_time: video_start_time,
|
|
|
|
|
end_time: video_end_time,
|
|
|
|
|
framerate: video_framerate,
|
|
|
|
|
width: video_width,
|
|
|
|
|
height: video_height,
|
|
|
|
|
frame_tx: Some(frame_tx),
|
|
|
|
|
gpu_resources: None,
|
|
|
|
|
readback_pipeline: None,
|
|
|
|
|
cpu_yuv_converter: None,
|
|
|
|
|
frames_in_flight: 0,
|
|
|
|
|
next_frame_to_encode: 0,
|
|
|
|
|
perf_metrics: Some(perf_metrics::ExportMetrics::new()),
|
|
|
|
|
};
|
|
|
|
|
(Some(handle), Some(state))
|
|
|
|
|
let (h, s) = Self::spawn_software_video(
|
|
|
|
|
video_settings.clone(), temp_video_path.clone(), frame_rx, frame_tx,
|
|
|
|
|
video_progress_tx, video_cancel_flag, total_frames,
|
|
|
|
|
video_start_time, video_end_time, video_framerate, video_width, video_height,
|
|
|
|
|
);
|
|
|
|
|
(Some(h), Some(s))
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
#[cfg(not(target_os = "linux"))]
|
|
|
|
|
let (video_thread, video_state) = {
|
|
|
|
|
// VAAPI/zero-copy is Linux-only; these scene-snapshot inputs are unused elsewhere.
|
|
|
|
|
let _ = (document, &video_manager, &raster_store, &container_path);
|
|
|
|
|
let (h, s) = Self::spawn_software_video(
|
|
|
|
|
video_settings.clone(), temp_video_path.clone(), frame_rx, frame_tx,
|
|
|
|
|
video_progress_tx, video_cancel_flag, total_frames,
|
|
|
|
|
video_start_time, video_end_time, video_framerate, video_width, video_height,
|
|
|
|
|
);
|
|
|
|
|
(Some(h), Some(s))
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Spawn audio export thread
|
|
|
|
|
let temp_audio_path_clone = temp_audio_path.clone();
|
|
|
|
|
@ -1355,7 +1400,8 @@ impl ExportOrchestrator {
|
|
|
|
|
/// `.mp4`. Runs entirely off the UI thread (its own device + a `Document` snapshot), so it's
|
|
|
|
|
/// not throttled by egui's vsync'd repaint loop. Reports progress through `progress_tx`
|
|
|
|
|
/// (the same channel the software encoder thread uses); `poll_parallel_progress` muxes with
|
|
|
|
|
/// the audio track once both stream's `Complete` arrive.
|
|
|
|
|
/// the audio track once both stream's `Complete` arrive. VAAPI is Linux-only.
|
|
|
|
|
#[cfg(target_os = "linux")]
|
|
|
|
|
#[allow(clippy::too_many_arguments)]
|
|
|
|
|
fn run_zerocopy_video_export(
|
|
|
|
|
mut zc: ZeroCopyVideo,
|
|
|
|
|
|