Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 66 additions & 10 deletions crates/enc-ffmpeg/src/video/h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ impl H264EncoderBuilder {
height: output_height,
});
}
if force_software_encoder()
if force_software_encoder(self.is_export)
|| requires_software_encoder(&input_config, self.preset, self.is_export)
{
return Err(H264EncoderError::VideoToolboxHwInput(
Expand Down Expand Up @@ -1189,7 +1189,7 @@ fn get_encoder_priority_with_override(
override_priority: Option<&'static [&'static str]>,
is_export: bool,
) -> &'static [&'static str] {
if force_software_encoder() {
if force_software_encoder(is_export) {
return &["libx264"];
}

Expand All @@ -1200,13 +1200,18 @@ fn get_encoder_priority_with_override(
override_priority.unwrap_or_else(|| get_default_encoder_priority(config))
}

fn force_software_encoder() -> bool {
std::env::var("CAP_EXPORT_FORCE_SOFTWARE_ENCODER").is_ok_and(|value| {
matches!(
value.to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
fn force_software_encoder(is_export: bool) -> bool {
let enabled = |name: &str| {
std::env::var(name).is_ok_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
};

enabled("CAP_EXPORT_FORCE_SOFTWARE_ENCODER")
|| (is_export && cfg!(target_os = "macos") && enabled("CAP_MACOS_SOFTWARE_CODECS"))
}

fn export_encoder_priority_override(
Expand Down Expand Up @@ -1677,6 +1682,34 @@ const MAX_BITRATE: usize = 1_000_000_000;
mod self_test_tests {
use super::*;

static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());

struct EnvVarGuard {
key: &'static str,
previous: Option<std::ffi::OsString>,
}

impl EnvVarGuard {
fn set(key: &'static str, value: &str) -> Self {
let previous = std::env::var_os(key);
// SAFETY: tests in this module serialize environment access with ENV_LOCK.
unsafe { std::env::set_var(key, value) };
Self { key, previous }
}
}

impl Drop for EnvVarGuard {
fn drop(&mut self) {
// SAFETY: tests in this module serialize environment access with ENV_LOCK.
unsafe {
match &self.previous {
Some(value) => std::env::set_var(self.key, value),
None => std::env::remove_var(self.key),
}
}
}
}

fn yuv420p_frame(y: u8, u: u8, v: u8) -> frame::Video {
let mut frame = frame::Video::new(format::Pixel::YUV420P, 64, 48);
frame.data_mut(0).fill(y);
Expand Down Expand Up @@ -1710,7 +1743,8 @@ mod self_test_tests {
#[cfg(target_os = "macos")]
#[test]
fn videotoolbox_export_uses_the_default_keyframe_interval() {
if force_software_encoder() {
let _env = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
if force_software_encoder(true) {
return;
}
ffmpeg::init().ok();
Expand All @@ -1737,6 +1771,28 @@ mod self_test_tests {
assert_eq!(realtime.get("g"), None);
}

#[cfg(target_os = "macos")]
#[test]
fn software_codec_env_only_forces_export_encoder_priority() {
let _env = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let _export_guard = EnvVarGuard::set("CAP_EXPORT_FORCE_SOFTWARE_ENCODER", "0");
let config = VideoInfo::from_raw(cap_media_info::RawVideoFormat::Nv12, 1920, 1080, 30);
let recording =
get_encoder_priority_with_override(&config, H264Preset::Medium, None, false);
assert_ne!(recording, &["libx264"]);

let _software_guard = EnvVarGuard::set("CAP_MACOS_SOFTWARE_CODECS", "1");

assert_eq!(
get_encoder_priority_with_override(&config, H264Preset::Medium, None, false),
recording
);
assert_eq!(
get_encoder_priority_with_override(&config, H264Preset::Medium, None, true),
&["libx264"]
);
}

#[cfg(target_os = "linux")]
#[test]
fn linux_nvidia_device_prefers_nvenc_and_keeps_software_fallback() {
Expand Down
1 change: 1 addition & 0 deletions crates/export/src/mp4.rs
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ impl Mp4ExportSettings {
on_progress: impl FnMut(u32) -> bool + Send + 'static,
mut mode: ExportNv12Mode,
) -> Result<PathBuf, String> {
mode.software_encoder |= cap_rendering::software_video_codecs_requested();
let on_progress = Arc::new(Mutex::new(on_progress));
let user_cancellation = base
.audio_cancellation
Expand Down
15 changes: 13 additions & 2 deletions crates/rendering/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1375,6 +1375,7 @@ pub fn spawn_managed_decoder<'a>(
offset: f64,
use_hw_acceleration: bool,
) -> Result<ManagedVideoDecoder, ManagedVideoError> {
let use_hw_acceleration = use_hw_acceleration && !crate::software_video_codecs_requested();
let input = ManagedVideoInput {
source,
paths: paths
Expand Down Expand Up @@ -1473,14 +1474,24 @@ pub async fn spawn_decoder(

#[cfg(target_os = "macos")]
{
if force_ffmpeg || path.is_dir() {
let software_codecs = crate::software_video_codecs_requested();
if force_ffmpeg || path.is_dir() || software_codecs {
Comment on lines +1477 to +1478

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Preview ignores software codecs

Setting CAP_MACOS_SOFTWARE_CODECS=1 does not force software decoding in the preparing editor preview. This check only covers spawn_decoder, while that preview uses spawn_managed_decoder. The desktop enables hardware decoding on macOS, and the managed decoder forwards that choice unchanged, so FFmpeg still selects VideoToolbox. Users can therefore still get the integrated-GPU activity this option is meant to avoid. Apply the software-codec setting to the managed decoder path too.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/rendering/src/decoder/mod.rs
Line: 1476-1477

Comment:
**Preview ignores software codecs**

Setting `CAP_MACOS_SOFTWARE_CODECS=1` does not force software decoding in the preparing editor preview. This check only covers `spawn_decoder`, while that preview uses `spawn_managed_decoder`. The desktop enables hardware decoding on macOS, and the managed decoder forwards that choice unchanged, so FFmpeg still selects VideoToolbox. Users can therefore still get the integrated-GPU activity this option is meant to avoid. Apply the software-codec setting to the managed decoder path too.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0550695. spawn_managed_decoder now applies CAP_MACOS_SOFTWARE_CODECS before constructing the FFmpeg decoder, so the preparing preview and camera managed decoders use software decoding too.

info!(
force_ffmpeg,
segmented = path.is_dir(),
software_codecs,
"Video '{}' using FFmpeg decoder",
name
);
return spawn_ffmpeg_decoder(name, path, fps, offset, timeout_duration, true).await;
return spawn_ffmpeg_decoder(
name,
path,
fps,
offset,
timeout_duration,
!software_codecs,
)
.await;
}

let avasset_result = {
Expand Down
15 changes: 15 additions & 0 deletions crates/rendering/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,21 @@ pub fn force_software_wgpu_adapter() -> bool {
})
}

/// `CAP_MACOS_SOFTWARE_CODECS=1` forces video decode and encode onto software
/// codecs on macOS. VideoToolbox schedules hardware H.264/HEVC work on the
/// integrated GPU on dual-GPU Macs, even when the renderer is on the discrete
/// GPU. Software codecs keep the integrated GPU idle at the cost of CPU time.
/// This only changes codec choice; the renderer keeps its own GPU selection.
pub fn software_video_codecs_requested() -> bool {
cfg!(target_os = "macos")
&& std::env::var("CAP_MACOS_SOFTWARE_CODECS").is_ok_and(|value| {
matches!(
value.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
})
}

pub fn create_wgpu_instance_sync() -> wgpu::Instance {
#[cfg(not(target_os = "windows"))]
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default());
Expand Down