Release 1.0.6-alpha
This commit is contained in:
commit
a933fc3ca8
|
|
@ -118,6 +118,19 @@ jobs:
|
|||
run: |
|
||||
sed -i.bak 's/ffmpeg-next = { version = "8.0", features = \["static"\] }/ffmpeg-next = { version = "8.0", features = ["build", "static"] }/' lightningbeam-ui/lightningbeam-editor/Cargo.toml
|
||||
|
||||
# ffmpeg-sys-next configures FFmpeg with --disable-autodetect and only passes --enable-vaapi
|
||||
# when its build-vaapi feature is set; ffmpeg-next 8.0 doesn't forward it, so depend on the
|
||||
# sys crate directly. Linux only (the feature is a no-op elsewhere and we only ship VAAPI on
|
||||
# Linux). With it set, a missing libva-dev makes FFmpeg configure fail loudly instead of
|
||||
# silently shipping a software-only build.
|
||||
- name: Enable VAAPI in FFmpeg build (Linux)
|
||||
if: matrix.platform == 'ubuntu-24.04'
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i '/^ffmpeg-next = /a ffmpeg-sys-next = { version = "8.0", features = ["build-vaapi"] }' \
|
||||
lightningbeam-ui/lightningbeam-editor/Cargo.toml
|
||||
grep -n 'ffmpeg-sys-next' lightningbeam-ui/lightningbeam-editor/Cargo.toml
|
||||
|
||||
- name: Setup FFmpeg (Windows)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
shell: pwsh
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -6107,7 +6107,14 @@ impl eframe::App for EditorApp {
|
|||
ExportResult::VideoOnly(settings, output_path) => {
|
||||
println!("🎬 [MAIN] Starting video-only export: {}", output_path.display());
|
||||
|
||||
match orchestrator.start_video_export(settings, output_path) {
|
||||
match orchestrator.start_video_export(
|
||||
settings,
|
||||
output_path,
|
||||
self.action_executor.document(),
|
||||
Arc::clone(&self.video_manager),
|
||||
self.raster_store.clone(),
|
||||
self.current_file_path.clone(),
|
||||
) {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
eprintln!("❌ Failed to start video export: {}", err);
|
||||
|
|
|
|||
|
|
@ -67,6 +67,9 @@ cd /build/src/lightningbeam-ui
|
|||
|
||||
# Build FFmpeg from source and link statically (no .so bundling needed)
|
||||
sed -i 's/ffmpeg-next = { version = \"8.0\", features = \[\"static\"\] }/ffmpeg-next = { version = \"8.0\", features = [\"build\", \"static\"] }/' lightningbeam-editor/Cargo.toml
|
||||
# Enable VAAPI: ffmpeg-sys-next configures with --disable-autodetect and only passes
|
||||
# --enable-vaapi when its build-vaapi feature is set (ffmpeg-next 8.0 doesn't forward it).
|
||||
sed -i '/^ffmpeg-next = /a ffmpeg-sys-next = { version = "8.0", features = ["build-vaapi"] }' lightningbeam-editor/Cargo.toml
|
||||
export FFMPEG_STATIC=1
|
||||
|
||||
# Stage factory presets for packaging
|
||||
|
|
|
|||
Loading…
Reference in New Issue