diff --git a/Cargo.lock b/Cargo.lock index 850c4e7..90612c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -145,6 +145,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" +[[package]] +name = "atomic" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d818003e740b63afc82337e3160717f4f63078720a810b7b903e70a5d1d2994" +dependencies = [ + "bytemuck", +] + [[package]] name = "atomic-polyfill" version = "1.0.3" @@ -262,6 +271,20 @@ name = "bytemuck" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965ab7eb5f8f97d2a083c799f3a1b994fc397b2fe2da5d1da1626ce15a39f2b1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.60", +] [[package]] name = "byteorder" @@ -1150,6 +1173,7 @@ version = "0.0.0" dependencies = [ "android-activity", "anyhow", + "atomic", "flume", "gilrs", "glam", @@ -1217,8 +1241,10 @@ name = "kubi-shared" version = "0.0.0" dependencies = [ "anyhow", + "atomic", "bincode", "bracket-noise", + "bytemuck", "glam", "hashbrown 0.14.3", "nohash-hasher", diff --git a/kubi-server/src/world/tasks.rs b/kubi-server/src/world/tasks.rs index 8094632..4e45c92 100644 --- a/kubi-server/src/world/tasks.rs +++ b/kubi-server/src/world/tasks.rs @@ -1,59 +1,61 @@ -use shipyard::{Unique, AllStoragesView}; -use flume::{unbounded, Sender, Receiver}; -use glam::IVec3; -use rayon::{ThreadPool, ThreadPoolBuilder}; -use anyhow::Result; -use kubi_shared::{ - chunk::BlockData, - worldgen::generate_world, - queue::QueuedBlock, -}; - -pub enum ChunkTask { - LoadChunk { - position: IVec3, - seed: u64, - } -} - -pub enum ChunkTaskResponse { - ChunkLoaded { - chunk_position: IVec3, - blocks: BlockData, - queue: Vec - } -} - -#[derive(Unique)] -pub struct ChunkTaskManager { - channel: (Sender, Receiver), - pool: ThreadPool, -} -impl ChunkTaskManager { - pub fn new() -> Result { - Ok(Self { - channel: unbounded(), - pool: ThreadPoolBuilder::new().build()? - }) - } - pub fn spawn_task(&self, task: ChunkTask) { - let sender = self.channel.0.clone(); - self.pool.spawn(move || { - sender.send(match task { - ChunkTask::LoadChunk { position: chunk_position, seed } => { - let (blocks, queue) = generate_world(chunk_position, seed); - ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue } - } - }).unwrap() - }) - } - pub fn receive(&self) -> Option { - self.channel.1.try_recv().ok() - } -} - -pub fn init_chunk_task_manager( - storages: AllStoragesView -) { - storages.add_unique(ChunkTaskManager::new().expect("ChunkTaskManager Init failed")); -} +use shipyard::{Unique, AllStoragesView}; +use flume::{unbounded, Sender, Receiver}; +use glam::IVec3; +use rayon::{ThreadPool, ThreadPoolBuilder}; +use anyhow::Result; +use kubi_shared::{ + chunk::BlockData, + worldgen::generate_world, + queue::QueuedBlock, +}; + +pub enum ChunkTask { + LoadChunk { + position: IVec3, + seed: u64, + } +} + +pub enum ChunkTaskResponse { + ChunkLoaded { + chunk_position: IVec3, + blocks: BlockData, + queue: Vec + } +} + +#[derive(Unique)] +pub struct ChunkTaskManager { + channel: (Sender, Receiver), + pool: ThreadPool, +} +impl ChunkTaskManager { + pub fn new() -> Result { + Ok(Self { + channel: unbounded(), + pool: ThreadPoolBuilder::new().build()? + }) + } + pub fn spawn_task(&self, task: ChunkTask) { + let sender = self.channel.0.clone(); + self.pool.spawn(move || { + sender.send(match task { + ChunkTask::LoadChunk { position: chunk_position, seed } => { + let Some((blocks, queue)) = generate_world(chunk_position, seed, None) else { + return + }; + ChunkTaskResponse::ChunkLoaded { chunk_position, blocks, queue } + } + }).unwrap() + }) + } + pub fn receive(&self) -> Option { + self.channel.1.try_recv().ok() + } +} + +pub fn init_chunk_task_manager( + storages: AllStoragesView +) { + storages.add_unique(ChunkTaskManager::new().expect("ChunkTaskManager Init failed")); +} diff --git a/kubi-shared/Cargo.toml b/kubi-shared/Cargo.toml index 2a05a9f..63cf255 100644 --- a/kubi-shared/Cargo.toml +++ b/kubi-shared/Cargo.toml @@ -19,9 +19,10 @@ rand = { version = "0.8", default_features = false, features = ["std", "min_cons rand_xoshiro = "0.6" hashbrown = { version = "0.14", features = ["serde"] } nohash-hasher = "0.2" -#bytemuck = { version = "1.14", features = ["derive"] } +bytemuck = { version = "1.14", features = ["derive"] } static_assertions = "1.1" nz = "0.3" +atomic = "0.6" [features] default = [] diff --git a/kubi-shared/src/block.rs b/kubi-shared/src/block.rs index b176b0f..9849071 100644 --- a/kubi-shared/src/block.rs +++ b/kubi-shared/src/block.rs @@ -26,9 +26,10 @@ pub enum BlockTexture { Water, } -#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, EnumIter, TryFromPrimitive)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default, PartialEq, Eq, EnumIter, TryFromPrimitive)] #[repr(u8)] pub enum Block { + #[default] Air, Marker, Stone, diff --git a/kubi-shared/src/worldgen.rs b/kubi-shared/src/worldgen.rs index 958ffb1..772aeb1 100644 --- a/kubi-shared/src/worldgen.rs +++ b/kubi-shared/src/worldgen.rs @@ -1,6 +1,8 @@ -use std::sync::atomic::AtomicBool; - -use glam::{ivec3, IVec3}; +use std::sync::Arc; +use atomic::Atomic; +use bytemuck::{CheckedBitPattern, NoUninit}; +use glam::IVec3; +use static_assertions::const_assert; use crate::{ block::Block, chunk::{BlockData, CHUNK_SIZE}, @@ -8,6 +10,18 @@ use crate::{ }; mod _01_terrain; +mod _02_water; +mod _03_caves; + +#[repr(u8)] +#[derive(Clone, Copy, Debug, Default, NoUninit, CheckedBitPattern)] +pub enum AbortState { + #[default] + Continue, + Abort, + Aborted, +} +const_assert!(Atomic::::is_lock_free()); trait WorldGenStep { fn initialize(generator: &WorldGenerator) -> Self; @@ -16,16 +30,29 @@ trait WorldGenStep { macro_rules! run_steps { ($gen: expr, $abort: expr, [$($step:ty),* $(,)?]) => { - { - let _abort: AtomicBool = $abort.unwrap_or(AtomicBool::new(false)); + (||{ + let _abort: ::std::sync::Arc<::atomic::Atomic<$crate::worldgen::AbortState>> = + $abort.unwrap_or_else(|| ::std::sync::Arc::new(::atomic::Atomic::new($crate::worldgen::AbortState::Continue))); + + let _chkabt = || _abort.compare_exchange( + $crate::worldgen::AbortState::Abort, + $crate::worldgen::AbortState::Aborted, + ::atomic::Ordering::Relaxed, + ::atomic::Ordering::Relaxed + ).is_ok(); + $({ - let _ensure_ref: &mut WorldGenerator = $gen; - struct _Ensure0(T); + let _ensure_ref: &mut $crate::worldgen::WorldGenerator = $gen; + struct _Ensure0(T); type _Ensure1 = _Ensure0<$step>; let mut step: _Ensure1 = _Ensure0(<$step>::initialize(&*_ensure_ref)); + if _chkabt() { return false } step.0.generate(_ensure_ref); + if _chkabt() { return false } })* - } + + true + })() }; } @@ -41,6 +68,32 @@ impl WorldGenerator { self.chunk_position * CHUNK_SIZE as i32 } + fn query(&self, position: IVec3) -> Block { + // let offset = self.offset(); + // let event_pos = offset + position; + // if let Some(block) = self.queue.iter().find(|block| block.position == event_pos) { + // block.block_type + // } else { + // self.blocks[position.x as usize][position.y as usize][position.z as usize] + // } + self.blocks[position.x as usize][position.y as usize][position.z as usize] + } + + fn place(&mut self, position: IVec3, block: Block) { + // let offset = self.offset(); + // let event_pos = offset + position; + // self.queue.retain(|block: &QueuedBlock| { + // block.position != event_pos + // }); + self.blocks[position.x as usize][position.y as usize][position.z as usize] = block; + } + + fn place_if_empty(&mut self, position: IVec3, block: Block) { + if self.query(position) == Block::Air { + self.place(position, block); + } + } + fn place_or_queue(&mut self, position: IVec3, block: Block) { let offset = self.offset(); if position.to_array().iter().any(|&x| !(0..CHUNK_SIZE).contains(&(x as usize))) { @@ -58,6 +111,21 @@ impl WorldGenerator { } } + fn global_position(&self, position: IVec3) -> IVec3 { + self.offset() + position + } + + fn local_height(&self, height: i32) -> i32 { + let offset = self.chunk_position * CHUNK_SIZE as i32; + (height - offset.y).clamp(0, CHUNK_SIZE as i32) + } + + fn local_y_position(&self, y: i32) -> Option { + let offset = self.chunk_position * CHUNK_SIZE as i32; + let position = y - offset.y; + (0..CHUNK_SIZE as i32).contains(&position).then_some(position) + } + pub fn new(chunk_position: IVec3, seed: u64) -> Self { Self { seed, @@ -67,14 +135,19 @@ impl WorldGenerator { } } - pub fn generate(mut self, abort: Option) -> (BlockData, Vec) { + /// Generate the chunk. + /// + /// Will return `None` only if the generation was aborted. + pub fn generate(mut self, abort: Option>>) -> Option<(BlockData, Vec)> { run_steps!(&mut self, abort, [ - _01_terrain::TerrainStep - ]); - (self.blocks, self.queue) + _01_terrain::TerrainStep, + _02_water::WaterStep, + _03_caves::CaveStep, + ]).then_some((self.blocks, self.queue)) } } -pub fn generate_world(chunk_position: IVec3, seed: u64) -> (BlockData, Vec) { - WorldGenerator::new(chunk_position, seed).generate(None) +pub fn generate_world(chunk_position: IVec3, seed: u64, abort: Option>>) -> Option<(BlockData, Vec)> { + //TODO: pass through None for abort + WorldGenerator::new(chunk_position, seed).generate(abort) } diff --git a/kubi-shared/src/worldgen/_01_terrain.rs b/kubi-shared/src/worldgen/_01_terrain.rs index 98a07b3..9e21aa6 100644 --- a/kubi-shared/src/worldgen/_01_terrain.rs +++ b/kubi-shared/src/worldgen/_01_terrain.rs @@ -6,21 +6,23 @@ use super::{WorldGenerator, WorldGenStep}; pub struct TerrainStep { noise: FastNoise, } + impl WorldGenStep for TerrainStep { fn initialize(generator: &WorldGenerator) -> Self { let mut noise = FastNoise::seeded(generator.seed); - noise.set_fractal_type(FractalType::FBM); - noise.set_fractal_octaves(4); + noise.set_fractal_type(FractalType::RigidMulti); + noise.set_fractal_octaves(5); noise.set_frequency(0.003); Self { noise } } - fn generate(&mut self, generator: &mut WorldGenerator) { + fn generate(&mut self, gen: &mut WorldGenerator) { for x in 0..CHUNK_SIZE as i32 { for z in 0..CHUNK_SIZE as i32 { - let height = (self.noise.get_noise(x as f32, z as f32) * 8.0) as i32; - for y in 0..height { - generator.place_or_queue(ivec3(x, y, z), Block::Stone); + let global_xz = gen.global_position(ivec3(x, 0, z)); + let height = (self.noise.get_noise(global_xz.x as f32, global_xz.z as f32) * 32.0) as i32; + for y in 0..gen.local_height(height) { + gen.place(ivec3(x, y, z), Block::Stone); } } } diff --git a/kubi-shared/src/worldgen/_02_water.rs b/kubi-shared/src/worldgen/_02_water.rs new file mode 100644 index 0000000..136bc41 --- /dev/null +++ b/kubi-shared/src/worldgen/_02_water.rs @@ -0,0 +1,19 @@ +use bracket_noise::prelude::{FastNoise, FractalType}; +use glam::ivec3; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::{WorldGenerator, WorldGenStep}; + +pub struct WaterStep; + +impl WorldGenStep for WaterStep { + fn initialize(_: &WorldGenerator) -> Self { Self } + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + for y in 0..gen.local_height(0) { + gen.place_if_empty(ivec3(x, y, z), Block::Water); + } + } + } + } +} diff --git a/kubi-shared/src/worldgen/_03_caves.rs b/kubi-shared/src/worldgen/_03_caves.rs new file mode 100644 index 0000000..5e6d73e --- /dev/null +++ b/kubi-shared/src/worldgen/_03_caves.rs @@ -0,0 +1,42 @@ +use bracket_noise::prelude::{FastNoise, FractalType}; +use glam::{ivec3, IVec3}; +use crate::{block::Block, chunk::CHUNK_SIZE}; +use super::{WorldGenStep, WorldGenerator}; + +pub struct CaveStep { + a: FastNoise, + b: FastNoise, +} + +impl WorldGenStep for CaveStep { + fn initialize(gen: &WorldGenerator) -> Self { + let mut a = FastNoise::seeded(gen.seed); + a.set_fractal_type(FractalType::FBM); + a.set_frequency(0.015); + + let mut b = FastNoise::seeded(gen.seed.rotate_left(1) + 1); + b.set_fractal_type(FractalType::FBM); + b.set_frequency(0.015); + + Self { a, b } + } + fn generate(&mut self, gen: &mut WorldGenerator) { + for x in 0..CHUNK_SIZE as i32 { + for z in 0..CHUNK_SIZE as i32 { + for y in 0..CHUNK_SIZE as i32 { + let pos: IVec3 = ivec3(x, y, z); + if gen.query(pos) != Block::Stone { continue } + + let gpos = gen.global_position(pos); + let noise_a = self.a.get_noise3d(gpos.x as f32, gpos.y as f32, gpos.z as f32); + let noise_b = self.b.get_noise3d(gpos.x as f32, gpos.y as f32, gpos.z as f32); + let noise_min = noise_a.min(noise_b); + + if noise_min > 0.5 { return } + + gen.place(ivec3(x, y, z), Block::Air); + } + } + } + } +} diff --git a/kubi/Cargo.toml b/kubi/Cargo.toml index 5b54d87..ed59a2c 100644 --- a/kubi/Cargo.toml +++ b/kubi/Cargo.toml @@ -37,6 +37,7 @@ static_assertions = "1.1" tinyset = "0.4" serde_json = { version = "1.0", optional = true } #only used for `generate_visualizer_data` rand = { version = "0.8", features = ["alloc", "small_rng"]} +atomic = "0.6" [target.'cfg(target_os = "android")'.dependencies] android-activity = "^0.5.2" diff --git a/kubi/src/world/chunk.rs b/kubi/src/world/chunk.rs index 07b2049..e8ddd3a 100644 --- a/kubi/src/world/chunk.rs +++ b/kubi/src/world/chunk.rs @@ -1,5 +1,8 @@ +use std::sync::Arc; use glam::IVec3; +use atomic::Atomic; use glium::{VertexBuffer, IndexBuffer}; +use kubi_shared::worldgen::AbortState; use crate::rendering::world::ChunkVertex; pub use kubi_shared::chunk::{CHUNK_SIZE, BlockData}; @@ -39,7 +42,7 @@ pub enum DesiredChunkState { Nothing, Loaded, Rendered, - ToUnload, + Unloaded, } impl DesiredChunkState { pub fn matches_current(self, current: CurrentChunkState) -> bool { @@ -55,8 +58,10 @@ pub struct Chunk { pub mesh_index: Option, pub current_state: CurrentChunkState, pub desired_state: DesiredChunkState, + pub abortion: Option>>, pub mesh_dirty: bool, } + impl Chunk { pub fn new(position: IVec3) -> Self { Self { @@ -65,6 +70,7 @@ impl Chunk { mesh_index: None, current_state: Default::default(), desired_state: Default::default(), + abortion: None, mesh_dirty: false, } } diff --git a/kubi/src/world/loading.rs b/kubi/src/world/loading.rs index 1d4face..5798408 100644 --- a/kubi/src/world/loading.rs +++ b/kubi/src/world/loading.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; +use atomic::{Atomic, Ordering}; use glam::{IVec3, ivec3}; use glium::{VertexBuffer, IndexBuffer, index::PrimitiveType}; -use kubi_shared::networking::messages::ClientToServerMessage; +use kubi_shared::{networking::messages::ClientToServerMessage, worldgen::AbortState}; use shipyard::{View, UniqueView, UniqueViewMut, IntoIter, Workload, IntoWorkload, NonSendSync, track}; use uflow::SendMode; use crate::{ @@ -15,10 +17,10 @@ use super::{ ChunkStorage, ChunkMeshStorage, chunk::{Chunk, DesiredChunkState, CHUNK_SIZE, ChunkMesh, CurrentChunkState, ChunkData}, tasks::{ChunkTaskManager, ChunkTaskResponse, ChunkTask}, - queue::BlockUpdateQueue + queue::BlockUpdateQueue, }; -const MAX_CHUNK_OPS_INGAME: usize = 6; +const MAX_CHUNK_OPS_INGAME: usize = 8; const MAX_CHUNK_OPS: usize = 32; pub fn update_loaded_world_around_player() -> Workload { @@ -56,7 +58,7 @@ pub fn update_chunks_if_player_moved( //Then, mark *ALL* chunks with ToUnload for (_, chunk) in &mut vm_world.chunks { - chunk.desired_state = DesiredChunkState::ToUnload; + chunk.desired_state = DesiredChunkState::Unloaded; } //Then mark chunks that are near to the player @@ -99,13 +101,27 @@ fn unload_downgrade_chunks( //TODO refactor this //TODO unsubscibe if in multiplayer vm_world.chunks.retain(|_, chunk| { - if chunk.desired_state == DesiredChunkState::ToUnload { + if chunk.desired_state == DesiredChunkState::Unloaded { if let Some(mesh_index) = chunk.mesh_index { vm_meshes.remove(mesh_index).unwrap(); } + if let Some(abortion) = &chunk.abortion { + let _ = abortion.compare_exchange( + AbortState::Continue, AbortState::Abort, + Ordering::Relaxed, Ordering::Relaxed + ); + } false } else { match chunk.desired_state { + DesiredChunkState::Nothing if matches!(chunk.current_state, CurrentChunkState::Loading) => { + if let Some(abortion) = &chunk.abortion { + let _ = abortion.compare_exchange( + AbortState::Continue, AbortState::Abort, + Ordering::Relaxed, Ordering::Relaxed + ); + } + }, DesiredChunkState::Loaded if matches!(chunk.current_state, CurrentChunkState::Rendered | CurrentChunkState::CalculatingMesh | CurrentChunkState::RecalculatingMesh) => { if let Some(mesh_index) = chunk.mesh_index { vm_meshes.remove(mesh_index).unwrap(); @@ -134,6 +150,7 @@ fn start_required_tasks( let chunk = world.chunks.get(&position).unwrap(); match chunk.desired_state { DesiredChunkState::Loaded | DesiredChunkState::Rendered if chunk.current_state == CurrentChunkState::Nothing => { + let mut abortion = None; //start load task if let Some(client) = &mut udp_client { client.0.send( @@ -144,14 +161,18 @@ fn start_required_tasks( SendMode::Reliable ); } else { + let atomic = Arc::new(Atomic::new(AbortState::Continue)); task_manager.spawn_task(ChunkTask::LoadChunk { seed: 0xbeef_face_dead_cafe, - position + position, + abortion: Some(Arc::clone(&atomic)), }); + abortion = Some(atomic); } //Update chunk state let chunk = world.chunks.get_mut(&position).unwrap(); chunk.current_state = CurrentChunkState::Loading; + chunk.abortion = abortion; // =========== //log::trace!("Started loading chunk {position}"); }, @@ -173,6 +194,7 @@ fn start_required_tasks( chunk.current_state = CurrentChunkState::CalculatingMesh; } chunk.mesh_dirty = false; + chunk.abortion = None; //Can never abort at this point // =========== //log::trace!("Started generating mesh for chunk {position}"); } @@ -195,14 +217,15 @@ fn process_completed_tasks( ChunkTaskResponse::LoadedChunk { position, chunk_data, mut queued } => { //check if chunk exists let Some(chunk) = world.chunks.get_mut(&position) else { + //to compensate, actually push the ops counter back by one log::warn!("blocks data discarded: chunk doesn't exist"); - return + continue }; //check if chunk still wants it if !matches!(chunk.desired_state, DesiredChunkState::Loaded | DesiredChunkState::Rendered) { log::warn!("block data discarded: state undesirable: {:?}", chunk.desired_state); - return + continue } //set the block data @@ -228,13 +251,13 @@ fn process_completed_tasks( //check if chunk exists let Some(chunk) = world.chunks.get_mut(&position) else { log::warn!("mesh discarded: chunk doesn't exist"); - return + continue }; //check if chunk still wants it if chunk.desired_state != DesiredChunkState::Rendered { log::warn!("mesh discarded: state undesirable: {:?}", chunk.desired_state); - return + continue } //apply the mesh diff --git a/kubi/src/world/tasks.rs b/kubi/src/world/tasks.rs index 6c22cdf..87bda90 100644 --- a/kubi/src/world/tasks.rs +++ b/kubi/src/world/tasks.rs @@ -1,6 +1,8 @@ +use std::sync::Arc; +use atomic::Atomic; use flume::{Sender, Receiver}; use glam::IVec3; -use kubi_shared::queue::QueuedBlock; +use kubi_shared::{queue::QueuedBlock, worldgen::AbortState}; use shipyard::Unique; use rayon::{ThreadPool, ThreadPoolBuilder}; use super::{ @@ -13,7 +15,8 @@ use crate::rendering::world::ChunkVertex; pub enum ChunkTask { LoadChunk { seed: u64, - position: IVec3 + position: IVec3, + abortion: Option>>, }, GenerateMesh { position: IVec3, @@ -67,8 +70,11 @@ impl ChunkTaskManager { trans_vertices, trans_indices, } }, - ChunkTask::LoadChunk { position, seed } => { - let (chunk_data, queued) = generate_world(position, seed); + ChunkTask::LoadChunk { position, seed, abortion } => { + let Some((chunk_data, queued)) = generate_world(position, seed, abortion) else { + log::warn!("aborted operation"); + return + }; ChunkTaskResponse::LoadedChunk { position, chunk_data, queued } } });