ifc_lite_server/services/
parquet_shape_plan.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//! The SHAPE PLAN: which mesh row of the flat transport draws which block of
6//! vertex/index rows.
7//!
8//! Split out of `parquet_mesh_tables.rs`, which packs the columns a plan
9//! describes. One planner serves both routes that share shapes. The buffered
10//! `POST /parse/parquet` plans a whole model against nothing (#3888, #5130).
11//! The streaming route plans one batch at a time against the shapes EARLIER
12//! batches already emitted (#5407), which is the only difference: the prior
13//! registry is an argument, never a second implementation of the grouping.
14
15use crate::services::axis::zup_to_yup_f64;
16use crate::services::parquet_instancing::{
17    collate_rotation_aware_placements, mesh_geometry_key, rotation_zup_to_yup, MeshGeometryKey,
18    IDENTITY_ROTATION,
19};
20use crate::types::MeshData;
21use rustc_hash::{FxHashMap, FxHashSet};
22
23/// Where an already-emitted shape's block lives in the WHOLE-STREAM vertex and
24/// index tables. `index_start` / `index_count` count indices, not triangles,
25/// matching the mesh table's columns.
26#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub(super) struct EmittedShape {
28    pub vertex_start: u32,
29    pub vertex_count: u32,
30    pub index_start: u32,
31    pub index_count: u32,
32}
33
34/// What earlier stream batches leave behind for later ones (#5407).
35#[derive(Default)]
36pub(super) struct PriorShapes {
37    /// Every shape emitted so far, by content hash: where it landed. No
38    /// geometry, so it costs the same whatever the shape's size.
39    pub(super) by_hash: FxHashMap<MeshGeometryKey, EmittedShape>,
40    /// Per instanced representation (`InstanceMeta::rep_identity`), the
41    /// collator template emitted for it and where. The ONE piece of geometry
42    /// carried across a batch boundary, because stage 1 verifies every
43    /// occurrence against its template's vertices; a rotated repeat is not
44    /// bit-identical to anything, so the hash alone cannot share it. What gets
45    /// retained, and the budget capping it, is the caller's decision.
46    pub(super) templates: FxHashMap<u128, RetainedTemplate>,
47}
48
49/// An earlier batch's collator template, kept so a later batch's occurrences
50/// of the same representation can be verified against it and drawn from its
51/// block rather than from a fresh copy.
52pub(super) struct RetainedTemplate {
53    pub(super) mesh: MeshData,
54    pub(super) at: EmittedShape,
55}
56
57/// One shape a stream batch emits that no earlier batch did.
58pub(super) struct NewShape {
59    pub(super) key: MeshGeometryKey,
60    /// `Some(rep_identity)` when the shape is an instanceable mesh, i.e. a
61    /// candidate template for [`PriorShapes::templates`]. Not only this
62    /// batch's collator templates: a representation that occurs ONCE per
63    /// batch is never collated inside any one of them, and the buffered
64    /// collator's template is just a group's first member, so the first one
65    /// the stream emits is as valid a template as any.
66    pub(super) template_of: Option<u128>,
67}
68
69/// The block of vertex/index rows one mesh row points at.
70#[derive(Clone, Copy)]
71pub(super) enum ShapeRef {
72    /// Index into [`ShapePlan::Shared::shapes`]: a block THIS table set emits.
73    Slot(usize),
74    /// A block an earlier stream batch emitted; the row carries its
75    /// whole-stream range as-is.
76    Emitted(EmittedShape),
77}
78
79/// One mesh row's placement against the shape blocks.
80pub(super) struct PlannedRow {
81    shape: ShapeRef,
82    /// Y-up metres. `world = origin + R * p`.
83    origin_yup: [f64; 3],
84    /// Row-major 3x3, Y-up.
85    rotation: [f32; 9],
86}
87
88/// Which shapes get their vertices written, and how each mesh row reaches one.
89///
90/// `Identity` carries NO data on purpose: its plan is a function of the mesh
91/// index alone, so materializing it would allocate ~80 bytes per mesh — on
92/// every streamed batch of a writer that shares nothing — to store `i` and
93/// nine repeated constants.
94pub(super) enum ShapePlan {
95    /// The `-parquet-v8` layout: every mesh writes its own geometry. What the
96    /// streaming route uses unless the client opted in to cross-batch sharing.
97    Identity,
98    /// The `-parquet-v9` layout.
99    Shared {
100        /// Mesh indices whose vertex/index data is emitted, in emission order.
101        shapes: Vec<usize>,
102        /// One entry per input mesh, parallel to the mesh slice.
103        rows: Vec<PlannedRow>,
104    },
105}
106
107impl ShapePlan {
108    /// Plan a whole model (the buffered route) in the SAME two-stage order
109    /// `/optimized` uses (issue #5130): the rotation-aware collator
110    /// ([`collate_rotation_aware_placements`]) runs first, and every
111    /// occurrence it did not place falls through to a content hash of its
112    /// (origin-relative) vertex and index buffers ([`mesh_geometry_key`]), so
113    /// bit-identical occurrences the collator has no group for still collapse
114    /// onto one shape.
115    ///
116    /// Stage 1 reuses the collator verbatim — the same grouping, the same
117    /// per-vertex residual check, the same all-or-nothing per group — so the
118    /// flat route can never share a shape the `/optimized` route would have
119    /// refused to share. Stage 2 mirrors `serialize_to_parquet_optimized`'s
120    /// `None` branch: the hash's first occurrence (by mesh index, not by slot)
121    /// becomes the shared shape every later bit-identical occurrence points at.
122    ///
123    /// Returns [`ShapePlan::Identity`] only when NEITHER stage shared anything
124    /// — a hash-only match must still return `Shared`, not just a collator
125    /// match — so a model with no repeats at all emits the v5 layout through
126    /// the same path it always did rather than through a `Shared` plan that
127    /// happens to be one-to-one.
128    pub(super) fn shared_shapes(
129        meshes: &[MeshData],
130        baked_basis: Option<&ifc_lite_geometry::Matrix4<f64>>,
131    ) -> Self {
132        plan(meshes, baked_basis, None).0
133    }
134
135    /// Plan one stream batch against the shapes earlier batches emitted
136    /// (#5407). Everything [`Self::shared_shapes`] shares inside the batch is
137    /// still shared; on top of that, a shape whose content hash matches one
138    /// in `prior` is not written again — the row carries the earlier block's
139    /// whole-stream range instead.
140    ///
141    /// Stage 1 reaches across batches the same way: every retained template
142    /// whose representation occurs in this batch is put in FRONT of it for
143    /// the collator, which takes a group's first member as its template, so
144    /// this batch's occurrences are verified against the very geometry an
145    /// earlier batch emitted and placed on its block.
146    ///
147    /// Also returns every shape this batch DOES emit, in emission order, with
148    /// its content hash, so the caller can record where each one lands without
149    /// hashing its geometry a second time.
150    ///
151    /// `baked_basis` is the stream's frame, which the pipeline publishes
152    /// before its first batch; `None` costs a site-rotated model its rotated
153    /// sharing (#4118), never correctness.
154    pub(super) fn shared_shapes_after(
155        meshes: &[MeshData],
156        baked_basis: Option<&ifc_lite_geometry::Matrix4<f64>>,
157        prior: &PriorShapes,
158    ) -> (Self, Vec<NewShape>) {
159        plan(meshes, baked_basis, Some(prior))
160    }
161
162    /// The meshes whose geometry is emitted, in emission order.
163    pub(super) fn shape_meshes<'a>(&self, meshes: &'a [MeshData]) -> Vec<&'a MeshData> {
164        match self {
165            Self::Identity => meshes.iter().collect(),
166            Self::Shared { shapes, .. } => shapes.iter().map(|&i| &meshes[i]).collect(),
167        }
168    }
169
170    /// Mesh row `i`'s shape and the placement mapping that shape's geometry
171    /// onto this occurrence. Computed, not stored, under `Identity`.
172    pub(super) fn row(&self, meshes: &[MeshData], i: usize) -> (ShapeRef, [f64; 3], [f32; 9]) {
173        match self {
174            Self::Identity => (
175                ShapeRef::Slot(i),
176                zup_to_yup_f64(meshes[i].origin),
177                IDENTITY_ROTATION,
178            ),
179            Self::Shared { rows, .. } => {
180                let row = &rows[i];
181                (row.shape, row.origin_yup, row.rotation)
182            }
183        }
184    }
185
186    /// The mesh count this plan was built for; `None` fits any slice.
187    pub(super) fn row_count(&self) -> Option<usize> {
188        match self {
189            Self::Identity => None,
190            Self::Shared { rows, .. } => Some(rows.len()),
191        }
192    }
193
194    /// How many mesh geometries this plan emits.
195    #[cfg(test)]
196    pub(super) fn shape_count(&self, meshes: &[MeshData]) -> usize {
197        match self {
198            Self::Identity => meshes.len(),
199            Self::Shared { shapes, .. } => shapes.len(),
200        }
201    }
202}
203
204/// The one planner behind both entry points above.
205///
206/// With `prior == None` nothing is carried, looked up or additionally hashed,
207/// so the buffered route's plan is exactly what it was before #5407. With
208/// `Some`, the retained templates go in front of the batch (see
209/// [`ShapePlan::shared_shapes_after`]), and every shape the batch would emit
210/// is first looked up by content hash: a collator template as well as a
211/// stage-2 first occurrence, since a group whose template is bit-identical to
212/// an earlier batch's shape can point at that block just as a lone repeat can.
213fn plan(
214    meshes: &[MeshData],
215    baked_basis: Option<&ifc_lite_geometry::Matrix4<f64>>,
216    prior: Option<&PriorShapes>,
217) -> (ShapePlan, Vec<NewShape>) {
218    let carried = prior.map_or_else(Vec::new, |prior| carried_templates(meshes, prior));
219    let view: Vec<&MeshData> = carried.iter().map(|t| &t.mesh).chain(meshes).collect();
220    let placements = collate_rotation_aware_placements(&view, baked_basis);
221    let mut shapes: Vec<usize> = Vec::with_capacity(meshes.len());
222    let mut new_shapes: Vec<NewShape> = Vec::new();
223    let mut shape_of: FxHashMap<usize, ShapeRef> = FxHashMap::default();
224    // Content-hash fallback (stage 2): first occurrence of a hash wins the
225    // slot for every later bit-identical occurrence the collator left out.
226    let mut hash_slot: FxHashMap<MeshGeometryKey, usize> = FxHashMap::default();
227    let mut rows: Vec<PlannedRow> = Vec::with_capacity(meshes.len());
228    for (i, mesh) in meshes.iter().enumerate() {
229        // The template is itself an occurrence of its own group, so it takes
230        // the shared branch too; keying `shape_of` by mesh index in BOTH
231        // branches means a template can never also be emitted a second time as
232        // its own unshared shape.
233        let (shape_mesh, known_key, origin_yup, rotation) = match placements.get(&(i + carried.len())) {
234            Some(placement) => {
235                let origin_yup = zup_to_yup_f64(placement.origin_zup);
236                let rotation = rotation_zup_to_yup(&placement.rotation_zup);
237                let Some(template) = placement.template_mesh_index.checked_sub(carried.len()) else {
238                    // Verified against a template an earlier batch emitted.
239                    let at = carried[placement.template_mesh_index].at;
240                    rows.push(PlannedRow { shape: ShapeRef::Emitted(at), origin_yup, rotation });
241                    continue;
242                };
243                (template, None, origin_yup, rotation)
244            }
245            None => {
246                let key = mesh_geometry_key(mesh);
247                let first = *hash_slot.entry(key).or_insert(i);
248                (first, Some(key), zup_to_yup_f64(mesh.origin), IDENTITY_ROTATION)
249            }
250        };
251        let shape = *shape_of.entry(shape_mesh).or_insert_with(|| {
252            let Some(prior) = prior else {
253                shapes.push(shape_mesh);
254                return ShapeRef::Slot(shapes.len() - 1);
255            };
256            let key = known_key.unwrap_or_else(|| mesh_geometry_key(&meshes[shape_mesh]));
257            if let Some(&emitted) = prior.by_hash.get(&key) {
258                return ShapeRef::Emitted(emitted);
259            }
260            let template_of = meshes[shape_mesh]
261                .instance
262                .as_ref()
263                .filter(|im| im.instanceable)
264                .map(|im| im.rep_identity);
265            shapes.push(shape_mesh);
266            new_shapes.push(NewShape { key, template_of });
267            ShapeRef::Slot(shapes.len() - 1)
268        });
269        rows.push(PlannedRow {
270            shape,
271            origin_yup,
272            rotation,
273        });
274    }
275    if shapes.len() == meshes.len() {
276        // Nothing shared, within the slice or with an earlier batch: every row
277        // emits its own shape, in mesh order.
278        return (ShapePlan::Identity, new_shapes);
279    }
280    (ShapePlan::Shared { shapes, rows }, new_shapes)
281}
282
283/// The retained templates of the instanced representations `meshes` contains,
284/// each once, in first-occurrence order (the collator's group order is
285/// first-seen too, so this keeps the plan deterministic).
286fn carried_templates<'p>(meshes: &[MeshData], prior: &'p PriorShapes) -> Vec<&'p RetainedTemplate> {
287    if prior.templates.is_empty() {
288        return Vec::new();
289    }
290    let mut seen: FxHashSet<u128> = FxHashSet::default();
291    meshes
292        .iter()
293        .filter_map(|m| m.instance.as_ref())
294        .filter(|im| im.instanceable && seen.insert(im.rep_identity))
295        .filter_map(|im| prior.templates.get(&im.rep_identity))
296        .collect()
297}