diff --git a/crates/oak-app/src/main.rs b/crates/oak-app/src/main.rs index 5f6b5199d..92a569122 100644 --- a/crates/oak-app/src/main.rs +++ b/crates/oak-app/src/main.rs @@ -16,6 +16,12 @@ //! The `oakapp` binary: opens the Oak main window (see [`oakapp::app`]). +// Windows release builds are GUI applications: without the subsystem +// attribute the linker marks the executable as a console program and +// launching Oak opens a console window. Debug builds keep the console +// (`cargo run` shows logs). The attribute is ignored elsewhere. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + fn main() { oakapp::run(); } diff --git a/crates/oak-core/src/miscutils.rs b/crates/oak-core/src/miscutils.rs index 283018158..1b7f02e85 100644 --- a/crates/oak-core/src/miscutils.rs +++ b/crates/oak-core/src/miscutils.rs @@ -316,6 +316,25 @@ impl Current { } } +/// Hide the console window a child process would otherwise create on +/// Windows: a GUI binary (the release `oak-editor.exe`) spawning a +/// console-subsystem child — `oak-worker`, the OFX host, `ffmpeg` — +/// flashes a console window even when every stream is piped. No-op on +/// other platforms. +pub fn hide_console_window(command: &mut std::process::Command) { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + /// `CREATE_NO_WINDOW` (winbase.h). + const CREATE_NO_WINDOW: u32 = 0x0800_0000; + command.creation_flags(CREATE_NO_WINDOW); + } + #[cfg(not(windows))] + { + let _ = command; + } +} + #[cfg(test)] mod tests { use std::ffi::c_void; diff --git a/crates/oak-render/src/ofxhost.rs b/crates/oak-render/src/ofxhost.rs index 83a5584e5..191e2ce49 100644 --- a/crates/oak-render/src/ofxhost.rs +++ b/crates/oak-render/src/ofxhost.rs @@ -466,6 +466,8 @@ impl OfxHost { for (key, value) in &config.env { command.env(key, value); } + // A GUI Oak spawning a console child would flash a console window. + oak_core::miscutils::hide_console_window(&mut command); let mut child = command .spawn() .map_err(|e| Error::Failed(format!("spawn OFX host {}: {e}", bin.display())))?; diff --git a/crates/oak-render/src/procpool.rs b/crates/oak-render/src/procpool.rs index 9332c797d..6009390b6 100644 --- a/crates/oak-render/src/procpool.rs +++ b/crates/oak-render/src/procpool.rs @@ -1950,14 +1950,18 @@ impl ProcessDispatcher { ); let shm = ShmRegionView::create(&key, inner.slots, inner.slot_bytes)?; - let mut child = Command::new(&inner.bin) + let mut command = Command::new(&inner.bin); + command // Auto backend: prefer the GPU, fall back to the CPU renderer // (M16 S1 — the worker tolerates a GPU init failure and keeps // evaluating headless). .args(["--backend", "auto"]) .stdin(Stdio::piped()) .stdout(Stdio::piped()) - .stderr(Stdio::inherit()) + .stderr(Stdio::inherit()); + // A GUI Oak spawning a console child would flash a console window. + oak_core::miscutils::hide_console_window(&mut command); + let mut child = command .spawn() .map_err(|e| Error::Failed(format!("spawn oak-worker: {e}")))?; let stdin = child.stdin.take(); diff --git a/crates/oak-task/src/proxy.rs b/crates/oak-task/src/proxy.rs index 79c259984..b5597bbad 100644 --- a/crates/oak-task/src/proxy.rs +++ b/crates/oak-task/src/proxy.rs @@ -213,9 +213,10 @@ impl ProxyTask { } fn probe_hw_encoder_uncached(ffmpeg_path: &str) -> HwEncoder { - let out = Command::new(ffmpeg_path) - .args(["-hide_banner", "-encoders"]) - .output(); + let mut command = Command::new(ffmpeg_path); + command.args(["-hide_banner", "-encoders"]); + oak_core::miscutils::hide_console_window(&mut command); + let out = command.output(); let Ok(out) = out else { return HwEncoder::Software; }; @@ -251,24 +252,25 @@ impl ProxyTask { /// Whether `encoder` can initialize and encode one tiny frame; see the /// caller for why the `-encoders` list is not enough. fn encoder_usable(ffmpeg_path: &str, encoder: &str) -> bool { - let out = Command::new(ffmpeg_path) - .args([ - "-hide_banner", - "-loglevel", - "error", - "-f", - "lavfi", - "-i", - "color=size=64x64:rate=1:duration=1", - "-frames:v", - "1", - "-c:v", - encoder, - "-f", - "null", - "-", - ]) - .output(); + let mut command = Command::new(ffmpeg_path); + command.args([ + "-hide_banner", + "-loglevel", + "error", + "-f", + "lavfi", + "-i", + "color=size=64x64:rate=1:duration=1", + "-frames:v", + "1", + "-c:v", + encoder, + "-f", + "null", + "-", + ]); + oak_core::miscutils::hide_console_window(&mut command); + let out = command.output(); matches!(out, Ok(out) if out.status.success()) } @@ -508,6 +510,8 @@ impl TaskBehavior for ProxyTask { .args(&args) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + // A GUI Oak spawning a console child would flash a console window. + oak_core::miscutils::hide_console_window(&mut command); let mut child = match command.spawn() { Ok(child) => child, Err(_) => { @@ -601,17 +605,18 @@ fn probe_source_duration_seconds(ffmpeg_path: &str, source_filename: &str) -> f6 if !ffprobe.exists() { return 0.0; } - let output = Command::new(&ffprobe) - .args([ - "-v", - "error", - "-show_entries", - "format=duration", - "-of", - "default=noprint_wrappers=1:nokey=1", - source_filename, - ]) - .output(); + let mut command = Command::new(&ffprobe); + command.args([ + "-v", + "error", + "-show_entries", + "format=duration", + "-of", + "default=noprint_wrappers=1:nokey=1", + source_filename, + ]); + oak_core::miscutils::hide_console_window(&mut command); + let output = command.output(); match output { Ok(out) if out.status.success() => { let text = String::from_utf8_lossy(&out.stdout);