editor: gate zero-copy VAAPI export to Linux (fix macOS/Windows build)
gpu_video_encoder's encoder/vaapi/vk_device/dmabuf modules are #[cfg(linux)] (VAAPI is Linux-only), but the editor referenced them unconditionally, so macOS/Windows failed to compile (cannot find `encoder` in `gpu_video_encoder`). Gate the zero-copy path (ZeroCopyVideo, try_build_zero_copy, run_zerocopy_video_export, and the branches in start_video_export / start_video_with_audio_export) behind cfg(target_os = "linux"). Factor the software encoder-thread spawn into spawn_software_video so both the None arm and the non-Linux path share it without duplication. Non-Linux always uses the software path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
05b5bf5f85
commit
7ebd64391c
|
|
@ -70,6 +70,9 @@ pub struct VideoExportState {
|
||||||
|
|
||||||
/// Zero-copy VAAPI video production: renders each frame to RGBA and hardware-encodes it
|
/// 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).
|
/// 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 {
|
struct ZeroCopyVideo {
|
||||||
encoder: gpu_video_encoder::encoder::ZeroCopyEncoder,
|
encoder: gpu_video_encoder::encoder::ZeroCopyEncoder,
|
||||||
renderer: vello::Renderer,
|
renderer: vello::Renderer,
|
||||||
|
|
@ -833,11 +836,53 @@ impl ExportOrchestrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
/// falls back to the software encoder path. Used by both the video-only and video+audio
|
||||||
/// export entry points.
|
/// export entry points. VAAPI is Linux-only, so this whole path is `cfg`-gated.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
fn try_build_zero_copy(
|
fn try_build_zero_copy(
|
||||||
settings: &VideoExportSettings,
|
settings: &VideoExportSettings,
|
||||||
width: u32,
|
width: u32,
|
||||||
|
|
@ -938,65 +983,39 @@ impl ExportOrchestrator {
|
||||||
self.cancel_flag.store(false, Ordering::Relaxed);
|
self.cancel_flag.store(false, Ordering::Relaxed);
|
||||||
let cancel_flag = Arc::clone(&self.cancel_flag);
|
let cancel_flag = Arc::clone(&self.cancel_flag);
|
||||||
|
|
||||||
// Zero-copy VAAPI H.264 writes the final output directly (no audio → no mux), reporting
|
// Zero-copy VAAPI H.264 (Linux only) writes the final output directly on a background
|
||||||
// through the single-export progress channel. Otherwise use the software encoder thread.
|
// thread (no audio → no mux), reporting through the single-export progress channel. On
|
||||||
let zero_copy = Self::try_build_zero_copy(&settings, width, height, framerate, &output_path);
|
// success it returns here; otherwise we fall through to the software encoder thread.
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
let (handle, video_state) = match zero_copy {
|
{
|
||||||
Some(zc) => {
|
if let Some(zc) = Self::try_build_zero_copy(&settings, width, height, framerate, &output_path) {
|
||||||
drop(frame_rx);
|
drop(frame_rx);
|
||||||
let document_snapshot = document.clone();
|
let document_snapshot = document.clone();
|
||||||
let mut image_cache = ImageCache::new();
|
let mut image_cache = ImageCache::new();
|
||||||
image_cache.set_container_path(container_path);
|
image_cache.set_container_path(container_path);
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
Self::run_zerocopy_video_export(
|
Self::run_zerocopy_video_export(
|
||||||
zc,
|
zc, document_snapshot, image_cache, video_manager, raster_store,
|
||||||
document_snapshot,
|
total_frames, start_time, framerate, width, height,
|
||||||
image_cache,
|
output_path, progress_tx, cancel_flag,
|
||||||
video_manager,
|
|
||||||
raster_store,
|
|
||||||
total_frames,
|
|
||||||
start_time,
|
|
||||||
framerate,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
output_path,
|
|
||||||
progress_tx,
|
|
||||||
cancel_flag,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
(handle, None)
|
self.thread_handle = Some(handle);
|
||||||
}
|
self.video_state = None;
|
||||||
None => {
|
println!("🎬 [VIDEO EXPORT] zero-copy thread spawned");
|
||||||
let handle = std::thread::spawn(move || {
|
return Ok(());
|
||||||
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, Some(state))
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// 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);
|
self.thread_handle = Some(handle);
|
||||||
|
|
@ -1093,21 +1112,14 @@ impl ExportOrchestrator {
|
||||||
let video_cancel_flag = Arc::clone(&self.cancel_flag);
|
let video_cancel_flag = Arc::clone(&self.cancel_flag);
|
||||||
let audio_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 (renders + hardware-encodes on its own device,
|
// Spawn the video thread: on Linux, try the background zero-copy renderer/encoder (its own
|
||||||
// writing the temp .mp4 directly); falls back to the software encoder thread when the
|
// device + a document snapshot, decoupled from the UI vsync loop, writing the temp .mp4
|
||||||
// codec isn't H.264 or VAAPI is unavailable.
|
// directly); otherwise (non-H.264, no VAAPI, or non-Linux) the software encoder thread fed
|
||||||
let zero_copy = Self::try_build_zero_copy(
|
// by `render_next_video_frame` on the UI thread.
|
||||||
&video_settings,
|
#[cfg(target_os = "linux")]
|
||||||
video_width,
|
let (video_thread, video_state) = match Self::try_build_zero_copy(
|
||||||
video_height,
|
&video_settings, video_width, video_height, video_framerate, &temp_video_path,
|
||||||
video_framerate,
|
) {
|
||||||
&temp_video_path,
|
|
||||||
);
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
Some(zc) => {
|
Some(zc) => {
|
||||||
drop(frame_rx); // the zero-copy path renders internally, no frame channel
|
drop(frame_rx); // the zero-copy path renders internally, no frame channel
|
||||||
let document_snapshot = document.clone();
|
let document_snapshot = document.clone();
|
||||||
|
|
@ -1118,56 +1130,34 @@ impl ExportOrchestrator {
|
||||||
let temp_video_path = temp_video_path.clone();
|
let temp_video_path = temp_video_path.clone();
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
Self::run_zerocopy_video_export(
|
Self::run_zerocopy_video_export(
|
||||||
zc,
|
zc, document_snapshot, image_cache, video_manager, raster_store,
|
||||||
document_snapshot,
|
total_frames, video_start_time, video_framerate, video_width, video_height,
|
||||||
image_cache,
|
temp_video_path, video_progress_tx, video_cancel_flag,
|
||||||
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.
|
// No UI-thread video state: rendering happens entirely on the background thread.
|
||||||
(Some(handle), None)
|
(Some(handle), None)
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let video_settings_clone = video_settings.clone();
|
let (h, s) = Self::spawn_software_video(
|
||||||
let temp_video_path_clone = temp_video_path.clone();
|
video_settings.clone(), temp_video_path.clone(), frame_rx, frame_tx,
|
||||||
let handle = std::thread::spawn(move || {
|
video_progress_tx, video_cancel_flag, total_frames,
|
||||||
Self::run_video_encoder(
|
video_start_time, video_end_time, video_framerate, video_width, video_height,
|
||||||
video_settings_clone,
|
);
|
||||||
temp_video_path_clone,
|
(Some(h), Some(s))
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
#[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
|
// Spawn audio export thread
|
||||||
let temp_audio_path_clone = temp_audio_path.clone();
|
let temp_audio_path_clone = temp_audio_path.clone();
|
||||||
|
|
@ -1410,7 +1400,8 @@ impl ExportOrchestrator {
|
||||||
/// `.mp4`. Runs entirely off the UI thread (its own device + a `Document` snapshot), so it's
|
/// `.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`
|
/// 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 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)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn run_zerocopy_video_export(
|
fn run_zerocopy_video_export(
|
||||||
mut zc: ZeroCopyVideo,
|
mut zc: ZeroCopyVideo,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue