From 9ca19a9db8189729e65fab26d6710199f792c1e4 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 16 Jun 2026 17:11:20 +0300 Subject: [PATCH] feat(wasm): sequential rayon shim for the web build wasm32 has no threads, so rayon's global pool would panic at runtime. Add `crate::par::prelude`: native re-exports `rayon::prelude`, wasm provides sequential `std`-iterator equivalents (par_iter / into_par_iter / flat_map_iter over slices, Vec, HashMap, HashSet). All call sites now `use crate::par::prelude::*;` instead of rayon directly. - wire_gpu's rayon `reduce` (2-arg, no std equivalent) is cfg-split to a sequential flat_map concat on wasm. - rayon moves to a non-wasm target dependency. Native still runs in parallel; all 75 tests pass. wasm bin compiles. Co-Authored-By: Claude Opus 4.8 --- Cargo.toml | 2 +- src/io/mod.rs | 2 +- src/lib.rs | 1 + src/main.rs | 1 + src/par.rs | 77 ++++++++++++++++++++++++++++++++++ src/scene/cache/block_cache.rs | 4 +- src/scene/mod.rs | 8 ++-- src/scene/pipeline/wire_gpu.rs | 11 ++++- 8 files changed, 96 insertions(+), 10 deletions(-) create mode 100644 src/par.rs diff --git a/Cargo.toml b/Cargo.toml index 15a5c8141..cb4476557 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,6 @@ acadrust = "0.3.4" printpdf = "0.9.1" flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } -rayon = "1" inventory = "0.3" truck-shapeops = { version = "0.4", optional = true } # Fast non-cryptographic hasher. Handle/u64 keys dominate the hot block, @@ -77,6 +76,7 @@ windows-sys = { version = "0.61", features = ["Win32_UI_Shell", "Win32_UI_Window acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "main" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rayon = "1" open = "5" ureq = { version = "3", default-features = false, features = ["rustls"] } diff --git a/src/io/mod.rs b/src/io/mod.rs index 24a1fd0f9..956ed924b 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -602,7 +602,7 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool { } pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize { - use rayon::prelude::*; + use crate::par::prelude::*; // Detection is pure and read-only; the per-vertex finite/extent checks on // large polylines dominate, so fan the scan out across cores. Gather // entity references in one pass, test in parallel, then remove serially diff --git a/src/lib.rs b/src/lib.rs index ccb62f5dc..fdfbc5c01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,5 +11,6 @@ pub mod patterns; pub mod scene; pub mod snap; pub mod ui; +pub mod par; pub mod sys; pub mod update_check; diff --git a/src/main.rs b/src/main.rs index c7cff8cf4..d7c2b29d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,6 +15,7 @@ mod patterns; mod scene; mod snap; mod ui; +mod par; mod sys; mod update_check; diff --git a/src/par.rs b/src/par.rs new file mode 100644 index 000000000..737bf2b90 --- /dev/null +++ b/src/par.rs @@ -0,0 +1,77 @@ +// Parallel-iterator prelude that degrades to sequential iteration on the web. +// +// Native builds re-export `rayon::prelude`. wasm32 has no threads, so the same +// call sites (`.par_iter()`, `.into_par_iter()`, `.flat_map_iter()`) resolve to +// plain sequential `std` iterators instead — identical results, just single +// threaded. Bring it in with `use crate::par::prelude::*;`. + +#[cfg(not(target_arch = "wasm32"))] +pub mod prelude { + pub use rayon::prelude::*; +} + +#[cfg(target_arch = "wasm32")] +pub mod prelude { + use std::collections::{HashMap, HashSet}; + + /// `.par_iter()` over a shared reference → sequential `std` iterator. + pub trait ParRefShim<'a> { + type Iter; + fn par_iter(&'a self) -> Self::Iter; + } + + impl<'a, T: 'a> ParRefShim<'a> for [T] { + type Iter = core::slice::Iter<'a, T>; + fn par_iter(&'a self) -> Self::Iter { + self.iter() + } + } + + impl<'a, T: 'a> ParRefShim<'a> for Vec { + type Iter = core::slice::Iter<'a, T>; + fn par_iter(&'a self) -> Self::Iter { + self.iter() + } + } + + impl<'a, K: 'a, V: 'a, S> ParRefShim<'a> for HashMap { + type Iter = std::collections::hash_map::Iter<'a, K, V>; + fn par_iter(&'a self) -> Self::Iter { + self.iter() + } + } + + impl<'a, T: 'a, S> ParRefShim<'a> for HashSet { + type Iter = std::collections::hash_set::Iter<'a, T>; + fn par_iter(&'a self) -> Self::Iter { + self.iter() + } + } + + /// `.into_par_iter()` → sequential `into_iter()`. + pub trait IntoParShim { + type IntoIter; + fn into_par_iter(self) -> Self::IntoIter; + } + + impl IntoParShim for I { + type IntoIter = I::IntoIter; + fn into_par_iter(self) -> Self::IntoIter { + self.into_iter() + } + } + + /// rayon's `flat_map_iter` has no `std` equivalent name; map it to + /// `flat_map`. + pub trait IterParExt: Iterator + Sized { + fn flat_map_iter(self, f: F) -> std::iter::FlatMap + where + F: FnMut(Self::Item) -> U, + U: IntoIterator, + { + self.flat_map(f) + } + } + + impl IterParExt for I {} +} diff --git a/src/scene/cache/block_cache.rs b/src/scene/cache/block_cache.rs index faf766d8b..e33e9bcc5 100644 --- a/src/scene/cache/block_cache.rs +++ b/src/scene/cache/block_cache.rs @@ -125,7 +125,7 @@ impl BlockCache { /// because their entities are emitted as top-level wires, not via the /// cache. pub fn build(doc: &CadDocument, anno_scale: f32, bg_color: [f32; 4]) -> Self { - use rayon::prelude::*; + use crate::par::prelude::*; let mut cache = Self::new(); let referenced = collect_referenced_blocks(doc); // Each defn is built independently: nested INSERTs are stored as @@ -151,7 +151,7 @@ impl BlockCache { /// called in. Cycle guard: a self-referential block keeps an empty AABB /// (will fail every frustum test → not emitted, which is correct). fn compute_block_aabbs(&mut self, names: &[String]) { - use rayon::prelude::*; + use crate::par::prelude::*; // Phase 1 (parallel, read-only): each defn's union AABB is resolved // by `defn_aabb_recursive`, which only *reads* `self.defns` (the map // is fully built by now). There's no memoization, so a defn shared by diff --git a/src/scene/mod.rs b/src/scene/mod.rs index 42cf933c4..de5bd323c 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -155,7 +155,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { // filters that `cache::block_cache::build_defn` already uses for block defns) // and prefer the entity-scan when the header center drifts more than // 10× its own half-span away from the entity centroid. - use rayon::prelude::*; + use crate::par::prelude::*; // Single pass over entities does triple duty: classify cache-kind handle // lists (hatch / image / mesh) AND accumulate per-entity centroids for the @@ -2245,7 +2245,7 @@ impl Scene { }; // Tessellate in parallel across all available CPU cores. - use rayon::prelude::*; + use crate::par::prelude::*; let doc = &self.document; // Selection / hover highlight is NOT baked into tessellation. It is // applied per frame in the GPU xray overlay pass from the live @@ -4297,7 +4297,7 @@ impl Scene { }) .collect(); - use rayon::prelude::*; + use crate::par::prelude::*; self.hatches = entries .into_par_iter() .filter_map(|(handle, kind)| { @@ -4350,7 +4350,7 @@ impl Scene { }) .collect(); - use rayon::prelude::*; + use crate::par::prelude::*; let facet_res = self.document.header.facet_resolution; let woff = self.world_offset; self.meshes = entries diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index afa1356de..0215e0c84 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -24,7 +24,7 @@ use crate::scene::model::wire_model::WireModel; use iced::wgpu; -use rayon::prelude::*; +use crate::par::prelude::*; /// Allocate a VERTEX buffer with `mapped_at_creation` and write `data` directly /// into the mapped slice. Skips the intermediate staging copy that @@ -295,7 +295,7 @@ impl WireGpu { scissor: Option<[f32; 4]>, mesh_edge: bool, ) -> Vec { - use rayon::prelude::*; + use crate::par::prelude::*; const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::(); let per: Vec> = wires .par_iter() @@ -340,6 +340,7 @@ impl WireGpu { // within a batch does not affect correctness. `reduce` concatenates the // per-wire Vec chunks while letting rayon spread work // across cores. + #[cfg(not(target_arch = "wasm32"))] let instances: Vec = wires .par_iter() .map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map))) @@ -353,6 +354,12 @@ impl WireGpu { acc } }); + // Web: no thread pool — concatenate the per-wire streams sequentially. + #[cfg(target_arch = "wasm32")] + let instances: Vec = wires + .iter() + .flat_map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map))) + .collect(); if instances.is_empty() { return vec![];