control: keep channel responsive when window is covered (disable App Nap); fail fast on occluded screenshots

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-01 20:49:29 -04:00
1 parent 9f0cfb3373
commit 214345df13
4 files changed
+31

No files matched your search

Generated
+8
View File
@@ -1048,6 +1048,7 @@ dependencies = [
"effectcraft-ui-egui",
"eframe",
"egui",
"objc2-foundation 0.3.2",
"rfd",
"serde_json",
"winit",
@@ -5465,6 +5466,13 @@ version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.1.0"
dependencies = [
"serde_json",
]
[[package]]
name = "yoke"
version = "0.8.3"
+2
View File
@@ -21,3 +21,5 @@ rfd = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
# Same winit as eframe: macOS event-loop options (no app activation when driven by an agent).
winit = { version = "0.30", default-features = false }
# Keep the control channel responsive when the window is covered (App Nap / occlusion throttling).
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSProcessInfo", "NSString", "NSObject"] }
+15
View File
@@ -70,6 +70,7 @@ fn main() -> eframe::Result {
app.hooks.pick_open_project =
Some(Box::new(|| rfd::FileDialog::new().add_filter("EffectCraft Project", &["ecproj"]).pick_file().map(|p| p.to_string_lossy().to_string())));
if let Some(port) = control_port {
disable_app_nap();
let rx = control_server::start(port, cc.egui_ctx.clone());
app = app.with_control(rx);
}
@@ -91,3 +92,17 @@ fn agent_event_loop(agent: bool) -> Option<eframe::EventLoopBuilderHook> {
let _ = agent;
None
}
/// Agents drive the app while its window is covered or on another Space. macOS App Nap would
/// throttle the event loop then, stalling the control channel; opt out for `--control` runs.
fn disable_app_nap() {
#[cfg(target_os = "macos")]
{
use objc2_foundation::{NSActivityOptions, NSProcessInfo, NSString};
let info = NSProcessInfo::processInfo();
let reason = NSString::from_str("EffectCraft control channel");
let opts = NSActivityOptions::UserInitiatedAllowingIdleSystemSleep | NSActivityOptions::LatencyCritical;
// Leaked on purpose: the activity lasts for the life of the process.
std::mem::forget(info.beginActivityWithOptions_reason(opts, &reason));
}
}
+6
View File
@@ -313,6 +313,12 @@ impl EffectcraftApp {
}
control::Outcome::AfterInput => self.input_waiters.push(reply),
control::Outcome::Screenshot { path, crop } => {
// A covered or minimized window presents no frames (macOS skips its redraws), so
// a screenshot would never arrive and nothing would tick its timeout.
if ctx.input(|i| i.viewport().occluded == Some(true) || i.viewport().minimized == Some(true)) {
let _ = reply.send(json!({"ok": false, "error": "window is covered or minimized: call ui.focus first (render.frame renders comp pixels without the window)"}));
continue;
}
let token = self.next_token;
self.next_token += 1;
let settle = now + 0.35;