From bca65600cd5f6bf0fe8313a60b98fb2c1bd3de84 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Sat, 25 Jul 2026 21:34:29 +0300 Subject: [PATCH] fix: stabilize web loading and paper rendering --- .gitignore | 1 - Cargo.lock | 1 + Trunk.toml | 2 +- crates/ocs_web_worker/Cargo.toml | 1 + crates/ocs_web_worker/src/lib.rs | 13 +++++++++++-- index.html | 1 - scripts/build-web-worker.sh | 5 +++-- src/app/document.rs | 2 ++ src/app/mod.rs | 9 +++++++++ src/app/update/file.rs | 4 ++-- src/app/update/mod.rs | 3 --- src/app/update/viewport.rs | 2 +- src/config.rs | 5 +++++ src/io/mod.rs | 22 ++++++++++++---------- src/io/pdf_export.rs | 2 ++ src/io/single_instance.rs | 2 +- src/io/thumbnail.rs | 1 + src/scene/convert/solid3d_tess.rs | 1 + src/scene/mod.rs | 21 ++++----------------- src/scene/pick/interaction_index.rs | 8 ++++---- src/scene/pipeline/mod.rs | 23 +++++++++++++++++++++-- src/scene/pipeline/wire_arena.rs | 6 +++--- src/scene/pipeline/wire_gpu.rs | 3 --- src/scene/view/render.rs | 20 +++++++++++++------- web/ocs-parse-worker.js | 13 +++++++++++-- 25 files changed, 109 insertions(+), 62 deletions(-) diff --git a/.gitignore b/.gitignore index 0bdfeceb..80e29202 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target # Trunk web build output (wasm/js bundle) dist -web/worker_pkg # These are backup files generated by rustfmt **/*.rs.bk diff --git a/Cargo.lock b/Cargo.lock index 48ec007e..8dcee12d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3674,6 +3674,7 @@ version = "0.1.0" dependencies = [ "acadrust", "bincode", + "console_error_panic_hook", "getrandom 0.3.4", "js-sys", "wasm-bindgen", diff --git a/Trunk.toml b/Trunk.toml index 86abdd79..4deb7b03 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -13,7 +13,7 @@ target = "index.html" # cross-compile to wasm). [[hooks]] -stage = "pre_build" +stage = "post_build" command = "sh" command_arguments = ["scripts/build-web-worker.sh"] diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index f18c2c37..a6a957f8 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -10,6 +10,7 @@ crate-type = ["cdylib"] [dependencies] acadrust = { version = "0.4", features = ["serde"] } bincode = "1.3" +console_error_panic_hook = "0.1" getrandom = { version = "0.3", features = ["wasm_js"] } js-sys = "0.3" wasm-bindgen = "0.2" diff --git a/crates/ocs_web_worker/src/lib.rs b/crates/ocs_web_worker/src/lib.rs index 2d8b7465..9263f963 100644 --- a/crates/ocs_web_worker/src/lib.rs +++ b/crates/ocs_web_worker/src/lib.rs @@ -2,15 +2,22 @@ use std::io::Cursor; use acadrust::io::dwg::DwgReader; use acadrust::DxfReader; -use js_sys::Uint8Array; +use js_sys::{Function, Uint8Array}; use wasm_bindgen::prelude::*; /// Parse DWG/DXF on a dedicated browser worker and return a compact serialized /// document. The main wasm instance only deserializes and installs it, so the /// expensive bit/handle/object decode never occupies the browser UI thread. #[wasm_bindgen] -pub fn parse_document(name: String, bytes: Uint8Array) -> Result { +pub fn parse_document( + name: String, + bytes: Uint8Array, + report_stage: &Function, +) -> Result { + console_error_panic_hook::set_once(); + report_stage.call1(&JsValue::NULL, &JsValue::from_str("copy input"))?; let bytes = bytes.to_vec(); + report_stage.call1(&JsValue::NULL, &JsValue::from_str("parse document"))?; let ext = name.rsplit('.').next().unwrap_or_default().to_lowercase(); let document = match ext.as_str() { "dwg" => DwgReader::from_stream(Cursor::new(bytes)) @@ -26,7 +33,9 @@ pub fn parse_document(name: String, bytes: Uint8Array) -> Result -
diff --git a/scripts/build-web-worker.sh b/scripts/build-web-worker.sh index 38da25ec..0a5e1d44 100644 --- a/scripts/build-web-worker.sh +++ b/scripts/build-web-worker.sh @@ -2,9 +2,10 @@ set -eu cargo build --release --target wasm32-unknown-unknown --package ocs_web_worker -mkdir -p web/worker_pkg +worker_out="${TRUNK_STAGING_DIR:?}/worker_pkg" +mkdir -p "$worker_out" wasm-bindgen \ --target web \ - --out-dir web/worker_pkg \ + --out-dir "$worker_out" \ --out-name ocs_web_worker \ target/wasm32-unknown-unknown/release/ocs_web_worker.wasm diff --git a/src/app/document.rs b/src/app/document.rs index b8929458..091a7ba7 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -228,6 +228,7 @@ pub(super) struct DocumentTab { pub(super) last_synced_camera_gen: u64, /// Render-state key of `scene.document.preview`. Matching saves reuse the /// encoded DWG thumbnail instead of rescanning every resident wire. + #[cfg(not(target_arch = "wasm32"))] pub(super) thumbnail_cache_key: Option, /// Sentinel "Welcome / Start" tab. Always at index 0 when present. /// Cannot be closed; the viewport area renders a welcome page instead @@ -463,6 +464,7 @@ impl DocumentTab { block_edit: None, active_mleader_style: "Standard".to_string(), last_synced_camera_gen: 0, + #[cfg(not(target_arch = "wasm32"))] thumbnail_cache_key: None, is_start: false, pan_mode: false, diff --git a/src/app/mod.rs b/src/app/mod.rs index 4fa26597..da913a1b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -755,7 +755,9 @@ pub(super) struct OpenCADStudio { pending_close: Option, /// Latest save job per stable tab id. Older completions may finish, but /// cannot mark a newer document state clean or redirect its path. + #[cfg(not(target_arch = "wasm32"))] active_save_jobs: std::collections::HashMap, + #[cfg(not(target_arch = "wasm32"))] save_job_serial: u64, /// OS window for the unsaved-changes confirmation dialog. @@ -866,6 +868,7 @@ pub(super) enum PendingClose { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(not(target_arch = "wasm32"))] pub(super) enum SavePurpose { Manual, SaveAs, @@ -873,6 +876,7 @@ pub(super) enum SavePurpose { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(not(target_arch = "wasm32"))] pub(super) enum SaveContinuation { None, CloseTab, @@ -880,6 +884,7 @@ pub(super) enum SaveContinuation { } #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[cfg(not(target_arch = "wasm32"))] pub(super) struct ThumbnailCacheKey { epoch: u64, camera_generation: u64, @@ -889,6 +894,7 @@ pub(super) struct ThumbnailCacheKey { } #[derive(Debug, Clone)] +#[cfg(not(target_arch = "wasm32"))] pub struct SaveOutcome { job_id: u64, tab_id: u64, @@ -1463,6 +1469,7 @@ pub enum Message { /// Periodic autosave tick — write `.sv$` recovery files for dirty tabs. AutoSave, /// Native background save/autosave completed. + #[cfg(not(target_arch = "wasm32"))] SaveFinished(SaveOutcome), // ───────────────────────────────────────────────────────────────────── CommandInput(String), @@ -2505,7 +2512,9 @@ impl OpenCADStudio { active_interaction_index: None, queued_interaction_indices: std::collections::VecDeque::new(), pending_close: None, + #[cfg(not(target_arch = "wasm32"))] active_save_jobs: std::collections::HashMap::new(), + #[cfg(not(target_arch = "wasm32"))] save_job_serial: 0, save_dialog_format: "DWG 2018".to_string(), save_dialog_filename: "drawing.dwg".to_string(), diff --git a/src/app/update/file.rs b/src/app/update/file.rs index 608b7d69..72a33af2 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -783,7 +783,7 @@ pub(super) fn on_open_file(&mut self) -> Task { ) }) }; - let clone_started = std::time::Instant::now(); + let clone_started = iced::time::Instant::now(); let mut snapshot = self.tabs[i].scene.document.clone(); let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0; if crate::perf::enabled() { @@ -807,7 +807,7 @@ pub(super) fn on_open_file(&mut self) -> Task { let (result, refreshed_preview) = std::thread::spawn(move || { let mut refreshed_preview = None; if let Some((wires, camera, bg_color, png, viewport)) = thumbnail { - let started = std::time::Instant::now(); + let started = iced::time::Instant::now(); snapshot.preview = crate::io::thumbnail::from_snapshot( &wires, &camera, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index fa2e3d04..a04d6059 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -3954,9 +3954,6 @@ impl OpenCADStudio { #[cfg(not(target_arch = "wasm32"))] Message::SaveFinished(outcome) => self.on_save_finished(outcome), - #[cfg(target_arch = "wasm32")] - Message::SaveFinished(_) => Task::none(), - // ── Page Setup ──────────────────────────────────────────────── Message::UpdateCheckResult(latest) => { let Some(info) = latest else { diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 8d598c49..cf5d36e1 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3639,7 +3639,7 @@ impl OpenCADStudio { let weak = Arc::downgrade(&wires); Some(Task::perform( async move { - let started = std::time::Instant::now(); + let started = iced::time::Instant::now(); let index = Arc::new( crate::scene::pick::interaction_index::InteractionIndex::build(&wires), ); diff --git a/src/config.rs b/src/config.rs index 52eb3306..46bb238c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,9 +31,12 @@ pub fn config_dir() -> Option { // ── Last file-dialog directory ─────────────────────────────────────────────── +#[cfg(not(target_arch = "wasm32"))] use std::path::Path; +#[cfg(not(target_arch = "wasm32"))] use std::sync::{Mutex, OnceLock}; +#[cfg(not(target_arch = "wasm32"))] fn last_dir_store() -> &'static Mutex> { static STORE: OnceLock>> = OnceLock::new(); STORE.get_or_init(|| { @@ -50,11 +53,13 @@ fn last_dir_store() -> &'static Mutex> { /// The directory the last file dialog picked or saved into, if it still /// exists — used to seed the next dialog so pickers reopen where the user /// left off. Persisted across runs. +#[cfg(not(target_arch = "wasm32"))] pub fn last_dialog_dir() -> Option { last_dir_store().lock().ok()?.clone().filter(|p| p.is_dir()) } /// Record the directory of a path a file dialog just returned. +#[cfg(not(target_arch = "wasm32"))] pub fn remember_dialog_dir(file_path: &Path) { let Some(dir) = file_path.parent().filter(|d| d.is_dir()) else { return; diff --git a/src/io/mod.rs b/src/io/mod.rs index de49e9be..635a29f6 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -242,6 +242,7 @@ pub async fn pick_and_load_web( /// a browser file picker (no filesystem path). Shares the post-load fixups with /// [`load_file`]; raster-image path resolution is skipped (there is no sibling /// directory to search on the web). +#[cfg(not(target_arch = "wasm32"))] pub fn load_bytes(name: &str, bytes: Vec) -> Result { use std::io::Cursor; let ext = name.rsplit('.').next().unwrap_or_default().to_lowercase(); @@ -284,6 +285,7 @@ fn sniff_dwg_or_dxf(path: &Path) -> String { } } +#[cfg(not(target_arch = "wasm32"))] pub fn load_file(path: &Path) -> Result { load_file_with_progress(path, None) } @@ -553,7 +555,7 @@ pub fn save_as_version( path: &Path, version: acadrust::DxfVersion, ) -> Result<(), String> { - let clone_started = std::time::Instant::now(); + let clone_started = iced::time::Instant::now(); let snapshot = doc.clone(); let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0; save_owned_as_version_inner(snapshot, path, version, false, clone_ms) @@ -581,12 +583,12 @@ fn save_owned_as_version_inner( clone_ms: f64, ) -> Result<(), String> { let perf = crate::perf::enabled(); - let total_started = std::time::Instant::now(); + let total_started = iced::time::Instant::now(); doc.version = version; - let styles_started = std::time::Instant::now(); + let styles_started = iced::time::Instant::now(); sync_current_styles_on_save(&mut doc); let styles_ms = styles_started.elapsed().as_secs_f64() * 1000.0; - let dimensions_started = std::time::Instant::now(); + let dimensions_started = iced::time::Instant::now(); crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc); let dimensions_ms = dimensions_started.elapsed().as_secs_f64() * 1000.0; let temp_path = save_temp_path(path); @@ -594,7 +596,7 @@ fn save_owned_as_version_inner( .extension() .map(|e| e.to_string_lossy().to_lowercase()) .unwrap_or_default(); - let write_started = std::time::Instant::now(); + let write_started = iced::time::Instant::now(); let result = match ext.as_str() { "dxf" => DxfWriter::new(&doc) .write_to_file(&temp_path) @@ -697,18 +699,18 @@ pub fn save_to_bytes( version: acadrust::DxfVersion, ) -> Result, String> { let perf = crate::perf::enabled(); - let total_started = std::time::Instant::now(); - let clone_started = std::time::Instant::now(); + let total_started = iced::time::Instant::now(); + let clone_started = iced::time::Instant::now(); let mut doc = doc.clone(); let clone_ms = clone_started.elapsed().as_secs_f64() * 1000.0; doc.version = version; - let styles_started = std::time::Instant::now(); + let styles_started = iced::time::Instant::now(); sync_current_styles_on_save(&mut doc); let styles_ms = styles_started.elapsed().as_secs_f64() * 1000.0; - let dimensions_started = std::time::Instant::now(); + let dimensions_started = iced::time::Instant::now(); crate::modules::draw::modify::explode::bake_dimension_blocks(&mut doc); let dimensions_ms = dimensions_started.elapsed().as_secs_f64() * 1000.0; - let write_started = std::time::Instant::now(); + let write_started = iced::time::Instant::now(); let result = match ext.to_lowercase().as_str() { "dxf" => DxfWriter::new(&doc).write_to_vec().map_err(|e| e.to_string()), _ => { diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 6aa9a0e8..a12ecd03 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -49,12 +49,14 @@ pub async fn pick_pdf_path_owned(_stem: String) -> Option { } /// mm to PDF points (1 mm = 2.834645 pt). +#[cfg(not(target_arch = "wasm32"))] const MM_TO_PT: f32 = 2.834645; /// `wire.line_weight_px` is the on-screen pixel weight: mm × (96/25.4) × 2.0, /// where the ×2 is a screen-legibility boost (see render.rs). Print wants the /// true physical weight, so undo both the 96-dpi scaling and the boost before /// converting to points — otherwise weights export ~2× too heavy in pixels /// (and the old `× 0.35278` left them inconsistent with the physical mm). +#[cfg(not(target_arch = "wasm32"))] const LW_PX_TO_PT: f32 = MM_TO_PT / ((96.0 / 25.4) * 2.0); // ── Public entry point ──────────────────────────────────────────────────── diff --git a/src/io/single_instance.rs b/src/io/single_instance.rs index d55ca9ba..feb6d25b 100644 --- a/src/io/single_instance.rs +++ b/src/io/single_instance.rs @@ -420,7 +420,7 @@ mod tests { std::thread::sleep(IO_TIMEOUT * 3); }); let s = TcpStream::connect((Ipv4Addr::LOCALHOST, port)).unwrap(); - let start = std::time::Instant::now(); + let start = iced::time::Instant::now(); assert!(!handoff(s, &[PathBuf::from("/tmp/a.dwg")])); assert!( start.elapsed() < IO_TIMEOUT * 2, diff --git a/src/io/thumbnail.rs b/src/io/thumbnail.rs index 1d911588..8b485053 100644 --- a/src/io/thumbnail.rs +++ b/src/io/thumbnail.rs @@ -388,6 +388,7 @@ pub fn extract_to_png(input: &std::path::Path, output: &std::path::Path, size: u /// Read a DWG's embedded preview and decode it to an iced image handle for the /// Start page's recent-file thumbnails. `None` for DXF/other files, a missing /// preview, or an undecodable format (WMF). +#[cfg(not(target_arch = "wasm32"))] pub fn read_handle(path: &std::path::Path) -> Option { let img = dwg_thumbnailer::extract(path, MAX_DIM)?; let (w, h) = (img.width(), img.height()); diff --git a/src/scene/convert/solid3d_tess.rs b/src/scene/convert/solid3d_tess.rs index b67fd6c6..64b9c0f2 100644 --- a/src/scene/convert/solid3d_tess.rs +++ b/src/scene/convert/solid3d_tess.rs @@ -39,6 +39,7 @@ use crate::scene::model::mesh_model::{MeshLodSet, MeshModel}; pub(crate) const EDGE_CHORD_FRAC: f64 = 0.002; /// Truck's own triangulation chord tolerance for the cone faces still routed /// through its kernel, as a fraction of the surface radius. +#[cfg(feature = "solid3d")] pub(crate) const TRUCK_CHORD_FRAC: f64 = 0.1; /// Boundary-loop sampling for parameter-range classification (which arc of a /// sphere/torus a face covers): a fine fraction so the classification is diff --git a/src/scene/mod.rs b/src/scene/mod.rs index e199c200..29de53fb 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -58,7 +58,6 @@ struct SceneDependencyIndex { text_styles: HashMap, dim_styles: HashMap, object_styles: HashMap, - blocks: HashMap>, } fn hatch_interaction_aabb(hatch: &model::hatch_model::HatchModel) -> Option<[f64; 4]> { @@ -270,6 +269,7 @@ impl std::fmt::Debug for PreparedOpenGeometry { } #[derive(Debug)] +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub(crate) struct WireGpuPatch { pub(crate) changes: Arc>, pub(crate) runs: Arc>>>, @@ -280,6 +280,7 @@ pub(crate) struct WireGpuPatch { } #[derive(Clone, Copy, Debug)] +#[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub(crate) struct WireIndexEdit { pub(crate) handle: Handle, pub(crate) start: usize, @@ -414,6 +415,7 @@ pub struct OpenTimings { /// Build hatch / image / mesh caches from a document without needing `&mut Scene`. /// Intended to run on a background thread during file load. +#[cfg(target_arch = "wasm32")] pub fn build_derived_caches(doc: &CadDocument) -> DerivedCaches { build_derived_caches_impl(doc, None) } @@ -6631,10 +6633,7 @@ impl Scene { } } - let mut index = SceneDependencyIndex { - blocks: roots.clone(), - ..SceneDependencyIndex::default() - }; + let mut index = SceneDependencyIndex::default(); for entity in self.document.entities() { let common = entity.common(); let owner = if common.owner_handle.is_null() { @@ -6792,18 +6791,6 @@ impl Scene { self.invalidate_dependency_targets(combined); } - pub fn block_dependency_handles(&self, name: &str) -> Vec { - if self.dependency_index_cache.borrow().is_none() { - *self.dependency_index_cache.borrow_mut() = Some(self.rebuild_dependency_index()); - } - self.dependency_index_cache - .borrow() - .as_ref() - .and_then(|index| index.blocks.get(&name.to_ascii_uppercase())) - .map(|handles| handles.iter().copied().collect()) - .unwrap_or_default() - } - pub(crate) fn invalidate_dependency_index(&self) { self.dependency_index_cache.borrow_mut().take(); } diff --git a/src/scene/pick/interaction_index.rs b/src/scene/pick/interaction_index.rs index 6edb0e00..99a220a8 100644 --- a/src/scene/pick/interaction_index.rs +++ b/src/scene/pick/interaction_index.rs @@ -750,7 +750,7 @@ impl InteractionIndex { #[cfg(not(target_arch = "wasm32"))] let perf = crate::perf::enabled(); #[cfg(not(target_arch = "wasm32"))] - let build_started = std::time::Instant::now(); + let build_started = iced::time::Instant::now(); let wire_handles: Vec> = wires .iter() .map(|wire| wire.name.parse::().ok()) @@ -770,7 +770,7 @@ impl InteractionIndex { #[cfg(not(target_arch = "wasm32"))] let handles_elapsed = build_started.elapsed(); #[cfg(not(target_arch = "wasm32"))] - let collect_started = std::time::Instant::now(); + let collect_started = iced::time::Instant::now(); let mut wire_entries = Vec::with_capacity(wires.len()); let mut unbounded_wires = Vec::new(); let mut max_line_half_width_px = 0.0f32; @@ -793,7 +793,7 @@ impl InteractionIndex { #[cfg(not(target_arch = "wasm32"))] let collect_elapsed = collect_started.elapsed(); #[cfg(not(target_arch = "wasm32"))] - let flatten_started = std::time::Instant::now(); + let flatten_started = iced::time::Instant::now(); let mut segment_parts = Vec::with_capacity(per_wire.len()); let mut snap_point_parts = Vec::with_capacity(per_wire.len()); let mut key_vertex_parts = Vec::with_capacity(per_wire.len()); @@ -868,7 +868,7 @@ impl InteractionIndex { #[cfg(not(target_arch = "wasm32"))] let flatten_elapsed = flatten_started.elapsed(); #[cfg(not(target_arch = "wasm32"))] - let spatial_started = std::time::Instant::now(); + let spatial_started = iced::time::Instant::now(); #[cfg(not(target_arch = "wasm32"))] let ( diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index a2b9e6ef..9e407ade 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -3219,22 +3219,40 @@ impl MultiPipeline { self.slot_clock = self.slot_clock.wrapping_add(1).max(1); let now = self.slot_clock; let reserved: rustc_hash::FxHashSet = instance_ids.iter().copied().collect(); + // `inner.slot_id` is updated later in `prepare`, after this whole + // method returns. Without a per-call claim set, two new viewports in + // the same primitive both see the freshly-grown slot as `MAX` and get + // assigned to it. Paper then blits the final occupant's resolve texture + // once as the full sheet and again as the floating viewport. + let mut claimed: rustc_hash::FxHashSet = + rustc_hash::FxHashSet::default(); let mut slots = Vec::with_capacity(instance_ids.len()); for &instance_id in instance_ids { - let slot = if let Some(&slot) = self.slot_by_instance.get(&instance_id) { + let existing = self + .slot_by_instance + .get(&instance_id) + .copied() + .filter(|slot| !claimed.contains(slot)); + let slot = if let Some(slot) = existing { slot } else { + self.slot_by_instance.remove(&instance_id); let vacant = self .inners .iter() - .position(|inner| inner.slot_id == u64::MAX); + .enumerate() + .find(|(slot, inner)| { + !claimed.contains(slot) && inner.slot_id == u64::MAX + }) + .map(|(slot, _)| slot); let recyclable = vacant.or_else(|| { (self.inners.len() >= SOFT_LIMIT) .then(|| { self.inners .iter() .enumerate() + .filter(|(slot, _)| !claimed.contains(slot)) .filter(|(_, inner)| !reserved.contains(&inner.slot_id)) .filter(|(slot, _)| { now.saturating_sub(self.slot_last_used[*slot]) > HOT_WINDOW @@ -3256,6 +3274,7 @@ impl MultiPipeline { self.slot_by_instance.insert(instance_id, slot); slot }; + claimed.insert(slot); self.slot_last_used[slot] = now; slots.push(slot); } diff --git a/src/scene/pipeline/wire_arena.rs b/src/scene/pipeline/wire_arena.rs index e3ab6912..f791ff92 100644 --- a/src/scene/pipeline/wire_arena.rs +++ b/src/scene/pipeline/wire_arena.rs @@ -310,7 +310,7 @@ impl WireArena { ) -> Option { let ranges = handle_ranges(wires)?; let perf = crate::perf::enabled(); - let total_started = std::time::Instant::now(); + let total_started = iced::time::Instant::now(); // Reject an oversized batch before parallel emission allocates hundreds // of megabytes. `points.len() - 1` is an upper bound because NaN-break @@ -363,7 +363,7 @@ impl WireArena { }) .collect(); - let pack_started = std::time::Instant::now(); + let pack_started = iced::time::Instant::now(); use crate::par::prelude::*; let packed: Vec = plans .par_iter() @@ -441,7 +441,7 @@ impl WireArena { let const_cap = ((const_tail as u64 * HEADROOM_NUM / HEADROOM_DEN) .max(MIN_CONST_CAP) .min(MAX_CONSTS)) as u32; - let upload_started = std::time::Instant::now(); + let upload_started = iced::time::Instant::now(); let inst_buf = alloc_inst_initialized(device, inst_cap as u64, &instances); let const_buf = alloc_const_initialized(device, const_cap as u64, &consts_cpu); let upload_ms = upload_started.elapsed().as_secs_f64() * 1000.0; diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index d571e880..a14ecc5d 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -224,9 +224,6 @@ impl PackedWireInstance { } } -#[cfg(target_arch = "wasm32")] -pub type WireInstance = PackedWireInstance; - /// Wire and hatch pipelines switch together: the fast path uses storage /// buffers; the compatibility path carries wire constants in packed vertex /// attributes and hatch data in a texture. diff --git a/src/scene/view/render.rs b/src/scene/view/render.rs index 35f19113..372b87ce 100644 --- a/src/scene/view/render.rs +++ b/src/scene/view/render.rs @@ -239,7 +239,7 @@ impl shader::Primitive for Primitive { bounds: &Rectangle, viewport: &Viewport, ) { - let nav_prepare_started = std::time::Instant::now(); + let nav_prepare_started = iced::time::Instant::now(); let phys = viewport.physical_size(); let full_size = Size::new(phys.width, phys.height); let scale = viewport.scale_factor() as f32; @@ -436,11 +436,14 @@ impl shader::Primitive for Primitive { // the whole wire buffer. Only for the scissor-free, mesh-free // (single-batch) Model set; scissored paper viewports and mixed // 2D/3D sets fall through to the shared batched path below. + #[cfg(not(target_arch = "wasm32"))] let mut arena_served = false; + #[cfg(target_arch = "wasm32")] + let arena_served = false; #[cfg(not(target_arch = "wasm32"))] let _perf = crate::perf::enabled(); #[cfg(not(target_arch = "wasm32"))] - let _t0 = std::time::Instant::now(); + let _t0 = iced::time::Instant::now(); #[cfg(not(target_arch = "wasm32"))] let mut _patched = false; #[cfg(not(target_arch = "wasm32"))] @@ -884,7 +887,7 @@ impl shader::Primitive for Primitive { target: &iced::wgpu::TextureView, clip: &Rectangle, ) { - let nav_render_started = std::time::Instant::now(); + let nav_render_started = iced::time::Instant::now(); let cw = clip.width as f32; let ch = clip.height as f32; let clip_right = clip.x + clip.width; @@ -1403,7 +1406,7 @@ impl Scene { _hover_region: Option, show_viewcube: bool, ) -> Primitive { - let nav_build_started = std::time::Instant::now(); + let nav_build_started = iced::time::Instant::now(); let perf_nav = self.take_nav_perf(); // Hover comes from the scene cell driven by the app-level // `CursorMoved` handler — the cube overlay sits above the shader @@ -1463,7 +1466,7 @@ impl Scene { }; let active = self.active_model_tile.get(); let is_active = tile_idx == active; - let nav_build_started = std::time::Instant::now(); + let nav_build_started = iced::time::Instant::now(); let perf_nav = if is_active { self.take_nav_perf() } else { @@ -1749,14 +1752,17 @@ impl Scene { // id so an unchanged wire set is not re-walked every frame. // The reuse fallback inside the gather is keyed per wire SOURCE — the // sheet, a Model tile, a content viewport and the implicit view carry - // different glyph sets even at the same geometry epoch (#403). Tiles + // different glyph sets even at the same geometry epoch (#403). Paper + // sheets must also include their layout block: switching layouts does + // not change the geometry epoch, and a role-only key reused the prior + // sheet's glyph coordinates while its wires moved correctly. Tiles // share the resident Model set, so they share one key; the implicit // view mixes in the current layout block (BEDIT swaps sets without a // geometry delta). let text_source_key: u64 = if inst.tile_idx.is_some() { 0x1000_0000_0000_0000 } else if inst.paper_sheet { - 0x2000_0000_0000_0000 + 0x2000_0000_0000_0000 | self.current_layout_block_handle().value() } else if inst.handle == acadrust::Handle::NULL { 0x4000_0000_0000_0000 | self.current_layout_block_handle().value() } else { diff --git a/web/ocs-parse-worker.js b/web/ocs-parse-worker.js index 772f5d15..ca98b1ec 100644 --- a/web/ocs-parse-worker.js +++ b/web/ocs-parse-worker.js @@ -3,9 +3,16 @@ import init, { parse_document } from "./worker_pkg/ocs_web_worker.js"; const ready = init(); self.onmessage = async ({ data }) => { + let stage = "initialize worker"; try { await ready; - const encoded = parse_document(data.name, new Uint8Array(data.bytes)); + const encoded = parse_document( + data.name, + new Uint8Array(data.bytes), + (next) => { + stage = next; + }, + ); // wasm-bindgen returns a view into WebAssembly.Memory. Copy to a standalone // ArrayBuffer before transferring it, otherwise the worker's wasm memory // itself would be detached. @@ -14,7 +21,9 @@ self.onmessage = async ({ data }) => { } catch (error) { self.postMessage({ ok: false, - error: error instanceof Error ? error.message : String(error), + error: `${stage}: ${ + error instanceof Error ? error.stack || error.message : String(error) + }`, }); } };