diff --git a/Cargo.lock b/Cargo.lock index 62f084afc..ff1d18be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6065,6 +6065,7 @@ dependencies = [ "smithay-clipboard", "socket2", "vectorcraft-engine", + "vectorcraft-render", "vectorcraft-text", "vectorcraft-ui-egui", "wayland-backend", diff --git a/apps/vectorcraft-cli/Cargo.toml b/apps/vectorcraft-cli/Cargo.toml index cb9183315..8f39e3b58 100644 --- a/apps/vectorcraft-cli/Cargo.toml +++ b/apps/vectorcraft-cli/Cargo.toml @@ -34,8 +34,9 @@ winsafe = { version = "0.0.29", default-features = false, features = ["kernel", # DirectWrite's system font collection and GDI's font list (the desktop app's src/system_fonts.rs, # shared): fonts that font services such as Adobe Fonts load in place (#579, #1258). MIT OR -# Apache-2.0. -windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi"] } +# Apache-2.0. Win32_System_Threading has `GetProcessAffinityMask`, which the desktop app's +# src/windows_cores.rs, shared, uses to count the cores the process may run on. +windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_System_Threading"] } # CoreText's font manager (the desktop app's src/mac_fonts.rs, shared): fonts that apps and font # managers register from their own folders (#579). Zlib OR Apache-2.0 OR MIT. diff --git a/apps/vectorcraft-cli/src/main.rs b/apps/vectorcraft-cli/src/main.rs index bba1976c1..b7334586c 100644 --- a/apps/vectorcraft-cli/src/main.rs +++ b/apps/vectorcraft-cli/src/main.rs @@ -49,6 +49,10 @@ mod prefs_dir; #[cfg(windows)] #[path = "../../vectorcraft/src/system_fonts.rs"] mod system_fonts; +/// The desktop app's affinity-mask reader, shared: renders start workers only for usable cores. +#[cfg(windows)] +#[path = "../../vectorcraft/src/windows_cores.rs"] +mod windows_cores; use serde_json::{Value, json}; use vectorcraft_engine::file_access::{self, AutomationRoots}; @@ -161,6 +165,9 @@ fn help() -> Result<(), String> { } fn main() -> ExitCode { + // Before the first render: the cores the process may run on. + #[cfg(windows)] + windows_cores::install(); // Before any font lookup: the fonts font services load (#579). #[cfg(windows)] system_fonts::install(); diff --git a/apps/vectorcraft/Cargo.toml b/apps/vectorcraft/Cargo.toml index 0657c93c8..7af8c51ef 100644 --- a/apps/vectorcraft/Cargo.toml +++ b/apps/vectorcraft/Cargo.toml @@ -54,7 +54,11 @@ pollster = "1" # Win32_UI_WindowsAndMessaging has `MSG` and `WM_POINTERDOWN`, which src/windows_stylus.rs uses to # read a pen's barrel button from its pointer messages. Win32_UI_Input_Pointer has # `GetPointerPenInfo`, which it uses to read the pen's pressure and which end of the pen is down. -windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_UI_Input_Pointer", "Win32_UI_WindowsAndMessaging"] } +# Win32_System_Threading has `GetProcessAffinityMask`, which src/windows_cores.rs uses to count the +# cores the process may run on. +windows = { version = "0.62", features = ["Win32_Graphics_DirectWrite", "Win32_Graphics_Gdi", "Win32_System_Threading", "Win32_UI_Input_Pointer", "Win32_UI_WindowsAndMessaging"] } +# The renderer's worker count follows those cores (src/windows_cores.rs). +vectorcraft-render = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] # The macOS menu bar (src/mac_menu.rs): Tauri's menu crate, AppKit menus behind a safe API (no GTK diff --git a/apps/vectorcraft/src/main.rs b/apps/vectorcraft/src/main.rs index e90977726..1e2ab4fae 100644 --- a/apps/vectorcraft/src/main.rs +++ b/apps/vectorcraft/src/main.rs @@ -50,6 +50,8 @@ mod system_fonts; mod tablet; mod window; mod window_frame; +#[cfg(windows)] +mod windows_cores; mod windows_stylus; mod xwayland; @@ -184,6 +186,9 @@ fn main() -> std::process::ExitCode { // Before desktop services, so every start-up warning is recorded (`logging`). let logger = logging::install(); vectorcraft_ui_egui::i18n::detect_system_lang_in_background(); + // Before the first render: the cores the process may run on. + #[cfg(windows)] + windows_cores::install(); // Before the first font scan (the app's start): the fonts font services load (#579). #[cfg(windows)] system_fonts::install(); diff --git a/apps/vectorcraft/src/windows_cores.rs b/apps/vectorcraft/src/windows_cores.rs new file mode 100644 index 000000000..b35c0876c --- /dev/null +++ b/apps/vectorcraft/src/windows_cores.rs @@ -0,0 +1,40 @@ +//! Windows: the cores the process may run on, from its affinity mask, for the renderer's worker +//! count (`vectorcraft_render::set_usable_cores`). `available_parallelism` counts every processor +//! in the machine, also when the process may run on fewer (started with `start /affinity`, or +//! pinned by a launcher). The desktop app and `vectorcraft-cli` (exports, MCP) both install it; the +//! CLI shares this file. +//! +//! `GetProcessAffinityMask` is a C API with no safe binding, hence the scoped `unsafe_code` +//! allowance. +#![allow(unsafe_code)] + +use windows::Win32::System::Threading::{GetCurrentProcess, GetProcessAffinityMask}; + +/// Tells the renderer how many cores the process may run on. +pub fn install() { + vectorcraft_render::set_usable_cores(usable_cores()); +} + +/// The number of cores in the process's affinity mask, or `None` when Windows reports none (a +/// process with threads in more than one processor group). +fn usable_cores() -> Option { + let (mut process, mut system) = (0usize, 0usize); + // SAFETY: `GetCurrentProcess` returns a pseudo-handle that needs no closing, and both pointers + // point at live `usize`s for the length of the call. + unsafe { GetProcessAffinityMask(GetCurrentProcess(), &mut process, &mut system) }.ok()?; + (process != 0).then(|| process.count_ones() as usize) +} + +#[cfg(test)] +mod tests { + /// The mask Windows reports for this process gives at least one core and no more than the + /// machine has. (A child process started with `start /affinity` would test a one-core mask + /// end to end, but a process spawned while other tests hold sockets inherits them and resets + /// their connections; `automatic_threads` covers the one-core count.) + #[test] + fn the_affinity_mask_gives_the_cores_the_process_may_use() { + let cores = super::usable_cores().unwrap(); + let all = std::thread::available_parallelism().unwrap().get(); + assert!((1..=all).contains(&cores), "{cores} of {all}"); + } +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index d5c601dc1..8bf6b0d23 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -227,6 +227,6 @@ pub(crate) use text_geom::is_opentype_alternate; #[cfg(test)] use text_geom::text_geom; pub(crate) use threads::single_threaded_context; -pub use threads::{default_threads, set_default_threads}; +pub use threads::{default_threads, set_default_threads, set_usable_cores}; #[cfg(test)] mod tests; diff --git a/crates/render/src/threads.rs b/crates/render/src/threads.rs index eb1fb66ea..dfd6bf757 100644 --- a/crates/render/src/threads.rs +++ b/crates/render/src/threads.rs @@ -9,14 +9,25 @@ pub(crate) fn single_threaded_context(w: u16, h: u16) -> RenderContext { /// Preferred rasterizer thread count set by the app (Preferences → Performance); negative = automatic. static THREADS_OVERRIDE: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(-1); +/// The cores the process may run on, as the app read them from the system; 0 = not known. +static USABLE_CORES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + /// Override the worker-thread count used by renderers created from now on (`None` = automatic). pub fn set_default_threads(n: Option) { THREADS_OVERRIDE.store(n.map_or(-1, i32::from), std::sync::atomic::Ordering::Relaxed); } -/// Rasterizer worker threads: 0 on wasm; otherwise one per core up to 8, leaving a core free. On a -/// 10-core machine 8 workers render a 50,000-path page 1.4× faster than 4 (9 are slower again); -/// the pixels don't depend on the count (`pixels_do_not_depend_on_the_thread_count`). +/// The number of cores the process may run on, when the app knows a smaller number than +/// `available_parallelism` gives (`None` = not known). On Windows `available_parallelism` counts +/// every processor in the machine, also when the process's affinity mask allows fewer. +pub fn set_usable_cores(n: Option) { + USABLE_CORES.store(n.unwrap_or(0), std::sync::atomic::Ordering::Relaxed); +} + +/// Rasterizer worker threads: 0 on wasm; otherwise one per core the process may run on, up to 8, +/// leaving a core free. On a 10-core machine 8 workers render a 50,000-path page 1.4× faster than 4 +/// (9 are slower again); the pixels don't depend on the count +/// (`pixels_do_not_depend_on_the_thread_count`). pub fn default_threads() -> u16 { #[cfg(target_arch = "wasm32")] { @@ -31,6 +42,31 @@ pub fn default_threads() -> u16 { if std::env::var_os("VECTORCRAFT_RENDER_THREADS").is_some() { return std::env::var("VECTORCRAFT_RENDER_THREADS").ok().and_then(|v| v.parse().ok()).unwrap_or(0); } - std::thread::available_parallelism().map(|n| (n.get().saturating_sub(1)).min(8) as u16).unwrap_or(0) + let usable = USABLE_CORES.load(std::sync::atomic::Ordering::Relaxed); + std::thread::available_parallelism().map(|n| automatic_threads(n.get(), usable)).unwrap_or(0) + } +} + +/// [`default_threads`] when automatic, on `cpus` processors of which the process may use `usable` +/// (0 = all). +fn automatic_threads(cpus: usize, usable: usize) -> u16 { + let cores = if usable == 0 { cpus } else { cpus.min(usable) }; + (cores.saturating_sub(1)).min(8) as u16 +} + +#[cfg(test)] +mod tests { + use super::automatic_threads; + + /// A process that may run on one core renders on the calling thread. With workers, vello_cpu's + /// flush spins on that core while the workers it waits for get no time, and a render crawls or + /// stalls. + #[test] + fn workers_follow_the_cores_the_process_may_use() { + assert_eq!(automatic_threads(24, 0), 8); + assert_eq!(automatic_threads(24, 1), 0); + assert_eq!(automatic_threads(24, 4), 3); + assert_eq!(automatic_threads(4, 16), 3); + assert_eq!(automatic_threads(1, 0), 0); } }