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}