diff --git a/apps/photocraft/src/gpu_startup.rs b/apps/photocraft/src/gpu_startup.rs index 66acb9a42f..e79b4bafac 100644 --- a/apps/photocraft/src/gpu_startup.rs +++ b/apps/photocraft/src/gpu_startup.rs @@ -443,6 +443,14 @@ pub fn read_display_server(path: Option<&Path>) -> photocraft_engine::prefs::Lin .unwrap_or_default() } +/// Preferences › Performance › Low-Latency Drawing (`performance.lowLatencyDrawing`, off unless +/// turned on), read before the window opens: it picks the surface's present mode. +pub fn read_low_latency(path: Option<&Path>) -> bool { + let v: Value = path.and_then(|p| std::fs::read_to_string(p).ok()).and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(Value::Null); + let default = photocraft_engine::prefs::Preferences::default().performance.low_latency_drawing; + v.get("performance").and_then(|p| p.get("lowLatencyDrawing")).and_then(Value::as_bool).unwrap_or(default) +} + /// Read the policy leniently at startup, including old settings without renderingMode. pub fn read_rendering_prefs(path: Option<&Path>) -> (GpuBackend, RenderingMode) { let v: Value = path.and_then(|p| std::fs::read_to_string(p).ok()).and_then(|t| serde_json::from_str(&t).ok()).unwrap_or(Value::Null); @@ -463,6 +471,23 @@ mod tests { Marker { backend: backend.into(), adapter: "Intel(R) UHD Graphics".into(), adapter_backend: adapter_backend.into(), ..Default::default() } } + #[test] + fn low_latency_drawing_is_read_before_the_window_opens_and_defaults_off() { + let dir = std::env::temp_dir().join(format!("photocraft-low-latency-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let file = dir.join("preferences.json"); + let read = |json: &str| { + std::fs::write(&file, json).unwrap(); + read_low_latency(Some(&file)) + }; + assert!(read(r#"{"performance":{"lowLatencyDrawing":true}}"#)); + assert!(!read(r#"{"performance":{"lowLatencyDrawing":false}}"#)); + assert!(!read(r#"{"performance":{"lowLatencyDrawing":"nope"}}"#), "garbage keeps the default (off)"); + assert!(!read("not json")); + assert!(!read_low_latency(None), "no settings file: the default (off)"); + let _ = std::fs::remove_dir_all(&dir); + } + #[test] fn display_server_preference_is_read_before_the_window_opens() { use photocraft_engine::prefs::LinuxDisplayServer; diff --git a/apps/photocraft/src/main.rs b/apps/photocraft/src/main.rs index de1155edbd..0b82237a39 100644 --- a/apps/photocraft/src/main.rs +++ b/apps/photocraft/src/main.rs @@ -2,7 +2,13 @@ //! //! Usage: `photocraft [--control ] [--control-token <64-hex> | //! --control-token-file ] [--automation-read-root ] -//! [--automation-write-root ] [--safe-gpu] [--in-window-menus] [files…]` +//! [--automation-write-root ] [--safe-gpu] [--in-window-menus] [--present-mode ] +//! [--frame-latency ] [--stroke-trace ] [files…]` +//! +//! `--present-mode` (`fifo`, `autovsync`, `novsync`) and `--frame-latency` (1–3) override how +//! frames reach the display for this launch (`PHOTOCRAFT_PRESENT_MODE` / `…_FRAME_LATENCY`; +//! `presentation`); `--stroke-trace` times each brush stroke's frames (`PHOTOCRAFT_STROKE_TRACE`; +//! `photocraft_ui_egui::stroke_trace`). //! //! `--safe-gpu` starts with the CPU renderer (no GPU canvas; a software adapter for the window //! where the platform has one) for this launch, e.g. after a graphics driver crash. A start that @@ -44,6 +50,7 @@ mod linux_libs; mod logging; mod monitor_profile; mod photoshop_settings; +mod presentation; mod screen_color; mod services; // Windows gets pen pressure from winit (WM_POINTER); the web runner has its own listener. @@ -191,6 +198,7 @@ fn main() -> eframe::Result { // reported in a notice once the app is up; a control token is a usage error like a bad port. let mut unreadable_paths: Vec = Vec::new(); let mut safe_gpu = false; + let (mut present_mode_arg, mut frame_latency_arg, mut stroke_trace_arg): (Option, Option, Option) = (None, None, None); let mut in_window_menus = std::env::var_os("PHOTOCRAFT_IN_WINDOW_MENUS").is_some_and(|v| !v.is_empty() && v != "0"); let mut args = std::env::args_os().skip(1); while let Some(a) = args.next() { @@ -211,6 +219,18 @@ fn main() -> eframe::Result { Some("--automation-read-root") => automation_read_root = args.next().map(std::path::PathBuf::from), Some("--automation-write-root") => automation_write_root = args.next().map(std::path::PathBuf::from), Some("--safe-gpu") => safe_gpu = true, + Some(flag @ ("--present-mode" | "--frame-latency" | "--stroke-trace")) => { + let flag = flag.to_string(); + match args.next().map(std::ffi::OsString::into_string) { + Some(Ok(v)) => match flag.as_str() { + "--present-mode" => present_mode_arg = Some(v), + "--frame-latency" => frame_latency_arg = Some(v), + _ => stroke_trace_arg = Some(v), + }, + // A bad value is reported and ignored; it never fails the launch. + _ => eprintln!("photocraft: {flag}: missing or unreadable value; ignored"), + } + } Some("--in-window-menus") => in_window_menus = true, Some("--version") => { println!("photocraft {}", photocraft_engine::build_info::long_version()); @@ -364,6 +384,21 @@ fn main() -> eframe::Result { let gpu_note: std::sync::Arc>> = Default::default(); // The adapter's real texture limits (egui asks for 8192 px), so big documents stay on the GPU. gpu_startup::configure(&mut options.wgpu_options.wgpu_setup, &plan, os, sentinel.clone(), gpu_note.clone()); + // Preferences › Performance › Low-Latency Drawing (#1343): without vsync a stroke shows about + // 50 ms sooner; wgpu picks Immediate, else Mailbox, else Fifo by what the driver offers. + if gpu_startup::read_low_latency(services::prefs_file().as_deref()) { + options.wgpu_options.surface.present_mode = eframe::wgpu::PresentMode::AutoNoVsync; + } + // Testers' present mode and frame latency overrides (#1343), after the preference. + let present = presentation::Overrides::from_args_or_env(present_mode_arg.as_deref(), frame_latency_arg.as_deref()); + options.wgpu_options.surface = present.apply(options.wgpu_options.surface); + let present_desc = presentation::describe(&options.wgpu_options.surface, &present); + // Without vsync, frames are capped at 240 fps (#4472) so repaint loops don't spin flat out. + let uncapped = !matches!( + options.wgpu_options.surface.present_mode, + eframe::wgpu::PresentMode::Fifo | eframe::wgpu::PresentMode::FifoRelaxed | eframe::wgpu::PresentMode::AutoVsync + ); + log::info!("presentation: {present_desc}"); let sentinel_ms = t_sentinel.elapsed().as_secs_f64() * 1000.0; log::info!("GPU startup: {:?} ({sentinel_ms:.2} ms)", plan); let retry_cpu = !safe_gpu && plan.backend != photocraft_engine::prefs::GpuBackend::Cpu; @@ -392,6 +427,7 @@ fn main() -> eframe::Result { } } let mut app = PhotocraftApp::new(Session::new(), services); + app.min_frame_ms = uncapped.then_some(1000.0 / 240.0); app.integrated_titlebar = cfg!(target_os = "macos"); app.custom_titlebar = custom_titlebar; // Only the title bar's free gap drags the window, never the menus (mac_window.rs). @@ -423,6 +459,10 @@ fn main() -> eframe::Result { // composites on the CPU). let info = &mut app.perf.gpu_info; info.preference = pref.name().to_string(); + info.present = present_desc.clone(); + if let Some(v) = &stroke_trace_arg { + app.stroke_trace.set(v); + } info.selected = if plan.env.is_some() { "env".into() } else { plan.backend.name().to_string() }; info.fallback = match (plan.reason.clone(), gpu_note.lock().unwrap_or_else(std::sync::PoisonError::into_inner).clone()) { (Some(reason), Some(note)) => Some(format!("{reason}. {note}")), diff --git a/apps/photocraft/src/presentation.rs b/apps/photocraft/src/presentation.rs new file mode 100644 index 0000000000..c6f7e4196a --- /dev/null +++ b/apps/photocraft/src/presentation.rs @@ -0,0 +1,121 @@ +//! Launch overrides for how frames reach the display, so testers can compare input-to-pixel +//! latency without a rebuild (#1343): `PHOTOCRAFT_PRESENT_MODE` (`fifo`, `autovsync`, or `novsync`, +//! also spelled `immediate`, `mailbox` or `autonovsync`: no vsync, wgpu's Immediate → Mailbox → Fifo +//! by what the driver offers) and `PHOTOCRAFT_FRAME_LATENCY` (`1` to `3`, frames the swap chain may +//! queue). Unset, the platform default applies. A value that isn't one of these is ignored with a +//! warning in the log, never a failed start; a mode the driver may not offer is never requested. +//! The result is logged at start and shown in the stroke trace. + +use eframe::egui_wgpu::SurfaceConfig; +use eframe::wgpu::PresentMode; + +/// The present mode and frame latency this launch asked for (`None`: the default). +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Overrides { + pub mode: Option, + pub latency: Option, +} + +/// Parse the two variables' values. Returns the overrides and a warning per rejected value. +pub fn parse(mode: Option<&str>, latency: Option<&str>) -> (Overrides, Vec) { + let mut warnings = Vec::new(); + let mode = mode.map(str::trim).filter(|v| !v.is_empty()).and_then(|v| { + let m = match v.to_ascii_lowercase().as_str() { + // Only modes every surface takes: wgpu crashes on an explicit Immediate, Mailbox or + // FifoRelaxed the driver doesn't offer (X11/NVIDIA has no Mailbox, #1343). Without + // vsync, wgpu picks Immediate, else Mailbox, else Fifo. + "fifo" | "vsync" => PresentMode::Fifo, + "autovsync" => PresentMode::AutoVsync, + "novsync" | "immediate" | "mailbox" | "autonovsync" => PresentMode::AutoNoVsync, + _ => { + warnings.push(format!("PHOTOCRAFT_PRESENT_MODE={v:?} isn't fifo, autovsync or novsync (immediate, mailbox); ignored")); + return None; + } + }; + Some(m) + }); + let latency = latency.map(str::trim).filter(|v| !v.is_empty()).and_then(|v| match v.parse::() { + Ok(n @ 1..=3) => Some(n), + _ => { + warnings.push(format!("PHOTOCRAFT_FRAME_LATENCY={v:?} isn't 1, 2 or 3; ignored")); + None + } + }); + (Overrides { mode, latency }, warnings) +} + +impl Overrides { + /// `--present-mode` / `--frame-latency` when given, else `PHOTOCRAFT_PRESENT_MODE` / + /// `PHOTOCRAFT_FRAME_LATENCY`; rejected values are logged. + pub fn from_args_or_env(mode: Option<&str>, latency: Option<&str>) -> Self { + let env_mode = std::env::var("PHOTOCRAFT_PRESENT_MODE").ok(); + let env_latency = std::env::var("PHOTOCRAFT_FRAME_LATENCY").ok(); + let (o, warnings) = parse(mode.or(env_mode.as_deref()), latency.or(env_latency.as_deref())); + for w in warnings { + log::warn!("{w}"); + } + o + } + + /// `surface` with the overrides applied. + pub fn apply(&self, surface: SurfaceConfig) -> SurfaceConfig { + SurfaceConfig { + present_mode: self.mode.unwrap_or(surface.present_mode), + desired_maximum_frame_latency: self.latency.or(surface.desired_maximum_frame_latency), + } + } +} + +/// How `surface` presents, for the stroke trace and System Info: "fifo, latency 2", with +/// "(override)" after a value an environment variable set. wgpu's GL backend ignores the latency. +pub fn describe(surface: &SurfaceConfig, overrides: &Overrides) -> String { + let mode = match surface.present_mode { + PresentMode::AutoVsync => "autovsync", + PresentMode::AutoNoVsync => "autonovsync", + PresentMode::Fifo => "fifo", + PresentMode::FifoRelaxed => "fiforelaxed", + PresentMode::Immediate => "immediate", + PresentMode::Mailbox => "mailbox", + }; + let latency = surface.desired_maximum_frame_latency.map_or("default".to_string(), |n| n.to_string()); + let tag = |set: bool| if set { " (override)" } else { "" }; + format!("{mode}{}, latency {latency}{}", tag(overrides.mode.is_some()), tag(overrides.latency.is_some())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn values_parse_and_bad_ones_are_ignored_with_a_warning() { + assert_eq!(parse(None, None), (Overrides::default(), vec![])); + assert_eq!(parse(Some(""), Some(" ")), (Overrides::default(), vec![])); + let (o, w) = parse(Some(" Mailbox "), Some("1")); + assert_eq!((o.mode, o.latency, w.len()), (Some(PresentMode::AutoNoVsync), Some(1), 0)); + assert_eq!(parse(Some("vsync"), None).0.mode, Some(PresentMode::Fifo)); + // Never a mode a driver may lack (wgpu crashes on it): no vsync falls back by support. + for v in ["IMMEDIATE", "mailbox", "novsync", "autonovsync"] { + assert_eq!(parse(Some(v), None).0.mode, Some(PresentMode::AutoNoVsync), "{v}"); + } + for (mode, latency) in + [(Some("fast"), None), (None, Some("0")), (None, Some("4")), (None, Some("-1")), (None, Some("one")), (None, Some("99999999999"))] + { + let (o, w) = parse(mode, latency); + assert_eq!((o, w.len()), (Overrides::default(), 1), "{mode:?} {latency:?}"); + } + } + + #[test] + fn overrides_replace_only_what_they_set_and_are_described() { + let default = SurfaceConfig::HIGH_THROUGHPUT; + assert_eq!(Overrides::default().apply(default), default); + let latency_only = Overrides { mode: None, latency: Some(1) }; + let s = latency_only.apply(default); + assert_eq!((s.present_mode, s.desired_maximum_frame_latency), (PresentMode::AutoVsync, Some(1))); + assert_eq!(describe(&s, &latency_only), "autovsync, latency 1 (override)"); + let novsync = Overrides { mode: Some(PresentMode::AutoNoVsync), latency: None }; + assert_eq!(describe(&novsync.apply(default), &novsync), "autonovsync (override), latency 2"); + let none = SurfaceConfig { present_mode: PresentMode::Fifo, desired_maximum_frame_latency: None }; + assert_eq!(describe(&none, &Overrides::default()), "fifo, latency default"); + } +} diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index c732daeefd..838e6fe25e 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -529,6 +529,13 @@ pub struct Performance { /// sliders while they drag) render on a reduced copy: fast, but blocky when zoomed in. Off: /// they render at full resolution. pub low_resolution_previews: bool, + /// Low-latency drawing (applies at next launch): frames reach the screen without waiting for + /// vsync, so a stroke follows the pen about 50 ms sooner (the swap chain's wait, #1343). wgpu + /// picks Immediate where the driver has it (most Vulkan and DX12 drivers on Windows and X11), + /// so the canvas usually tears while it changes; Mailbox (no tearing) only where Immediate is + /// missing. Frames are capped at 240 fps. Off (the default): vsync, smooth but late. Desktop + /// only. + pub low_latency_drawing: bool, /// Linux display server (applies at next launch; see [`LinuxDisplayServer`]). pub linux_display_server: LinuxDisplayServer, /// Memory budget of the layer-effect cache, in MB. @@ -560,6 +567,7 @@ impl Default for Performance { rendering_mode: None, gpu_backend: GpuBackend::Auto, low_resolution_previews: true, + low_latency_drawing: false, linux_display_server: LinuxDisplayServer::Auto, effect_cache_mb: 768, legacy_compositing: false, diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index ff786be197..8cddc60b23 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -131,8 +131,9 @@ pub enum Outcome { /// field's value is validated before the first one is applied, so a typo, an unknown field, a /// bad value or a bad nested key can't reply with success while nothing — or only half of it — /// changed (#412). -pub const UI_SET_FIELDS: [&str; 37] = [ +pub const UI_SET_FIELDS: [&str; 38] = [ "tool", + "strokeTrace", "panels", "dock", "dockTabs", @@ -492,6 +493,19 @@ fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest) None => None, }; let gradient_classic = bool_field(p, "gradientClassic")?; + // The stroke latency trace (`stroke_trace`), at runtime: on (stderr) or off. A CSV + // file is only for the user's own launch (`--stroke-trace `): a path over the + // control channel would write outside the automation write root. + let stroke_trace = match p.get("strokeTrace") { + None | Some(Value::Null) => None, + Some(Value::Bool(b)) => Some(if *b { "stderr" } else { "off" }), + Some(Value::String(v)) => match v.trim() { + "stderr" | "on" | "true" => Some("stderr"), + "off" | "false" => Some("off"), + _ => return Err("strokeTrace must be true, false, \"stderr\" or \"off\"; a CSV file is set at launch (--stroke-trace )".into()), + }, + Some(_) => return Err("strokeTrace must be true, false, \"stderr\" or \"off\"".into()), + }; // The Eyedropper's options bar (#1649): Sample Size, Sample, Show Sampling Ring. let eyedropper_size = match p.get("eyedropperSampleSize") { Some(v) => Some( @@ -663,6 +677,9 @@ fn dispatch(app: &mut PhotocraftApp, ctx: &egui::Context, req: &ControlRequest) if let Some(mode) = gradient_blend { app.ui.tool_options.gradient_blend_mode = mode; } + if let Some(v) = &stroke_trace { + app.stroke_trace.set(v); + } if let Some(classic) = gradient_classic { app.ui.tool_options.gradient_classic = classic; } @@ -1385,6 +1402,26 @@ mod tests { } } + #[test] + fn ui_set_turns_the_stroke_trace_on_and_off_at_runtime() { + let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default()); + let ctx = egui::Context::default(); + assert!(!app.stroke_trace.enabled()); + assert_eq!(call(&mut app, &ctx, "ui.set", json!({"strokeTrace": true}))["ok"], true); + assert!(app.stroke_trace.enabled()); + assert_eq!(call(&mut app, &ctx, "ui.set", json!({"strokeTrace": "off"}))["ok"], true); + assert!(!app.stroke_trace.enabled()); + let bad = call(&mut app, &ctx, "ui.set", json!({"strokeTrace": 3, "tool": "Pencil"})); + assert_eq!(bad["ok"], false); + // A file path isn't accepted over the channel (it would write outside the automation + // write root); nothing is created. + let path = std::env::temp_dir().join(format!("photocraft-trace-{}.csv", std::process::id())); + let bad = call(&mut app, &ctx, "ui.set", json!({"strokeTrace": path.to_str().unwrap(), "tool": "Pencil"})); + assert_eq!(bad["ok"], false); + assert!(!path.exists()); + assert!(!app.stroke_trace.enabled() && app.ui.tool != crate::state::Tool::Pencil, "a rejected call applies nothing"); + } + #[test] fn camera_raw_control_updates_and_commits_or_cancels_an_open_raw() { use photocraft_raw::testgen::{DngSpec, mosaic, scene}; diff --git a/crates/ui-egui/src/gpu_canvas.rs b/crates/ui-egui/src/gpu_canvas.rs index 2e583e8e5b..391b0b64a3 100644 --- a/crates/ui-egui/src/gpu_canvas.rs +++ b/crates/ui-egui/src/gpu_canvas.rs @@ -945,6 +945,9 @@ pub struct GpuInfo { pub canvas: String, /// Why the GPU canvas was dropped during this session (device lost or a GPU error). pub lost: Option, + /// Present mode and frame latency ("fifo, latency 2"; launch overrides marked, see the + /// app's `presentation`). Empty in hosts that don't set it. + pub present: String, } impl GpuInfo { diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 376757e866..1329378dc2 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -3804,3 +3804,4 @@ navigator Zoom Zvětšení Take the shortcut from the other command and select it Odebrat zkratku druhému příkazu a vybrat ho Undo Changes Vrátit změny {shortcut} is reserved by the system and can't be assigned. {shortcut} je vyhrazena systémem a nelze ji přiřadit. + Low-Latency Drawing (vsync off, applies at next launch) Kreslení s nízkou latencí (bez vertikální synchronizace, platí po dalším spuštění) diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 59a9d470cd..5f56421233 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -3198,3 +3198,4 @@ curves Smooth Glätten Take the shortcut from the other command and select it Den Kurzbefehl dem anderen Befehl entziehen und diesen auswählen Undo Changes Änderungen rückgängig machen {shortcut} is reserved by the system and can't be assigned. {shortcut} ist vom System reserviert und kann nicht zugewiesen werden. + Low-Latency Drawing (vsync off, applies at next launch) Zeichnen mit geringer Latenz (ohne VSync, gilt ab dem nächsten Start) diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 0a2e6ddc7c..715e41cb1f 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -3896,3 +3896,4 @@ curves Smooth Εξομάλυνση Take the shortcut from the other command and select it Αφαίρεση της συντόμευσης από την άλλη εντολή και επιλογή της Undo Changes Αναίρεση αλλαγών {shortcut} is reserved by the system and can't be assigned. Η συντόμευση {shortcut} δεσμεύεται από το σύστημα και δεν μπορεί να εκχωρηθεί. + Low-Latency Drawing (vsync off, applies at next launch) Σχεδίαση χαμηλής καθυστέρησης (χωρίς vsync, ισχύει στην επόμενη εκκίνηση) diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 2069b068e5..4a0f1c8556 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -4158,3 +4158,4 @@ navigator Zoom Zoom Take the shortcut from the other command and select it Quitar el método abreviado al otro comando y seleccionarlo Undo Changes Deshacer cambios {shortcut} is reserved by the system and can't be assigned. {shortcut} está reservado por el sistema y no se puede asignar. + Low-Latency Drawing (vsync off, applies at next launch) Dibujo de baja latencia (sin sincronización vertical, se aplica al volver a iniciar) diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index d528f1e7ce..2402aec160 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -3201,3 +3201,4 @@ curves Smooth Lisser Take the shortcut from the other command and select it Retirer le raccourci de l’autre commande et la sélectionner Undo Changes Annuler les modifications {shortcut} is reserved by the system and can't be assigned. {shortcut} est réservé par le système et ne peut pas être attribué. + Low-Latency Drawing (vsync off, applies at next launch) Dessin à faible latence (sans synchronisation verticale, au prochain lancement) diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index e9f3d33907..343dcadc0c 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -4115,3 +4115,4 @@ cameraRaw Yellow Sárga Take the shortcut from the other command and select it A billentyűparancs elvétele a másik parancstól, és annak kijelölése Undo Changes Változások visszavonása {shortcut} is reserved by the system and can't be assigned. A(z) {shortcut} a rendszer számára fenntartott, nem rendelhető hozzá. + Low-Latency Drawing (vsync off, applies at next launch) Kis késleltetésű rajzolás (vsync nélkül, a következő indításkor lép életbe) diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 141f956c8e..e4ba61e1a1 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -3188,3 +3188,4 @@ curves Smooth Haluskan Take the shortcut from the other command and select it Ambil pintasan dari perintah lain dan pilih perintah itu Undo Changes Batalkan Perubahan {shortcut} is reserved by the system and can't be assigned. {shortcut} dicadangkan oleh sistem dan tidak dapat ditetapkan. + Low-Latency Drawing (vsync off, applies at next launch) Menggambar latensi rendah (tanpa vsync, berlaku saat peluncuran berikutnya) diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 714932312e..6b933287c6 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -4317,3 +4317,4 @@ shape Appearance Aspetto Take the shortcut from the other command and select it Togli la scorciatoia all'altro comando e selezionalo Undo Changes Annulla modifiche {shortcut} is reserved by the system and can't be assigned. {shortcut} è riservata dal sistema e non può essere assegnata. + Low-Latency Drawing (vsync off, applies at next launch) Disegno a bassa latenza (senza vsync, si applica al prossimo avvio) diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index a78fe8e7ca..2f6b7d7f5e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -3872,3 +3872,4 @@ curves Smooth スムーズ Take the shortcut from the other command and select it もう一方のコマンドからショートカットを外して選択 Undo Changes 変更を取り消し {shortcut} is reserved by the system and can't be assigned. {shortcut} はシステムで予約されているため割り当てられません。 + Low-Latency Drawing (vsync off, applies at next launch) 低遅延描画(垂直同期オフ、次回起動時に適用) diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 2e12c33799..3c056c627b 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -3374,3 +3374,4 @@ curves Smooth 매끄럽게 Take the shortcut from the other command and select it 다른 명령에서 단축키를 빼고 그 명령 선택 Undo Changes 변경 사항 취소 {shortcut} is reserved by the system and can't be assigned. {shortcut}은(는) 시스템에서 예약되어 할당할 수 없습니다. + Low-Latency Drawing (vsync off, applies at next launch) 저지연 그리기(수직 동기화 끔, 다음 실행 시 적용) diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index 32df1e20a2..c80f86bf81 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -3956,3 +3956,4 @@ curves Smooth Vloeiend maken Take the shortcut from the other command and select it De sneltoets van de andere opdracht afnemen en die selecteren Undo Changes Wijzigingen ongedaan maken {shortcut} is reserved by the system and can't be assigned. {shortcut} is door het systeem gereserveerd en kan niet worden toegewezen. + Low-Latency Drawing (vsync off, applies at next launch) Tekenen met lage latentie (zonder vsync, na de volgende start) diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 8b571ff7d6..85df6b8157 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -3794,3 +3794,4 @@ curves Smooth Wygładź Take the shortcut from the other command and select it Odbierz skrót drugiemu poleceniu i zaznacz je Undo Changes Cofnij zmiany {shortcut} is reserved by the system and can't be assigned. {shortcut} jest zarezerwowany przez system i nie można go przypisać. + Low-Latency Drawing (vsync off, applies at next launch) Rysowanie z małym opóźnieniem (bez synchronizacji pionowej, od następnego uruchomienia) diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 8e6dcc4993..211442ba20 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -3986,3 +3986,4 @@ layerLabel No Color Sem cor Take the shortcut from the other command and select it Tirar o atalho do outro comando e selecioná-lo Undo Changes Desfazer alterações {shortcut} is reserved by the system and can't be assigned. {shortcut} é reservado pelo sistema e não pode ser atribuído. + Low-Latency Drawing (vsync off, applies at next launch) Desenho de baixa latência (sem sincronização vertical, aplicado na próxima inicialização) diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 6c80c470cf..9dfcc85093 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -3166,3 +3166,4 @@ curves Smooth Сгладить Take the shortcut from the other command and select it Снять комбинацию с другой команды и выбрать её Undo Changes Отменить изменения {shortcut} is reserved by the system and can't be assigned. Комбинация {shortcut} зарезервирована системой и не может быть назначена. + Low-Latency Drawing (vsync off, applies at next launch) Рисование с малой задержкой (без вертикальной синхронизации, после перезапуска) diff --git a/crates/ui-egui/src/i18n/sr.tsv b/crates/ui-egui/src/i18n/sr.tsv index 9b30313e22..bbc0ef528d 100644 --- a/crates/ui-egui/src/i18n/sr.tsv +++ b/crates/ui-egui/src/i18n/sr.tsv @@ -4269,3 +4269,4 @@ shape Appearance Izgled Take the shortcut from the other command and select it Uzmi prečicu drugoj komandi i izaberi je Undo Changes Poništi izmene {shortcut} is reserved by the system and can't be assigned. {shortcut} je rezervisana od strane sistema i ne može se dodeliti. + Low-Latency Drawing (vsync off, applies at next launch) Crtanje sa malim kašnjenjem (bez vsync-a, primenjuje se pri sledećem pokretanju) diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 8e20ad53dc..b63bdd7349 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -3926,3 +3926,4 @@ cameraData Lens Lens Take the shortcut from the other command and select it Kısayolu diğer komuttan al ve o komutu seç Undo Changes Değişiklikleri Geri Al {shortcut} is reserved by the system and can't be assigned. {shortcut} sistem tarafından ayrılmıştır ve atanamaz. + Low-Latency Drawing (vsync off, applies at next launch) Düşük gecikmeli çizim (dikey eşitleme kapalı, sonraki açılışta geçerli) diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 9fcf5e85bc..923ec694af 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -3895,3 +3895,4 @@ curves Smooth Згладити Take the shortcut from the other command and select it Забрати скорочення в іншої команди й вибрати її Undo Changes Скасувати зміни {shortcut} is reserved by the system and can't be assigned. {shortcut} зарезервовано системою, його не можна призначити. + Low-Latency Drawing (vsync off, applies at next launch) Малювання з малою затримкою (без вертикальної синхронізації, після перезапуску) diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 515966c515..be40230ca3 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -3682,3 +3682,4 @@ curves Smooth Làm mượt Take the shortcut from the other command and select it Lấy phím tắt khỏi lệnh kia và chọn lệnh đó Undo Changes Hoàn tác thay đổi {shortcut} is reserved by the system and can't be assigned. {shortcut} được hệ thống dành riêng và không thể gán. + Low-Latency Drawing (vsync off, applies at next launch) Vẽ độ trễ thấp (tắt vsync, áp dụng ở lần khởi động sau) diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index cbf3c9d08f..7c563780db 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -2812,3 +2812,4 @@ curves Smooth 平滑 Take the shortcut from the other command and select it 从另一个命令移除该快捷键并选中它 Undo Changes 撤消更改 {shortcut} is reserved by the system and can't be assigned. {shortcut} 由系统保留,无法指定。 + Low-Latency Drawing (vsync off, applies at next launch) 低延迟绘画(关闭垂直同步,下次启动时生效) diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 30aa3f5a94..9376cb05bb 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -2797,3 +2797,4 @@ curves Smooth 平滑 Take the shortcut from the other command and select it 從另一個指令移除該快速鍵並選取它 Undo Changes 還原變更 {shortcut} is reserved by the system and can't be assigned. {shortcut} 由系統保留,無法指定。 + Low-Latency Drawing (vsync off, applies at next launch) 低延遲繪圖(關閉垂直同步,下次啟動時生效) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 3a981c41f3..34b346af00 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -800,6 +800,12 @@ pub struct PhotocraftApp { /// `jobs_ui`). The desktop app turns it on; off (the default), everything runs inline as /// before, which tests and scripts rely on. pub background_jobs: bool, + /// Shortest time between frames (ms), or `None` for no cap. A present mode that doesn't wait + /// for the display (Low-Latency Drawing) would otherwise redraw as fast as it can while + /// anything keeps requesting repaints (a drag hovering, autoscroll, Liquify held still). + /// After #4472 (astigmata). + pub min_frame_ms: Option, + last_frame_start_ms: f64, /// Background job bookkeeping: opening tabs, control replies waiting on a job. pub jobs: jobs_ui::JobsUi, #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] @@ -935,6 +941,8 @@ impl PhotocraftApp { allow_close: false, stylus: Default::default(), background_jobs: false, + min_frame_ms: None, + last_frame_start_ms: 0.0, jobs: Default::default(), #[cfg(all(debug_assertions, not(target_arch = "wasm32")))] live_tokens: theme::live::LiveTokens::from_env(), @@ -1759,6 +1767,20 @@ impl PhotocraftApp { } } +impl PhotocraftApp { + /// Wait out the rest of [`Self::min_frame_ms`] since the last frame started. + fn cap_frame_rate(&mut self) { + #[cfg(not(target_arch = "wasm32"))] + if let Some(min) = self.min_frame_ms.filter(|m| m.is_finite() && *m > 0.0) { + let wait = min.min(100.0) - (gpu_canvas::now_ms() - self.last_frame_start_ms); + if wait > 0.0 { + std::thread::sleep(std::time::Duration::from_secs_f64(wait / 1000.0)); + } + } + self.last_frame_start_ms = gpu_canvas::now_ms(); + } +} + impl eframe::App for PhotocraftApp { fn on_exit(&mut self) { if let Err(err) = tool_memory::save_on_exit(self) { @@ -1767,6 +1789,7 @@ impl eframe::App for PhotocraftApp { } fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + self.cap_frame_rate(); i18n::set_current(i18n::Lang::from_pref(&self.session.prefs().interface.language)); // Caps Lock state read from the OS once per frame (see `Services::caps_lock`): the canvas // cursor block needs it before the frame renders. `None` (Wayland/web) keeps `false`. @@ -2004,7 +2027,10 @@ impl eframe::App for PhotocraftApp { if self.stroke_trace.enabled() { self.stroke_trace.canvas(self.perf.frame_refresh_ms); let info = &self.perf.gpu_info; - self.stroke_trace.end_frame(ui_ms, self.live_stroke.is_some(), || format!("{} {}", info.backend, info.adapter).trim().to_string()); + self.stroke_trace.end_frame(ui_ms, self.live_stroke.is_some(), || { + let gpu = format!("{} {}", info.backend, info.adapter).trim().to_string(); + if info.present.is_empty() { gpu } else { format!("{gpu}, {}", info.present) } + }); } // Synthetic input is injected one press/release step per frame: keep frames coming until // the queue is empty, then release control replies waiting on it. diff --git a/crates/ui-egui/src/prefs_ui.rs b/crates/ui-egui/src/prefs_ui.rs index 99750ae7f1..795ea84cea 100644 --- a/crates/ui-egui/src/prefs_ui.rs +++ b/crates/ui-egui/src/prefs_ui.rs @@ -1120,6 +1120,11 @@ fn humanize(key: &str) -> String { if key == "nudgeBundlePauseMs" { return "Nudge Bundle Pause (ms)".into(); } + // Performance: presentation without vsync (#1343); like the other graphics settings, it + // takes effect when the window next opens. + if key == "lowLatencyDrawing" { + return "Low-Latency Drawing (vsync off, applies at next launch)".into(); + } let mut s = String::new(); for (i, ch) in key.chars().enumerate() { if i == 0 { @@ -1556,6 +1561,8 @@ fn field_visible(section: &str, obj: &Map, k: &str, query: &str) // View › Rulers (⌘R) sets it, as in Photoshop. || path == "unitsAndRulers.showRulers" || (section == "performance" && matches!(k, "useGpu" | "gpuBackend" | "renderingMode")) + // The browser presents frames itself: Low-Latency Drawing does nothing there. + || (cfg!(target_arch = "wasm32") && path == "performance.lowLatencyDrawing") || (section == "export" && !export_field_visible(obj, k)) || (section == "tools" && !tools_field_visible(obj, k)); (query.is_empty() || preference_matches(section, k, query)) diff --git a/crates/ui-egui/src/stroke_trace.rs b/crates/ui-egui/src/stroke_trace.rs index 206cc7e81a..809414ed49 100644 --- a/crates/ui-egui/src/stroke_trace.rs +++ b/crates/ui-egui/src/stroke_trace.rs @@ -113,13 +113,32 @@ impl StrokeTrace { /// Reads `PHOTOCRAFT_STROKE_TRACE` (off on the web, where there is no clock). pub fn from_env() -> Self { #[cfg(not(target_arch = "wasm32"))] - if let Some(v) = std::env::var_os("PHOTOCRAFT_STROKE_TRACE").filter(|v| !v.is_empty()) { - let output = if v == "1" || v == "stderr" { Output::Stderr } else { Output::Csv(v.into()) }; - return Self { output, ..Default::default() }; + if let Some(v) = std::env::var_os("PHOTOCRAFT_STROKE_TRACE") { + return Self::from_value(&v.to_string_lossy()); } Self::default() } + /// A trace set by value: `""`, `"0"` or `"off"` is off, `"1"` or `"stderr"` prints + /// summaries, anything else is a CSV path (`--stroke-trace`, `PHOTOCRAFT_STROKE_TRACE`; the + /// control channel's `ui.set {"strokeTrace"}` passes only on or off, never a path). + pub fn from_value(v: &str) -> Self { + let v = v.trim(); + let output = match v { + _ if cfg!(target_arch = "wasm32") => Output::Off, + "" | "0" | "off" | "false" => Output::Off, + "1" | "stderr" | "true" => Output::Stderr, + path => Output::Csv(path.into()), + }; + Self { output, ..Default::default() } + } + + /// Switch the output, keeping the last summary. + pub fn set(&mut self, v: &str) { + let last = self.last.take(); + *self = Self { last, ..Self::from_value(v) }; + } + pub fn enabled(&self) -> bool { self.output != Output::Off } @@ -265,6 +284,19 @@ mod tests { assert!(off.last.is_none() && off.rows.is_empty()); } + #[test] + fn a_trace_is_set_by_value() { + assert_eq!(StrokeTrace::from_value("off").output, Output::Off); + assert_eq!(StrokeTrace::from_value(" 0 ").output, Output::Off); + assert_eq!(StrokeTrace::from_value("stderr").output, Output::Stderr); + assert_eq!(StrokeTrace::from_value("true").output, Output::Stderr); + assert_eq!(StrokeTrace::from_value("t.csv").output, Output::Csv("t.csv".into())); + let mut t = on(); + t.last = Some(Summary::default()); + t.set("off"); + assert!(!t.enabled() && t.last.is_some(), "turning it off keeps the last summary"); + } + #[test] fn csv_rows_and_odd_numbers() { let rows = [Row { t: 1.0, interval: 16.5, moves: 2, stroke: 0.25, canvas: 1.0, ui: 3.0, cpu_prev: None }]; diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 8536507104..2bbcab2469 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -53,6 +53,7 @@ The transport is `apps/photocraft/src/control_server.rs`, and the handlers are i - `ui.set` also accepts `gradientBlendMode` (a layer blend mode name, such as `Difference`) and `gradientClassic` (boolean) for the Gradient tool options bar. - `ui.set` also accepts `camTransformOnDrop` (boolean), the Content-Aware Move tool's Transform On Drop checkbox: a drop then puts a scale/rotate box on the patch (`ui.inspect` › `camTransform`: its document/revision, drag `offset` and `frame`), driven with `ui.pointer` (handles as Free Transform's; outside the box rotates), committed with `ui.key Enter` (one `paint.contentAwareMove` with `scaleX`, `scaleY`, `rotate`) or dropped with `Escape`. - `ui.set` also accepts `tileGrid` (boolean), a developer overlay of the active layer's storage tiles: the tile grid over the document, every stored tile filled and labelled `tx,ty`, tiles with identical pixels in a shared colour and unique ones in blue. It has no menu item and View › Extras doesn't hide it. `ui.inspect` reports it as `view.tile_grid`. +- `ui.set` also accepts `strokeTrace` (`true`, `false`, `"stderr"` or `"off"`): turn the stroke latency trace on or off at runtime; each stroke's summary is in `ui.inspect` `perf.strokeTrace`. A CSV file of every frame is set only at launch (`PHOTOCRAFT_STROKE_TRACE=` / `--stroke-trace `): the control channel doesn't write files outside the automation write root - `ui.set` also accepts `layerFilter` (#5048), the Layers panel filter row: `{by?, kinds?, name?, mode?}`, any subset. `by` is `kind`, `name` or `mode` (the filter-type dropdown); `kinds` lists the Kind toggles that are on (`pixel`, `adjustment`, `type`, `shape`, `smart`); `name` matches part of a layer name, ignoring case; `mode` is a blend mode name (`Multiply`, `Pass Through`). Kind and Name filter only once a kind is on or text is typed; Mode always filters, by Normal at first, as in Photoshop. Fields left out keep their value; a bad one is an error and changes nothing. - Each painting tool keeps its own brush, as in Photoshop: `tools.setBrush` and `ui.set {brushSize}` change the active tool's brush, and switching tools swaps in that tool's own (size, hardness, mode, opacity, flow, dynamics, smoothing). A tool used for the first time starts from the brush in hand. The colours, the erase flag and the stroke seed stay shared. So `ui.set {tool: "eraser", brushSize: 12}` sets the Eraser's size, and `ui.set {tool: "brush"}` afterwards brings the Brush's own size back. - `ui.set` also accepts the Eyedropper options bar (#1649): `eyedropperSampleSize` (`"point"` or 1, 3, 5, 11, 31, 51, 101: the side of the averaged square, shared by the Eyedropper, a painting tool's Alt-click eyedropper, the Curves/Color Picker eyedroppers and the Info panel), `eyedropperSample` (`current`, `currentAndBelow`, `all`, `allNoAdjustments`, `currentAndBelowNoAdjustments`) and `eyedropperRing` (boolean, Show Sampling Ring). `ui.inspect` reports them under `toolOptions` (`eyedropper_size`, `eyedropper_sample`, `eyedropper_ring`). The engine command `document.sampleColor {x, y, size, sampleLayer}` returns the colour such a sample picks. diff --git a/docs/development.md b/docs/development.md index 3e7f5fb66e..76b4a2e31c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -119,7 +119,9 @@ By default PhotoCraft's own crates log at `info` and everything else at `warn`. | `PHOTOCRAFT_CPU_COMPOSE=1` | Keep the wgpu canvas but composite on the CPU (compare GPU vs CPU renders, e.g. with the snapshot example) | | `PHOTOCRAFT_LOCALE` | Override Auto's native UI language for this launch; unsupported tags use English. See [system language detection](localization.md#first-launch-and-system-language). | | `PHOTOCRAFT_FX_TRACE=1` | Print the CPU time spent on GPU effect shapes and distance fields per rebuild | -| `PHOTOCRAFT_STROKE_TRACE=1` (or `=`) | Time every frame of each brush stroke; print a summary per stroke, and with a path also append each frame to that CSV (see [Stroke latency trace](#stroke-latency-trace)) | +| `PHOTOCRAFT_STROKE_TRACE=1` (or `=`; also `--stroke-trace `, or `ui.set {"strokeTrace": true|false}` at runtime, without a file) | Time every frame of each brush stroke; print a summary per stroke, and with a path also append each frame to that CSV (see [Stroke latency trace](#stroke-latency-trace)) | +| `PHOTOCRAFT_PRESENT_MODE=novsync` (or `--present-mode`) | Present mode for this launch: `fifo`, `autovsync`, or `novsync` (also `immediate`, `mailbox`: no vsync, the first of Immediate, Mailbox, Fifo the driver offers). Default: vsync, or no vsync when Preferences › Performance › Low-Latency Drawing is on (it ships off). For latency comparisons; shown in the stroke trace | +| `PHOTOCRAFT_FRAME_LATENCY=1` (or `--frame-latency`) | Frames the swap chain may queue, 1 to 3 (wgpu's GL backend ignores it). For latency comparisons; shown in the stroke trace | | `PHOTOCRAFT_THEME_FILE=tokens.json` | **Debug builds only:** live design-token overrides, re-read on change | ### Live design tokens @@ -148,7 +150,7 @@ stroke trace: 62 frames on vulkan NVIDIA GeForce RTX 5090 (p50/p95 ms): interval | `cpu` | eframe's CPU time per frame: UI, tessellation and GPU submission, without the vsync wait | | `wait` | `interval − cpu`: waiting on vsync or the swap chain, or idle | -What the app can't time is the rest of input-to-photon: the OS input queue before a frame starts (up to one `interval`), frames queued for the display (the surface keeps up to two; the GL backend ignores that setting), and the compositor. So the first line names the backend. The last summary is also in the control channel's `perf.strokeTrace` (`ui.inspect`). The code is `crates/ui-egui/src/stroke_trace.rs`. +What the app can't time is the rest of input-to-photon: the OS input queue before a frame starts (up to one `interval`), frames queued for the display (the surface keeps up to two; the GL backend ignores that setting), and the compositor. So the first line names the backend and how frames are presented. To compare presentation settings without rebuilding, launch with `PHOTOCRAFT_PRESENT_MODE` and `PHOTOCRAFT_FRAME_LATENCY` (see [Environment variables](#environment-variables)): with `novsync` the app no longer waits on vsync (`wait` near 0): each refresh shows the newest frame, and where the driver only offers Immediate, the screen may tear. The last summary is also in the control channel's `perf.strokeTrace` (`ui.inspect`). The code is `crates/ui-egui/src/stroke_trace.rs`. ## Driving the app programmatically diff --git a/docs/scorecard.md b/docs/scorecard.md index 1e0576ba02..8dbbea612e 100644 --- a/docs/scorecard.md +++ b/docs/scorecard.md @@ -9,7 +9,7 @@ Generated by `cargo xtask scorecard` from `perf/budgets.toml`, `perf/baseline.js | [Performance](#performance) | 3 | 22 | 22 | 56 scenarios: 3 within budget, 22 over budget or failing, 9 tracked only, 17 not run, 5 not measurable yet (baseline 2026-10-05) | | [File compatibility](#file-compatibility) | 7 | 14 | 6 | io corpus: oracle ≥ 148/170, round trip ≥ 169/170; psd-tools corpus: oracle ≥ 249/309, round trip ≥ 308/309; photoshop oracles: oracle ≥ 188/256, round trip ≥ 258/256 | | [Tools](#tools) | 33 | 11 | 0 | | -| [Workspace and UI](#workspace-and-ui) | 13 | 6 | 4 | settings that do nothing: 26 of 166 (target 0) | +| [Workspace and UI](#workspace-and-ui) | 13 | 6 | 4 | settings that do nothing: 26 of 167 (target 0) | | [Type](#type) | 4 | 3 | 2 | | | [Automation](#automation) | 5 | 2 | 4 | 631 of 631 menu items live (100.0%) | | [Reliability](#reliability) | 5 | 2 | 0 | never-crash lints on 29 of 29 crates; panic_hunt present | @@ -177,7 +177,7 @@ Corpus floors (`crates/io/tests/corpus.rs`): the enforced minimums; the corpus t ## Workspace and UI -**Settings that do nothing: 26 of 166** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't. +**Settings that do nothing: 26 of 167** (target 0, #204). Counted by the prefs audit (`xtask/src/scorecard.rs`): a `Preferences` field counts as read when code outside the prefs model (`prefs.rs` except its `impl Session` blocks; tests excluded; the schema-driven dialog never names a field) accesses `section.field`, or `.section` and `.field` in the same file, or calls a section method that reads `self.field`. It is a heuristic lower bound: a generic name can look read when it isn't. Unread: `general.color_picker`, `general.animated_zoom`, `general.zoom_resizes_windows`, `workspace.auto_show_hidden_panels`, `workspace.enable_floating_document_window_docking`, `file_handling.save_in_background`, `performance.cache_levels`, `performance.legacy_compositing`, `scratch_disks.disks`, `units_and_rulers.column_width`, `units_and_rulers.gutter`, `plug_ins.show_extension_panels`, `plug_ins.allow_scripts_to_connect`, `plug_ins.generator_enabled`, `type_.text_engine`, `type_.recent_fonts`, `enhanced_controls.scrubby_slider_acceleration`, `enhanced_controls.touch_gestures`, `raw_defaults.color_space`, `raw_defaults.bit_depth`, `raw_defaults.resolution`, `raw_defaults.sharpen_for`, `raw_defaults.open_as_smart_object`, `raw_defaults.apply_auto_tone`, `integrations.allow_agent_control`, `integrations.control_port`.