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}
101
102#[derive(Debug)]
106pub enum FromSnapshotError {
107 ChunkParse {
110 grid: GridId,
111 chunk: IVec3,
112 source: ParseError,
113 },
114}
115
116impl std::fmt::Display for FromSnapshotError {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 match self {
119 Self::ChunkParse {
120 grid,
121 chunk,
122 source,
123 } => {
124 write!(
125 f,
126 "scene snapshot: grid {} chunk {chunk:?} parse failed: {source:?}",
127 grid.raw()
128 )
129 }
130 }
131 }
132}
133
134impl std::error::Error for FromSnapshotError {}
135
136impl Scene {
137 #[must_use]
147 pub fn to_snapshot(&self) -> SceneSnapshot {
148 let mut grid_ids: Vec<GridId> = self.grids.keys().copied().collect();
149 grid_ids.sort_unstable();
150
151 let mut grids = Vec::with_capacity(grid_ids.len());
152 for id in grid_ids {
153 let grid = &self.grids[&id];
154 let mut chunk_addrs: Vec<IVec3> = grid.chunks.keys().copied().collect();
155 chunk_addrs.sort_unstable_by_key(|a| (a.x, a.y, a.z));
156 let chunks = chunk_addrs
157 .into_iter()
158 .map(|addr| (addr, compact_serialize_chunk(&grid.chunks[&addr])))
159 .collect();
160 grids.push((
161 id,
162 GridSnapshot {
163 transform: grid.transform,
164 chunks,
165 },
166 ));
167 }
168 SceneSnapshot {
169 next_grid_id: self.next_grid_id,
170 grids,
171 }
172 }
173
174 pub fn from_snapshot(snap: &SceneSnapshot) -> Result<Self, FromSnapshotError> {
185 let mut scene = Self::new();
186 scene.next_grid_id = snap.next_grid_id;
187 for (id, gsnap) in &snap.grids {
188 let mut grid = Grid::new(gsnap.transform);
189 for (addr, bytes) in &gsnap.chunks {
190 let mut vxl =
191 vxl::parse(bytes).map_err(|source| FromSnapshotError::ChunkParse {
192 grid: *id,
193 chunk: *addr,
194 source,
195 })?;
196 let n_cols = (vxl.vsid as usize) * (vxl.vsid as usize);
197 vxl.reserve_edit_capacity(n_cols * RESTORE_EDIT_HEADROOM_PER_COLUMN);
198 grid.chunks.insert(*addr, vxl);
199 }
200 scene.grids.insert(*id, grid);
201 }
202 Ok(scene)
203 }
204}
205
206#[cfg(test)]
207#[allow(clippy::cast_possible_wrap, clippy::type_complexity)]
208mod tests {
209 use super::*;
210 use crate::chunks::tests::voxel_is_solid;
211 use crate::CHUNK_SIZE_XY;
212 use glam::DVec3;
213
214 impl GridId {
215 pub(crate) fn from_raw_for_test(raw: u32) -> Self {
216 Self(raw)
217 }
218 }
219
220 fn build_two_grid_scene() -> (Scene, Vec<(GridId, IVec3, u32, u32, u32, u32)>) {
226 let mut scene = Scene::new();
230 let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 0.0, 0.0)));
231 let g1 = scene.add_grid(GridTransform::at(DVec3::new(1000.0, 0.0, 0.0)));
232 let mut expected = Vec::new();
233 for chz in 0..2 {
237 for chy in 0..5 {
238 for chx in 0..5 {
239 let chunk_idx = IVec3::new(chx, chy, chz);
240 #[allow(clippy::cast_sign_loss)]
241 let color =
242 0x80_00_00_00 | ((chx as u32) << 16) | ((chy as u32) << 8) | (chz as u32);
243 let global_voxel = chunk_idx
244 * IVec3::new(
245 CHUNK_SIZE_XY as i32,
246 CHUNK_SIZE_XY as i32,
247 crate::CHUNK_SIZE_Z as i32,
248 )
249 + IVec3::new(5, 6, 7);
250 scene
251 .grid_mut(g0)
252 .unwrap()
253 .set_voxel(global_voxel, Some(color));
254 expected.push((g0, chunk_idx, 5, 6, 7, color));
255 }
256 }
257 }
258 for chz in 0..2 {
261 for chy in 0..5 {
262 for chx in 0..5 {
263 let chunk_idx = IVec3::new(chx, chy, chz);
264 #[allow(clippy::cast_sign_loss)]
265 let color =
266 0x80_ff_00_00 | ((chx as u32) << 16) | ((chy as u32) << 8) | (chz as u32);
267 let global_voxel = chunk_idx
268 * IVec3::new(
269 CHUNK_SIZE_XY as i32,
270 CHUNK_SIZE_XY as i32,
271 crate::CHUNK_SIZE_Z as i32,
272 )
273 + IVec3::new(10, 11, 12);
274 scene
275 .grid_mut(g1)
276 .unwrap()
277 .set_voxel(global_voxel, Some(color));
278 expected.push((g1, chunk_idx, 10, 11, 12, color));
279 }
280 }
281 }
282 (scene, expected)
283 }
284
285 fn assert_voxels_match(scene: &Scene, expected: &[(GridId, IVec3, u32, u32, u32, u32)]) {
286 for &(grid_id, chunk_idx, vx, vy, vz, _color) in expected {
287 let grid = scene.grid(grid_id).expect("grid present");
288 let chunk = grid.chunk(chunk_idx).expect("chunk present");
289 assert!(
290 voxel_is_solid(chunk, vx, vy, vz),
291 "voxel ({vx},{vy},{vz}) in grid={} chunk={chunk_idx:?} not solid post-restore",
292 grid_id.raw()
293 );
294 }
295 }
296
297 #[test]
298 fn snapshot_round_trip_preserves_two_grid_100_chunk_scene() {
299 let (scene, expected) = build_two_grid_scene();
300 assert_eq!(scene.grid_count(), 2);
301 let total_chunks: usize = scene.grids().map(|(_, g)| g.chunks.len()).sum();
302 assert_eq!(total_chunks, 100, "test setup should produce 100 chunks");
303
304 let snap = scene.to_snapshot();
306 let bytes = bincode::serialize(&snap).expect("bincode serialize");
307 let snap_back: SceneSnapshot = bincode::deserialize(&bytes).expect("bincode deserialize");
308 let restored = Scene::from_snapshot(&snap_back).expect("restore");
309
310 assert_eq!(restored.grid_count(), 2);
312 let total_restored: usize = restored.grids().map(|(_, g)| g.chunks.len()).sum();
313 assert_eq!(total_restored, 100);
314
315 assert_voxels_match(&restored, &expected);
317 }
318
319 #[test]
320 fn snapshot_preserves_next_grid_id_and_transforms() {
321 let mut scene = Scene::new();
322 let g0 = scene.add_grid(GridTransform::at(DVec3::new(10.0, 20.0, 30.0)));
323 let _g1 = scene.add_grid(GridTransform::at(DVec3::new(40.0, 50.0, 60.0)));
324 scene.remove_grid(g0); let _g2 = scene.add_grid(GridTransform::at(DVec3::new(70.0, 80.0, 90.0)));
326 let snap = scene.to_snapshot();
328 assert_eq!(snap.next_grid_id, 3);
329
330 let restored = Scene::from_snapshot(&snap).expect("restore");
331 assert_eq!(restored.grid_count(), 2);
332 let mut restored_mut = restored;
335 let new_id = restored_mut.add_grid(GridTransform::identity());
336 assert_eq!(new_id.raw(), 3);
337 }
338
339 #[test]
340 fn restored_scene_is_editable() {
341 let (scene, _) = build_two_grid_scene();
346 let snap = scene.to_snapshot();
347 let mut restored = Scene::from_snapshot(&snap).expect("restore");
348
349 let g0 = GridId::from_raw_for_test(0);
350 let new_voxel = IVec3::new(50, 51, 52);
351 restored
352 .grid_mut(g0)
353 .expect("grid 0 present")
354 .set_voxel(new_voxel, Some(0x80_de_ad_be));
355 let chunk = restored
356 .grid(g0)
357 .unwrap()
358 .chunk(IVec3::ZERO)
359 .expect("chunk created");
360 assert!(voxel_is_solid(chunk, 50, 51, 52));
361 }
362
363 #[test]
364 fn snapshot_is_deterministic() {
365 let (scene, _) = build_two_grid_scene();
366 let s1 = bincode::serialize(&scene.to_snapshot()).unwrap();
367 let s2 = bincode::serialize(&scene.to_snapshot()).unwrap();
368 assert_eq!(s1, s2, "snapshot bytes should be deterministic");
369 }
370}