Skip to main content

ifc_lite_wasm/api/gpu_meshes/
prepass_sharded.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5//! Sharded pre-pass wasm APIs (split from `prepass.rs`): the per-worker
6//! entity-index shard scan, the per-worker styled-item slice resolver, and
7//! the canonical styles finalize that merges the shard results. The main
8//! sharded pre-pass entry (`buildPrePassStreamingSharded`) stays in
9//! `prepass.rs` beside its serial twin.
10
11use crate::api::IfcAPI;
12use js_sys::Function;
13use wasm_bindgen::prelude::*;
14
15/// Serialize the shared [`StreamMeta`] onto a JS object as the wire fields the
16/// host reads: `unitScale`, `planeAngleToRadians`, `rtcOffset` (`[x,y,z]`),
17/// `needsShift`, `buildingRotation` (`null` when absent). Used by both the
18/// `buildPrePassOnce` result object and the streaming `meta` events so all
19/// three emission points serialize identically.
20pub(super) fn set_stream_meta_props(
21    obj: &js_sys::Object,
22    meta: &ifc_lite_processing::stream_meta::StreamMeta,
23) {
24    crate::api::set_js_prop(obj, "unitScale", &meta.length_unit_scale.into());
25    crate::api::set_js_prop(obj, "planeAngleToRadians", &meta.plane_angle_to_radians.into());
26    let rtc_arr = js_sys::Float64Array::new_with_length(3);
27    rtc_arr.set_index(0, meta.rtc_offset.0);
28    rtc_arr.set_index(1, meta.rtc_offset.1);
29    rtc_arr.set_index(2, meta.rtc_offset.2);
30    crate::api::set_js_prop(obj, "rtcOffset", &rtc_arr);
31    crate::api::set_js_prop(obj, "needsShift", &meta.needs_shift.into());
32    match meta.building_rotation {
33        Some(rot) => crate::api::set_js_prop(obj, "buildingRotation", &rot.into()),
34        None => crate::api::set_js_prop(obj, "buildingRotation", &JsValue::NULL),
35    };
36}
37
38#[wasm_bindgen]
39impl IfcAPI {
40    /// Sharded pre-pass variant: same scan/discovery/jobs/columns pipeline as
41    /// `buildPrePassStreaming`, but
42    ///  1. the entity index is PREBUILT from the host's stitched shard columns
43    ///     (file order; see `scanEntityIndexShard`) — the scan skips its inline
44    ///     index build, the meta RTC ladder resolves against the FULL index
45    ///     (no partial-ladder full-rescan detour), and the post-scan
46    ///     `entity-index` event is skipped (the host already delivered it), and
47    ///  2. styles resolution is EXTERNAL: the styled-item spans are resolved as
48    ///     shard slices on the geometry workers (`resolveStyledItemsShard`);
49    ///     this call stashes the SUPPORT spans + plane-angle scale, and the
50    ///     follow-up `finalizePrepassStyles` merges + flattens into the exact
51    ///     styles payload the serial path emits. NO `styles` event is emitted
52    ///     here.
53    #[wasm_bindgen(js_name = buildPrePassStreamingSharded)]
54    #[allow(clippy::too_many_arguments)]
55    pub fn build_pre_pass_streaming_sharded(
56        &self,
57        data: &[u8],
58        on_event: &Function,
59        chunk_size: u32,
60        disabled_type_names: Option<Vec<String>>,
61        skip_type_geometry: bool,
62        index_ids: &[u32],
63        index_starts: &[u32],
64        index_lengths: &[u32],
65        index_classes: &[u8],
66    ) -> Result<JsValue, JsValue> {
67        let prebuilt = ifc_lite_core::ColumnarEntityIndex::from_columns(
68            index_ids,
69            index_starts,
70            index_lengths,
71        );
72        self.pre_pass_streaming_impl(
73            data,
74            on_event,
75            chunk_size,
76            disabled_type_names,
77            skip_type_geometry,
78            Some(prebuilt),
79            true,
80            Some((index_ids, index_starts, index_lengths, index_classes)),
81        )
82    }
83
84    /// SPIKE (sharded pre-pass): scan the entity index over a single byte range.
85    ///
86    /// Each idle browser geometry worker calls this on its `[range_start,
87    /// range_end)` shard; the main thread stitches the returned columns into the
88    /// full entity index (byte-identical to the single-threaded
89    /// `build_entity_index`) by binary-searching each shard for the previous
90    /// shard's `handoff`. Delegates to `ifc_lite_processing::scan_shard_classified`
91    /// — a separately-maintained loop over the same `EntityScanner` primitive as
92    /// `scan_shard` (the one the native `build_entity_index_parallel` fans across
93    /// cores), plus a per-record class column this sharded path also needs. The
94    /// two loops' records/handoff are kept in parity by a dedicated test
95    /// (`rust/processing/tests/issue_2053_shard_scan_parity.rs`), not by
96    /// delegation — edit one without the other and that test catches the drift.
97    ///
98    /// Byte offsets returned are GLOBAL (relative to file start), so shards
99    /// concatenate without rewriting. Returns a plain object:
100    ///   `{ ids: Uint32Array, starts: Uint32Array, lengths: Uint32Array,
101    ///      classes: Uint8Array, handoff: number }`
102    /// where `classes` is the parallel per-record prepass class byte
103    /// (`PREPASS_CLASS_*`: named code in the low bits plus the geometry-job /
104    /// type-candidate flags) the host filters on to rebuild pre-pass span
105    /// lists, and `handoff` is the global start of the first entity at/after
106    /// `range_end` (the next shard's first real entity), or `-1` at EOF.
107    #[wasm_bindgen(js_name = scanEntityIndexShard)]
108    pub fn scan_entity_index_shard(
109        &self,
110        data: &[u8],
111        range_start: u32,
112        range_end: u32,
113    ) -> JsValue {
114        let total = data.len();
115        let start = (range_start as usize).min(total);
116        let end = (range_end as usize).min(total);
117        let (records, classes, handoff) =
118            ifc_lite_processing::scan_shard_classified(data, start, end);
119
120        let n = records.len() as u32;
121        let ids = js_sys::Uint32Array::new_with_length(n);
122        let starts = js_sys::Uint32Array::new_with_length(n);
123        let lengths = js_sys::Uint32Array::new_with_length(n);
124        for (i, &(id, s, e)) in records.iter().enumerate() {
125            let i = i as u32;
126            ids.set_index(i, id);
127            starts.set_index(i, s as u32);
128            lengths.set_index(i, (e - s) as u32);
129        }
130
131        let result = js_sys::Object::new();
132        crate::api::set_js_prop(&result, "ids", &ids);
133        crate::api::set_js_prop(&result, "starts", &starts);
134        crate::api::set_js_prop(&result, "lengths", &lengths);
135        // Per-record prepass class (PREPASS_CLASS_*): styled items plus the
136        // colour-map/material/void/fills/aggregate support classes are tagged,
137        // letting the host extract every span list resolveStyledItemsShard +
138        // finalizePrepassStyles need from the stitched columns without waiting
139        // for the serial pre-pass scan.
140        crate::api::set_js_prop(&result, "classes", &js_sys::Uint8Array::from(classes.as_slice()));
141        let handoff_val: f64 = match handoff {
142            Some(h) => h as f64,
143            None => -1.0,
144        };
145        crate::api::set_js_prop(&result, "handoff", &handoff_val.into());
146        result.into()
147    }
148
149    /// Sharded pre-pass: resolve ONE contiguous (file-ordered) slice of the
150    /// styled-item span list on this worker, against the entity index installed
151    /// by `setEntityIndex`. Returns raw resolved maps as flat columns:
152    /// `{ orphanIds, orphanColors (f32 rgba per id), geomIds, geomColors }`.
153    /// The host merges shard results IN SHARD ORDER with first-wins per
154    /// geometry id, reproducing the serial resolver's file-order precedence,
155    /// then hands the merged columns to `finalizePrepassStyles`.
156    /// `spans` is `[id, start, len]` triples.
157    #[wasm_bindgen(js_name = resolveStyledItemsShard)]
158    pub fn resolve_styled_items_shard(&self, data: &[u8], spans: &[u32]) -> Result<JsValue, JsValue> {
159        use ifc_lite_core::EntityDecoder;
160        let index = {
161            let slot = self
162                .cached_entity_index
163                .lock()
164                .unwrap_or_else(std::sync::PoisonError::into_inner);
165            slot.clone()
166        };
167        let Some(index) = index else {
168            return Err(JsValue::from_str(
169                "resolveStyledItemsShard: no entity index installed (setEntityIndex must run first)",
170            ));
171        };
172        let mut decoder = EntityDecoder::with_arc_columnar_index(data, index);
173        let styled: Vec<(u32, usize, usize)> = spans
174            .chunks_exact(3)
175            .map(|c| (c[0], c[1] as usize, c[1] as usize + c[2] as usize))
176            .collect();
177        let mut orphan = rustc_hash::FxHashMap::default();
178        let mut geom = rustc_hash::FxHashMap::default();
179        let mut deferred = Vec::new();
180        ifc_lite_processing::prepass::resolve_styled_items_into(
181            &styled,
182            &mut decoder,
183            false,
184            &mut orphan,
185            &mut geom,
186            &mut deferred,
187        );
188
189        let orphan_ids = js_sys::Uint32Array::new_with_length(orphan.len() as u32);
190        let orphan_colors = js_sys::Float32Array::new_with_length((orphan.len() * 4) as u32);
191        for (i, (&id, color)) in orphan.iter().enumerate() {
192            orphan_ids.set_index(i as u32, id);
193            for (j, &c) in color.iter().enumerate() {
194                orphan_colors.set_index((i * 4 + j) as u32, c);
195            }
196        }
197        let geom_ids = js_sys::Uint32Array::new_with_length(geom.len() as u32);
198        let geom_colors = js_sys::Float32Array::new_with_length((geom.len() * 4) as u32);
199        for (i, (&id, info)) in geom.iter().enumerate() {
200            geom_ids.set_index(i as u32, id);
201            for (j, &c) in info.color.iter().enumerate() {
202                geom_colors.set_index((i * 4 + j) as u32, c);
203            }
204        }
205        let result = js_sys::Object::new();
206        crate::api::set_js_prop(&result, "orphanIds", &orphan_ids);
207        crate::api::set_js_prop(&result, "orphanColors", &orphan_colors);
208        crate::api::set_js_prop(&result, "geomIds", &geom_ids);
209        crate::api::set_js_prop(&result, "geomColors", &geom_colors);
210        Ok(result.into())
211    }
212
213    /// Sharded pre-pass: merge the shard-resolved styled-item columns with the
214    /// SUPPORT spans (extracted host-side from the shard classes) and run the
215    /// CANONICAL styles flatten. Returns the exact `styles` event payload the
216    /// serial path emits. Runs on any worker with `setEntityIndex` installed.
217    /// Span arguments are `[id, start, len]` triples; `plane_angle_to_radians`
218    /// comes from the meta event.
219    #[wasm_bindgen(js_name = finalizePrepassStyles)]
220    #[allow(clippy::too_many_arguments)]
221    pub fn finalize_prepass_styles(
222        &self,
223        data: &[u8],
224        orphan_ids: &[u32],
225        orphan_colors: &[f32],
226        geom_ids: &[u32],
227        geom_colors: &[f32],
228        colour_map_spans: &[u32],
229        material_def_spans: &[u32],
230        rel_material_spans: &[u32],
231        void_spans: &[u32],
232        fills_spans: &[u32],
233        aggregate_spans: &[u32],
234        plane_angle_to_radians: f64,
235    ) -> Result<JsValue, JsValue> {
236        use ifc_lite_core::EntityDecoder;
237        fn triples(v: &[u32]) -> Vec<(u32, usize, usize)> {
238            v.chunks_exact(3)
239                .map(|c| (c[0], c[1] as usize, c[1] as usize + c[2] as usize))
240                .collect()
241        }
242        let stashed_support = ifc_lite_processing::prepass::PrepassSpans {
243            styled_items: Vec::new(),
244            indexed_colour_maps: triples(colour_map_spans),
245            material_def_reprs: triples(material_def_spans),
246            rel_associates_material: triples(rel_material_spans),
247            void_rels: triples(void_spans),
248            fills_rels: triples(fills_spans),
249            aggregate_rels: triples(aggregate_spans),
250        };
251        let index = {
252            let slot = self
253                .cached_entity_index
254                .lock()
255                .unwrap_or_else(std::sync::PoisonError::into_inner);
256            slot.clone()
257        };
258        let Some(index) = index else {
259            return Err(JsValue::from_str("finalizePrepassStyles: no entity index"));
260        };
261        let mut decoder = EntityDecoder::with_arc_columnar_index(data, index);
262        decoder.seed_unit_scales(1.0, plane_angle_to_radians);
263
264        // Rebuild the shard-merged styled maps and SEED them into the resolver
265        // BEFORE the support-span loops: the material chain consults
266        // orphan_styled_items, so injecting after resolve_prepass loses
267        // material-dependent styles.
268        let mut orphan_seed = rustc_hash::FxHashMap::default();
269        for (i, &id) in orphan_ids.iter().enumerate() {
270            orphan_seed.insert(id, [
271                orphan_colors[i * 4],
272                orphan_colors[i * 4 + 1],
273                orphan_colors[i * 4 + 2],
274                orphan_colors[i * 4 + 3],
275            ]);
276        }
277        // Geometry styles stay as COLUMNS (stage 2): the support resolution
278        // only consults the ORPHAN map (material chain), and the column-based
279        // flatten below never builds the 4M-entry geometry hashmap.
280        let resolved = ifc_lite_processing::prepass::resolve_prepass_with_style_seeds(
281            &stashed_support,
282            &mut decoder,
283            ifc_lite_processing::prepass::ResolveOptions {
284                collect_indexed_colour_full: false,
285                defer_attached_styles: false,
286            },
287            Some((orphan_seed, rustc_hash::FxHashMap::default())),
288        );
289        let flat = ifc_lite_processing::flat_styles_rgba8_from_geometry_columns(
290            geom_ids,
291            geom_colors,
292            &resolved,
293            &mut decoder,
294        );
295
296        Ok(styles_payload_with_flat(flat, &resolved).into())
297    }
298}
299
300/// Serialize a [`ResolvedPrepass`] into the flat `styles` wire payload
301/// (styleIds/styleColors/voidKeys/voidCounts/voidValues/materialElementIds/
302/// materialColorCounts/materialColors). Shared by the serial pre-pass's
303/// styles event and the sharded finalize so the two cannot drift.
304pub(super) fn styles_payload(
305    resolved: &ifc_lite_processing::prepass::ResolvedPrepass,
306    decoder: &mut ifc_lite_core::EntityDecoder,
307) -> js_sys::Object {
308    let flat = ifc_lite_processing::prepass::flat_styles_rgba8(resolved, decoder);
309    styles_payload_with_flat(flat, resolved)
310}
311
312/// [`styles_payload`] with the style columns precomputed (the sharded
313/// finalize computes them via the column-based flatten).
314pub(super) fn styles_payload_with_flat(
315    (style_ids_vec, style_colors_vec): (Vec<u32>, Vec<u8>),
316    resolved: &ifc_lite_processing::prepass::ResolvedPrepass,
317) -> js_sys::Object {
318    let (void_keys_vec, void_counts_vec, void_values_vec) =
319        ifc_lite_processing::prepass::flat_voids(&resolved.void_index);
320    let (mat_ids_vec, mat_counts_vec, mat_colors_vec) =
321        ifc_lite_processing::prepass::flat_material_colors(&resolved.element_material_colors);
322    let result = js_sys::Object::new();
323    crate::api::set_js_prop(&result, "styleIds", &js_sys::Uint32Array::from(style_ids_vec.as_slice()));
324    crate::api::set_js_prop(&result, "styleColors", &js_sys::Uint8Array::from(style_colors_vec.as_slice()));
325    crate::api::set_js_prop(&result, "voidKeys", &js_sys::Uint32Array::from(void_keys_vec.as_slice()));
326    crate::api::set_js_prop(&result, "voidCounts", &js_sys::Uint32Array::from(void_counts_vec.as_slice()));
327    crate::api::set_js_prop(&result, "voidValues", &js_sys::Uint32Array::from(void_values_vec.as_slice()));
328    crate::api::set_js_prop(&result, "materialElementIds", &js_sys::Uint32Array::from(mat_ids_vec.as_slice()));
329    crate::api::set_js_prop(&result, "materialColorCounts", &js_sys::Uint32Array::from(mat_counts_vec.as_slice()));
330    crate::api::set_js_prop(&result, "materialColors", &js_sys::Uint8Array::from(mat_colors_vec.as_slice()));
331    result
332}