use glam::IVec3;
use roxlap_formats::vxl::{self, ParseError, Vxl};
use serde::{Deserialize, Serialize};
use crate::{Grid, GridId, GridTransform, Scene};
fn compact_serialize_chunk(vxl: &Vxl) -> Vec<u8> {
let n_cols = (vxl.vsid as usize) * (vxl.vsid as usize);
let mut data: Vec<u8> = Vec::new();
let mut column_offset: Vec<u32> = Vec::with_capacity(n_cols + 1);
for i in 0..n_cols {
column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
data.extend_from_slice(vxl.column_data(i));
}
column_offset.push(u32::try_from(data.len()).expect("offset fits in u32"));
let compact = Vxl {
vsid: vxl.vsid,
ipo: vxl.ipo,
ist: vxl.ist,
ihe: vxl.ihe,
ifo: vxl.ifo,
data: data.into_boxed_slice(),
column_offset: column_offset.into_boxed_slice(),
mip_base_offsets: Box::new([0, n_cols + 1]),
vbit: Box::new([]),
vbiti: 0,
};
vxl::serialize(&compact)
}
const RESTORE_EDIT_HEADROOM_PER_COLUMN: usize = 256;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SceneSnapshot {
pub next_grid_id: u32,
pub grids: Vec<(GridId, GridSnapshot)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GridSnapshot {
pub transform: GridTransform,
pub chunks: Vec<(IVec3, Vec<u8>)>,
#[serde(default)]
pub chunk_versions: Vec<(IVec3, u64)>,
}
#[derive(Debug)]
pub enum FromSnapshotError {
ChunkParse {
grid: GridId,
chunk: IVec3,
source: ParseError,
},
}
impl std::fmt::Display for FromSnapshotError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ChunkParse {
grid,
chunk,
source,
} => {
write!(
f,
"scene snapshot: grid {} chunk {chunk:?} parse failed: {source:?}",
grid.raw()
)
}
}
}
}
impl std::error::Error for FromSnapshotError {}
impl Scene {
#[must_use]
pub fn to_snapshot(&self) -> SceneSnapshot {
let mut grid_ids: Vec<GridId> = self.grids.keys().copied().collect();
grid_ids.sort_unstable();
let mut grids = Vec::with_capacity(grid_ids.len());
for id in grid_ids {
let grid = &self.grids[&id];
let mut chunk_addrs: Vec<IVec3> = grid.chunks.keys().copied().collect();
chunk_addrs.sort_unstable_by_key(|a| (a.x, a.y, a.z));
let chunks = chunk_addrs
.into_iter()
.map(|addr| (addr, compact_serialize_chunk(&grid.chunks[&addr])))
.collect();
let mut version_addrs: Vec<IVec3> = grid
.chunk_versions
.iter()
.filter_map(|(a, v)| if *v != 0 { Some(*a) } else { None })
.collect();
version_addrs.sort_unstable_by_key(|a| (a.x, a.y, a.z));
let chunk_versions = version_addrs
.into_iter()
.map(|addr| (addr, grid.chunk_versions[&addr]))
.collect();
grids.push((
id,
GridSnapshot {
transform: grid.transform,
chunks,
chunk_versions,
},
));
}
SceneSnapshot {
next_grid_id: self.next_grid_id,
grids,
}
}
pub fn from_snapshot(snap: &SceneSnapshot) -> Result<Self, FromSnapshotError> {
let mut scene = Self::new();
scene.next_grid_id = snap.next_grid_id;
for (id, gsnap) in &snap.grids {
let mut grid = Grid::new(gsnap.transform);
for (addr, bytes) in &gsnap.chunks {
let mut vxl =
vxl::parse(bytes).map_err(|source| FromSnapshotError::ChunkParse {
grid: *id,
chunk: *addr,
source,
})?;
let n_cols = (vxl.vsid as usize) * (vxl.vsid as usize);
vxl.reserve_edit_capacity(n_cols * RESTORE_EDIT_HEADROOM_PER_COLUMN);
grid.chunks.insert(*addr, vxl);
}
for (addr, ver) in &gsnap.chunk_versions {
grid.chunk_versions.insert(*addr, *ver);
}
scene.grids.insert(*id, grid);
}
Ok(scene)
}
}
#[cfg(test)]
#[allow(clippy::cast_possible_wrap, clippy::type_complexity)]
mod tests {
use super::*;
use crate::chunks::tests::voxel_is_solid;
use crate::CHUNK_SIZE_XY;
use glam::DVec3;
impl GridId {
pub(crate) fn from_raw_for_test(raw: u32) -> Self {
Self(raw)
}
}
fn build_two_grid_scene() -> (Scene, Vec<(GridId, IVec3, u32, u32, u32, u32)>) {
let mut scene = Scene::new();
let g0 = scene.add_grid(GridTransform::at(DVec3::new(0.0, 0.0, 0.0)));
let g1 = scene.add_grid(GridTransform::at(DVec3::new(1000.0, 0.0, 0.0)));
let mut expected = Vec::new();
for chz in 0..2 {
for chy in 0..5 {
for chx in 0..5 {
let chunk_idx = IVec3::new(chx, chy, chz);
#[allow(clippy::cast_sign_loss)]
let color =
0x80_00_00_00 | ((chx as u32) << 16) | ((chy as u32) << 8) | (chz as u32);
let global_voxel = chunk_idx
* IVec3::new(
CHUNK_SIZE_XY as i32,
CHUNK_SIZE_XY as i32,
crate::CHUNK_SIZE_Z as i32,
)
+ IVec3::new(5, 6, 7);
scene
.grid_mut(g0)
.unwrap()
.set_voxel(global_voxel, Some(color));
expected.push((g0, chunk_idx, 5, 6, 7, color));
}
}
}
for chz in 0..2 {
for chy in 0..5 {
for chx in 0..5 {
let chunk_idx = IVec3::new(chx, chy, chz);
#[allow(clippy::cast_sign_loss)]
let color =
0x80_ff_00_00 | ((chx as u32) << 16) | ((chy as u32) << 8) | (chz as u32);
let global_voxel = chunk_idx
* IVec3::new(
CHUNK_SIZE_XY as i32,
CHUNK_SIZE_XY as i32,
crate::CHUNK_SIZE_Z as i32,
)
+ IVec3::new(10, 11, 12);
scene
.grid_mut(g1)
.unwrap()
.set_voxel(global_voxel, Some(color));
expected.push((g1, chunk_idx, 10, 11, 12, color));
}
}
}
(scene, expected)
}
fn assert_voxels_match(scene: &Scene, expected: &[(GridId, IVec3, u32, u32, u32, u32)]) {
for &(grid_id, chunk_idx, vx, vy, vz, _color) in expected {
let grid = scene.grid(grid_id).expect("grid present");
let chunk = grid.chunk(chunk_idx).expect("chunk present");
assert!(
voxel_is_solid(chunk, vx, vy, vz),
"voxel ({vx},{vy},{vz}) in grid={} chunk={chunk_idx:?} not solid post-restore",
grid_id.raw()
);
}
}
#[test]
fn snapshot_round_trip_preserves_two_grid_100_chunk_scene() {
let (scene, expected) = build_two_grid_scene();
assert_eq!(scene.grid_count(), 2);
let total_chunks: usize = scene.grids().map(|(_, g)| g.chunks.len()).sum();
assert_eq!(total_chunks, 100, "test setup should produce 100 chunks");
let snap = scene.to_snapshot();
let bytes = bincode::serialize(&snap).expect("bincode serialize");
let snap_back: SceneSnapshot = bincode::deserialize(&bytes).expect("bincode deserialize");
let restored = Scene::from_snapshot(&snap_back).expect("restore");
assert_eq!(restored.grid_count(), 2);
let total_restored: usize = restored.grids().map(|(_, g)| g.chunks.len()).sum();
assert_eq!(total_restored, 100);
assert_voxels_match(&restored, &expected);
}
#[test]
fn snapshot_preserves_next_grid_id_and_transforms() {
let mut scene = Scene::new();
let g0 = scene.add_grid(GridTransform::at(DVec3::new(10.0, 20.0, 30.0)));
let _g1 = scene.add_grid(GridTransform::at(DVec3::new(40.0, 50.0, 60.0)));
scene.remove_grid(g0); let _g2 = scene.add_grid(GridTransform::at(DVec3::new(70.0, 80.0, 90.0)));
let snap = scene.to_snapshot();
assert_eq!(snap.next_grid_id, 3);
let restored = Scene::from_snapshot(&snap).expect("restore");
assert_eq!(restored.grid_count(), 2);
let mut restored_mut = restored;
let new_id = restored_mut.add_grid(GridTransform::identity());
assert_eq!(new_id.raw(), 3);
}
#[test]
fn restored_scene_is_editable() {
let (scene, _) = build_two_grid_scene();
let snap = scene.to_snapshot();
let mut restored = Scene::from_snapshot(&snap).expect("restore");
let g0 = GridId::from_raw_for_test(0);
let new_voxel = IVec3::new(50, 51, 52);
restored
.grid_mut(g0)
.expect("grid 0 present")
.set_voxel(new_voxel, Some(0x80_de_ad_be));
let chunk = restored
.grid(g0)
.unwrap()
.chunk(IVec3::ZERO)
.expect("chunk created");
assert!(voxel_is_solid(chunk, 50, 51, 52));
}
#[test]
fn snapshot_round_trip_preserves_chunk_versions() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let g = scene.grid_mut(id).unwrap();
g.set_voxel(IVec3::new(0, 0, 0), Some(0x80_aa_bb_cc));
g.set_voxel(IVec3::new(1, 1, 1), Some(0x80_dd_ee_ff));
g.set_voxel(IVec3::new(2, 2, 2), Some(0x80_11_22_33));
g.set_voxel(IVec3::new(128, 0, 0), Some(0x80_44_55_66));
assert_eq!(g.chunk_version(IVec3::ZERO), 3);
assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
let snap = scene.to_snapshot();
let bytes = bincode::serialize(&snap).expect("bincode serialize");
let snap_back: SceneSnapshot = bincode::deserialize(&bytes).expect("bincode deserialize");
let restored = Scene::from_snapshot(&snap_back).expect("restore");
let g = restored.grid(id).expect("grid present");
assert_eq!(g.chunk_version(IVec3::ZERO), 3);
assert_eq!(g.chunk_version(IVec3::new(1, 0, 0)), 1);
assert_eq!(g.chunk_versions.len(), 2);
}
#[test]
fn snapshot_chunk_versions_zero_entries_are_dropped_from_wire() {
let mut scene = Scene::new();
let id = scene.add_grid(GridTransform::identity());
let g = scene.grid_mut(id).unwrap();
g.chunk_versions.insert(IVec3::ZERO, 0);
let snap = scene.to_snapshot();
let g_snap = &snap.grids[0].1;
assert!(g_snap.chunk_versions.is_empty(), "zero entries dropped");
}
#[test]
fn snapshot_is_deterministic() {
let (scene, _) = build_two_grid_scene();
let s1 = bincode::serialize(&scene.to_snapshot()).unwrap();
let s2 = bincode::serialize(&scene.to_snapshot()).unwrap();
assert_eq!(s1, s2, "snapshot bytes should be deterministic");
}
}