1use glam::IVec3;
25use roxlap_formats::vxl::{self, ParseError, Vxl};
26use serde::{Deserialize, Serialize};
27
28use crate::{Grid, GridId, GridTransform, Scene};
29
30fn compact_serialize_chunk(vxl: &Vxl) -> Vec<u8> {
46 let n_cols = (vxl.vsid as usize) * (vxl.vsid as usize);
47 let mut data: Vec<u8> = Vec::new();
48 let mut column_offset: Vec<u32> = Vec::with_capacity(n_cols + 1);
49 for i in 0..n_cols {
50 column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
51 data.extend_from_slice(vxl.column_data(i));
52 }
53 column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
54
55 let compact = Vxl {
56 vsid: vxl.vsid,
57 ipo: vxl.ipo,
58 ist: vxl.ist,
59 ihe: vxl.ihe,
60 ifo: vxl.ifo,
61 data: data.into_boxed_slice(),
62 column_offset: column_offset.into_boxed_slice(),
63 mip_base_offsets: Box::new([0, n_cols + 1]),
64 vbit: Box::new([]),
65 vbiti: 0,
66 };
67 vxl::serialize(&compact)
68}
69
70const RESTORE_EDIT_HEADROOM_PER_COLUMN: usize = 256;
75
76#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct SceneSnapshot {
85 pub next_grid_id: u32,
88 pub grids: Vec<(GridId, GridSnapshot)>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct GridSnapshot {
95 pub transform: GridTransform,
96 pub chunks: Vec<(IVec3, Vec<u8>)>,
100 #[serde(default)]
106 pub chunk_versions: Vec<(IVec3, u64)>,
107}
108
109#[derive(Debug)]
113pub enum FromSnapshotError {
114 ChunkParse {
117 grid: GridId,
118 chunk: IVec3,
119 source: ParseError,
120 },
121}
122
123impl std::fmt::Display for FromSnapshotError {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 match self {
126 Self::ChunkParse {
127 grid,
128 chunk,
129 source,
130 } => {
131 write!(
132 f,
133 "scene snapshot: grid {} chunk {chunk:?} parse failed: {source:?}",
134 grid.raw()
135 )
136 }
137 }
138 }
139}
140
141impl std::error::Error for FromSnapshotError {}
142
143impl Scene {
144 #[must_use]
154 pub fn to_snapshot(&self) -> SceneSnapshot {
155 let mut grid_ids: Vec<GridId> = self.grids.keys().copied().collect();
156 grid_ids.sort_unstable();
157
158 let mut grids = Vec::with_capacity(grid_ids.len());
159 for id in grid_ids {
160 let grid = &self.grids[&id];
161 let mut chunk_addrs: Vec<IVec3> = grid.chunks.keys().copied().collect();
162 chunk_addrs.sort_unstable_by_key(|a| (a.x, a.y, a.z));
163 let chunks = chunk_addrs
164 .into_iter()
165 .map(|addr| (addr, compact_serialize_chunk(&grid.chunks[&addr])))
166 .collect();
167 let mut version_addrs: Vec<IVec3> = grid
173 .chunk_versions
174 .iter()
175 .filter_map(|(a, v)| if *v != 0 { Some(*a) } else { None })
176 .collect();
177 version_addrs.sort_unstable_by_key(|a| (a.x, a.y, a.z));
178 let chunk_versions = version_addrs
179 .into_iter()
180 .map(|addr| (addr, grid.chunk_versions[&addr]))
181 .collect();
182 grids.push((
183 id,
184 GridSnapshot {
185 transform: grid.transform,
186 chunks,
187 chunk_versions,
188 },
189 ));
190 }
191 SceneSnapshot {
192 next_grid_id: self.next_grid_id,
193 grids,
194 }
195 }
196
197 pub fn from_snapshot(snap: &SceneSnapshot) -> Result<Self, FromSnapshotError> {
208 let mut scene = Self::new();
209 scene.next_grid_id = snap.next_grid_id;
210 for (id, gsnap) in &snap.grids {
211 let mut grid = Grid::new(gsnap.transform);
212 for (addr, bytes) in &gsnap.chunks {
213 let mut vxl =
214 vxl::parse(bytes).map_err(|source| FromSnapshotError::ChunkParse {
215 grid: *id,
216 chunk: *addr,
217 source,
218 })?;
219 let n_cols = (vxl.vsid as usize) * (vxl.vsid as usize);
220 vxl.reserve_edit_capacity(n_cols * RESTORE_EDIT_HEADROOM_PER_COLUMN);
221 grid.chunks.insert(*addr, vxl);
222 }
223 for (addr, ver) in &gsnap.chunk_versions {
227 grid.chunk_versions.insert(*addr, *ver);
228 }
229 scene.grids.insert(*id, grid);
230 }
231 Ok(scene)
232 }
233}
234
235#[cfg(test)]
236#[allow(clippy::cast_possible_wrap, clippy::type_complexity)]
237mod tests {
238 use super::*;
239 use crate::chunks::tests::voxel_is_solid;
240 use crate::CHUNK_SIZE_XY;
241 use glam::DVec3;
242
243 impl GridId {
244 pub(crate) fn from_raw_for_test(raw: u32) -> Self {
245 Self(raw)
246 }
247 }
248
249 fn build_two_grid_scene() -> (Scene, Vec<(GridId, IVec3, u32, u32, u32, u32)>) {
255 let mut scene = Scene::new();
259 let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 0.0, 0.0)));
260 let g1 = scene.add_grid(GridTransform::at(DVec3::new(1000.0, 0.0, 0.0)));
261 let mut expected = Vec::new();
262 for chz in 0..2 {
266 for chy in 0..5 {
267 for chx in 0..5 {
268 let chunk_idx = IVec3::new(chx, chy, chz);
269 #[allow(clippy::cast_sign_loss)]
270 let color =
271 0x80_00_00_00 | ((chx as u32) << 16) | ((chy as u32) << 8) | (chz as u32);
272 let global_voxel = chunk_idx
273 * IVec3::new(
274 CHUNK_SIZE_XY as i32,
275 CHUNK_SIZE_XY as i32,
276 crate::CHUNK_SIZE_Z as i32,
277 )
278 + IVec3::new(5, 6, 7);
279 scene
280 .grid_mut(g0)
281 .unwrap()
282 .set_voxel(global_voxel, Some(color));
283 expected.push((g0, chunk_idx, 5, 6, 7, color));
284 }
285 }
286 }
287 for chz in 0..2 {
290 for chy in 0..5 {
291 for chx in 0..5 {
292 let chunk_idx = IVec3::new(chx, chy, chz);
293 #[allow(clippy::cast_sign_loss)]
294 let color =
295 0x80_ff_00_00 | ((chx as u32) << 16) | ((chy as u32) << 8) | (chz as u32);
296 let global_voxel = chunk_idx
297 * IVec3::new(
298 CHUNK_SIZE_XY as i32,
299 CHUNK_SIZE_XY as i32,
300 crate::CHUNK_SIZE_Z as i32,
301 )
302 + IVec3::new(10, 11, 12);
303 scene
304 .grid_mut(g1)
305 .unwrap()
306 .set_voxel(global_voxel, Some(color));
307 expected.push((g1, chunk_idx, 10, 11, 12, color));
308 }
309 }
310 }
311 (scene, expected)
312 }
313
314 fn assert_voxels_match(scene: &Scene, expected: &[(GridId, IVec3, u32, u32, u32, u32)]) {
315 for &(grid_id, chunk_idx, vx, vy, vz, _color) in expected {
316 let grid = scene.grid(grid_id).expect("grid present");
317 let chunk = grid.chunk(chunk_idx).expect("chunk present");
318 assert!(
319 voxel_is_solid(chunk, vx, vy, vz),
320 "voxel ({vx},{vy},{vz}) in grid={} chunk={chunk_idx:?} not solid post-restore",
321 grid_id.raw()
322 );
323 }
324 }
325
326 #[test]
327 fn snapshot_round_trip_preserves_two_grid_100_chunk_scene() {
328 let (scene, expected) = build_two_grid_scene();
329 assert_eq!(scene.grid_count(), 2);
330 let total_chunks: usize = scene.grids().map(|(_, g)| g.chunks.len()).sum();
331 assert_eq!(total_chunks, 100, "test setup should produce 100 chunks");
332
333 let snap = scene.to_snapshot();
335 let bytes = bincode::serialize(&snap).expect("bincode serialize");
336 let snap_back: SceneSnapshot = bincode::deserialize(&bytes).expect("bincode deserialize");
337 let restored = Scene::from_snapshot(&snap_back).expect("restore");
338
339 assert_eq!(restored.grid_count(), 2);
341 let total_restored: usize = restored.grids().map(|(_, g)| g.chunks.len()).sum();
342 assert_eq!(total_restored, 100);
343
344 assert_voxels_match(&restored, &expected);
346 }
347
348 #[test]
349 fn snapshot_preserves_next_grid_id_and_transforms() {
350 let mut scene = Scene::new();
351 let g0 = scene.add_grid(GridTransform::at(DVec3::new(10.0, 20.0, 30.0)));
352 let _g1 = scene.add_grid(GridTransform::at(DVec3::new(40.0, 50.0, 60.0)));
353 scene.remove_grid(g0); let _g2 = scene.add_grid(GridTransform::at(DVec3::new(70.0, 80.0, 90.0)));
355 let snap = scene.to_snapshot();
357 assert_eq!(snap.next_grid_id, 3);
358
359 let restored = Scene::from_snapshot(&snap).expect("restore");
360 assert_eq!(restored.grid_count(), 2);
361 let mut restored_mut = restored;
364 let new_id = restored_mut.add_grid(GridTransform::identity());
365 assert_eq!(new_id.raw(), 3);
366 }
367
368 #[test]
369 fn restored_scene_is_editable() {
370 let (scene, _) = build_two_grid_scene();
375 let snap = scene.to_snapshot();
376 let mut restored = Scene::from_snapshot(&snap).expect("restore");
377
378 let g0 = GridId::from_raw_for_test(0);
379 let new_voxel = IVec3::new(50, 51, 52);
380 restored
381 .grid_mut(g0)
382 .expect("grid 0 present")
383 .set_voxel(new_voxel, Some(0x80_de_ad_be));
384 let chunk = restored
385 .grid(g0)
386 .unwrap()
387 .chunk(IVec3::ZERO)
388 .expect("chunk created");
389 assert!(voxel_is_solid(chunk, 50, 51, 52));
390 }
391
392 #[test]
395 fn snapshot_round_trip_preserves_chunk_versions() {
396 let mut scene = Scene::new();
400 let id = scene.add_grid(GridTransform::identity());
401 let g = scene.grid_mut(id).unwrap();
402 g.set_voxel(IVec3::new(0, 0, 0), Some(0x80_aa_bb_cc));
404 g.set_voxel(IVec3::new(1, 1, 1), Some(0x80_dd_ee_ff));
405 g.set_voxel(IVec3::new(2, 2, 2), Some(0x80_11_22_33));
406 g.set_voxel(IVec3::new(128, 0, 0), Some(0x80_44_55_66));
408 assert_eq!(g.chunk_version(IVec3::ZERO), 3);
409 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
410
411 let snap = scene.to_snapshot();
412 let bytes = bincode::serialize(&snap).expect("bincode serialize");
413 let snap_back: SceneSnapshot = bincode::deserialize(&bytes).expect("bincode deserialize");
414 let restored = Scene::from_snapshot(&snap_back).expect("restore");
415
416 let g = restored.grid(id).expect("grid present");
417 assert_eq!(g.chunk_version(IVec3::ZERO), 3);
418 assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
419 assert_eq!(g.chunk_versions.len(), 2);
420 }
421
422 #[test]
423 fn snapshot_chunk_versions_zero_entries_are_dropped_from_wire() {
424 let mut scene = Scene::new();
427 let id = scene.add_grid(GridTransform::identity());
428 let g = scene.grid_mut(id).unwrap();
429 g.chunk_versions.insert(IVec3::ZERO, 0);
433 let snap = scene.to_snapshot();
434 let g_snap = &snap.grids[0].1;
435 assert!(g_snap.chunk_versions.is_empty(), "zero entries dropped");
436 }
437
438 #[test]
439 fn snapshot_is_deterministic() {
440 let (scene, _) = build_two_grid_scene();
441 let s1 = bincode::serialize(&scene.to_snapshot()).unwrap();
442 let s2 = bincode::serialize(&scene.to_snapshot()).unwrap();
443 assert_eq!(s1, s2, "snapshot bytes should be deterministic");
444 }
445}