Low-Latency Drawing preference (vsync off, opt-in), plus present-mode overrides for testers (#1343) (#5274)

* PHOTOCRAFT_PRESENT_MODE / PHOTOCRAFT_FRAME_LATENCY launch overrides for latency A/B tests (#1343)

Linux traces put the remaining stroke latency in presentation queueing (the app's frame fits
in one refresh; the rest waits on the swap chain), and an NVIDIA user cured it by forcing
vsync off. Testers can now compare without rebuilding: PHOTOCRAFT_PRESENT_MODE (fifo,
mailbox, immediate, autovsync, autonovsync) and PHOTOCRAFT_FRAME_LATENCY (1-3), applied over
the platform default. Bad values are logged and ignored. The active mode is logged at start,
kept in GpuInfo.present and shown in every stroke trace line.

RTX 5090, Vulkan, 120 Hz: default autovsync/latency 1 waits 6 ms per 8.3 ms frame; mailbox
renders every 2.3 ms with 0.2 ms wait, so each vsync shows the newest frame.

* docs: present mode and frame latency overrides

* Present mode overrides never request a mode the driver may lack (Mailbox crashed on X11/NVIDIA)

* Preferences › Performance › Low-Latency Drawing, on by default: no vsync wait while drawing (#1343)

Testers' traces put the ~50 ms stroke lag in the swap chain's vsync wait. With vsync off it
disappears (X11/NVIDIA; Windows/NVIDIA via the driver), and a shorter queue doesn't help. The
surface now presents with AutoNoVsync by default: wgpu picks Immediate, else Mailbox, else
Fifo by what the driver offers, so no unsupported mode is ever requested. Turning the
preference off brings vsync back (smooth, but late). It applies at the next launch, like the
other graphics settings. Where only Immediate exists the canvas may tear while it changes;
frames render as fast as input arrives during a stroke and stop when idle.

* Runtime and launch controls: ui.set strokeTrace, --stroke-trace, --present-mode, --frame-latency

The retro's remaining gap: the stroke trace and the presentation overrides were env-only. The trace now switches at runtime over the control channel (ui.set {"strokeTrace": true|false|"stderr"|path}) and with --stroke-trace; --present-mode / --frame-latency sit beside their env vars and win over them. Bad values are reported and ignored; none fails a launch.

* docs: strokeTrace beside the other ui.set fields

* docs: regenerate the scorecard after rebasing onto main

* Low-Latency Drawing: frames capped at 240 fps without vsync (after #4472); ui.set strokeTrace takes on/off only (a CSV path is launch-only); tearing documented as usual (Immediate); pref hidden on web; doc comment order

* stroke_trace: from_value doc says ui.set passes only on or off

* Low-Latency Drawing ships off (opt-in), per the owner's decision

* docs: present mode default is vsync (Low-Latency Drawing ships off)
This commit is contained in:
Tanish Manku 2026-10-15 15:23:47 -07:00 committed by GitHub
parent af54f4aed1
commit b09aeae2e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 332 additions and 10 deletions

View File

@ -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;

View File

@ -2,7 +2,13 @@
//!
//! Usage: `photocraft [--control <port>] [--control-token <64-hex> |
//! --control-token-file <path>] [--automation-read-root <dir>]
//! [--automation-write-root <dir>] [--safe-gpu] [--in-window-menus] [files…]`
//! [--automation-write-root <dir>] [--safe-gpu] [--in-window-menus] [--present-mode <mode>]
//! [--frame-latency <n>] [--stroke-trace <stderr|file.csv>] [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<String> = Vec::new();
let mut safe_gpu = false;
let (mut present_mode_arg, mut frame_latency_arg, mut stroke_trace_arg): (Option<String>, Option<String>, Option<String>) = (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<std::sync::Mutex<Option<String>>> = 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}")),

View File

@ -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<PresentMode>,
pub latency: Option<u32>,
}
/// 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<String>) {
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::<u32>() {
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");
}
}

View File

@ -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,

View File

@ -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 <path>`): 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 <path>)".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};

View File

@ -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<String>,
/// 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 {

View File

@ -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í)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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, ισχύει στην επόμενη εκκίνηση)

Can't render this file because it has a wrong number of fields in line 7.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 4 and column 47.

View File

@ -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)

Can't render this file because it has a wrong number of fields in line 16.

View File

@ -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) 低遅延描画(垂直同期オフ、次回起動時に適用)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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) 저지연 그리기(수직 동기화 끔, 다음 실행 시 적용)

Can't render this file because it has a wrong number of fields in line 11.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 8 and column 34.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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) Рисование с малой задержкой (без вертикальной синхронизации, после перезапуска)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 8 and column 34.

View File

@ -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) Малювання з малою затримкою (без вертикальної синхронізації, після перезапуску)

Can't render this file because it has a wrong number of fields in line 12.

View File

@ -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)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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) 低延迟绘画(关闭垂直同步,下次启动时生效)

Can't render this file because it has a wrong number of fields in line 15.

View File

@ -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) 低延遲繪圖(關閉垂直同步,下次啟動時生效)

Can't render this file because it contains an unexpected character in line 6 and column 47.

View File

@ -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<f64>,
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.

View File

@ -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<String, Value>, 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))

View File

@ -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 }];

View File

@ -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=<file.csv>` / `--stroke-trace <file.csv>`): 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.

View File

@ -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 `=<file.csv>`) | 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 `=<file.csv>`; also `--stroke-trace <stderr|file.csv>`, 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

View File

@ -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`.