From 01ec312e903fdaf1ef2b584b763c1d7f98dfe3a7 Mon Sep 17 00:00:00 2001 From: Elias Stepanik <40958815+eliasstepanik@users.noreply.github.com> Date: Tue, 17 Jun 2025 23:01:50 +0200 Subject: [PATCH] Add chunk I/O and unloading --- .../plugins/environment/environment_plugin.rs | 11 ++- .../environment/systems/voxels/chunk_io.rs | 35 ++++++++ .../plugins/environment/systems/voxels/mod.rs | 3 +- .../environment/systems/voxels/octree.rs | 79 +++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 client/src/plugins/environment/systems/voxels/chunk_io.rs diff --git a/client/src/plugins/environment/environment_plugin.rs b/client/src/plugins/environment/environment_plugin.rs index 3f65b4c..6d61956 100644 --- a/client/src/plugins/environment/environment_plugin.rs +++ b/client/src/plugins/environment/environment_plugin.rs @@ -1,21 +1,24 @@ +use crate::plugins::environment::systems::voxels::atlas::VoxelTextureAtlas; +use crate::plugins::environment::systems::voxels::chunk_io::{ + save_dirty_chunks_system, unload_far_chunks, +}; use crate::plugins::environment::systems::voxels::debug::{draw_grid, visualize_octree_system}; use crate::plugins::environment::systems::voxels::lod::update_chunk_lods; use crate::plugins::environment::systems::voxels::meshing_gpu::{ GpuMeshingWorker, queue_gpu_meshing, }; -use bevy_app_compute::prelude::{AppComputePlugin, AppComputeWorkerPlugin}; use crate::plugins::environment::systems::voxels::queue_systems; use crate::plugins::environment::systems::voxels::queue_systems::{ enqueue_visible_chunks, process_chunk_queue, }; use crate::plugins::environment::systems::voxels::render_chunks::rebuild_dirty_chunks; -use crate::plugins::environment::systems::voxels::atlas::{VoxelTextureAtlas}; use crate::plugins::environment::systems::voxels::structure::{ ChunkBudget, ChunkCullingCfg, ChunkQueue, MeshBufferPool, PrevCameraChunk, SparseVoxelOctree, SpawnedChunks, }; use bevy::app::{App, Plugin, PreStartup, PreUpdate, Startup}; use bevy::prelude::*; +use bevy_app_compute::prelude::{AppComputePlugin, AppComputeWorkerPlugin}; pub struct EnvironmentPlugin; impl Plugin for EnvironmentPlugin { @@ -39,7 +42,7 @@ impl Plugin for EnvironmentPlugin { }); app.insert_resource(ChunkBudget { per_frame: 20 }); app.init_resource::(); - /* app.add_systems(Update, log_mesh_count);*/ + /* app.add_systems(Update, log_mesh_count);*/ app // ------------------------------------------------------------------------ // resources @@ -57,8 +60,10 @@ impl Plugin for EnvironmentPlugin { enqueue_visible_chunks, process_chunk_queue.after(enqueue_visible_chunks), update_chunk_lods.after(process_chunk_queue), + unload_far_chunks.after(update_chunk_lods), rebuild_dirty_chunks.after(process_chunk_queue), // 4. (re)mesh dirty chunks queue_gpu_meshing.after(rebuild_dirty_chunks), + save_dirty_chunks_system.after(rebuild_dirty_chunks), /* ---------- optional debug drawing ------- */ visualize_octree_system .run_if(should_visualize_octree) diff --git a/client/src/plugins/environment/systems/voxels/chunk_io.rs b/client/src/plugins/environment/systems/voxels/chunk_io.rs new file mode 100644 index 0000000..fc7ba82 --- /dev/null +++ b/client/src/plugins/environment/systems/voxels/chunk_io.rs @@ -0,0 +1,35 @@ +use crate::plugins::environment::systems::voxels::structure::*; +use bevy::prelude::*; +use std::path::Path; + +const CHUNK_DIR: &str = "chunks"; + +/// Save all dirty chunks to disk. +pub fn save_dirty_chunks_system(mut tree_q: Query<&mut SparseVoxelOctree>) { + let Ok(mut tree) = tree_q.get_single_mut() else { + return; + }; + let _ = tree.save_dirty_chunks(Path::new(CHUNK_DIR)); +} + +/// Unload chunks that reached the maximum LOD distance. +pub fn unload_far_chunks( + mut commands: Commands, + mut tree_q: Query<&mut SparseVoxelOctree>, + mut spawned: ResMut, + chunks: Query<(Entity, &Chunk, &ChunkLod)>, +) { + let Ok(mut tree) = tree_q.get_single_mut() else { + return; + }; + for (ent, chunk, lod) in chunks.iter() { + if lod.0 == tree.max_depth - 1 { + if let Err(e) = tree.save_chunk(chunk.key, Path::new(CHUNK_DIR)) { + error!("failed to save chunk {:?}: {e}", chunk.key); + } + tree.unload_chunk(chunk.key); + spawned.0.remove(&chunk.key); + commands.entity(ent).despawn_recursive(); + } + } +} diff --git a/client/src/plugins/environment/systems/voxels/mod.rs b/client/src/plugins/environment/systems/voxels/mod.rs index b4a3129..9050d2c 100644 --- a/client/src/plugins/environment/systems/voxels/mod.rs +++ b/client/src/plugins/environment/systems/voxels/mod.rs @@ -3,11 +3,12 @@ pub mod helper; pub mod octree; pub mod structure; +pub mod atlas; mod chunk; +pub mod chunk_io; pub mod culling; pub mod lod; mod meshing; pub mod meshing_gpu; pub mod queue_systems; pub mod render_chunks; -pub mod atlas; diff --git a/client/src/plugins/environment/systems/voxels/octree.rs b/client/src/plugins/environment/systems/voxels/octree.rs index c51f54d..1b8fe60 100644 --- a/client/src/plugins/environment/systems/voxels/octree.rs +++ b/client/src/plugins/environment/systems/voxels/octree.rs @@ -572,4 +572,83 @@ impl SparseVoxelOctree { self.occupied_chunks.insert(key); } } + + /// Collect all voxels contained within a chunk. + pub fn voxels_in_chunk(&self, key: ChunkKey) -> Vec<(Vec3, Voxel)> { + let step = self.get_spacing_at_depth(self.max_depth); + let chunk_size = CHUNK_SIZE as f32 * step; + let center = self.chunk_center_world(key); + let half = Vec3::splat(chunk_size / 2.0); + let min = center - half; + let max = center + half; + + Self::collect_voxels_from_node(&self.root, self.size, self.center) + .into_iter() + .filter_map(|(pos, voxel, _)| { + if pos.x >= min.x + && pos.x < max.x + && pos.y >= min.y + && pos.y < max.y + && pos.z >= min.z + && pos.z < max.z + { + Some((pos, voxel)) + } else { + None + } + }) + .collect() + } + + /// Save a single chunk to disk inside the specified directory. + pub fn save_chunk>(&self, key: ChunkKey, dir: P) -> io::Result<()> { + let voxels = self.voxels_in_chunk(key); + if voxels.is_empty() { + return Ok(()); + } + std::fs::create_dir_all(&dir)?; + let path = dir + .as_ref() + .join(format!("chunk_{}_{}_{}.bin", key.0, key.1, key.2)); + let data = + bincode::serialize(&voxels).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + std::fs::write(path, data) + } + + /// Load a single chunk from disk and insert its voxels. + pub fn load_chunk>(&mut self, key: ChunkKey, dir: P) -> io::Result<()> { + let path = dir + .as_ref() + .join(format!("chunk_{}_{}_{}.bin", key.0, key.1, key.2)); + if !path.exists() { + return Ok(()); + } + let bytes = std::fs::read(path)?; + let voxels: Vec<(Vec3, Voxel)> = + bincode::deserialize(&bytes).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + for (pos, voxel) in voxels { + self.insert(pos, voxel); + } + self.clear_dirty_flags(); + Ok(()) + } + + /// Remove all voxels from the specified chunk. + pub fn unload_chunk(&mut self, key: ChunkKey) { + let voxels = self.voxels_in_chunk(key); + for (pos, _) in voxels { + self.remove(pos); + } + self.clear_dirty_flags(); + } + + /// Save all currently dirty chunks to disk and clear the dirty set. + pub fn save_dirty_chunks>(&mut self, dir: P) -> io::Result<()> { + let keys: Vec<_> = self.dirty_chunks.iter().copied().collect(); + for key in keys { + let _ = self.save_chunk(key, &dir); + } + self.clear_dirty_flags(); + Ok(()) + } }