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 <noreply@anthropic.com>
This commit is contained in:
parent
157d54a4d4
commit
9ca19a9db8
8 changed files with 96 additions and 10 deletions
|
|
@ -43,7 +43,6 @@ acadrust = "0.3.4"
|
||||||
printpdf = "0.9.1"
|
printpdf = "0.9.1"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||||
rayon = "1"
|
|
||||||
inventory = "0.3"
|
inventory = "0.3"
|
||||||
truck-shapeops = { version = "0.4", optional = true }
|
truck-shapeops = { version = "0.4", optional = true }
|
||||||
# Fast non-cryptographic hasher. Handle/u64 keys dominate the hot block,
|
# 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" }
|
acadrust = { git = "https://github.com/HakanSeven12/acadrust", branch = "main" }
|
||||||
|
|
||||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||||
|
rayon = "1"
|
||||||
open = "5"
|
open = "5"
|
||||||
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
ureq = { version = "3", default-features = false, features = ["rustls"] }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -602,7 +602,7 @@ pub(crate) fn is_entity_corrupt(e: &EntityType) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn purge_corrupt_entities(doc: &mut CadDocument) -> usize {
|
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
|
// Detection is pure and read-only; the per-vertex finite/extent checks on
|
||||||
// large polylines dominate, so fan the scan out across cores. Gather
|
// large polylines dominate, so fan the scan out across cores. Gather
|
||||||
// entity references in one pass, test in parallel, then remove serially
|
// entity references in one pass, test in parallel, then remove serially
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ pub mod patterns;
|
||||||
pub mod scene;
|
pub mod scene;
|
||||||
pub mod snap;
|
pub mod snap;
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
pub mod par;
|
||||||
pub mod sys;
|
pub mod sys;
|
||||||
pub mod update_check;
|
pub mod update_check;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ mod patterns;
|
||||||
mod scene;
|
mod scene;
|
||||||
mod snap;
|
mod snap;
|
||||||
mod ui;
|
mod ui;
|
||||||
|
mod par;
|
||||||
mod sys;
|
mod sys;
|
||||||
mod update_check;
|
mod update_check;
|
||||||
|
|
||||||
|
|
|
||||||
77
src/par.rs
Normal file
77
src/par.rs
Normal file
|
|
@ -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<T> {
|
||||||
|
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<K, V, S> {
|
||||||
|
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<T, S> {
|
||||||
|
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<I: IntoIterator> 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<U, F>(self, f: F) -> std::iter::FlatMap<Self, U, F>
|
||||||
|
where
|
||||||
|
F: FnMut(Self::Item) -> U,
|
||||||
|
U: IntoIterator,
|
||||||
|
{
|
||||||
|
self.flat_map(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<I: Iterator> IterParExt for I {}
|
||||||
|
}
|
||||||
4
src/scene/cache/block_cache.rs
vendored
4
src/scene/cache/block_cache.rs
vendored
|
|
@ -125,7 +125,7 @@ impl BlockCache {
|
||||||
/// because their entities are emitted as top-level wires, not via the
|
/// because their entities are emitted as top-level wires, not via the
|
||||||
/// cache.
|
/// cache.
|
||||||
pub fn build(doc: &CadDocument, anno_scale: f32, bg_color: [f32; 4]) -> Self {
|
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 mut cache = Self::new();
|
||||||
let referenced = collect_referenced_blocks(doc);
|
let referenced = collect_referenced_blocks(doc);
|
||||||
// Each defn is built independently: nested INSERTs are stored as
|
// 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
|
/// called in. Cycle guard: a self-referential block keeps an empty AABB
|
||||||
/// (will fail every frustum test → not emitted, which is correct).
|
/// (will fail every frustum test → not emitted, which is correct).
|
||||||
fn compute_block_aabbs(&mut self, names: &[String]) {
|
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
|
// Phase 1 (parallel, read-only): each defn's union AABB is resolved
|
||||||
// by `defn_aabb_recursive`, which only *reads* `self.defns` (the map
|
// 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
|
// is fully built by now). There's no memoization, so a defn shared by
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches {
|
||||||
// filters that `cache::block_cache::build_defn` already uses for block defns)
|
// filters that `cache::block_cache::build_defn` already uses for block defns)
|
||||||
// and prefer the entity-scan when the header center drifts more than
|
// and prefer the entity-scan when the header center drifts more than
|
||||||
// 10× its own half-span away from the entity centroid.
|
// 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
|
// Single pass over entities does triple duty: classify cache-kind handle
|
||||||
// lists (hatch / image / mesh) AND accumulate per-entity centroids for the
|
// 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.
|
// Tessellate in parallel across all available CPU cores.
|
||||||
use rayon::prelude::*;
|
use crate::par::prelude::*;
|
||||||
let doc = &self.document;
|
let doc = &self.document;
|
||||||
// Selection / hover highlight is NOT baked into tessellation. It is
|
// Selection / hover highlight is NOT baked into tessellation. It is
|
||||||
// applied per frame in the GPU xray overlay pass from the live
|
// applied per frame in the GPU xray overlay pass from the live
|
||||||
|
|
@ -4297,7 +4297,7 @@ impl Scene {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
use rayon::prelude::*;
|
use crate::par::prelude::*;
|
||||||
self.hatches = entries
|
self.hatches = entries
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|(handle, kind)| {
|
.filter_map(|(handle, kind)| {
|
||||||
|
|
@ -4350,7 +4350,7 @@ impl Scene {
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
use rayon::prelude::*;
|
use crate::par::prelude::*;
|
||||||
let facet_res = self.document.header.facet_resolution;
|
let facet_res = self.document.header.facet_resolution;
|
||||||
let woff = self.world_offset;
|
let woff = self.world_offset;
|
||||||
self.meshes = entries
|
self.meshes = entries
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
|
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use iced::wgpu;
|
use iced::wgpu;
|
||||||
use rayon::prelude::*;
|
use crate::par::prelude::*;
|
||||||
|
|
||||||
/// Allocate a VERTEX buffer with `mapped_at_creation` and write `data` directly
|
/// Allocate a VERTEX buffer with `mapped_at_creation` and write `data` directly
|
||||||
/// into the mapped slice. Skips the intermediate staging copy that
|
/// into the mapped slice. Skips the intermediate staging copy that
|
||||||
|
|
@ -295,7 +295,7 @@ impl WireGpu {
|
||||||
scissor: Option<[f32; 4]>,
|
scissor: Option<[f32; 4]>,
|
||||||
mesh_edge: bool,
|
mesh_edge: bool,
|
||||||
) -> Vec<Self> {
|
) -> Vec<Self> {
|
||||||
use rayon::prelude::*;
|
use crate::par::prelude::*;
|
||||||
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
const MAX_INSTANCES: usize = 268_435_456 / std::mem::size_of::<WireInstance>();
|
||||||
let per: Vec<Vec<WireInstance>> = wires
|
let per: Vec<Vec<WireInstance>> = wires
|
||||||
.par_iter()
|
.par_iter()
|
||||||
|
|
@ -340,6 +340,7 @@ impl WireGpu {
|
||||||
// within a batch does not affect correctness. `reduce` concatenates the
|
// within a batch does not affect correctness. `reduce` concatenates the
|
||||||
// per-wire Vec<WireInstance> chunks while letting rayon spread work
|
// per-wire Vec<WireInstance> chunks while letting rayon spread work
|
||||||
// across cores.
|
// across cores.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let instances: Vec<WireInstance> = wires
|
let instances: Vec<WireInstance> = wires
|
||||||
.par_iter()
|
.par_iter()
|
||||||
.map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map)))
|
.map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map)))
|
||||||
|
|
@ -353,6 +354,12 @@ impl WireGpu {
|
||||||
acc
|
acc
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
// Web: no thread pool — concatenate the per-wire streams sequentially.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
let instances: Vec<WireInstance> = wires
|
||||||
|
.iter()
|
||||||
|
.flat_map(|wire| emit_wire_instances(wire, wire.color, wire_draw_depth(wire, depth_map)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
if instances.is_empty() {
|
if instances.is_empty() {
|
||||||
return vec![];
|
return vec![];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue