mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 13:47:21 -06:00
Part of #4516: Windows renders start workers only for the cores the process may use
The renderer sized its worker pool from available_parallelism, which on Windows counts every processor in the machine, also when the process's affinity mask allows fewer. A process allowed one core started 8 workers, and the renderer's flush spun on that core while the workers got no time, so renders crawled or stalled. The desktop app and vectorcraft-cli now read the affinity mask at startup (src/windows_cores.rs, shared) and pass the count to the renderer with vectorcraft_render::set_usable_cores. With one usable core the renderer renders on the calling thread.
This commit is contained in:
Generated
+1
@@ -6065,6 +6065,7 @@ dependencies = [
|
||||
"smithay-clipboard",
|
||||
"socket2",
|
||||
"vectorcraft-engine",
|
||||
"vectorcraft-render",
|
||||
"vectorcraft-text",
|
||||
"vectorcraft-ui-egui",
|
||||
"wayland-backend",
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
//! 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<usize> {
|
||||
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 {
|
||||
/// A process that may run on one core renders on the calling thread. With workers, the
|
||||
/// renderer's flush spins on that core while the workers it waits for get no time, and a render
|
||||
/// crawls or stalls.
|
||||
#[test]
|
||||
fn a_process_limited_to_one_core_starts_no_render_workers() {
|
||||
const CHILD: &str = "VECTORCRAFT_TEST_ONE_CORE";
|
||||
const NAME: &str = "windows_cores::tests::a_process_limited_to_one_core_starts_no_render_workers";
|
||||
if std::env::var_os(CHILD).is_some() {
|
||||
assert_eq!(super::usable_cores(), Some(1));
|
||||
super::install();
|
||||
assert_eq!(vectorcraft_render::default_threads(), 0);
|
||||
return;
|
||||
}
|
||||
// `start /affinity 1` runs this test again in a process allowed only the first core.
|
||||
use std::os::windows::process::CommandExt;
|
||||
let exe = std::env::current_exe().unwrap();
|
||||
let output = std::process::Command::new("cmd")
|
||||
.raw_arg(format!("/C start \"\" /affinity 1 /wait /b \"{}\" --exact {NAME} --nocapture", exe.display()))
|
||||
.env(CHILD, "1")
|
||||
.env_remove("VECTORCRAFT_RENDER_THREADS")
|
||||
.output()
|
||||
.unwrap();
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
assert!(
|
||||
output.status.success() && stdout.contains("test result: ok. 1 passed; 0 failed;"),
|
||||
"one-core test failed:\n{stdout}\n{}",
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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<u16>) {
|
||||
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<usize>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user