diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 000000000..c33e64236 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,2 @@ +[profile.default] +slow-timeout = { period = "15s", terminate-after = 3 } diff --git a/.etc/raw_chunk.dat b/.etc/raw_chunk.dat index 0138c8752..88b4af97a 100644 Binary files a/.etc/raw_chunk.dat and b/.etc/raw_chunk.dat differ diff --git a/Cargo.toml b/Cargo.toml index 8b3b143f6..cbb91b469 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,8 +45,6 @@ members = [ [workspace.lints.rust] unsafe_code = "allow" unused_unsafe = "deny" -#unsafe_op_in_unsafe_fn = "deny" -#unused_crate_dependencies = "deny" unused_import_braces = "deny" unused_lifetimes = "deny" keyword_idents_2018 = "deny" @@ -119,9 +117,9 @@ tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time", # Logging tracing = "0.1.41" -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } tracing-appender = "0.2.3" -log = "0.4.27" +log = "0.4.28" console-subscriber = "0.4.1" # Concurrency/Parallelism @@ -132,52 +130,51 @@ rusty_pool = "0.7.0" crossbeam-queue = "0.3.12" # Network -reqwest = { version = "0.12.22", features = ["json"] } -ureq = "3.1.0" +reqwest = { version = "0.12.23", features = ["json"] } +ureq = "3.1.2" # Error handling -thiserror = "2.0.15" +thiserror = "2.0.16" # Cryptography -rand = "0.9.2" +rand = "0.10.0-rc.0" fnv = "1.0.7" -wyhash = "0.6.0" ahash = "0.8.12" +simplehash = "0.1.3" # Encoding/Serialization -serde = { version = "1.0.219", features = ["derive"] } -serde_json = "1.0.142" -serde_derive = "1.0.219" -serde_yaml_ng = "0.9.36" +serde = { version = "1.0.226", features = ["derive"] } +serde_json = "1.0.145" +serde_derive = "1.0.226" base64 = "0.22.1" bitcode = "0.6.7" bitcode_derive = "0.6.7" -toml = "0.9.5" +toml = "0.9.7" craftflow-nbt = "2.1.0" figment = { version = "0.10.19", features = ["toml", "env"] } -simd-json = "0.15.1" +simd-json = "0.16.0" # Bit manipulation byteorder = "1.5.0" # Data types dashmap = "7.0.0-rc2" -uuid = { version = "1.18.0", features = ["v4", "v3", "serde"] } -indexmap = { version = "2.10.0", features = ["serde"] } +uuid = { version = "1.18.1", features = ["v4", "v3", "serde"] } +indexmap = { version = "2.11.4", features = ["serde"] } # Macros lazy_static = "1.5.0" quote = "1.0.40" syn = "2.0.106" proc-macro2 = "1.0.101" -proc-macro-crate = "3.3.0" +proc-macro-crate = "3.4.0" paste = "1.0.15" maplit = "1.0.2" macro_rules_attribute = "0.2.2" # Magic dhat = "0.3.3" -ctor = "0.4.2" +ctor = "0.5.0" # Compression/Decompression flate2 = { version = "1.1.2", features = ["zlib"], default-features = false } @@ -188,10 +185,10 @@ lz4_flex = "0.11.5" # Database heed = "0.22.0" -moka = "0.12.10" +moka = "0.12.11" # CLI -clap = "4.5.45" +clap = "4.5.48" indicatif = "0.18.0" colored = "3.0.0" @@ -199,17 +196,20 @@ colored = "3.0.0" deepsize = "0.2.0" page_size = "0.6.0" enum-ordinalize = "4.3.0" -regex = "1.11.1" +regex = "1.11.3" noise = "0.9.0" -ctrlc = "3.4.7" +ctrlc = "3.5.0" num_cpus = "1.17.0" typename = "0.1.2" -bevy_ecs = { version = "0.16.1", features = ["multi_threaded", "trace"] } once_cell = "1.21.3" +# Bevy +bevy_math = "0.16.1" +bevy_ecs = { version = "0.16.1", features = ["multi_threaded", "trace"] } + # I/O -memmap2 = "0.9.7" -tempfile = "3.20.0" +memmap2 = "0.9.8" +tempfile = "3.23.0" # Benchmarking criterion = { version = "0.7.0", features = ["html_reports"] } diff --git a/src/bin/Cargo.toml b/src/bin/Cargo.toml index c7c757724..4e374d349 100644 --- a/src/bin/Cargo.toml +++ b/src/bin/Cargo.toml @@ -33,6 +33,7 @@ ferrumc-threadpool = { workspace = true } ferrumc-inventories = { workspace = true } once_cell = { workspace = true } serde_json = { workspace = true } +bevy_math = { workspace = true } tracing = { workspace = true } clap = { workspace = true, features = ["derive", "env"] } diff --git a/src/bin/src/main.rs b/src/bin/src/main.rs index 38731ff06..71348dc58 100644 --- a/src/bin/src/main.rs +++ b/src/bin/src/main.rs @@ -1,6 +1,7 @@ #![feature(try_blocks)] use crate::errors::BinaryError; +use bevy_math::IVec2; use clap::Parser; use ferrumc_config::server_config::get_global_config; use ferrumc_config::whitelist::create_whitelist; @@ -79,23 +80,23 @@ fn generate_chunks(state: GlobalState) -> Result<(), BinaryError> { let radius = get_global_config().chunk_render_distance as i32; for x in -radius..=radius { for z in -radius..=radius { - chunks.push((x, z)); + chunks.push(IVec2::new(x, z)); } } let mut batch = state.thread_pool.batch(); - for (x, z) in chunks { + for chunk_pos in chunks { let state_clone = state.clone(); batch.execute(move || { let chunk = state_clone .terrain_generator - .generate_chunk(x, z) + .generate_chunk(chunk_pos) .map(Arc::new); if let Err(e) = chunk { - error!("Error generating chunk ({}, {}): {:?}", x, z, e); + error!("Error generating chunk ({}): {:?}", chunk_pos, e); } else { let chunk = chunk.unwrap(); if let Err(e) = state_clone.world.save_chunk(chunk) { - error!("Error saving chunk ({}, {}): {:?}", x, z, e); + error!("Error saving chunk ({}): {:?}", chunk_pos, e); } } }); @@ -109,7 +110,10 @@ fn entry(start_time: Instant) -> Result<(), BinaryError> { let state = create_state(start_time)?; let global_state = Arc::new(state); create_whitelist(); - if !global_state.world.chunk_exists(0, 0, "overworld")? { + if !global_state + .world + .chunk_exists(IVec2::new(0, 0), "overworld")? + { generate_chunks(global_state.clone())?; } diff --git a/src/bin/src/packet_handlers/play_packets/place_block.rs b/src/bin/src/packet_handlers/play_packets/place_block.rs index 926aac8ac..4df45ba44 100644 --- a/src/bin/src/packet_handlers/play_packets/place_block.rs +++ b/src/bin/src/packet_handlers/play_packets/place_block.rs @@ -12,8 +12,10 @@ use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_state::GlobalStateResource; use tracing::{debug, error, trace}; +use bevy_math::{IVec2, IVec3}; use ferrumc_inventories::hotbar::Hotbar; use ferrumc_inventories::inventory::Inventory; +use ferrumc_world::block_id::BlockId; use once_cell::sync::Lazy; use std::collections::HashMap; use std::str::FromStr; @@ -65,8 +67,7 @@ pub fn handle( item_id.0, mapped_block_id ); let mut chunk = match state.0.world.load_chunk_owned( - event.position.x >> 4, - event.position.z >> 4, + IVec2::new(event.position.x >> 4, event.position.z >> 4), "overworld", ) { Ok(chunk) => chunk, @@ -75,14 +76,11 @@ pub fn handle( continue 'ev_loop; } }; - let Ok(block_clicked) = chunk.get_block( + let block_clicked = chunk.get_block(IVec3::new( event.position.x, event.position.y as i32, event.position.z, - ) else { - debug!("Failed to get block at position: {:?}", event.position); - continue 'ev_loop; - }; + )); trace!("Block clicked: {:?}", block_clicked); // Use the face to determine the offset of the block to place let (x_block_offset, y_block_offset, z_block_offset) = match event.face.0 { @@ -128,9 +126,10 @@ pub fn handle( continue 'ev_loop; } - if let Err(err) = - chunk.set_block(x & 0xF, y as i32, z & 0xF, VarInt::new(*mapped_block_id)) - { + if let Err(err) = chunk.set_block( + IVec3::new(x & 0xF, y as i32, z & 0xF), + BlockId::from(VarInt::new(*mapped_block_id)), + ) { error!("Failed to set block: {:?}", err); continue 'ev_loop; } diff --git a/src/bin/src/packet_handlers/play_packets/player_action.rs b/src/bin/src/packet_handlers/play_packets/player_action.rs index c380c7de0..7c940001e 100644 --- a/src/bin/src/packet_handlers/play_packets/player_action.rs +++ b/src/bin/src/packet_handlers/play_packets/player_action.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use crate::errors::BinaryError; use bevy_ecs::prelude::{Entity, Query, Res}; +use bevy_math::{IVec2, IVec3}; use ferrumc_net::connection::StreamWriter; use ferrumc_net::packets::outgoing::block_change_ack::BlockChangeAck; use ferrumc_net::packets::outgoing::block_update::BlockUpdate; @@ -9,7 +10,6 @@ use ferrumc_net::PlayerActionReceiver; use ferrumc_net_codec::net_types::var_int::VarInt; use ferrumc_state::GlobalStateResource; use ferrumc_world::block_id::BlockId; -use ferrumc_world::vanilla_chunk_format::BlockData; use tracing::{debug, error, trace}; pub fn handle( @@ -23,8 +23,7 @@ pub fn handle( match event.status.0 { 0 => { let mut chunk = match state.0.clone().world.load_chunk_owned( - event.location.x >> 4, - event.location.z >> 4, + IVec2::new(event.location.x >> 4, event.location.z >> 4), "overworld", ) { Ok(chunk) => chunk, @@ -34,7 +33,10 @@ pub fn handle( .0 .clone() .terrain_generator - .generate_chunk(event.location.x >> 4, event.location.z >> 4)? + .generate_chunk(IVec2::new( + event.location.x >> 4, + event.location.z >> 4, + ))? } }; let (relative_x, relative_y, relative_z) = ( @@ -42,7 +44,10 @@ pub fn handle( event.location.y as i32, event.location.z.abs() % 16, ); - chunk.set_block(relative_x, relative_y, relative_z, BlockData::default())?; + chunk.set_block( + IVec3::new(relative_x, relative_y, relative_z), + BlockId::default(), + )?; // Save the chunk to disk state.0.world.save_chunk(Arc::new(chunk))?; for (eid, conn) in query { diff --git a/src/bin/src/packet_handlers/play_packets/player_loaded.rs b/src/bin/src/packet_handlers/play_packets/player_loaded.rs index 48dce958e..b92fba047 100644 --- a/src/bin/src/packet_handlers/play_packets/player_loaded.rs +++ b/src/bin/src/packet_handlers/play_packets/player_loaded.rs @@ -24,13 +24,13 @@ pub fn handle( ); continue; } - let head_block = state.0.world.get_block_and_fetch( - player_pos.x as i32, - player_pos.y as i32, - player_pos.z as i32, - "overworld", - ); - if let Ok(head_block) = head_block { + let chunk_coords = ferrumc_world::get_chunk_coordinates(player_pos.as_vec3()); + let head_block = + state + .0 + .world + .get_block_and_fetch(chunk_coords, player_pos.as_vec3(), "overworld"); + if let Some(head_block) = head_block { if head_block == BlockId(0) { tracing::info!( "Player {} loaded at position: ({}, {}, {})", diff --git a/src/bin/src/packet_handlers/play_packets/set_player_position.rs b/src/bin/src/packet_handlers/play_packets/set_player_position.rs index 78f00d5dc..edea052ce 100644 --- a/src/bin/src/packet_handlers/play_packets/set_player_position.rs +++ b/src/bin/src/packet_handlers/play_packets/set_player_position.rs @@ -1,4 +1,5 @@ use bevy_ecs::prelude::{Entity, EventWriter, Query, Res}; +use bevy_math::IVec2; use ferrumc_core::chunks::cross_chunk_boundary_event::CrossChunkBoundaryEvent; use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_net::SetPlayerPositionPacketReceiver; @@ -46,9 +47,9 @@ pub fn handle( ((new_position.z * 4096.0) - (position.z * 4096.0)) as i16, )); - let old_chunk = (position.x as i32 >> 4, position.z as i32 >> 4); + let old_chunk = IVec2::new(position.x as i32 >> 4, position.z as i32 >> 4); - let new_chunk = (new_position.x as i32 >> 4, new_position.z as i32 >> 4); + let new_chunk = IVec2::new(new_position.x as i32 >> 4, new_position.z as i32 >> 4); if old_chunk != new_chunk { cross_chunk_events.write(CrossChunkBoundaryEvent { diff --git a/src/bin/src/systems/cross_chunk_boundary.rs b/src/bin/src/systems/cross_chunk_boundary.rs index 27bd0d880..b8d610341 100644 --- a/src/bin/src/systems/cross_chunk_boundary.rs +++ b/src/bin/src/systems/cross_chunk_boundary.rs @@ -1,5 +1,6 @@ use crate::systems::send_chunks::send_chunks; use bevy_ecs::prelude::{EventReader, Query, Res}; +use bevy_math::IVec2; use ferrumc_config::server_config::get_global_config; use ferrumc_core::chunks::cross_chunk_boundary_event::CrossChunkBoundaryEvent; use ferrumc_net::connection::StreamWriter; @@ -21,28 +22,24 @@ pub fn cross_chunk_boundary( let radius = get_global_config().chunk_render_distance as i32; let mut old_chunk_seen = HashSet::new(); - for x in event.old_chunk.0 - radius..event.old_chunk.0 + radius { - for z in event.old_chunk.1 - radius..event.old_chunk.1 + radius { - old_chunk_seen.insert((x, z)); + for x in event.old_chunk.x - radius..event.old_chunk.x + radius { + for z in event.old_chunk.y - radius..event.old_chunk.y + radius { + old_chunk_seen.insert(IVec2::new(x, z)); } } let mut new_chunk_seen = HashSet::new(); - for x in event.new_chunk.0 - radius..event.new_chunk.0 + radius { - for z in event.new_chunk.1 - radius..event.new_chunk.1 + radius { - new_chunk_seen.insert((x, z)); + for x in event.new_chunk.x - radius..event.new_chunk.x + radius { + for z in event.new_chunk.y - radius..event.new_chunk.y + radius { + new_chunk_seen.insert(IVec2::new(x, z)); } } let needed_chunks: Vec<_> = new_chunk_seen .iter() .filter(|chunk| !old_chunk_seen.contains(chunk)) - .map(|chunk| { - let (x, z) = *chunk; - (x, z, "overworld".to_string()) - }) + .map(|chunk| (*chunk, "overworld".to_string())) .collect(); - let center_chunk = (event.new_chunk.0, event.new_chunk.1); let mut conn = query.get_mut(event.player).expect("Player does not exist"); - send_chunks(state.0.clone(), needed_chunks, &mut conn, center_chunk) + send_chunks(state.0.clone(), needed_chunks, &mut conn, event.new_chunk) .expect("Failed to send chunks") } } diff --git a/src/bin/src/systems/send_chunks.rs b/src/bin/src/systems/send_chunks.rs index 2e434cc3c..5015bad3b 100644 --- a/src/bin/src/systems/send_chunks.rs +++ b/src/bin/src/systems/send_chunks.rs @@ -1,5 +1,6 @@ use crate::errors::BinaryError; use bevy_ecs::prelude::Mut; +use bevy_math::IVec2; use ferrumc_net::compression::compress_packet; use ferrumc_net::connection::StreamWriter; use ferrumc_net::errors::NetError; @@ -16,21 +17,18 @@ use tracing::{error, trace}; pub fn send_chunks( state: GlobalState, - mut chunk_coords: Vec<(i32, i32, String)>, + mut chunk_coords: Vec<(IVec2, String)>, conn: &mut Mut, - // recv: &mut Mut, - center_chunk: (i32, i32), + center_chunk: IVec2, ) -> Result<(), BinaryError> { - let (center_x, center_z) = center_chunk; - // Sort the chunks by distance from the center - chunk_coords.sort_by(|(x1, z1, _), (x2, z2, _)| { - let dist1 = (((center_x - x1).pow(2) + (center_z - z1).pow(2)) as f64).sqrt(); - let dist2 = (((center_x - x2).pow(2) + (center_z - z2).pow(2)) as f64).sqrt(); - (dist1 as i32).cmp(&(dist2 as i32)) + chunk_coords.sort_by(|(p1, _), (p2, _)| { + let d1 = p1.distance_squared(center_chunk); + let d2 = p2.distance_squared(center_chunk); + d1.partial_cmp(&d2).unwrap() }); - let center_chunk_packet = SetCenterChunk::new(center_x, center_z); + let center_chunk_packet = SetCenterChunk::new(center_chunk); conn.send_packet_ref(¢er_chunk_packet)?; let batch_start_packet = ChunkBatchStart {}; @@ -42,40 +40,39 @@ pub fn send_chunks( let is_compressed = conn.compress.load(Ordering::Relaxed); - for (x, z, dim) in chunk_coords { + for (pos, dim) in chunk_coords { let state_clone = state.clone(); batch.execute(move || { - let (packet, x, z) = if state_clone.world.chunk_exists(x, z, &dim).unwrap_or(false) { + let (packet, pos) = if state_clone.world.chunk_exists(pos, &dim).unwrap_or(false) { let chunk = state_clone .world - .load_chunk(x, z, &dim) + .load_chunk(pos, &dim) .map_err(|err| NetError::Misc(err.to_string()))?; - Ok::<(Result, i32, i32), NetError>(( + Ok::<(Result, IVec2), NetError>(( ChunkAndLightData::from_chunk(&chunk), - x, - z, + pos, )) } else { - trace!("Generating chunk {}x{} in dimension {}", x, z, dim); + trace!("Generating chunk {} in dimension {}", pos, dim); // Don't bother saving the chunk if it hasn't been edited yet let chunk = state_clone .terrain_generator - .generate_chunk(x, z) + .generate_chunk(pos) .map_err(|err| NetError::Misc(err.to_string()))?; - Ok((ChunkAndLightData::from_chunk(&chunk), x, z)) + Ok((ChunkAndLightData::from_chunk(&chunk), pos)) }?; match packet { Ok(packet) => { if is_compressed { // Compress the packet if compression is enabled let compressed_packet = compress_packet(&packet, true, &WithLength)?; - Ok((compressed_packet, x, z)) + Ok((compressed_packet, pos)) } else { let mut buffer = Vec::new(); packet .encode(&mut buffer, &WithLength) .map_err(|e| NetError::Misc(e.to_string()))?; - Ok((buffer, x, z)) + Ok((buffer, pos)) } } Err(e) => { @@ -90,8 +87,8 @@ pub fn send_chunks( for packet in packets { match packet { - Ok((packet, x, z)) => { - trace!("Sending chunk data for chunk at coordinates ({}, {})", x, z); + Ok((packet, pos)) => { + trace!("Sending chunk data for chunk at coordinates ({})", pos); conn.send_raw_packet(packet)?; chunks_sent += 1; } diff --git a/src/lib/core/Cargo.toml b/src/lib/core/Cargo.toml index 1a9e9855d..09154cd76 100644 --- a/src/lib/core/Cargo.toml +++ b/src/lib/core/Cargo.toml @@ -13,6 +13,7 @@ ferrumc-text = { workspace = true } ferrumc-net-codec = { workspace = true } uuid = { workspace = true } crossbeam-queue = { workspace = true } +bevy_math = { workspace = true } [dev-dependencies] criterion = { workspace = true } diff --git a/src/lib/core/src/chunks/cross_chunk_boundary_event.rs b/src/lib/core/src/chunks/cross_chunk_boundary_event.rs index 33e7b472d..d7d50475e 100644 --- a/src/lib/core/src/chunks/cross_chunk_boundary_event.rs +++ b/src/lib/core/src/chunks/cross_chunk_boundary_event.rs @@ -1,9 +1,10 @@ use bevy_ecs::prelude::{Entity, Event}; +use bevy_math::IVec2; // Fired when a player crosses a chunk boundary. Assumes dimensions are the same #[derive(Event)] pub struct CrossChunkBoundaryEvent { pub player: Entity, - pub old_chunk: (i32, i32), - pub new_chunk: (i32, i32), + pub old_chunk: IVec2, + pub new_chunk: IVec2, } diff --git a/src/lib/core/src/transform/position.rs b/src/lib/core/src/transform/position.rs index 7605f66e6..0983ffc4e 100644 --- a/src/lib/core/src/transform/position.rs +++ b/src/lib/core/src/transform/position.rs @@ -10,6 +10,16 @@ pub struct Position { pub z: f64, } +impl Position { + pub fn as_vec3(&self) -> bevy_math::IVec3 { + bevy_math::IVec3::new(self.x as i32, self.y as i32, self.z as i32) + } + + pub fn from_vec3(vec: bevy_math::IVec3) -> Self { + Self::new(vec.x as f64, vec.y as f64, vec.z as f64) + } +} + impl From for Position { fn from(pos: NetworkPosition) -> Self { Self::new(pos.x as f64, pos.y as f64, pos.z as f64) diff --git a/src/lib/derive_macros/src/lib.rs b/src/lib/derive_macros/src/lib.rs index b40c061fa..dd7b49474 100644 --- a/src/lib/derive_macros/src/lib.rs +++ b/src/lib/derive_macros/src/lib.rs @@ -69,7 +69,7 @@ pub fn lookup_packet(input: TokenStream) -> TokenStream { /// /// Usage example: /// -/// ``` +/// ```ignore /// #[command("hello")] /// fn command(#[sender] sender: Sender) { /// sender.send_message(TextComponent::from("Hello, world!"), false); diff --git a/src/lib/net/Cargo.toml b/src/lib/net/Cargo.toml index f205d5c6d..82b91362b 100644 --- a/src/lib/net/Cargo.toml +++ b/src/lib/net/Cargo.toml @@ -36,6 +36,8 @@ indexmap = { workspace = true } lazy_static = { workspace = true } yazi = { workspace = true } ferrumc-inventories = { workspace = true } +ferrumc-general-purpose = { workspace = true } +bevy_math = { workspace = true } [dev-dependencies] diff --git a/src/lib/net/benches/packets.rs b/src/lib/net/benches/packets.rs index 1362f25e4..3a6b289d4 100644 --- a/src/lib/net/benches/packets.rs +++ b/src/lib/net/benches/packets.rs @@ -1,3 +1,4 @@ +use bevy_math::IVec2; use criterion::measurement::WallTime; use ferrumc_net_codec::encode::{NetEncode, NetEncodeOpts}; use std::hint::black_box; @@ -8,7 +9,7 @@ pub fn bench_packets(c: &mut criterion::BenchmarkGroup) { fn bench_chunk_packet(c: &mut criterion::BenchmarkGroup) { let chunk = ferrumc_world_gen::WorldGenerator::new(0) - .generate_chunk(0, 0) + .generate_chunk(black_box(IVec2::new(0, 0))) .unwrap(); let chunk_packet = black_box( ferrumc_net::packets::outgoing::chunk_and_light_data::ChunkAndLightData::from_chunk(&chunk) diff --git a/src/lib/net/src/conn_init/login.rs b/src/lib/net/src/conn_init/login.rs index bdb50ce41..04da4306e 100644 --- a/src/lib/net/src/conn_init/login.rs +++ b/src/lib/net/src/conn_init/login.rs @@ -6,6 +6,7 @@ use crate::errors::{NetError, PacketError}; use crate::packets::incoming::packet_skeleton::PacketSkeleton; use crate::packets::outgoing::{commands::CommandsPacket, registry_data::REGISTRY_PACKETS}; use crate::ConnState::*; +use bevy_math::IVec2; use ferrumc_config::server_config::get_global_config; use ferrumc_core::identity::player_identity::PlayerIdentity; use ferrumc_macros::lookup_packet; @@ -258,7 +259,8 @@ pub(super) async fn login( // ============================================================================================= // 16 Send center chunk packet (player spawn location) - let center_chunk = crate::packets::outgoing::set_center_chunk::SetCenterChunk::new(0, 0); + let center_chunk = + crate::packets::outgoing::set_center_chunk::SetCenterChunk::new(IVec2::new(0, 0)); conn_write.send_packet(center_chunk)?; // ============================================================================================= @@ -272,7 +274,7 @@ pub(super) async fn login( batch.execute({ let state = state.clone(); move || -> Result, NetError> { - let chunk = state.world.load_chunk(x, z, "overworld")?; + let chunk = state.world.load_chunk(IVec2::new(x, z), "overworld")?; let chunk_data = crate::packets::outgoing::chunk_and_light_data::ChunkAndLightData::from_chunk( &chunk, diff --git a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs index b7bf647b2..46dc3fa38 100644 --- a/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs +++ b/src/lib/net/src/packets/outgoing/chunk_and_light_data.rs @@ -1,11 +1,13 @@ use crate::errors::NetError; use byteorder::{BigEndian, WriteBytesExt}; +use ferrumc_general_purpose::palette::PaletteType; use ferrumc_macros::{packet, NetEncode}; use ferrumc_net_codec::net_types::bitset::BitSet; use ferrumc_net_codec::net_types::byte_array::ByteArray; use ferrumc_net_codec::net_types::length_prefixed_vec::LengthPrefixedVec; use ferrumc_net_codec::net_types::var_int::VarInt; -use ferrumc_world::chunk_format::{Chunk, PaletteType}; +use ferrumc_world::block_id::BlockId; +use ferrumc_world::chunk_format::Chunk; use std::io::Cursor; use std::ops::Not; use tracing::warn; @@ -97,25 +99,35 @@ impl ChunkAndLightData { }; block_light_data.push(section_block_light_data); - raw_data.write_u16::(section.block_states.non_air_blocks)?; + let non_air_blocks = { + // Air + let mut air_blocks = section.block_states.get_count(&BlockId(0)); + // Cave air + air_blocks += section.block_states.get_count(&BlockId(13982)); + // Void air + air_blocks += section.block_states.get_count(&BlockId(13981)); + 4096 - air_blocks as u16 + }; + + raw_data.write_u16::(non_air_blocks)?; - match §ion.block_states.block_data { + match §ion.block_states.palette_type { PaletteType::Single(val) => { // debug!("Single palette type: {:?}", (chunk.x, chunk.z)); raw_data.write_u8(0)?; - val.write(&mut raw_data)?; + VarInt::from(*val).write(&mut raw_data)?; // VarInt::new(0).write(&mut raw_data)?; } PaletteType::Indirect { - bits_per_block, + bits_per_entry, data, palette, } => { // debug!("Indirect palette type: {:?}", (chunk.x, chunk.z)); - raw_data.write_u8(*bits_per_block)?; + raw_data.write_u8(*bits_per_entry)?; VarInt::new(palette.len() as i32).write(&mut raw_data)?; for palette_entry in palette { - palette_entry.write(&mut raw_data)?; + VarInt::from(palette_entry.1).write(&mut raw_data)?; } // VarInt::new(data.len() as i32).write(&mut raw_data)?; for data_entry in data { diff --git a/src/lib/net/src/packets/outgoing/set_center_chunk.rs b/src/lib/net/src/packets/outgoing/set_center_chunk.rs index 8bf3126d3..b152f9195 100644 --- a/src/lib/net/src/packets/outgoing/set_center_chunk.rs +++ b/src/lib/net/src/packets/outgoing/set_center_chunk.rs @@ -1,3 +1,4 @@ +use bevy_math::IVec2; use ferrumc_macros::{packet, NetEncode}; use ferrumc_net_codec::net_types::var_int::VarInt; @@ -9,10 +10,10 @@ pub struct SetCenterChunk { } impl SetCenterChunk { - pub fn new(x: i32, z: i32) -> Self { + pub fn new(pos: IVec2) -> Self { Self { - x: VarInt::new(x), - z: VarInt::new(z), + x: VarInt::new(pos.x), + z: VarInt::new(pos.y), } } } diff --git a/src/lib/storage/Cargo.toml b/src/lib/storage/Cargo.toml index 0485e12dc..706b44eea 100644 --- a/src/lib/storage/Cargo.toml +++ b/src/lib/storage/Cargo.toml @@ -18,7 +18,7 @@ parking_lot = { workspace = true } [dev-dependencies] criterion = { workspace = true } tempfile = { workspace = true } -wyhash = { workspace = true } +simplehash = { workspace = true } [[bench]] name = "storage_bench" diff --git a/src/lib/storage/src/lmdb.rs b/src/lib/storage/src/lmdb.rs index 0d652ede2..e75569cf8 100644 --- a/src/lib/storage/src/lmdb.rs +++ b/src/lib/storage/src/lmdb.rs @@ -251,14 +251,13 @@ impl LmdbBackend { mod tests { use super::*; use std::fs::remove_dir_all; - use std::hash::Hasher; use tempfile::tempdir; fn hash_2_to_u128(a: u64, b: u64) -> u128 { - let mut hasher = wyhash::WyHash::with_seed(0); - hasher.write_u64(a); - hasher.write_u64(b); - hasher.finish() as u128 + let mut hasher = simplehash::MurmurHasher128::new(0); + hasher.write(&a.to_le_bytes()); + hasher.write(&b.to_be_bytes()); + hasher.finish_u128() } #[test] diff --git a/src/lib/utils/general_purpose/Cargo.toml b/src/lib/utils/general_purpose/Cargo.toml index 0d05e8ceb..40a5016ae 100644 --- a/src/lib/utils/general_purpose/Cargo.toml +++ b/src/lib/utils/general_purpose/Cargo.toml @@ -7,4 +7,9 @@ edition = "2021" thiserror = { workspace = true } tracing = { workspace = true } fnv = { workspace = true } +deepsize = { workspace = true } +bitcode = { workspace = true } + +[dev-dependencies] +rand = { workspace = true } diff --git a/src/lib/utils/general_purpose/src/data_packing/i32.rs b/src/lib/utils/general_purpose/src/data_packing/i32.rs index 41ec07da5..0c430b961 100644 --- a/src/lib/utils/general_purpose/src/data_packing/i32.rs +++ b/src/lib/utils/general_purpose/src/data_packing/i32.rs @@ -19,7 +19,7 @@ use crate::data_packing::errors::DataPackingError; /// * `DataPackingError::NotEnoughBits` - If `offset + size` exceeds 64 bits. /// Reads an n-bit integer from a packed `i64`. pub fn read_nbit_i32( - word: &i64, + word: &u64, bit_size: usize, bit_offset: u32, ) -> Result { @@ -37,7 +37,7 @@ pub fn read_nbit_i32( let mask = (1u64 << bit_size) - 1; // Extract the value from the word - let value = ((*word as u64) >> bit_offset) & mask; + let value = ((*word) >> bit_offset) & mask; // Cast to i32 and return Ok(value as i32) @@ -86,7 +86,7 @@ mod tests { /// Tests the `read_nbit_i32` function with various inputs. #[test] fn test_read_nbit_i32() { - let data: i64 = 0b110101011; + let data: u64 = 0b110101011; assert_eq!(read_nbit_i32(&data, 3, 0).unwrap(), 0b011); assert_eq!(read_nbit_i32(&data, 3, 3).unwrap(), 0b101); assert_eq!(read_nbit_i32(&data, 3, 6).unwrap(), 0b110); diff --git a/src/lib/utils/general_purpose/src/lib.rs b/src/lib/utils/general_purpose/src/lib.rs index d6eb177fe..36b5dabdf 100644 --- a/src/lib/utils/general_purpose/src/lib.rs +++ b/src/lib/utils/general_purpose/src/lib.rs @@ -1,4 +1,7 @@ +#![feature(assert_matches)] + pub mod data_packing; pub mod hashing; +pub mod palette; pub mod paths; pub mod simd; diff --git a/src/lib/utils/general_purpose/src/palette/count.rs b/src/lib/utils/general_purpose/src/palette/count.rs new file mode 100644 index 000000000..6a5a7722e --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/count.rs @@ -0,0 +1,79 @@ +use crate::palette::{Palette, PaletteType}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + /// Retrieves the count of a specific value in the palette. + /// + /// # Arguments + /// * `value` - A reference to the value whose count is to be determined. + /// + /// # Returns + /// * `usize` - The count of the specified value in the palette. + /// + /// # Variants + /// The behavior depends on the `PaletteType`: + /// - `Single`: Returns the length if the value matches, otherwise 0. + /// - `Indirect`: Searches for the value in the palette and returns its count. If not found, returns 0. + /// - `Direct`: Counts the occurrences of the value in the list of values. + pub fn get_count(&self, value: &T) -> usize + where + T: Eq, + { + match &self.palette_type { + // Single variant: Check if the value matches the stored value. + PaletteType::Single(_) => self.count_single(value), + // Indirect variant: Search for the value in the palette and return its count. + PaletteType::Indirect { .. } => self.count_indirect(value), + // Direct variant: Count the occurrences of the value in the list of values. + PaletteType::Direct(_) => self.count_direct(value), + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn test_single_variant_match() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_count(&42), 5); + } + + #[test] + fn test_single_variant_no_match() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_count(&7), 0); + } + + #[test] + fn test_indirect_variant() { + let palette = Palette::from([1, 2, 1].to_vec()); + assert_eq!(palette.get_count(&1), 2); + assert_eq!(palette.get_count(&2), 1); + assert_eq!(palette.get_count(&3), 0); + } + + #[test] + fn test_direct_variant() { + let palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 1, 3]), + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_count(&1), 2); + assert_eq!(palette.get_count(&2), 1); + assert_eq!(palette.get_count(&3), 1); + assert_eq!(palette.get_count(&4), 0); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/direct/count.rs b/src/lib/utils/general_purpose/src/palette/direct/count.rs new file mode 100644 index 000000000..404697b46 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/direct/count.rs @@ -0,0 +1,59 @@ +use crate::palette::{Palette, PaletteType}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn count_direct(&self, value: &T) -> usize { + if let PaletteType::Direct(data) = &self.palette_type { + data.iter().filter(|&v| v == value).count() + } else { + 0 + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn count_direct_value_present_returns_correct_count() { + let palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3, 2, 1]), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_direct(&2), 2); + } + + #[test] + fn count_direct_value_not_present_returns_zero() { + let palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3]), + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_direct(&4), 0); + } + + #[test] + fn count_direct_empty_palette_returns_zero() { + let palette = Palette { + palette_type: PaletteType::Direct(Vec::new()), + length: 0, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_direct(&1), 0); + } + + #[test] + fn count_direct_non_direct_palette_returns_zero() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_direct(&42), 0); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/direct/get.rs b/src/lib/utils/general_purpose/src/palette/direct/get.rs new file mode 100644 index 000000000..6a3f92b2a --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/direct/get.rs @@ -0,0 +1,64 @@ +use crate::palette::{Palette, PaletteType}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn get_direct(&self, index: usize) -> Option<&T> { + if let PaletteType::Direct(values) = &self.palette_type { + values.get(index) + } else { + panic!("Called get_direct on a non-direct palette"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn get_direct_within_bounds_returns_correct_value() { + let palette = Palette { + palette_type: PaletteType::Direct(vec![10, 20, 30, 40]), + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_direct(0), Some(&10)); + assert_eq!(palette.get_direct(1), Some(&20)); + assert_eq!(palette.get_direct(2), Some(&30)); + assert_eq!(palette.get_direct(3), Some(&40)); + } + + #[test] + fn get_direct_out_of_bounds_returns_none() { + let palette = Palette { + palette_type: PaletteType::Direct(vec![10, 20, 30, 40]), + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_direct(4), None); + assert_eq!(palette.get_direct(100), None); + } + + #[test] + #[should_panic(expected = "Called get_direct on a non-direct palette")] + fn get_direct_non_direct_palette_panics() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 1, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.get_direct(0); + } + + #[test] + fn get_direct_empty_palette_returns_none() { + let palette = Palette { + palette_type: PaletteType::Direct(Vec::::new()), + length: 0, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_direct(0), None); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/direct/mod.rs b/src/lib/utils/general_purpose/src/palette/direct/mod.rs new file mode 100644 index 000000000..dfa2ccad0 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/direct/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod count; +pub(crate) mod get; +pub(crate) mod optimise; +pub(crate) mod resize; +pub(crate) mod set; diff --git a/src/lib/utils/general_purpose/src/palette/direct/optimise.rs b/src/lib/utils/general_purpose/src/palette/direct/optimise.rs new file mode 100644 index 000000000..c701b4f08 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/direct/optimise.rs @@ -0,0 +1,74 @@ +// Downgrade to indirect if bpe drops below threshold, downgrade to single if there is only one type of element + +use crate::palette::{Palette, PaletteType}; +use std::hash::Hash; + +impl Palette +where + T: Clone + Default + PartialEq + Eq + Hash, +{ + pub(crate) fn optimise_direct(&mut self) { + if let PaletteType::Direct(values) = &self.palette_type { + let unique_values = crate::palette::utils::calculate_unique_values(values); + let new_bpe = crate::palette::utils::calculate_bits_per_entry(unique_values); + if new_bpe < self.indirect_threshold { + // Downgrade to indirect + let new_palette = Palette::from(values.clone()); + self.palette_type = new_palette.palette_type; + } else if unique_values == 1 { + // Downgrade to single value + let single_value = values[0].clone(); + self.palette_type = PaletteType::Single(single_value); + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType}; + + #[test] + fn optimise_direct_downgrades_to_indirect_when_bpe_below_threshold() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3, 4, 5, 6]), + indirect_threshold: 8, + length: 6, + }; + palette.optimise_direct(); + assert!(matches!(palette.palette_type, PaletteType::Indirect { .. })); + } + + #[test] + fn optimise_direct_downgrades_to_single_when_only_one_unique_value() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 1, 1]), + indirect_threshold: 4, + length: 3, + }; + palette.optimise_direct(); + assert!(matches!(palette.palette_type, PaletteType::Single(value) if value == 1)); + } + + #[test] + fn optimise_direct_does_not_downgrade_when_bpe_above_threshold() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3]), + indirect_threshold: 2, + length: 3, + }; + palette.optimise_direct(); + assert!(matches!(palette.palette_type, PaletteType::Direct(_))); + } + + #[test] + fn optimise_direct_handles_empty_values_gracefully() { + let mut palette = Palette { + palette_type: PaletteType::::Direct(vec![]), + indirect_threshold: 4, + length: 0, + }; + palette.optimise_direct(); + assert!(matches!(palette.palette_type, PaletteType::Direct(_))); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/direct/resize.rs b/src/lib/utils/general_purpose/src/palette/direct/resize.rs new file mode 100644 index 000000000..44bc1e80c --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/direct/resize.rs @@ -0,0 +1,82 @@ +use crate::palette::{Palette, PaletteType}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn resize_direct(&mut self, new_size: usize) { + if let PaletteType::Direct(values) = &mut self.palette_type { + values.resize(new_size, T::default()); + self.length = new_size; + } else { + panic!("Palette is not in direct mode"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn resize_direct_increases_size_with_default_values() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3]), + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_direct(5); + assert_eq!(palette.length, 5); + assert_eq!( + palette.palette_type, + PaletteType::Direct(vec![1, 2, 3, 0, 0]) + ); + } + + #[test] + fn resize_direct_decreases_size_truncating_values() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3, 4, 5]), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_direct(3); + assert_eq!(palette.length, 3); + assert_eq!(palette.palette_type, PaletteType::Direct(vec![1, 2, 3])); + } + + #[test] + #[should_panic(expected = "Palette is not in direct mode")] + fn resize_direct_non_direct_palette_panics() { + let mut palette = Palette { + palette_type: PaletteType::Single(42), + length: 1, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_direct(5); + } + + #[test] + fn resize_direct_to_same_size_does_nothing() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3]), + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_direct(3); + assert_eq!(palette.length, 3); + assert_eq!(palette.palette_type, PaletteType::Direct(vec![1, 2, 3])); + } + + #[test] + fn resize_direct_empty_palette_increases_size() { + let mut palette = Palette { + palette_type: PaletteType::Direct(Vec::new()), + length: 0, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_direct(4); + assert_eq!(palette.length, 4); + assert_eq!(palette.palette_type, PaletteType::Direct(vec![0, 0, 0, 0])); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/direct/set.rs b/src/lib/utils/general_purpose/src/palette/direct/set.rs new file mode 100644 index 000000000..491793cd4 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/direct/set.rs @@ -0,0 +1,55 @@ +use crate::palette::{Palette, PaletteType}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn set_direct(&mut self, index: usize, value: T) { + if let PaletteType::Direct(values) = &mut self.palette_type { + if index >= values.len() { + panic!("Index out of bounds"); + } + values[index] = value; + } else { + panic!("Palette is not in direct mode"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn set_direct_within_bounds_updates_value() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3, 4]), + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_direct(2, 99); + assert_eq!(palette.palette_type, PaletteType::Direct(vec![1, 2, 99, 4])); + } + + #[test] + #[should_panic(expected = "Index out of bounds")] + fn set_direct_out_of_bounds_panics() { + let mut palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3]), + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_direct(5, 99); + } + + #[test] + #[should_panic(expected = "Palette is not in direct mode")] + fn set_direct_non_direct_palette_panics() { + let mut palette = Palette { + palette_type: PaletteType::Single(42), + length: 1, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_direct(0, 99); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/get.rs b/src/lib/utils/general_purpose/src/palette/get.rs new file mode 100644 index 000000000..ab78e7783 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/get.rs @@ -0,0 +1,48 @@ +use crate::palette::Palette; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub fn get(&self, index: usize) -> Option<&T> { + if index >= self.length { + return None; + } + match &self.palette_type { + crate::palette::PaletteType::Single(_) => self.get_single(index), + crate::palette::PaletteType::Indirect { .. } => self.get_indirect(index), + crate::palette::PaletteType::Direct(_) => self.get_direct(index), + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn test_get_single_palette() { + let palette = Palette::new(1, 42, INDIRECT_THRESHOLD); + assert_eq!(palette.get(0), Some(&42)); + assert_eq!(palette.get(1), None); + } + + #[test] + fn test_get_direct_palette() { + let palette = Palette::from(vec![10, 20, 30]); + assert_eq!(palette.get(0), Some(&10)); + assert_eq!(palette.get(1), Some(&20)); + assert_eq!(palette.get(2), Some(&30)); + assert_eq!(palette.get(3), None); + } + + #[test] + fn test_get_indirect_palette() { + let palette = Palette::from(vec![200, 300, 200, 300]); + assert_eq!(palette.get(0), Some(&200)); + assert_eq!(palette.get(1), Some(&300)); + assert_eq!(palette.get(2), Some(&200)); + assert_eq!(palette.get(3), Some(&300)); + assert_eq!(palette.get(4), None); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/indirect/count.rs b/src/lib/utils/general_purpose/src/palette/indirect/count.rs new file mode 100644 index 000000000..7f9d7447e --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/indirect/count.rs @@ -0,0 +1,62 @@ +use crate::palette::{Palette, PaletteType}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn count_indirect(&self, value: &T) -> usize { + if let PaletteType::Indirect { palette, .. } = &self.palette_type { + palette + .iter() + .find(|v| v.1 == *value) + .map(|v| v.0) + .unwrap_or(0) as usize + } else { + panic!("indirect count called on non-indirect palette"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn count_existing_value_returns_correct_count() { + let palette = Palette { + palette_type: PaletteType::Indirect { + palette: vec![(3, 42), (2, 7)], + data: vec![], + bits_per_entry: 4, + }, + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_indirect(&42), 3); + } + + #[test] + fn count_non_existing_value_returns_zero() { + let palette = Palette { + palette_type: PaletteType::Indirect { + palette: vec![(3, 42), (2, 7)], + data: vec![], + bits_per_entry: 4, + }, + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_indirect(&99), 0); + } + + #[test] + #[should_panic(expected = "indirect count called on non-indirect palette")] + fn count_non_indirect_palette_panics() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.count_indirect(&42); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/indirect/get.rs b/src/lib/utils/general_purpose/src/palette/indirect/get.rs new file mode 100644 index 000000000..895be1fa8 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/indirect/get.rs @@ -0,0 +1,68 @@ +use crate::palette::Palette; + +use crate::palette::utils::read_index; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn get_indirect(&self, index: usize) -> Option<&T> { + if let crate::palette::PaletteType::Indirect { + bits_per_entry, + data, + palette, + } = &self.palette_type + { + if index >= self.length { + return None; + } + let pi = read_index(data, *bits_per_entry, index) as usize; + palette.get(pi).map(|x| &x.1) + } else { + panic!("get_indirect called on non-indirect palette"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn get_indirect_within_bounds_returns_correct_value() { + let palette = Palette::from(vec![10, 20, 30, 40]); + assert!(matches!( + palette.palette_type, + crate::palette::PaletteType::Indirect { .. } + )); + assert_eq!(palette.get_indirect(0), Some(&10)); + assert_eq!(palette.get_indirect(1), Some(&20)); + assert_eq!(palette.get_indirect(2), Some(&30)); + assert_eq!(palette.get_indirect(3), Some(&40)); + } + + #[test] + fn get_indirect_out_of_bounds_returns_none() { + let palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: vec![0b0001_0010_0011_0100], + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_indirect(4), None); + } + + #[test] + #[should_panic(expected = "get_indirect called on non-indirect palette")] + fn get_indirect_non_indirect_palette_panics() { + let palette = Palette { + palette_type: crate::palette::PaletteType::Single(42), + length: 1, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.get_indirect(0); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/indirect/mod.rs b/src/lib/utils/general_purpose/src/palette/indirect/mod.rs new file mode 100644 index 000000000..dfa2ccad0 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/indirect/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod count; +pub(crate) mod get; +pub(crate) mod optimise; +pub(crate) mod resize; +pub(crate) mod set; diff --git a/src/lib/utils/general_purpose/src/palette/indirect/optimise.rs b/src/lib/utils/general_purpose/src/palette/indirect/optimise.rs new file mode 100644 index 000000000..67b9398a0 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/indirect/optimise.rs @@ -0,0 +1,259 @@ +use crate::palette::{Palette, PaletteType, MIN_BITS_PER_ENTRY}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn optimise_indirect(&mut self) { + let (old_bits_per_entry, old_data, old_palette, length) = + match std::mem::take(&mut self.palette_type) { + PaletteType::Indirect { + bits_per_entry, + data, + palette, + } => (bits_per_entry, data, palette, self.length), + other => { + self.palette_type = other; + panic!("optimise_indirect called on non-indirect palette"); + } + }; + + // Filter out unused entries and build old_index -> new_index map + let mut index_map: Vec> = Vec::with_capacity(old_palette.len()); + let mut new_palette = Vec::with_capacity(old_palette.len()); + for (count, value) in old_palette.into_iter() { + if count > 0 { + let new_idx = new_palette.len() as u16; + index_map.push(Some(new_idx)); + new_palette.push((count, value)); + } else { + index_map.push(None); + } + } + + // If nothing removed, just restore (but still allow bits_per_entry shrink) + let removed_any = index_map.iter().any(|m| m.is_none()); + // Extract all old indices + let get_index = |data: &[i64], bpe: u8, i: usize| -> i64 { + let bpe_usize = bpe as usize; + let bit_index = i * bpe_usize; + let i64_index = bit_index / 64; + let bit_offset = bit_index % 64; + let mask = if bpe == 64 { + i64::MAX + } else { + (1i64 << bpe) - 1 + }; + if bit_offset + bpe_usize <= 64 { + (data[i64_index] >> bit_offset) & mask + } else { + let low = data[i64_index] >> bit_offset; + let high = data[i64_index + 1] << (64 - bit_offset); + (low | high) & mask + } + }; + + let mut remapped_indices: Vec = Vec::with_capacity(length); + for i in 0..length { + let old_index = get_index(&old_data, old_bits_per_entry, i) as usize; + if old_index >= index_map.len() { + panic!("Corrupt data: palette index out of range"); + } + let new_index = if removed_any { + index_map[old_index].expect("Data referenced a removed palette entry") + } else { + old_index as u16 + }; + remapped_indices.push(new_index); + } + + // Recalculate bits_per_entry + let needed_bits = if new_palette.is_empty() || new_palette.len() == 1 { + MIN_BITS_PER_ENTRY + } else { + let max_index = new_palette.len() - 1; + let mut bits = 0u8; + while (1usize << bits) <= max_index { + bits += 1; + } + bits.max(MIN_BITS_PER_ENTRY) + }; + + // Pack new indices + let total_bits = length * needed_bits as usize; + let mut new_data = vec![0i64; total_bits.div_ceil(64)]; + + let put_index = |data: &mut [i64], bpe: u8, i: usize, value: i64| { + let bpe_usize = bpe as usize; + let bit_index = i * bpe_usize; + let i64_index = bit_index / 64; + let bit_offset = bit_index % 64; + let mask = if bpe == 64 { + i64::MAX + } else { + (1i64 << bpe) - 1 + }; + if bit_offset + bpe_usize <= 64 { + data[i64_index] &= !(mask << bit_offset); + data[i64_index] |= (value & mask) << bit_offset; + } else { + let low_bits = 64 - bit_offset; + let high_bits = bpe_usize - low_bits; + let low_mask = (1i64 << low_bits) - 1; + let high_mask = (1i64 << high_bits) - 1; + + data[i64_index] &= !(low_mask << bit_offset); + data[i64_index] |= (value & low_mask) << bit_offset; + + let high_part = value >> low_bits; + data[i64_index + 1] &= !high_mask; + data[i64_index + 1] |= high_part & high_mask; + } + }; + + for (i, &idx) in remapped_indices.iter().enumerate() { + put_index(&mut new_data, needed_bits, i, idx as i64); + } + + if new_palette.len() == 1 { + // If only one entry remains, convert to Single + self.palette_type = PaletteType::Single(new_palette[0].1.clone()); + self.length = length; + return; + } + + self.palette_type = PaletteType::Indirect { + bits_per_entry: needed_bits, + data: new_data, + palette: new_palette, + }; + } +} + +#[cfg(test)] +mod tests { + use crate::palette::utils::pack_indices; + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn optimise_indirect_removes_unused_palette_entries() { + // Palette entries (index -> (count, value)): + // 0: (2, 10) + // 1: (0, 20) <-- will be removed + // 2: (1, 30) + // Data indices reference only 0,2,0 to avoid pointing at the removed entry. + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: pack_indices(&[0u16, 2, 0], 4), + palette: vec![(2, 10), (0, 20), (1, 30)], + }, + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.optimise_indirect(); + match &palette.palette_type { + crate::palette::PaletteType::Indirect { + bits_per_entry, + data, + palette, + } => { + // After removal, palette order becomes: old0 -> new0, old2 -> new1 + // Remapped indices: [0,1,0] + assert_eq!(*bits_per_entry, 1.max(crate::palette::MIN_BITS_PER_ENTRY)); // stays at MIN_BITS_PER_ENTRY (4) + let _expected = pack_indices(&[0u16, 1, 0], 1); + // Because MIN_BITS_PER_ENTRY = 4, data is stored with 4 bits per entry; re-pack to 4-bit form for comparison. + let expected_padded = pack_indices(&[0u16, 1, 0], 4); + assert_eq!(*data, expected_padded); + assert_eq!(palette.len(), 2); + } + _ => panic!("expected Indirect"), + } + } + + #[test] + fn optimise_indirect_rebuilds_data_with_new_indices() { + use crate::palette::utils::pack_indices; + + // Palette entries (index -> (count, value)): + // 0: (2, 10) + // 1: (0, 20) <-- will be removed + // 2: (1, 30) + // 3: (1, 40) + // Data indices reference only 0,2,3,0 to avoid pointing at the removed entry. + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: pack_indices(&[0u16, 2, 3, 0], 4), + palette: vec![(2, 10), (0, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.optimise_indirect(); + match &palette.palette_type { + crate::palette::PaletteType::Indirect { + bits_per_entry, + data, + palette, + } => { + // After removal, palette order becomes: old0 -> new0, old2 -> new1, old3 -> new2 + // Remapped indices: [0,1,2,0] + assert_eq!(*bits_per_entry, 2.max(crate::palette::MIN_BITS_PER_ENTRY)); // stays at MIN_BITS_PER_ENTRY (4) + let _expected = pack_indices(&[0u16, 1, 2, 0], 2); + // Because MIN_BITS_PER_ENTRY = 4, data is stored with 4 bits per entry; re-pack to 4-bit form for comparison. + let expected_padded = pack_indices(&[0u16, 1, 2, 0], 4); + assert_eq!(*data, expected_padded); + assert_eq!(palette.len(), 3); + } + _ => panic!("expected Indirect"), + } + } + + #[test] + fn optimise_indirect_recalculates_bits_per_entry() { + use crate::palette::utils::pack_indices; + + // Palette entries (index -> (count, value)): + // 0: (1, 10) + // 1: (1, 20) + // 2: (1, 30) + // 3: (1, 40) + // Data indices reference all entries. + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: pack_indices(&[0u16, 1, 2, 3], 4), + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.optimise_indirect(); + match &palette.palette_type { + crate::palette::PaletteType::Indirect { + bits_per_entry, + data, + palette, + } => { + // No entries removed; bits_per_entry should remain the same. + assert_eq!(*bits_per_entry, 4); + let expected = pack_indices(&[0u16, 1, 2, 3], 4); + assert_eq!(*data, expected); + assert_eq!(palette.len(), 4); + } + _ => panic!("expected Indirect"), + } + } + + #[test] + #[should_panic(expected = "optimise_indirect called on non-indirect palette")] + fn optimise_indirect_non_indirect_palette_panics() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.optimise_indirect(); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/indirect/resize.rs b/src/lib/utils/general_purpose/src/palette/indirect/resize.rs new file mode 100644 index 000000000..ee77d3b77 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/indirect/resize.rs @@ -0,0 +1,113 @@ +use crate::palette::Palette; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn resize_indirect(&mut self, new_length: usize) { + if let crate::palette::PaletteType::Indirect { + bits_per_entry, + data, + palette, + } = &mut self.palette_type + { + if new_length == self.length { + return; + } + + let old_length = self.length; + + let entries_per_u64 = 64 / *bits_per_entry as usize; + let needed_i64s = new_length.div_ceil(entries_per_u64); + + if new_length < old_length { + data.truncate(needed_i64s); + // (Optional future improvement: decrement counts for removed tail entries.) + } else { + // Growing: ensure capacity, newly added indices default to palette index 0. + if data.len() < needed_i64s { + data.extend(std::iter::repeat_n(0i64, needed_i64s - data.len())); + } + let added = new_length - old_length; + if added > 0 { + if let Some(first) = palette.get_mut(0) { + first.0 = first.0.saturating_add(added as u32); + } + } + } + + self.length = new_length; + } else { + panic!("resize_indirect called on non-indirect palette"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::utils::pack_indices; + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn resize_indirect_increases_length_and_clears_new_entries() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: pack_indices(&[0, 1, 2, 3], 4), + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_indirect(8); + assert_eq!(palette.length, 8); + assert_eq!(palette.get(0), Some(&10)); + assert_eq!(palette.get(3), Some(&40)); + assert_eq!(palette.get(4), Some(&10)); + assert_eq!(palette.get(7), Some(&10)); + } + + #[test] + fn resize_indirect_decreases_length_and_truncates_data() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: vec![0b0001_0010_0011_0100, 0b0101_0110_0111_1000], + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 8, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_indirect(4); + assert_eq!(palette.length, 4); + assert_eq!(palette.get(4), None); + } + + #[test] + fn resize_indirect_same_length_no_change() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: pack_indices(&[0, 1, 2, 3], 4), + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_indirect(4); + assert_eq!(palette.length, 4); + assert_eq!(palette.get(0), Some(&10)); + assert_eq!(palette.get(3), Some(&40)); + } + + #[test] + #[should_panic(expected = "resize_indirect called on non-indirect palette")] + fn resize_indirect_non_indirect_palette_panics() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Single(42), + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.resize_indirect(8); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/indirect/set.rs b/src/lib/utils/general_purpose/src/palette/indirect/set.rs new file mode 100644 index 000000000..bff872e3f --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/indirect/set.rs @@ -0,0 +1,245 @@ +use crate::palette::utils::{calculate_bits_per_entry, read_index, write_index}; +use crate::palette::{Palette, PaletteType, MIN_BITS_PER_ENTRY}; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn set_indirect(&mut self, index: usize, value: T) { + let threshold = self.indirect_threshold; + if index >= self.length { + panic!("Index out of bounds"); + } + + // Work with mutable borrow now + let (bits_per_entry_mut, data, palette) = match &mut self.palette_type { + PaletteType::Indirect { + bits_per_entry, + data, + palette, + } => (bits_per_entry, data, palette), + _ => panic!("set_indirect called on non-indirect palette"), + }; + + let old_pi = read_index(data, *bits_per_entry_mut, index) as usize; + let old_value = &palette[old_pi].1; + if *old_value == value { + return; + } + + // Fast path: value exists already + if let Some(existing_pos) = palette.iter().position(|(_, v)| *v == value) { + // Update counts + if old_pi != existing_pos { + // decrement old + { + let old_entry = &mut palette[old_pi]; + old_entry.0 -= 1; + } + // increment existing + palette[existing_pos].0 += 1; + // write index + write_index(data, *bits_per_entry_mut, index, existing_pos as i64); + + // If old entry now zero, remove and remap indices > removed + if palette[old_pi].0 == 0 { + remove_palette_entry_and_reindex( + data, + palette, + *bits_per_entry_mut, + old_pi, + self.length, + ); + } + + if palette.len() == 1 { + // Collapse to Single + let only = palette[0].1.clone(); + self.palette_type = PaletteType::Single(only); + } + } + return; + } + + // New unique value + let current_unique = palette.len(); + let needed_bits = calculate_bits_per_entry(current_unique + 1).max(MIN_BITS_PER_ENTRY); + + // Convert to Direct if bpe would exceed threshold + if needed_bits > threshold { + // Build direct vector + let mut direct = Vec::with_capacity(self.length); + for i in 0..self.length { + if i == index { + direct.push(value.clone()); + } else { + let pi = read_index(data, *bits_per_entry_mut, i) as usize; + direct.push(palette[pi].1.clone()); + } + } + self.palette_type = PaletteType::Direct(direct); + return; + } + + // If bits need to grow, repack + if needed_bits > *bits_per_entry_mut { + let old_bits = *bits_per_entry_mut; + let entries_per_u64_new = 64 / needed_bits as usize; + let data_len_new = self.length.div_ceil(entries_per_u64_new); + let mut new_data = vec![0i64; data_len_new]; + + // Palette index for the new value will be appended (current_unique) + for i in 0..self.length { + if i == index { + write_index(&mut new_data, needed_bits, i, current_unique as i64); + } else { + let pi = read_index(data, old_bits, i); + write_index(&mut new_data, needed_bits, i, pi); + } + } + + // Adjust counts + palette[old_pi].0 -= 1; + palette.push((1, value)); + *bits_per_entry_mut = needed_bits; + *data = new_data; + + if palette[old_pi].0 == 0 { + remove_palette_entry_and_reindex( + data, + palette, + *bits_per_entry_mut, + old_pi, + self.length, + ); + } + + if palette.len() == 1 { + let only = palette[0].1.clone(); + self.palette_type = PaletteType::Single(only); + } + return; + } + + // Bits unchanged: just append palette entry if still capacity fits + // Write new index + write_index(data, *bits_per_entry_mut, index, current_unique as i64); + // Update counts + palette[old_pi].0 -= 1; + palette.push((1, value)); + + if palette[old_pi].0 == 0 { + remove_palette_entry_and_reindex( + data, + palette, + *bits_per_entry_mut, + old_pi, + self.length, + ); + } + + if palette.len() == 1 { + let only = palette[0].1.clone(); + self.palette_type = PaletteType::Single(only); + } + } +} + +fn remove_palette_entry_and_reindex( + data: &mut [i64], + palette: &mut Vec<(u32, T)>, + bits_per_entry: u8, + removed_index: usize, + length: usize, +) { + palette.remove(removed_index); + + if palette.is_empty() { + return; + } + + // If we removed the last element, indices remain valid + if removed_index == palette.len() { + return; + } + + // Need to decrement all stored palette indices > removed_index + for i in 0..length { + let pi = read_index(data, bits_per_entry, i); + if (pi as usize) > removed_index { + write_index(data, bits_per_entry, i, pi - 1); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn set_indirect_within_bounds_updates_value() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: vec![0b0000_0000_0000_0000], + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_indirect(2, 42); + assert_eq!(palette.get(2), Some(&42)); + } + + #[test] + #[should_panic(expected = "Index out of bounds")] + fn set_indirect_out_of_bounds_panics() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: vec![0b0000_0000_0000_0000], + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_indirect(4, 50); + } + + #[test] + fn set_indirect_adds_new_value_to_palette() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: vec![0b0000_0000_0000_0000], + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_indirect(1, 20); + assert_eq!(palette.get(1), Some(&20)); + } + + #[test] + fn set_indirect_existing_value_reuses_palette_index() { + let mut palette = Palette { + palette_type: crate::palette::PaletteType::Indirect { + bits_per_entry: 4, + data: vec![0b0000_0000_0000_0000], + palette: vec![(1, 10), (1, 20), (1, 30), (1, 40)], + }, + length: 4, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.set_indirect(1, 10); + assert_eq!(palette.get(1), Some(&10)); + } + + #[test] + #[should_panic(expected = "set_indirect called on non-indirect palette")] + fn set_indirect_on_non_indirect_panics() { + let mut palette = Palette::new(3, 9u32, INDIRECT_THRESHOLD); + palette.set_indirect(1, 10); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/mod.rs b/src/lib/utils/general_purpose/src/palette/mod.rs new file mode 100644 index 000000000..ca303ab0b --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/mod.rs @@ -0,0 +1,116 @@ +use bitcode::{Decode, Encode}; +use deepsize::DeepSizeOf; + +mod count; +mod direct; +mod get; +mod indirect; +mod optimise; +mod resize; +mod set; +mod single; +mod utils; + +const MIN_BITS_PER_ENTRY: u8 = 4; +const INDIRECT_THRESHOLD: u8 = 15; + +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] +pub struct Palette +where + T: Default + PartialEq + Clone, +{ + pub palette_type: PaletteType, + pub length: usize, + pub indirect_threshold: u8, +} +#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] +pub enum PaletteType { + Single(T), + Indirect { + bits_per_entry: u8, + data: Vec, + palette: Vec<(u32, T)>, + }, + Direct(Vec), +} + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub fn new(size: usize, value: T, indirect_threshold: u8) -> Self { + Self { + palette_type: PaletteType::Single(value), + length: size, + indirect_threshold, + } + } + + pub fn len(&self) -> usize { + self.length + } + + pub fn is_empty(&self) -> bool { + self.length == 0 + } +} + +impl From> for Palette +where + T: Clone + Default + PartialEq, +{ + fn from(values: Vec) -> Self { + let length = values.len(); + if length == 1 { + Self { + palette_type: PaletteType::Single(values.into_iter().next().unwrap()), + length, + indirect_threshold: INDIRECT_THRESHOLD, + } + } else { + let unique_values = utils::calculate_unique_values(&values); + if utils::calculate_bits_per_entry(unique_values) <= 15 { + let bits_per_entry = utils::calculate_bits_per_entry(unique_values); + use std::collections::HashMap; + let mut freq: HashMap<&T, u32> = HashMap::new(); + for v in &values { + *freq.entry(v).or_default() += 1; + } + let palette: Vec<(u32, T)> = + freq.into_iter().map(|(v, c)| (c, v.clone())).collect(); + let entries_per_u64 = 64 / bits_per_entry as usize; + let data_len = length.div_ceil(entries_per_u64); + let mut data = vec![0i64; data_len]; + for (i, value) in values.iter().enumerate() { + let palette_index = palette + .iter() + .position(|p| p.1 == *value) + .expect("Value not found in palette") + as i64; + utils::write_index(&mut data, bits_per_entry, i, palette_index); + } + Self { + palette_type: PaletteType::Indirect { + bits_per_entry, + data, + palette, + }, + length, + indirect_threshold: INDIRECT_THRESHOLD, + } + } else { + Self { + palette_type: PaletteType::Direct(values), + length, + indirect_threshold: INDIRECT_THRESHOLD, + } + } + } + } +} + +impl Default for PaletteType { + fn default() -> Self { + PaletteType::Single(T::default()) + } +} diff --git a/src/lib/utils/general_purpose/src/palette/optimise.rs b/src/lib/utils/general_purpose/src/palette/optimise.rs new file mode 100644 index 000000000..d9611a055 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/optimise.rs @@ -0,0 +1,71 @@ +use crate::palette::{Palette, PaletteType}; +use std::hash::Hash; + +impl Palette { + pub fn optimise(&mut self) { + match self.palette_type { + PaletteType::Single(_) => self.optimise_single(), + PaletteType::Indirect { .. } => self.optimise_indirect(), + PaletteType::Direct(_) => self.optimise_direct(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::palette::{PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn test_optimise_with_empty_palette() { + let mut palette: Palette = Palette { + length: 0, + palette_type: PaletteType::Indirect { + bits_per_entry: 0, + data: vec![], + palette: vec![], + }, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.optimise(); + if let PaletteType::Indirect { palette, .. } = palette.palette_type { + assert!(palette.is_empty()); + } else { + panic!("Expected PaletteType::Indirect"); + } + } + + #[test] + fn test_optimise_with_single_entry() { + let mut palette = Palette { + length: 1, + palette_type: PaletteType::Indirect { + bits_per_entry: 1, + data: vec![0], + palette: vec![(1, 42)], + }, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.optimise(); + match palette.palette_type { + PaletteType::Single(value) => assert_eq!(value, 42), + PaletteType::Indirect { .. } => panic!("Expected PaletteType::Single, got Indirect"), + PaletteType::Direct(_) => panic!("Expected PaletteType::Single, got Direct"), + } + } + + #[test] + fn test_optimise_removes_unused_entries() { + let mut p = Palette::from(vec![42, 43, 44, 42, 42]); + assert!(matches!(p.palette_type, PaletteType::Indirect { .. })); + // Manually add an unused entry + p.set(1, 42); + p.optimise(); + assert_eq!(*p.get(1).unwrap(), 42); + if let PaletteType::Indirect { palette, .. } = p.palette_type { + assert_eq!(palette.len(), 2); + } else { + panic!("Expected PaletteType::Indirect"); + } + } +} diff --git a/src/lib/utils/general_purpose/src/palette/resize.rs b/src/lib/utils/general_purpose/src/palette/resize.rs new file mode 100644 index 000000000..fd605c2ff --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/resize.rs @@ -0,0 +1,49 @@ +use crate::palette::{Palette, PaletteType}; +use std::hash::Hash; + +impl Palette { + pub fn resize(&mut self, new_length: usize) { + match self.palette_type { + PaletteType::Single(_) => self.resize_single(new_length), + PaletteType::Indirect { .. } => self.resize_indirect(new_length), + PaletteType::Direct(_) => self.resize_direct(new_length), + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + use std::assert_matches::assert_matches; + + #[test] + fn resize_single_same_value_stays_single() { + let mut p = Palette::new(2, 5u32, INDIRECT_THRESHOLD); + assert_matches!(p.palette_type, PaletteType::Single(_)); + p.resize(5); + assert_matches!(p.palette_type, PaletteType::Single(_)); + assert_eq!(p.len(), 5); + } + + #[test] + fn resize_indirect_with_existing_value_stays_indirect() { + let mut p: Palette = Palette::from(vec![1, 2, 1, 2]); + p.resize(6); + match p.palette_type { + PaletteType::Indirect { .. } => {} + _ => panic!("expected Indirect"), + } + assert_eq!(p.len(), 6); + } + + #[test] + fn resize_indirect_add_new_value_within_16bpe_stays_indirect() { + let mut p: Palette = Palette::from(vec![10, 20, 10, 20]); + p.resize(6); + match &p.palette_type { + PaletteType::Indirect { bits_per_entry, .. } => assert_eq!(*bits_per_entry, 4), + _ => panic!("expected Indirect"), + } + assert_eq!(p.len(), 6); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/set.rs b/src/lib/utils/general_purpose/src/palette/set.rs new file mode 100644 index 000000000..7df6fc007 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/set.rs @@ -0,0 +1,186 @@ +use crate::palette::{Palette, PaletteType}; +use std::hash::Hash; + +impl Palette { + pub fn set(&mut self, index: usize, new_value: T) { + if index >= self.length { + self.resize(index + 1); + } + match self.palette_type { + PaletteType::Single(_) => self.set_single(index, new_value), + PaletteType::Indirect { .. } => self.set_indirect(index, new_value), + PaletteType::Direct(_) => self.set_direct(index, new_value), + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + use std::assert_matches::assert_matches; + + #[test] + fn set_within_single_same_value_no_change() { + let mut p = Palette::new(3, 9u32, INDIRECT_THRESHOLD); + p.set(1, 9); + assert!(matches!(p.palette_type, PaletteType::Single(_))); + assert_eq!(p.get(0), Some(&9)); + assert_eq!(p.get(1), Some(&9)); + assert_eq!(p.get(2), Some(&9)); + } + + #[test] + fn set_within_single_new_value_becomes_indirect() { + let mut p = Palette::new(4, 1u32, INDIRECT_THRESHOLD); + assert_matches!(p.palette_type, PaletteType::Single(_)); + p.set(2, 7); + match &p.palette_type { + PaletteType::Indirect { bits_per_entry, .. } => assert_eq!(*bits_per_entry, 4), + _ => panic!("expected Indirect"), + } + assert_eq!(p.get(2), Some(&7)); + } + + #[test] + fn set_indirect_existing_value() { + let mut p: Palette = Palette::from(vec![1, 2, 1, 2]); + p.set(0, 2); + assert_eq!(p.get(0), Some(&2)); + assert!(matches!(p.palette_type, PaletteType::Indirect { .. })); + } + + #[test] + fn set_indirect_add_new_value_within_16bpe() { + let mut p: Palette = Palette::from(vec![10, 20, 10, 20]); + p.set(1, 30); + match &p.palette_type { + PaletteType::Indirect { bits_per_entry, .. } => assert_eq!(*bits_per_entry, 4), + _ => panic!("expected Indirect"), + } + assert_eq!(p.get(0), Some(&10)); + assert_eq!(p.get(1), Some(&30)); + } + + #[test] + fn set_out_of_bounds_triggers_resize() { + let mut p = Palette::new(2, 5, INDIRECT_THRESHOLD); + p.set(4, 7); + assert_eq!(p.len(), 5); + assert_eq!(p.get(4), Some(&7)); + } + + #[test] + fn set_indirect_exceed_16bpe_becomes_direct() { + let mut p: Palette = + Palette::from(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]); + p.indirect_threshold = 3; + assert_matches!(p.palette_type, PaletteType::Indirect { .. }); + p.set(0, 16); + assert_matches!(p.palette_type, PaletteType::Direct(_)); + assert_eq!(p.get(0), Some(&16)); + assert_eq!(p.get(1), Some(&2)); + assert_eq!(p.get(14), Some(&15)); + } + + #[test] + fn test_tiny() { + let mut p = Palette::new(1, 100u8, INDIRECT_THRESHOLD); + p.set(0, 100); + p.set(1, 101); + p.set(2, 102); + p.set(3, 103); + p.set(4, 104); + p.set(5, 105); + p.set(6, 106); + p.set(7, 107); + p.set(8, 108); + p.set(9, 109); + assert_eq!(p.length, 10); + assert_eq!(p.get(0), Some(&100)); + assert_eq!(p.get(1), Some(&101)); + assert_eq!(p.get(2), Some(&102)); + assert_eq!(p.get(3), Some(&103)); + assert_eq!(p.get(4), Some(&104)); + assert_eq!(p.get(5), Some(&105)); + assert_eq!(p.get(6), Some(&106)); + assert_eq!(p.get(7), Some(&107)); + assert_eq!(p.get(8), Some(&108)); + assert_eq!(p.get(9), Some(&109)); + } + + #[test] + fn test_scaling() { + for size in 1..100 { + use rand::Rng; + let mut p = Palette::new(size, 0u8, INDIRECT_THRESHOLD); + if let PaletteType::Single(v) = &p.palette_type { + assert_eq!(*v, 0, "Failed at size {}", size); + } else { + panic!("expected Single palette type"); + }; + let random_values: Vec = (0..size) + .map(|_| rand::rng().random_range(u8::MIN..=size as u8)) + .collect(); + for (i, &v) in random_values.iter().enumerate() { + p.set(i, v); + } + assert_eq!(p.length, size, "Failed at size {}", size); + for (i, &v) in random_values.iter().enumerate() { + assert_eq!(p.get(i), Some(&v), "Failed at size {}", size); + } + } + } + + #[test] + fn test_small_random() { + let size = 32; + use rand::Rng; + let mut p = Palette::new(size, 0u8, INDIRECT_THRESHOLD); + if let PaletteType::Single(v) = &p.palette_type { + assert_eq!(*v, 0); + } else { + panic!("expected Single palette type"); + }; + let random_values: Vec = (0..size) + .map(|_| rand::rng().random_range(u8::MIN..=size as u8)) + .collect(); + for (i, &v) in random_values.iter().enumerate() { + p.set(i, v); + } + assert_eq!(p.length, size); + for (i, &v) in random_values.iter().enumerate() { + assert_eq!(p.get(i), Some(&v)); + } + } + + #[test] + fn test_large() { + let values: Vec = (0..5000).map(|v| v as u16).collect(); + let p = Palette::from(values.clone()); + assert_eq!(p.length, 5000); + for (i, &v) in values.iter().enumerate() { + assert_eq!(p.get(i), Some(&v)); + } + } + + #[test] + fn test_massive_random() { + use rand::Rng; + let mut p = Palette::new(10000, 0u32, INDIRECT_THRESHOLD); + if let PaletteType::Single(v) = &p.palette_type { + assert_eq!(*v, 0); + } else { + panic!("expected Single palette type"); + }; + let random_values: Vec = (0..10000) + .map(|_| rand::rng().random_range(u32::MIN..=u32::MAX)) + .collect(); + for (i, &v) in random_values.iter().enumerate() { + p.set(i, v); + } + assert_eq!(p.length, 10000); + for (i, &v) in random_values.iter().enumerate() { + assert_eq!(p.get(i), Some(&v)); + } + } +} diff --git a/src/lib/utils/general_purpose/src/palette/single/count.rs b/src/lib/utils/general_purpose/src/palette/single/count.rs new file mode 100644 index 000000000..54b7c1854 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/single/count.rs @@ -0,0 +1,56 @@ +use crate::palette::Palette; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub fn count_single(&self, value: &T) -> usize { + match &self.palette_type { + // Single variant: Check if the value matches the stored value. + crate::palette::PaletteType::Single(v) => { + if v == value { + self.length + } else { + 0 + } + } + _ => panic!("count_single called on non-Single palette type"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn count_single_value_matches() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 10, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_single(&42), 10); + } + + #[test] + fn count_single_value_does_not_match() { + let palette = Palette { + palette_type: PaletteType::Single(42), + length: 10, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.count_single(&7), 0); + } + + #[test] + #[should_panic(expected = "count_single called on non-Single palette type")] + fn count_single_non_single_palette_type() { + let palette = Palette { + palette_type: PaletteType::Direct(vec![1, 2, 3]), + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.count_single(&1); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/single/get.rs b/src/lib/utils/general_purpose/src/palette/single/get.rs new file mode 100644 index 000000000..1f26235ac --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/single/get.rs @@ -0,0 +1,52 @@ +use crate::palette::Palette; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn get_single(&self, index: usize) -> Option<&T> { + if index >= self.length { + return None; + } + match &self.palette_type { + crate::palette::PaletteType::Single(value) => Some(value), + _ => panic!("get_single called on non-single palette"), + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn get_single_within_bounds_returns_value() { + let palette = Palette { + palette_type: crate::palette::PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_single(3), Some(&42)); + } + + #[test] + fn get_single_out_of_bounds_returns_none() { + let palette = Palette { + palette_type: crate::palette::PaletteType::Single(42), + length: 5, + indirect_threshold: INDIRECT_THRESHOLD, + }; + assert_eq!(palette.get_single(5), None); + } + + #[test] + #[should_panic(expected = "get_single called on non-single palette")] + fn get_single_non_single_palette_panics() { + let palette = Palette { + palette_type: crate::palette::PaletteType::Direct(vec![1, 2, 3]), + length: 3, + indirect_threshold: INDIRECT_THRESHOLD, + }; + palette.get_single(1); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/single/mod.rs b/src/lib/utils/general_purpose/src/palette/single/mod.rs new file mode 100644 index 000000000..dfa2ccad0 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/single/mod.rs @@ -0,0 +1,5 @@ +pub(crate) mod count; +pub(crate) mod get; +pub(crate) mod optimise; +pub(crate) mod resize; +pub(crate) mod set; diff --git a/src/lib/utils/general_purpose/src/palette/single/optimise.rs b/src/lib/utils/general_purpose/src/palette/single/optimise.rs new file mode 100644 index 000000000..7def53550 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/single/optimise.rs @@ -0,0 +1,10 @@ +use crate::palette::Palette; + +impl Palette +where + T: Clone + Default + PartialEq, +{ + pub(crate) fn optimise_single(&mut self) { + // Do nothing if already single + } +} diff --git a/src/lib/utils/general_purpose/src/palette/single/resize.rs b/src/lib/utils/general_purpose/src/palette/single/resize.rs new file mode 100644 index 000000000..516d9bf77 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/single/resize.rs @@ -0,0 +1,43 @@ +use crate::palette::Palette; +use std::hash::Hash; + +impl Palette +where + T: Clone + Default + Eq + Hash, +{ + pub(crate) fn resize_single(&mut self, new_length: usize) { + if let crate::palette::PaletteType::Single(_) = &self.palette_type { + self.length = new_length; + } else { + panic!("resize_single called on non-Single palette"); + } + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, INDIRECT_THRESHOLD}; + + #[test] + fn resize_single_same_value_extends_length() { + let mut palette = Palette::new(3, 42u32, INDIRECT_THRESHOLD); + palette.resize_single(5); + assert_eq!(palette.len(), 5); + } + + #[test] + fn resize_single_different_value_converts_to_direct() { + let mut palette = Palette::new(3, 42u32, INDIRECT_THRESHOLD); + palette.resize_single(5); + assert_eq!(palette.len(), 5); + assert_eq!(palette.get(0), Some(&42)); + assert_eq!(palette.get(2), Some(&42)); + } + + #[test] + #[should_panic(expected = "resize_single called on non-Single palette")] + fn resize_single_non_single_palette_panics() { + let mut palette = Palette::from(vec![1, 2, 3]); + palette.resize_single(5); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/single/set.rs b/src/lib/utils/general_purpose/src/palette/single/set.rs new file mode 100644 index 000000000..45b00115c --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/single/set.rs @@ -0,0 +1,104 @@ +use crate::palette::utils::{calculate_bits_per_entry, write_index}; +use crate::palette::{Palette, PaletteType, MIN_BITS_PER_ENTRY}; + +impl Palette +where + T: Clone + Default + PartialEq + Eq + std::hash::Hash, +{ + pub(crate) fn set_single(&mut self, index: usize, new_value: T) { + let current_value = match &self.palette_type { + PaletteType::Single(v) => v.clone(), + _ => panic!("set_single called on non-single palette"), + }; + + // Out-of-bounds same value: just extend length + if new_value == current_value { + if index >= self.length { + self.length = index + 1; + } + return; + } + + // Different value: transition to Indirect + let length = if index >= self.length { + // Extend logical length first + self.length = index + 1; + self.length + } else { + self.length + }; + + let old_count = (length - 1) as u32; + let new_count = 1u32; + + let mut bits_per_entry = calculate_bits_per_entry(2); + if bits_per_entry < MIN_BITS_PER_ENTRY { + bits_per_entry = MIN_BITS_PER_ENTRY; + } + + let entries_per_i64 = 64 / bits_per_entry as usize; + let data_len = length.div_ceil(entries_per_i64); + let mut data = vec![0i64; data_len]; + + for i in 0..length { + let palette_index = if i == index { 1 } else { 0 }; + write_index(&mut data, bits_per_entry, i, palette_index as i64); + } + + self.palette_type = PaletteType::Indirect { + bits_per_entry, + data, + palette: vec![(old_count, current_value), (new_count, new_value)], + }; + } +} + +#[cfg(test)] +mod tests { + use crate::palette::{Palette, PaletteType, INDIRECT_THRESHOLD}; + + #[test] + fn set_single_within_bounds_same_value_extends_length() { + let mut palette = Palette::new(4, 25u32, INDIRECT_THRESHOLD); + palette.set_single(2, 25); + assert!(matches!(palette.palette_type, PaletteType::Single(_))); + assert_eq!(palette.length, 4); + assert_eq!(palette.get(2), Some(&25)); + } + + #[test] + fn set_single_within_bounds_different_value_converts_to_indirect() { + let mut palette = Palette::new(3, 42u32, INDIRECT_THRESHOLD); + palette.set_single(2, 7); + match &palette.palette_type { + PaletteType::Indirect { bits_per_entry, .. } => assert_eq!(*bits_per_entry, 4), + _ => panic!("expected Indirect"), + } + } + + #[test] + fn set_single_out_of_bounds_same_value_extends_length() { + let mut palette = Palette::new(2, 10u32, INDIRECT_THRESHOLD); + palette.set_single(5, 10); + assert!(matches!(palette.palette_type, PaletteType::Single(_))); + assert_eq!(palette.length, 6); + assert_eq!(palette.get(5), Some(&10)); + } + + #[test] + fn set_single_out_of_bounds_different_value_converts_to_indirect() { + let mut palette = Palette::new(2, 15u32, INDIRECT_THRESHOLD); + palette.set_single(4, 30); + match &palette.palette_type { + PaletteType::Indirect { bits_per_entry, .. } => assert_eq!(*bits_per_entry, 4), + _ => panic!("expected Indirect"), + } + } + + #[test] + #[should_panic(expected = "set_single called on non-single palette")] + fn set_single_non_single_palette_panics() { + let mut palette = Palette::from(vec![1, 2, 3]); + palette.set_single(1, 4); + } +} diff --git a/src/lib/utils/general_purpose/src/palette/utils.rs b/src/lib/utils/general_purpose/src/palette/utils.rs new file mode 100644 index 000000000..eadb9d174 --- /dev/null +++ b/src/lib/utils/general_purpose/src/palette/utils.rs @@ -0,0 +1,55 @@ +use crate::palette::MIN_BITS_PER_ENTRY; + +pub(in crate::palette) fn read_index(data: &[i64], bits_per_entry: u8, index: usize) -> i64 { + let entries_per_u64 = 64 / bits_per_entry as usize; + let u64_index = index / entries_per_u64; + let bit_offset = (index % entries_per_u64) * bits_per_entry as usize; + let packed = data[u64_index]; + (packed >> bit_offset) & ((1i64 << bits_per_entry) - 1) +} + +pub(in crate::palette) fn write_index( + data: &mut [i64], + bits_per_entry: u8, + index: usize, + value: i64, +) { + let entries_per_u64 = 64 / bits_per_entry as usize; + let u64_index = index / entries_per_u64; + let bit_offset = (index % entries_per_u64) * bits_per_entry as usize; + let mask = ((1i64 << bits_per_entry) - 1) << bit_offset; + data[u64_index] = (data[u64_index] & !mask) | ((value << bit_offset) & mask); +} + +pub(crate) fn calculate_bits_per_entry(palette_size: usize) -> u8 { + match palette_size { + 0..=16 => MIN_BITS_PER_ENTRY, + 17..=32 => 5, + 33..=64 => 6, + 65..=128 => 7, + 129..=256 => 8, + 257..=512 => 9, + 513..=1024 => 10, + 1025..=2048 => 11, + 2049..=4096 => 12, + 4097..=8192 => 13, + 8193..=16384 => 14, + _ => 15, + } +} + +pub(crate) fn calculate_unique_values(values: &[T]) -> usize { + use std::collections::HashSet; + HashSet::<&T>::from_iter(values.iter()).len() +} + +#[allow(dead_code)] +pub(crate) fn pack_indices(indices: &[u16], bits_per_entry: u8) -> Vec { + let entries_per_u64 = 64 / bits_per_entry as usize; + let data_len = indices.len().div_ceil(entries_per_u64); + let mut data = vec![0i64; data_len]; + for (i, &idx) in indices.iter().enumerate() { + write_index(&mut data, bits_per_entry, i, idx as i64); + } + data +} diff --git a/src/lib/world/Cargo.toml b/src/lib/world/Cargo.toml index d2852e7fc..2de256960 100644 --- a/src/lib/world/Cargo.toml +++ b/src/lib/world/Cargo.toml @@ -22,15 +22,15 @@ ferrumc-anvil = { workspace = true } rayon = { workspace = true } ferrumc-general-purpose = { workspace = true } lazy_static = { workspace = true } -bzip2 = { workspace = true } serde_json = { workspace = true } indicatif = { workspace = true } -wyhash = { workspace = true } +simplehash = { workspace = true } moka = { workspace = true, features = ["sync"] } ahash = { workspace = true } rand = { workspace = true } yazi = { workspace = true } ferrumc-threadpool = { workspace = true } +bevy_math = { workspace = true } lz4_flex = { workspace = true } [[bench]] diff --git a/src/lib/world/src/benches/cache.rs b/src/lib/world/src/benches/cache.rs index 2819b1cd8..d17d0ebbd 100644 --- a/src/lib/world/src/benches/cache.rs +++ b/src/lib/world/src/benches/cache.rs @@ -1,24 +1,22 @@ -use std::hint::black_box; - +use bevy_math::{IVec2, IVec3}; use criterion::Criterion; use ferrumc_world::World; +use std::hint::black_box; pub(crate) fn bench_cache(c: &mut Criterion) { - let backend_path = std::env::current_dir() - .unwrap() - .join("../../../target/debug/world"); + let backend_path = std::env::current_dir().unwrap().join("../../../world"); let mut group = c.benchmark_group("world_load"); group.bench_function("Load chunk 1,1 uncached", |b| { b.iter_batched( || World::new(&backend_path), - |world| world.load_chunk(black_box(1), black_box(1), black_box("overworld")), + |world| world.load_chunk(black_box(IVec2::new(1, 1)), black_box("overworld")), criterion::BatchSize::PerIteration, ); }); group.bench_function("Load chunk 1,1 uncached, owned", |b| { b.iter_batched( || World::new(&backend_path), - |world| world.load_chunk_owned(black_box(1), black_box(1), black_box("overworld")), + |world| world.load_chunk_owned(black_box(IVec2::new(1, 1)), black_box("overworld")), criterion::BatchSize::PerIteration, ); }); @@ -27,9 +25,8 @@ pub(crate) fn bench_cache(c: &mut Criterion) { || World::new(&backend_path), |world| { world.get_block_and_fetch( - black_box(1), - black_box(1), - black_box(1), + black_box(IVec2::new(1, 1)), + black_box(IVec3::new(1, 1, 1)), black_box("overworld"), ) }, @@ -38,24 +35,23 @@ pub(crate) fn bench_cache(c: &mut Criterion) { }); let world = World::new(backend_path); let load_chunk = || { - world.load_chunk(1, 1, "overworld").expect( + world.load_chunk(IVec2::new(1, 1), "overworld").expect( "Failed to load chunk. If it's a bitcode error, chances are the chunk format \ has changed since last generating a world so you'll need to regenerate", ) }; _ = load_chunk(); group.bench_function("Load chunk 1,1 cached", |b| { - b.iter(|| world.load_chunk(black_box(1), black_box(1), black_box("overworld"))) + b.iter(|| world.load_chunk(black_box(IVec2::new(1, 1)), black_box("overworld"))) }); group.bench_function("Load chunk 1,1 cached, owned", |b| { - b.iter(|| world.load_chunk_owned(black_box(1), black_box(1), black_box("overworld"))) + b.iter(|| world.load_chunk_owned(black_box(IVec2::new(1, 1)), black_box("overworld"))) }); group.bench_function("Load block 1,1 cached", |b| { b.iter(|| { world.get_block_and_fetch( - black_box(1), - black_box(1), - black_box(1), + black_box(IVec2::new(1, 1)), + black_box(IVec3::new(1, 1, 1)), black_box("overworld"), ) }); diff --git a/src/lib/world/src/benches/edit_bench.rs b/src/lib/world/src/benches/edit_bench.rs index b7a36a5da..f5122d930 100644 --- a/src/lib/world/src/benches/edit_bench.rs +++ b/src/lib/world/src/benches/edit_bench.rs @@ -1,3 +1,4 @@ +use bevy_math::IVec3; use criterion::{Criterion, Throughput}; use ferrumc_world::chunk_format::Chunk; use ferrumc_world::vanilla_chunk_format::BlockData; @@ -18,20 +19,20 @@ pub(crate) fn bench_edits(c: &mut Criterion) { read_group.throughput(Throughput::Elements(1)); read_group.bench_function("Read 0,0,0", |b| { - b.iter(|| black_box(chunk.get_block(0, 0, 0))); + b.iter(|| black_box(chunk.get_block(IVec3::new(0, 0, 0)))); }); read_group.bench_function("Read 8,8,150", |b| { - b.iter(|| black_box(chunk.get_block(8, 8, 150))); + b.iter(|| black_box(chunk.get_block(IVec3::new(8, 8, 150)))); }); read_group.bench_function("Read rand", |b| { b.iter(|| { - black_box(chunk.get_block( + black_box(chunk.get_block(IVec3::new( get_rand_in_range(0, 15), get_rand_in_range(0, 15), get_rand_in_range(0, 255), - )) + ))) }); }); @@ -44,15 +45,16 @@ pub(crate) fn bench_edits(c: &mut Criterion) { write_group.bench_with_input("Write 0,0,0", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.set_block( - 0, - 0, - 0, - BlockData { - name: "minecraft:bricks".to_string(), - properties: None, - }, - )) + black_box( + chunk.set_block( + IVec3::new(0, 0, 0), + BlockData { + name: "minecraft:bricks".to_string(), + properties: None, + } + .to_block_id(), + ), + ) .unwrap(); }); }); @@ -60,15 +62,16 @@ pub(crate) fn bench_edits(c: &mut Criterion) { write_group.bench_with_input("Write 8,8,150", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.set_block( - 8, - 8, - 150, - BlockData { - name: "minecraft:bricks".to_string(), - properties: None, - }, - )) + black_box( + chunk.set_block( + IVec3::new(8, 8, 150), + BlockData { + name: "minecraft:bricks".to_string(), + properties: None, + } + .to_block_id(), + ), + ) .unwrap(); }); }); @@ -76,15 +79,20 @@ pub(crate) fn bench_edits(c: &mut Criterion) { write_group.bench_with_input("Write rand", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.set_block( - get_rand_in_range(0, 15), - get_rand_in_range(0, 15), - get_rand_in_range(0, 255), - BlockData { - name: "minecraft:bricks".to_string(), - properties: None, - }, - )) + black_box( + chunk.set_block( + IVec3::new( + get_rand_in_range(0, 15), + get_rand_in_range(0, 15), + get_rand_in_range(0, 255), + ), + BlockData { + name: "minecraft:bricks".to_string(), + properties: None, + } + .to_block_id(), + ), + ) .unwrap(); }); }); @@ -94,11 +102,13 @@ pub(crate) fn bench_edits(c: &mut Criterion) { write_group.bench_with_input("Fill", &chunk, |b, chunk| { b.iter(|| { let mut chunk = chunk.clone(); - black_box(chunk.fill(BlockData { - name: "minecraft:bricks".to_string(), - properties: None, - })) - .unwrap(); + chunk.fill(black_box( + BlockData { + name: "minecraft:bricks".to_string(), + properties: None, + } + .to_block_id(), + )) }); }); @@ -108,15 +118,16 @@ pub(crate) fn bench_edits(c: &mut Criterion) { for x in 0..16 { for y in 0..256 { for z in 0..16 { - black_box(chunk.set_block( - x, - y, - z, - BlockData { - name: "minecraft:bricks".to_string(), - properties: None, - }, - )) + black_box( + chunk.set_block( + IVec3::new(x, y, z), + BlockData { + name: "minecraft:bricks".to_string(), + properties: None, + } + .to_block_id(), + ), + ) .unwrap(); } } @@ -124,56 +135,56 @@ pub(crate) fn bench_edits(c: &mut Criterion) { }); }); - write_group.bench_with_input("Manual batch fill same", &chunk, |b, chunk| { - b.iter(|| { - let mut chunk = chunk.clone(); - let mut batch = ferrumc_world::edit_batch::EditBatch::new(&mut chunk); - for x in 0..16 { - for y in 0..256 { - for z in 0..16 { - batch.set_block( - x, - y, - z, - black_box(BlockData { - name: "minecraft:bricks".to_string(), - properties: None, - }), - ); - } - } - } - black_box(batch.apply()).unwrap(); - }); - }); - - write_group.bench_with_input("Manual batch fill diff", &chunk, |b, chunk| { - b.iter(|| { - let mut chunk = chunk.clone(); - let mut batch = ferrumc_world::edit_batch::EditBatch::new(&mut chunk); - for x in 0..16 { - for y in 0..256 { - for z in 0..16 { - let block = if (x + y + z) % 2 == 0 { - "minecraft:bricks" - } else { - "minecraft:stone" - }; - batch.set_block( - x, - y, - z, - black_box(BlockData { - name: block.to_string(), - properties: None, - }), - ); - } - } - } - black_box(batch.apply()).unwrap(); - }); - }); + // write_group.bench_with_input("Manual batch fill same", &chunk, |b, chunk| { + // b.iter(|| { + // let mut chunk = chunk.clone(); + // let mut batch = ferrumc_world::edit_batch::EditBatch::new(&mut chunk); + // for x in 0..16 { + // for y in 0..256 { + // for z in 0..16 { + // batch.set_block( + // x, + // y, + // z, + // black_box(BlockData { + // name: "minecraft:bricks".to_string(), + // properties: None, + // }), + // ); + // } + // } + // } + // black_box(batch.apply()).unwrap(); + // }); + // }); + // + // write_group.bench_with_input("Manual batch fill diff", &chunk, |b, chunk| { + // b.iter(|| { + // let mut chunk = chunk.clone(); + // let mut batch = ferrumc_world::edit_batch::EditBatch::new(&mut chunk); + // for x in 0..16 { + // for y in 0..256 { + // for z in 0..16 { + // let block = if (x + y + z) % 2 == 0 { + // "minecraft:bricks" + // } else { + // "minecraft:stone" + // }; + // batch.set_block( + // x, + // y, + // z, + // black_box(BlockData { + // name: block.to_string(), + // properties: None, + // }), + // ); + // } + // } + // } + // black_box(batch.apply()).unwrap(); + // }); + // }); write_group.finish(); } diff --git a/src/lib/world/src/block_id.rs b/src/lib/world/src/block_id.rs index c6ed8af84..0226ab3dc 100644 --- a/src/lib/world/src/block_id.rs +++ b/src/lib/world/src/block_id.rs @@ -111,6 +111,26 @@ impl From for VarInt { } } +impl From for BlockId { + /// Converts an i32 to a BlockId. Will panic if the ID is negative. + fn from(id: i32) -> Self { + if id < 0 { + panic!("Block ID cannot be negative"); + } + Self(id as u32) + } +} + +impl From for i32 { + /// Converts a BlockId to an i32. Will panic if the ID is greater than i32::MAX. + fn from(block_id: BlockId) -> Self { + if block_id.0 > i32::MAX as u32 { + panic!("Block ID cannot be greater than i32::MAX"); + } + block_id.0 as i32 + } +} + impl Default for BlockId { /// Returns a BlockId with ID 0, which is air. fn default() -> Self { diff --git a/src/lib/world/src/chunk_format.rs b/src/lib/world/src/chunk_format.rs index 2704711f8..d2a8db4ad 100644 --- a/src/lib/world/src/chunk_format.rs +++ b/src/lib/world/src/chunk_format.rs @@ -1,15 +1,14 @@ -use crate::block_id::{BlockId, BLOCK2ID}; +use crate::block_id::BlockId; use crate::vanilla_chunk_format; use crate::vanilla_chunk_format::VanillaChunk; use crate::{errors::WorldError, vanilla_chunk_format::VanillaHeightmaps}; use bitcode_derive::{Decode, Encode}; use deepsize::DeepSizeOf; use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; +use ferrumc_general_purpose::palette::Palette; use ferrumc_macros::{NBTDeserialize, NBTSerialize}; use ferrumc_net_codec::net_types::var_int::VarInt; use std::cmp::max; -use std::collections::HashMap; -use tracing::error; use vanilla_chunk_format::BlockData; // #[cfg(test)] // const BLOCKSFILE: &[u8] = &[0]; @@ -43,31 +42,11 @@ pub struct Heightmaps { #[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] pub struct Section { pub y: i8, - pub block_states: BlockStates, + pub block_states: Palette, pub biome_states: BiomeStates, pub block_light: Vec, pub sky_light: Vec, } -#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] -pub struct BlockStates { - pub non_air_blocks: u16, - pub block_data: PaletteType, - pub block_counts: HashMap, -} - -#[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] -pub enum PaletteType { - Single(VarInt), - Indirect { - bits_per_block: u8, - data: Vec, - palette: Vec, - }, - Direct { - bits_per_block: u8, - data: Vec, - }, -} #[derive(Encode, Decode, Clone, DeepSizeOf, Eq, PartialEq, Debug)] pub struct BiomeStates { @@ -76,19 +55,6 @@ pub struct BiomeStates { pub palette: Vec, } -fn convert_to_net_palette(vanilla_palettes: Vec) -> Result, WorldError> { - let mut new_palette = Vec::new(); - for palette in vanilla_palettes { - if let Some(id) = BLOCK2ID.get(&palette) { - new_palette.push(VarInt::from(*id)); - } else { - new_palette.push(VarInt::from(0)); - error!("Could not find block id for palette entry: {:?}", palette); - } - } - Ok(new_palette) -} - impl Heightmaps { pub fn new() -> Self { Heightmaps { @@ -118,10 +84,14 @@ impl VanillaChunk { let mut sections = Vec::new(); for section in self.sections.as_ref().unwrap() { let y = section.y; - let raw_block_data = section + let raw_block_data: Vec = section .block_states .as_ref() - .and_then(|bs| bs.data.clone()) + .and_then(|bs| { + bs.data + .clone() + .map(|d| d.iter().map(|&x| x as u64).collect()) + }) .unwrap_or_default(); let palette = section .block_states @@ -129,64 +99,49 @@ impl VanillaChunk { .and_then(|bs| bs.palette.clone()) .unwrap_or_default(); let bits_per_block = max((palette.len() as f32).log2().ceil() as u8, 4); - let mut block_counts = HashMap::new(); - for chunk in &raw_block_data { - let mut i = 0; - while i + bits_per_block < 64 { - let palette_index = read_nbit_i32(chunk, bits_per_block as usize, i as u32)?; - let block = match palette.get(palette_index as usize) { - Some(block) => block, - None => { - error!("Could not find block for palette index: {}", palette_index); - &BlockData::default() - } - }; - - if let Some(count) = block_counts.get_mut(&block.to_block_id()) { - *count += 1; - } else { - block_counts.insert(block.to_block_id(), 0); - } + let mut block_states = Palette::new(4096, BlockId(0), 15); - i += bits_per_block; + let mut blocks: Vec<(u8, u8, u8, BlockId)> = Vec::new(); + for chunk in &raw_block_data { + // let mut i = 0; + // while i + bits_per_block < 64 { + // let palette_index = read_nbit_i32(chunk, bits_per_block as usize, i as u32)?; + // let block = match palette.get(palette_index as usize) { + // Some(block) => block, + // None => { + // error!("Could not find block for palette index: {}", palette_index); + // &BlockData::default() + // } + // }; + // + // if let Some(count) = block_counts.get_mut(block.to_block_id()) { + // *count += 1; + // } else { + // block_counts.insert(block.to_block_id(), 0); + // } + // + // i += bits_per_block; + // } + for i in 0..4096u16 { + let palette_index = read_nbit_i32( + chunk, + bits_per_block as usize, + (i * bits_per_block as u16) as u32, + )?; + let block_id = palette + .get(palette_index as usize) + .cloned() + .unwrap_or(BlockData::from(BlockId(0))); + let x = (i & 0xF) as u8; + let y = ((i >> 8) & 0xF) as u8; + let z = ((i >> 4) & 0xF) as u8; + blocks.push((x, y, z, BlockId::from(block_id))); } } - let block_data = if raw_block_data.is_empty() { - block_counts.insert(BlockId::default(), 4096); - PaletteType::Single(VarInt::from(0)) - } else { - PaletteType::Indirect { - bits_per_block, - data: raw_block_data, - palette: convert_to_net_palette(palette)?, - } - }; - // Count the number of blocks that are either air, void air, or cave air - let mut air_blocks = *block_counts.get(&BlockId::default()).unwrap_or(&0) as u16; - air_blocks += *block_counts - .get( - &BlockData { - name: "minecraft:void_air".to_string(), - properties: None, - } - .to_block_id(), - ) - .unwrap_or(&0) as u16; - air_blocks += *block_counts - .get( - &BlockData { - name: "minecraft:cave_air".to_string(), - properties: None, - } - .to_block_id(), - ) - .unwrap_or(&0) as u16; - let non_air_blocks = 4096 - air_blocks; - let block_states = BlockStates { - block_counts, - non_air_blocks, - block_data, - }; + for (x, y, z, block) in blocks { + let index = (y as u16) << 8 | (z as u16) << 4 | (x as u16); + block_states.set(index as usize, block); + } let block_light = section .block_light .as_ref() @@ -231,127 +186,79 @@ impl VanillaChunk { } } -impl Chunk { - pub fn new(x: i32, z: i32, dimension: String) -> Self { - let mut sections: Vec
= (-4..20) - .map(|y| Section { - y: y as i8, - block_states: BlockStates { - non_air_blocks: 0, - block_data: PaletteType::Single(VarInt::from(0)), - block_counts: HashMap::from([(BlockId::default(), 4096)]), - }, - biome_states: BiomeStates { - bits_per_biome: 0, - data: vec![], - palette: vec![VarInt::from(0)], - }, - block_light: vec![255; 2048], - sky_light: vec![255; 2048], - }) - .collect(); - for section in &mut sections { - section.optimise().expect("Failed to optimise section"); - } - Chunk { - x, - z, - dimension, - sections, - heightmaps: Heightmaps::new(), - } - } -} - #[cfg(test)] mod tests { use super::*; + use bevy_math::{IVec2, IVec3}; + use ferrumc_general_purpose::palette::PaletteType; #[test] fn test_chunk_set_block() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(IVec2::new(0, 0), "overworld".to_string()); let block = BlockData { name: "minecraft:stone".to_string(), properties: None, } .to_block_id(); - chunk.set_block(0, 0, 0, block).unwrap(); - assert_eq!(chunk.get_block(0, 0, 0).unwrap(), block); + chunk.set_block(IVec3::new(0, 0, 0), block).unwrap(); + assert_eq!(chunk.get_block(IVec3::new(0, 0, 0)), block); } #[test] fn test_chunk_fill() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(IVec2::new(0, 0), "overworld".to_string()); let stone_block = BlockData { name: "minecraft:stone".to_string(), properties: None, - }; - chunk.fill(stone_block.clone()).unwrap(); - for section in &chunk.sections { - for (block, count) in §ion.block_states.block_counts { - assert_eq!(*block, stone_block.to_block_id()); - assert_eq!(count, &4096); + } + .to_block_id(); + chunk.fill(stone_block); + for y in 0..16 { + for z in -64..320 { + for x in 0..16 { + assert_eq!(chunk.get_block(IVec3::new(x, y, z)), stone_block); + } } } } #[test] fn test_section_fill() { - let mut section = Section { - y: 0, - block_states: BlockStates { - non_air_blocks: 0, - block_data: PaletteType::Single(VarInt::from(0)), - block_counts: HashMap::from([(BlockId::default(), 4096)]), - }, - biome_states: BiomeStates { - bits_per_biome: 0, - data: vec![], - palette: vec![VarInt::from(0)], - }, - block_light: vec![255; 2048], - sky_light: vec![255; 2048], - }; + let mut section = Section::new(0); let stone_block = BlockData { name: "minecraft:stone".to_string(), properties: None, - }; - section.fill(stone_block.clone()).unwrap(); - assert_eq!( - section.block_states.block_data, - PaletteType::Single(VarInt::from(1)) - ); + } + .to_block_id(); + section.fill(stone_block); assert_eq!( - section - .block_states - .block_counts - .get(&stone_block.to_block_id()) - .unwrap(), - &4096 + section.block_states.palette_type, + PaletteType::Single(BlockId::from(1)) ); + assert_eq!(section.block_states.get_count(&stone_block), 4096); } #[test] fn test_false_positive() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(IVec2::new(0, 0), "overworld".to_string()); let block = BlockData { name: "minecraft:stone".to_string(), properties: None, } .to_block_id(); - chunk.set_block(0, 0, 0, block).unwrap(); - assert_ne!(chunk.get_block(0, 1, 0).unwrap(), block); + chunk.set_block(IVec3::new(0, 0, 0), block).unwrap(); + assert_ne!(chunk.get_block(IVec3::new(0, 0, 0)), BlockId::from(0)); } #[test] fn test_doesnt_fail() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); + let mut chunk = Chunk::new(IVec2::new(0, 0), "overworld".to_string()); let block = BlockData { name: "minecraft:stone".to_string(), properties: None, - }; - assert!(chunk.set_block(0, 0, 0, block.clone()).is_ok()); - assert!(chunk.set_block(0, 0, 0, block.clone()).is_ok()); - assert!(chunk.get_block(0, 0, 0).is_ok()); + } + .to_block_id(); + chunk.set_block(IVec3::new(15, 255, 15), block).unwrap(); + assert_eq!(chunk.get_block(IVec3::new(15, 255, 15)), block); } } diff --git a/src/lib/world/src/chunk_ops.rs b/src/lib/world/src/chunk_ops.rs new file mode 100644 index 000000000..8d4ac8f6a --- /dev/null +++ b/src/lib/world/src/chunk_ops.rs @@ -0,0 +1,217 @@ +use crate::block_id::BlockId; +use crate::chunk_format::{Chunk, Heightmaps, Section}; +use bevy_math::{IVec2, IVec3}; + +impl Chunk { + /// Creates a new empty chunk at the given chunk coordinates and dimension. + /// + /// The chunk will contain sections from Y=-4 to Y=19 (16 sections). + /// + /// # Arguments + /// + /// * `pos` - The chunk coordinates (x, z) as IVec2. + /// * `dimension` - The dimension of the chunk (e.g., "minecraft:overworld"). + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Chunk; + /// use bevy_math::IVec2; + /// + /// let chunk = Chunk::new(IVec2::new(0, 0), "minecraft:overworld".to_string()); + pub fn new(pos: IVec2, dimension: String) -> Self { + let mut sections: Vec
= (-4..20).map(|y| Section::new(y as i8)).collect(); + for section in &mut sections { + section.optimise(); + } + Chunk { + x: pos.x, + z: pos.y, + dimension, + sections, + heightmaps: Heightmaps::new(), + } + } + /// Gets a block in the chunk at the given global position. + /// + /// The position is in global coordinates. The y coordinate is used to determine the section. + /// + /// # Arguments + /// + /// * `pos` - The global position of the block to get. + /// # Returns + /// * Returns the BlockId at the given position. If no block is found, returns BlockId::default(). + /// # Examples + /// ```rust + /// use bevy_math::{IVec3, IVec2}; + /// use ferrumc_world::chunk_format::Chunk; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut chunk = Chunk::new(IVec2::new(0, 0), "minecraft:overworld".to_string()); + /// let pos = IVec3::new(1, 18, 3); // y=18 means section 1 (16-31) + /// let block = BlockId(1); + /// chunk.set_block(pos, block)?; + /// let retrieved_block = chunk.get_block(pos); + /// assert_eq!(block, retrieved_block); + /// Ok(()) + /// } + pub fn get_block(&self, pos: IVec3) -> BlockId { + let section_index = (pos.y >> 4) as usize; + if section_index >= self.sections.len() { + return BlockId::default(); + } + let section = &self.sections[section_index]; + let local_pos = IVec3::new(pos.x & 0xF, pos.y & 0xF, pos.z & 0xF); + section.get_block(local_pos) + } + /// Sets a block in the chunk at the given global position. + /// + /// The position is in global coordinates. The y coordinate is used to determine the section. + /// + /// # Arguments + /// + /// * `pos` - The global position of the block to set. + /// * `block` - The BlockId to set at the given position. + /// # Errors + /// * Returns `WorldError::SectionOutOfBounds` if the section index is out of bounds. + /// # Examples + /// ```rust + /// use bevy_math::IVec3; + /// use ferrumc_world::chunk_format::Chunk; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// use bevy_math::IVec2; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut chunk = Chunk::new(IVec2::new(0, 0), "minecraft:overworld".to_string()); + /// let pos = IVec3::new(1, 18, 3); // y=18 means section 1 (16-31) + /// let block = BlockId(1); + /// chunk.set_block(pos, block)?; + /// let retrieved_block = chunk.get_block(pos); + /// assert_eq!(block, retrieved_block); + /// Ok(()) + /// } + pub fn set_block( + &mut self, + pos: IVec3, + block: BlockId, + ) -> Result<(), crate::errors::WorldError> { + let section_index = (pos.y >> 4) as usize; + if section_index >= self.sections.len() { + return Err(crate::errors::WorldError::SectionOutOfBounds( + section_index as i32, + )); + } + let section = &mut self.sections[section_index]; + let local_pos = IVec3::new(pos.x & 0xF, pos.y & 0xF, pos.z & 0xF); + section.set_block(local_pos, block) + } + /// Fills the entire chunk with the given block. + /// + /// # Arguments + /// + /// * `block` - The BlockId to fill the chunk with. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Chunk; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// use bevy_math::{IVec3, IVec2}; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut chunk = Chunk::new(IVec2::new(0, 0), "minecraft:overworld".to_string()); + /// let block = BlockId(1); + /// chunk.fill(block); + /// for section in &chunk.sections { + /// for y in 0..16 { + /// for z in 0..16 { + /// for x in 0..16 { + /// let pos = IVec3::new(x, y, z); + /// let retrieved_block = section.get_block(pos); + /// assert_eq!(block, retrieved_block); + /// } + /// } + /// } + /// } + /// Ok(()) + /// } + pub fn fill(&mut self, block: BlockId) { + self.sections = (-4..20).map(|y| Section::new(y as i8)).collect(); + for section in &mut self.sections { + section.fill(block); + } + } + + /// Fills a specific section of the chunk with the given block. + /// + /// The section is specified by its Y coordinate, which ranges from -4 to 19. + /// + /// # Arguments + /// + /// * `section_y` - The Y coordinate of the section to fill (-4 to 19). + /// * `block` - The BlockId to fill the section with. + /// # Errors + /// * Returns `WorldError::SectionOutOfBounds` if the section index is out of bounds. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Chunk; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// use bevy_math::{IVec3, IVec2}; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut chunk = Chunk::new(IVec2::new(0, 0), "minecraft:overworld".to_string()); + /// let block = BlockId(1); + /// chunk.fill_section(0, block)?; // Fill section at Y=0 (0-15) + /// let section = &chunk.sections[4]; // Section index 4 corresponds to Y=0 + /// for y in 0..16 { + /// for z in 0..16 { + /// for x in 0..16 { + /// let pos = IVec3::new(x, y, z); + /// let retrieved_block = section.get_block(pos); + /// assert_eq!(block, retrieved_block); + /// } + /// } + /// } + /// Ok(()) + /// } + pub fn fill_section( + &mut self, + section_y: i8, + block: BlockId, + ) -> Result<(), crate::errors::WorldError> { + let section_index = (section_y + 4) as usize; + if section_index >= self.sections.len() { + return Err(crate::errors::WorldError::SectionOutOfBounds( + section_index as i32, + )); + } + let section = &mut self.sections[section_index]; + section.fill(block); + Ok(()) + } + + /// Optimises the chunk's sections by reducing the bits per entry where possible. + /// + /// This function iterates through each section in the chunk and calls the `optimise` method + /// on the section's block states to reduce memory usage. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Chunk; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// use bevy_math::IVec2; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut chunk = Chunk::new(IVec2::new(0, 0), "minecraft:overworld".to_string()); + /// let block = BlockId(1); + /// chunk.fill(block); + /// chunk.optimise(); + /// Ok(()) + /// } + pub fn optimise(&mut self) { + for section in &mut self.sections { + section.block_states.optimise(); + } + } +} diff --git a/src/lib/world/src/db_functions.rs b/src/lib/world/src/db_functions.rs index c2af0de26..6de29cb08 100644 --- a/src/lib/world/src/db_functions.rs +++ b/src/lib/world/src/db_functions.rs @@ -4,8 +4,8 @@ use crate::errors::WorldError::CorruptedChunkData; // db_functions.rs use crate::warn; use crate::World; +use bevy_math::IVec2; use ferrumc_config::server_config::get_global_config; -use std::hash::Hasher; use std::sync::Arc; use tracing::trace; use yazi::CompressionLevel; @@ -25,20 +25,22 @@ impl World { /// Load a chunk from the storage backend. If the chunk is in the cache, it will be returned /// from the cache instead of the storage backend. If the chunk is not in the cache, it will be /// loaded from the storage backend and inserted into the cache. - pub fn load_chunk(&self, x: i32, z: i32, dimension: &str) -> Result, WorldError> { - if let Some(chunk) = self.cache.get(&(x, z, dimension.to_string())) { + pub fn load_chunk(&self, pos: IVec2, dimension: &str) -> Result, WorldError> { + if let Some(chunk) = self.cache.get(&(pos.x, pos.y, dimension.to_string())) { return Ok(chunk); } - let chunk = load_chunk_internal(self, x, z, dimension); + let chunk = load_chunk_internal(self, pos, dimension); if let Ok(ref chunk) = chunk { - self.cache - .insert((x, z, dimension.to_string()), Arc::from(chunk.clone())); + self.cache.insert( + (pos.x, pos.y, dimension.to_string()), + Arc::from(chunk.clone()), + ); } chunk.map(Arc::new) } - pub fn load_chunk_owned(&self, x: i32, z: i32, dimension: &str) -> Result { - self.load_chunk(x, z, dimension).map(|c| c.as_ref().clone()) + pub fn load_chunk_owned(&self, pos: IVec2, dimension: &str) -> Result { + self.load_chunk(pos, dimension).map(|c| c.as_ref().clone()) } /// Check if a chunk exists in the storage backend. @@ -46,19 +48,22 @@ impl World { /// It will first check if the chunk is in the cache and if it is, it will return true. If the /// chunk is not in the cache, it will check the storage backend for the chunk, returning true /// if it exists and false if it does not. - pub fn chunk_exists(&self, x: i32, z: i32, dimension: &str) -> Result { - if self.cache.contains_key(&(x, z, dimension.to_string())) { + pub fn chunk_exists(&self, pos: IVec2, dimension: &str) -> Result { + if self + .cache + .contains_key(&(pos.x, pos.y, dimension.to_string())) + { return Ok(true); } - chunk_exists_internal(self, x, z, dimension) + chunk_exists_internal(self, pos, dimension) } /// Delete a chunk from the storage backend. /// /// This function will remove the chunk from the cache and delete it from the storage backend. - pub fn delete_chunk(&self, x: i32, z: i32, dimension: &str) -> Result<(), WorldError> { - self.cache.remove(&(x, z, dimension.to_string())); - delete_chunk_internal(self, x, z, dimension) + pub fn delete_chunk(&self, pos: IVec2, dimension: &str) -> Result<(), WorldError> { + self.cache.remove(&(pos.x, pos.y, dimension.to_string())); + delete_chunk_internal(self, pos, dimension) } /// Sync the storage backend. @@ -81,12 +86,12 @@ impl World { /// returned as a vector. pub fn load_chunk_batch( &self, - coords: &[(i32, i32, &str)], + coords: &[(IVec2, &str)], ) -> Result>, WorldError> { let mut found_chunks = Vec::new(); let mut missing_chunks = Vec::new(); for coord in coords { - if let Some(chunk) = self.cache.get(&(coord.0, coord.1, coord.2.to_string())) { + if let Some(chunk) = self.cache.get(&(coord.0.x, coord.0.y, coord.1.to_string())) { found_chunks.push(chunk); } else { missing_chunks.push(*coord); @@ -107,11 +112,15 @@ impl World { /// This function will load a chunk from the storage backend and insert it into the cache /// without returning the chunk. This is useful for preloading chunks into the cache before /// they are needed. - pub fn pre_cache(&self, x: i32, z: i32, dimension: &str) -> Result<(), WorldError> { - if self.cache.get(&(x, z, dimension.to_string())).is_none() { - let chunk = load_chunk_internal(self, x, z, dimension)?; + pub fn pre_cache(&self, pos: IVec2, dimension: &str) -> Result<(), WorldError> { + if self + .cache + .get(&(pos.x, pos.y, dimension.to_string())) + .is_none() + { + let chunk = load_chunk_internal(self, pos, dimension)?; self.cache - .insert((x, z, dimension.to_string()), Arc::new(chunk)); + .insert((pos.x, pos.y, dimension.to_string()), Arc::new(chunk)); } Ok(()) } @@ -126,7 +135,7 @@ pub(crate) fn save_chunk_internal(world: &World, chunk: &Chunk) -> Result<(), Wo yazi::Format::Zlib, CompressionLevel::BestSpeed, )?; - let digest = create_key(chunk.dimension.as_str(), chunk.x, chunk.z); + let digest = create_key(chunk.dimension.as_str(), IVec2::new(chunk.x, chunk.z)); world .storage_backend .upsert("chunks".to_string(), digest, as_bytes)?; @@ -135,11 +144,10 @@ pub(crate) fn save_chunk_internal(world: &World, chunk: &Chunk) -> Result<(), Wo pub(crate) fn load_chunk_internal( world: &World, - x: i32, - z: i32, + pos: IVec2, dimension: &str, ) -> Result { - let digest = create_key(dimension, x, z); + let digest = create_key(dimension, pos); match world.storage_backend.get("chunks".to_string(), digest)? { Some(compressed) => { let (data, checksum) = yazi::decompress(compressed.as_slice(), yazi::Format::Zlib)?; @@ -163,11 +171,11 @@ pub(crate) fn load_chunk_internal( pub(crate) fn load_chunk_batch_internal( world: &World, - coords: &[(i32, i32, &str)], + coords: &[(IVec2, &str)], ) -> Result, WorldError> { let digests = coords .iter() - .map(|&(x, z, dim)| create_key(dim, x, z)) + .map(|&(pos, dim)| create_key(dim, pos)) .collect(); world .storage_backend @@ -197,24 +205,22 @@ pub(crate) fn load_chunk_batch_internal( pub(crate) fn chunk_exists_internal( world: &World, - x: i32, - z: i32, + pos: IVec2, dimension: &str, ) -> Result { if !world.storage_backend.table_exists("chunks".to_string())? { return Ok(false); } - let digest = create_key(dimension, x, z); + let digest = create_key(dimension, pos); Ok(world.storage_backend.exists("chunks".to_string(), digest)?) } pub(crate) fn delete_chunk_internal( world: &World, - x: i32, - z: i32, + pos: IVec2, dimension: &str, ) -> Result<(), WorldError> { - let digest = create_key(dimension, x, z); + let digest = create_key(dimension, pos); world.storage_backend.delete("chunks".to_string(), digest)?; Ok(()) } @@ -224,18 +230,10 @@ pub(crate) fn sync_internal(world: &World) -> Result<(), WorldError> { Ok(()) } -fn create_key(dimension: &str, x: i32, z: i32) -> u128 { - let mut key = 0u128; - let mut hasher = wyhash::WyHash::with_seed(0); +fn create_key(dimension: &str, pos: IVec2) -> u128 { + let mut hasher = simplehash::MurmurHasher128::new(0); hasher.write(dimension.as_bytes()); - hasher.write_u8(0xFF); - let dim_hash = hasher.finish(); - // Insert the dimension hash into the key as the first 32 bits - key |= (dim_hash as u128) << 96; - // Convert the x coordinate to a 48 bit integer and insert it into the key - key |= ((x as u128) & 0x0000_0000_FFFF_FFFF) << 48; - // Convert the z coordinate to a 48 bit integer and insert it into the key - key |= (z as u128) & 0x0000_0000_FFFF_FFFF; - - key + hasher.write(&pos.x.to_le_bytes()); + hasher.write(&pos.y.to_le_bytes()); + hasher.finish_u128() } diff --git a/src/lib/world/src/edit_batch.rs b/src/lib/world/src/edit_batch.rs index 0c629c166..b9fe57eea 100644 --- a/src/lib/world/src/edit_batch.rs +++ b/src/lib/world/src/edit_batch.rs @@ -1,370 +1,370 @@ -use crate::block_id::BlockId; -use crate::chunk_format::{BiomeStates, BlockStates, Chunk, PaletteType}; -use crate::WorldError; -use ahash::{AHashMap, AHashSet, AHasher}; -use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; -use ferrumc_general_purpose::data_packing::u32::write_nbit_u32; -use ferrumc_net_codec::net_types::var_int::VarInt; -use std::collections::HashMap; -use std::hash::{Hash, Hasher}; - -/// A batched block editing utility for a single Minecraft chunk. -/// -/// `EditBatch` lets you queue many block edits and apply them all at once with high efficiency. -/// It deduplicates edits, compresses palette usage, and minimizes packed data writes. -/// -/// # Example -/// ``` -/// # use ferrumc_world::chunk_format::Chunk; -/// # use ferrumc_world::edit_batch::EditBatch; -/// # use ferrumc_world::vanilla_chunk_format::BlockData; -/// # let mut chunk = Chunk::new(0, 0, "overworld".to_string()); -/// let mut batch = EditBatch::new(&mut chunk); -/// batch.set_block(1, 64, 1, BlockData { name: "minecraft:stone".to_string(), properties: None }); -/// batch.set_block(2, 64, 1, BlockData { name: "minecraft:bricks".to_string(), properties: None }); -/// batch.apply().unwrap(); -/// ``` -/// -/// `EditBatch` is single-use. After `apply()`, reuse it by creating a new one. -/// # Note -/// This is much faster than calling `set_block` for each block individually, but slower than filling -/// entire sections with the same block type. If you need to fill a section with the same block type, use -/// `Chunk::set_section` instead. However, there is a small amount of memory overhead for setting -/// up the batch, so if you only need to set one or two blocks, it's better to just call `set_block` -pub struct EditBatch<'a> { - pub(crate) edits: Vec, - chunk: &'a mut Chunk, - tmp_palette_map: AHashMap, - used: bool, -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(crate) struct Edit { - pub(crate) x: i32, - pub(crate) y: i32, - pub(crate) z: i32, - pub(crate) block: BlockId, -} - -fn get_palette_hash(palette: &[VarInt]) -> i32 { - let mut rolling = 0; - let mut hasher = AHasher::default(); - for block in palette.iter() { - (rolling + block.0).hash(&mut hasher); - rolling = hasher.finish() as i32; - } - rolling -} - -impl<'a> EditBatch<'a> { - /// Creates a new `EditBatch` for the given chunk. - /// - /// This doesn't return the modified chunk, as the edits are applied in place. - /// This means you should create the batch, add edits, apply and then use the original chunk - /// you passed in. - pub fn new(chunk: &'a mut Chunk) -> Self { - let map_capacity = 64; - Self { - edits: Vec::new(), - chunk, - tmp_palette_map: AHashMap::with_capacity(map_capacity), - used: false, - } - } - - /// Sets a block at the given chunk-relative coordinates. - /// - /// This won't have any effect until `apply()` is called. - pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: impl Into) { - self.edits.push(Edit { - x, - y, - z, - block: block.into(), - }); - } - - /// Applies all edits in the batch to the chunk. - /// - /// This will modify the chunk in place and clear the batch. - /// Will return an error if the batch has already been used or if there are no edits. - pub fn apply(&mut self) -> Result<(), WorldError> { - if self.used { - return Err(WorldError::InvalidBatchingOperation( - "EditBatch has already been used".to_string(), - )); - } - if self.edits.is_empty() { - return Err(WorldError::InvalidBatchingOperation( - "No edits to apply".to_string(), - )); - } - - let mut section_edits: AHashMap>> = AHashMap::new(); - let mut all_blocks = AHashSet::new(); - - // Convert edits into per-section sparse arrays (Vec>), - // using block index (0..4095) as the key instead of hashing 3D coords - for edit in &self.edits { - let section_index = (edit.y >> 4) as i8; - // Compute linear index within section (16x16x16 = 4096 blocks) - let index = ((edit.y & 0xf) * 256 + (edit.z & 0xf) * 16 + (edit.x & 0xf)) as usize; - let section_vec = section_edits - .entry(section_index) - .or_insert_with(|| vec![None; 4096]); - section_vec[index] = Some(edit); - all_blocks.insert(&edit.block); - } - - for (section_y, edits_vec) in section_edits { - if edits_vec.is_empty() || edits_vec.iter().all(|e| e.is_none()) { - continue; - } - let section_maybe = self.chunk.sections.iter_mut().find(|s| s.y == section_y); - // let first_edit = edits_vec - // .iter() - // .find(|e| e.is_some()) - // .expect("Section should have at least one edit") - // .as_ref() - // .unwrap(); - let mut block_count_adds = AHashMap::new(); - let mut block_count_removes = AHashMap::new(); - - let section = match section_maybe { - Some(section) => { - // If the section exists, we can just use it - section - } - None => &mut { - // If the section doesn't exist, create it - let new_section = crate::chunk_format::Section { - y: section_y, - block_states: BlockStates { - non_air_blocks: 0, - block_data: PaletteType::Single(VarInt::default()), - block_counts: HashMap::from([(BlockId::default(), 4096)]), - }, - // Biomes don't really matter for this, so we can just use empty data - biome_states: BiomeStates { - bits_per_biome: 0, - data: vec![], - palette: vec![], - }, - block_light: vec![255; 2048], - sky_light: vec![255; 2048], - }; - self.chunk.sections.push(new_section); - self.chunk - .sections - .iter_mut() - .find(|s| s.y == section_y) - .expect("Section should exist after push") - }, - }; - - // // check if all the edits in 1 section are the same - // let all_same = edits_vec - // .iter() - // .flatten() - // .all(|edit| edit.block == first_edit.block); - // // Check if applying all edits would result in a section full of the same block - // if all_same { - // if section - // .block_states - // .block_counts - // .get(&first_edit.block) - // .unwrap_or(&0) - // + edits_vec.len() as i32 - // == 4096 - // { - // // If all blocks are the same, we can just set the whole section to that block - // section.fill(first_edit.block.clone())?; - // } - // continue; - // } - - // Convert from Single to Indirect palette if needed to support multiple block types - if let PaletteType::Single(val) = §ion.block_states.block_data { - section.block_states.block_data = PaletteType::Indirect { - bits_per_block: 4, - data: vec![0; 256], - palette: vec![*val], - }; - } - - let PaletteType::Indirect { - bits_per_block, - data, - palette, - } = &mut section.block_states.block_data - else { - return Err(WorldError::InvalidBlockStateData( - "Unsupported palette type".to_string(), - )); - }; - - // Hash current palette so we can detect changes after edits - let palette_hash = get_palette_hash(palette); - - // Rebuild temporary palette index lookup (block ID -> palette index) - self.tmp_palette_map.clear(); - for (i, p) in palette.iter().enumerate() { - self.tmp_palette_map.insert(BlockId::from_varint(*p), i); - } - - // Determine how many blocks fit into each i64 (based on bits per block) - let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; - - for maybe_edit in edits_vec.iter() { - let Some(edit) = maybe_edit else { continue }; - let index = ((edit.y & 0xf) * 256 + (edit.z & 0xf) * 16 + (edit.x & 0xf)) as usize; - - let palette_index = if let Some(&idx) = self.tmp_palette_map.get(&edit.block) { - idx - } else { - let idx = palette.len(); - palette.push(edit.block.to_varint()); - self.tmp_palette_map.insert(edit.block, idx); - idx - }; - - // Calculate i64 slot and bit offset for packed storage - let i64_index = index / blocks_per_i64; - let offset = (index % blocks_per_i64) * (*bits_per_block as usize); - - debug_assert!( - i64_index < data.len(), - "i64_index {} out of bounds for data (len {})", - i64_index, - data.len() - ); - - // Unsafe is safe here because i64_index is verified by debug_assert - let packed = unsafe { data.get_unchecked_mut(i64_index) }; - - // get old block - let old_block_index = - read_nbit_i32(packed, *bits_per_block as usize, offset as u32).map_err( - |e| WorldError::InvalidBlockStateData(format!("Unpacking error: {e}")), - )?; - // If the block is the same, skip - if old_block_index == palette_index as i32 { - continue; - } - - if let Some(old_block_id) = palette.get(old_block_index as usize) { - if let Some(count) = - block_count_removes.get_mut(&BlockId::from_varint(*old_block_id)) - { - *count -= 1; - } else { - block_count_removes.insert(BlockId::from_varint(*old_block_id), 1); - } - } - - if let Some(count) = block_count_adds.get_mut(&edit.block) { - *count += 1; - } else { - block_count_adds.insert(edit.block, 1); - } - - write_nbit_u32(packed, offset as u32, palette_index as u32, *bits_per_block) - .map_err(|e| { - WorldError::InvalidBlockStateData(format!("Packing error: {e}")) - })?; - } - - // Update block counts - for (block_id, count) in block_count_adds { - let current_count = section - .block_states - .block_counts - .entry(block_id) - .or_insert(0); - *current_count += count; - } - - for (block_id, count) in block_count_removes { - let current_count = section - .block_states - .block_counts - .entry(block_id) - .or_insert(0); - *current_count -= count; - } - - section.block_states.non_air_blocks = *section - .block_states - .block_counts - .get(&BlockId::default()) - .unwrap_or(&4096) as u16; - - // Only optimise if the palette changed after edits - if get_palette_hash(palette) != palette_hash { - section.optimise()?; - } - } - - // Clear edits after applying - self.edits.clear(); - self.used = true; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::chunk_format::Chunk; - use crate::vanilla_chunk_format::BlockData; - - fn make_test_block(name: &str) -> BlockId { - BlockData { - name: name.to_string(), - properties: None, - } - .to_block_id() - } - - #[test] - fn test_single_block_edit() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); - let block = make_test_block("minecraft:stone"); - - let mut batch = EditBatch::new(&mut chunk); - batch.set_block(1, 1, 1, block); - batch.apply().unwrap(); - - let got = chunk.get_block(1, 1, 1).unwrap(); - assert_eq!(got, block); - } - - #[test] - fn test_multi_block_edits() { - let mut chunk = Chunk::new(0, 0, "overworld".to_string()); - let stone = make_test_block("minecraft:stone"); - let dirt = make_test_block("minecraft:dirt"); - - let mut batch = EditBatch::new(&mut chunk); - for x in 0..4 { - for y in 0..4 { - for z in 0..4 { - let block = if (x + y + z) % 2 == 0 { stone } else { dirt }; - batch.set_block(x, y, z, block); - } - } - } - batch.apply().unwrap(); - - for x in 0..4 { - for y in 0..4 { - for z in 0..4 { - let expected = if (x + y + z) % 2 == 0 { &stone } else { &dirt }; - let got = chunk.get_block(x, y, z).unwrap(); - assert_eq!(&got, expected); - } - } - } - } -} +// use crate::block_id::BlockId; +// use crate::chunk_format::{BiomeStates, BlockStates, Chunk, PaletteType}; +// use crate::WorldError; +// use ahash::{AHashMap, AHashSet, AHasher}; +// use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; +// use ferrumc_general_purpose::data_packing::u32::write_nbit_u32; +// use ferrumc_net_codec::net_types::var_int::VarInt; +// use intmap::IntMap; +// use std::hash::{Hash, Hasher}; +// +// /// A batched block editing utility for a single Minecraft chunk. +// /// +// /// `EditBatch` lets you queue many block edits and apply them all at once with high efficiency. +// /// It deduplicates edits, compresses palette usage, and minimizes packed data writes. +// /// +// /// # Example +// /// ``` +// /// # use ferrumc_world::chunk_format::Chunk; +// /// # use ferrumc_world::edit_batch::EditBatch; +// /// # use ferrumc_world::vanilla_chunk_format::BlockData; +// /// # let mut chunk = Chunk::new(0, 0, "overworld".to_string()); +// /// let mut batch = EditBatch::new(&mut chunk); +// /// batch.set_block(1, 64, 1, BlockData { name: "minecraft:stone".to_string(), properties: None }); +// /// batch.set_block(2, 64, 1, BlockData { name: "minecraft:bricks".to_string(), properties: None }); +// /// batch.apply().unwrap(); +// /// ``` +// /// +// /// `EditBatch` is single-use. After `apply()`, reuse it by creating a new one. +// /// # Note +// /// This is much faster than calling `set_block` for each block individually, but slower than filling +// /// entire sections with the same block type. If you need to fill a section with the same block type, use +// /// `Chunk::set_section` instead. However, there is a small amount of memory overhead for setting +// /// up the batch, so if you only need to set one or two blocks, it's better to just call `set_block` +// pub struct EditBatch<'a> { +// pub(crate) edits: Vec, +// chunk: &'a mut Chunk, +// tmp_palette_map: AHashMap, +// used: bool, +// } +// +// #[derive(Debug, Clone, PartialEq, Eq, Hash)] +// pub(crate) struct Edit { +// pub(crate) x: i32, +// pub(crate) y: i32, +// pub(crate) z: i32, +// pub(crate) block: BlockId, +// } +// +// fn get_palette_hash(palette: &[VarInt]) -> i32 { +// let mut rolling = 0; +// let mut hasher = AHasher::default(); +// for block in palette.iter() { +// (rolling + block.0).hash(&mut hasher); +// rolling = hasher.finish() as i32; +// } +// rolling +// } +// +// impl<'a> EditBatch<'a> { +// /// Creates a new `EditBatch` for the given chunk. +// /// +// /// This doesn't return the modified chunk, as the edits are applied in place. +// /// This means you should create the batch, add edits, apply and then use the original chunk +// /// you passed in. +// pub fn new(chunk: &'a mut Chunk) -> Self { +// let map_capacity = 64; +// Self { +// edits: Vec::new(), +// chunk, +// tmp_palette_map: AHashMap::with_capacity(map_capacity), +// used: false, +// } +// } +// +// /// Sets a block at the given chunk-relative coordinates. +// /// +// /// This won't have any effect until `apply()` is called. +// pub fn set_block(&mut self, x: i32, y: i32, z: i32, block: impl Into) { +// self.edits.push(Edit { +// x, +// y, +// z, +// block: block.into(), +// }); +// } +// +// /// Applies all edits in the batch to the chunk. +// /// +// /// This will modify the chunk in place and clear the batch. +// /// Will return an error if the batch has already been used or if there are no edits. +// pub fn apply(&mut self) -> Result<(), WorldError> { +// if self.used { +// return Err(WorldError::InvalidBatchingOperation( +// "EditBatch has already been used".to_string(), +// )); +// } +// if self.edits.is_empty() { +// return Err(WorldError::InvalidBatchingOperation( +// "No edits to apply".to_string(), +// )); +// } +// +// let mut section_edits: AHashMap>> = AHashMap::new(); +// let mut all_blocks = AHashSet::new(); +// +// // Convert edits into per-section sparse arrays (Vec>), +// // using block index (0..4095) as the key instead of hashing 3D coords +// for edit in &self.edits { +// let section_index = (edit.y >> 4) as i8; +// // Compute linear index within section (16x16x16 = 4096 blocks) +// let index = ((edit.y & 0xf) * 256 + (edit.z & 0xf) * 16 + (edit.x & 0xf)) as usize; +// let section_vec = section_edits +// .entry(section_index) +// .or_insert_with(|| vec![None; 4096]); +// section_vec[index] = Some(edit); +// all_blocks.insert(&edit.block); +// } +// +// for (section_y, edits_vec) in section_edits { +// if edits_vec.is_empty() || edits_vec.iter().all(|e| e.is_none()) { +// continue; +// } +// let section_maybe = self.chunk.sections.iter_mut().find(|s| s.y == section_y); +// // let first_edit = edits_vec +// // .iter() +// // .find(|e| e.is_some()) +// // .expect("Section should have at least one edit") +// // .as_ref() +// // .unwrap(); +// let mut block_count_adds = AHashMap::new(); +// let mut block_count_removes = AHashMap::new(); +// +// let section = match section_maybe { +// Some(section) => { +// // If the section exists, we can just use it +// section +// } +// None => &mut { +// // If the section doesn't exist, create it +// let new_section = crate::chunk_format::Section { +// y: section_y, +// block_states: BlockStates { +// non_air_blocks: 0, +// block_data: PaletteType::Single(VarInt::default()), +// block_counts: IntMap::from([(BlockId::default(), 4096)]), +// }, +// // Biomes don't really matter for this, so we can just use empty data +// biome_states: BiomeStates { +// bits_per_biome: 0, +// data: vec![], +// palette: vec![], +// }, +// block_light: vec![255; 2048], +// sky_light: vec![255; 2048], +// }; +// self.chunk.sections.push(new_section); +// self.chunk +// .sections +// .iter_mut() +// .find(|s| s.y == section_y) +// .expect("Section should exist after push") +// }, +// }; +// +// // // check if all the edits in 1 section are the same +// // let all_same = edits_vec +// // .iter() +// // .flatten() +// // .all(|edit| edit.block == first_edit.block); +// // // Check if applying all edits would result in a section full of the same block +// // if all_same { +// // if section +// // .block_states +// // .block_counts +// // .get(&first_edit.block) +// // .unwrap_or(&0) +// // + edits_vec.len() as i32 +// // == 4096 +// // { +// // // If all blocks are the same, we can just set the whole section to that block +// // section.fill(first_edit.block.clone())?; +// // } +// // continue; +// // } +// +// // Convert from Single to Indirect palette if needed to support multiple block types +// if let PaletteType::Single(val) = §ion.block_states.block_data { +// section.block_states.block_data = PaletteType::Indirect { +// bits_per_block: 4, +// data: vec![0; 256], +// palette: vec![*val], +// }; +// } +// +// let PaletteType::Indirect { +// bits_per_block, +// data, +// palette, +// } = &mut section.block_states.block_data +// else { +// return Err(WorldError::InvalidBlockStateData( +// "Unsupported palette type".to_string(), +// )); +// }; +// +// // Hash current palette so we can detect changes after edits +// let palette_hash = get_palette_hash(palette); +// +// // Rebuild temporary palette index lookup (block ID -> palette index) +// self.tmp_palette_map.clear(); +// for (i, p) in palette.iter().enumerate() { +// self.tmp_palette_map.insert(BlockId::from_varint(*p), i); +// } +// +// // Determine how many blocks fit into each i64 (based on bits per block) +// let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; +// +// for maybe_edit in edits_vec.iter() { +// let Some(edit) = maybe_edit else { continue }; +// let index = ((edit.y & 0xf) * 256 + (edit.z & 0xf) * 16 + (edit.x & 0xf)) as usize; +// +// let palette_index = if let Some(&idx) = self.tmp_palette_map.get(&edit.block) { +// idx +// } else { +// let idx = palette.len(); +// palette.push(edit.block.to_varint()); +// self.tmp_palette_map.insert(edit.block, idx); +// idx +// }; +// +// // Calculate i64 slot and bit offset for packed storage +// let i64_index = index / blocks_per_i64; +// let offset = (index % blocks_per_i64) * (*bits_per_block as usize); +// +// debug_assert!( +// i64_index < data.len(), +// "i64_index {} out of bounds for data (len {})", +// i64_index, +// data.len() +// ); +// +// // Unsafe is safe here because i64_index is verified by debug_assert +// let packed = unsafe { data.get_unchecked_mut(i64_index) }; +// +// // get old block +// let old_block_index = +// read_nbit_i32(packed, *bits_per_block as usize, offset as u32).map_err( +// |e| WorldError::InvalidBlockStateData(format!("Unpacking error: {e}")), +// )?; +// // If the block is the same, skip +// if old_block_index == palette_index as i32 { +// continue; +// } +// +// if let Some(old_block_id) = palette.get(old_block_index as usize) { +// if let Some(count) = +// block_count_removes.get_mut(&BlockId::from_varint(*old_block_id)) +// { +// *count -= 1; +// } else { +// block_count_removes.insert(BlockId::from_varint(*old_block_id), 1); +// } +// } +// +// if let Some(count) = block_count_adds.get_mut(&edit.block) { +// *count += 1; +// } else { +// block_count_adds.insert(edit.block, 1); +// } +// +// write_nbit_u32(packed, offset as u32, palette_index as u32, *bits_per_block) +// .map_err(|e| { +// WorldError::InvalidBlockStateData(format!("Packing error: {e}")) +// })?; +// } +// +// // Update block counts +// for (block_id, count) in block_count_adds { +// let current_count = section +// .block_states +// .block_counts +// .entry(block_id) +// .or_insert(0); +// *current_count += count; +// } +// +// for (block_id, count) in block_count_removes { +// let current_count = section +// .block_states +// .block_counts +// .entry(block_id) +// .or_insert(0); +// *current_count -= count; +// } +// +// section.block_states.non_air_blocks = *section +// .block_states +// .block_counts +// .get(BlockId::default()) +// .unwrap_or(&4096) as u16; +// +// // Only optimise if the palette changed after edits +// if get_palette_hash(palette) != palette_hash { +// section.optimise()?; +// } +// } +// +// // Clear edits after applying +// self.edits.clear(); +// self.used = true; +// +// Ok(()) +// } +// } +// +// #[cfg(test)] +// mod tests { +// use super::*; +// use crate::chunk_format::Chunk; +// use crate::vanilla_chunk_format::BlockData; +// +// fn make_test_block(name: &str) -> BlockId { +// BlockData { +// name: name.to_string(), +// properties: None, +// } +// .to_block_id() +// } +// +// #[test] +// fn test_single_block_edit() { +// let mut chunk = Chunk::new(0, 0, "overworld".to_string()); +// let block = make_test_block("minecraft:stone"); +// +// let mut batch = EditBatch::new(&mut chunk); +// batch.set_block(1, 1, 1, block); +// batch.apply().unwrap(); +// +// let got = chunk.get_block(1, 1, 1).unwrap(); +// assert_eq!(got, block); +// } +// +// #[test] +// fn test_multi_block_edits() { +// let mut chunk = Chunk::new(0, 0, "overworld".to_string()); +// let stone = make_test_block("minecraft:stone"); +// let dirt = make_test_block("minecraft:dirt"); +// +// let mut batch = EditBatch::new(&mut chunk); +// for x in 0..4 { +// for y in 0..4 { +// for z in 0..4 { +// let block = if (x + y + z) % 2 == 0 { stone } else { dirt }; +// batch.set_block(x, y, z, block); +// } +// } +// } +// batch.apply().unwrap(); +// +// for x in 0..4 { +// for y in 0..4 { +// for z in 0..4 { +// let expected = if (x + y + z) % 2 == 0 { &stone } else { &dirt }; +// let got = chunk.get_block(x, y, z).unwrap(); +// assert_eq!(&got, expected); +// } +// } +// } +// } +// } diff --git a/src/lib/world/src/edits.rs b/src/lib/world/src/edits.rs deleted file mode 100644 index d6ebc6efe..000000000 --- a/src/lib/world/src/edits.rs +++ /dev/null @@ -1,560 +0,0 @@ -use crate::block_id::{BlockId, BLOCK2ID, ID2BLOCK}; -use crate::chunk_format::{BlockStates, Chunk, PaletteType, Section}; -use crate::errors::WorldError; -use crate::vanilla_chunk_format::BlockData; -use crate::World; -use ferrumc_general_purpose::data_packing::i32::read_nbit_i32; -use ferrumc_net_codec::net_types::var_int::VarInt; -use std::collections::hash_map::Entry; -use std::collections::HashMap; -use std::sync::Arc; -use tracing::{debug, error, warn}; - -impl World { - /// Retrieves the block data at the specified coordinates in the given dimension. - /// Under the hood, this function just fetches the chunk containing the block and then calls - /// [`Chunk::get_block`] on it. - /// - /// # Arguments - /// - /// * `x` - The x-coordinate of the block. - /// * `y` - The y-coordinate of the block. - /// * `z` - The z-coordinate of the block. - /// * `dimension` - The dimension in which the block is located. - /// - /// # Returns - /// - /// * `Ok(BlockData)` - The block data at the specified coordinates. - /// * `Err(WorldError)` - If an error occurs while retrieving the block data. - /// - /// # Errors - /// - /// * `WorldError::SectionOutOfBounds` - If the section containing the block is out of bounds. - /// * `WorldError::ChunkNotFound` - If the chunk or block data is not found. - /// * `WorldError::InvalidBlockStateData` - If the block state data is invalid. - pub fn get_block_and_fetch( - &self, - x: i32, - y: i32, - z: i32, - dimension: &str, - ) -> Result { - let chunk_x = x >> 4; - let chunk_z = z >> 4; - let chunk = self.load_chunk(chunk_x, chunk_z, dimension)?; - chunk.get_block(x, y, z) - } - - /// Sets the block data at the specified coordinates in the given dimension. - /// Under the hood, this function just fetches the chunk containing the block and then calls - /// [`Chunk::set_block`] on it. - /// - /// # Arguments - /// - /// * `x` - The x-coordinate of the block. - /// * `y` - The y-coordinate of the block. - /// * `z` - The z-coordinate of the block. - /// * `dimension` - The dimension in which the block is located. - /// * `block` - The block data to set. - /// - /// # Returns - /// - /// * `Ok(())` - If the block data is successfully set. - /// * `Err(WorldError)` - If an error occurs while setting the block data. - pub fn set_block_and_fetch( - &self, - x: i32, - y: i32, - z: i32, - dimension: &str, - block: impl Into, - ) -> Result<(), WorldError> { - let block = block.into(); - if ID2BLOCK.get(block.0 as usize).is_none() { - return Err(WorldError::InvalidBlockId(block.0)); - }; - // Get chunk - let chunk_x = x >> 4; - let chunk_z = z >> 4; - let mut chunk = self.load_chunk_owned(chunk_x, chunk_z, dimension)?; - - debug!("Chunk: {}, {}", chunk_x, chunk_z); - - chunk.set_block(x, y, z, block)?; - for section in &mut chunk.sections { - section.optimise()?; - } - - // Save chunk - self.save_chunk(Arc::new(chunk))?; - Ok(()) - } -} - -impl BlockStates { - pub fn resize(&mut self, new_bit_size: usize) -> Result<(), WorldError> { - match &mut self.block_data { - PaletteType::Single(val) => { - let block = ID2BLOCK - .get(val.0 as usize) - .cloned() - .unwrap_or(BlockData::default()); - let mut new_palette = vec![VarInt::from(0); 1]; - if let Some(id) = BLOCK2ID.get(&block) { - new_palette[0] = VarInt::from(*id); - } else { - error!("Could not find block id for palette entry: {:?}", block); - } - self.block_data = PaletteType::Indirect { - bits_per_block: new_bit_size as u8, - data: vec![], - palette: new_palette, - } - } - PaletteType::Indirect { - bits_per_block, - data, - palette, - } => { - // Step 1: Read existing packed data into a list of normal integers - let mut normalised_ints = Vec::with_capacity(4096); - let mut values_read = 0; - - for long in data { - let mut bit_offset = 0; - - while bit_offset + *bits_per_block as usize <= 64 { - if values_read >= 4096 { - break; - } - - // Extract value at the current bit offset - let value = - read_nbit_i32(long, *bits_per_block as usize, bit_offset as u32)?; - let max_int_value = (1 << new_bit_size) - 1; - if value > max_int_value { - return Err(WorldError::InvalidBlockStateData(format!( - "Value {value} exceeds maximum value for {new_bit_size}-bit block state" - ))); - } - normalised_ints.push(value); - values_read += 1; - - bit_offset += *bits_per_block as usize; - } - - // Stop reading if we’ve already hit 4096 values - if values_read >= 4096 { - break; - } - } - - // Check if we read exactly 4096 block states - if normalised_ints.len() != 4096 { - return Err(WorldError::InvalidBlockStateData(format!( - "Expected 4096 block states, but got {}", - normalised_ints.len() - ))); - } - - // Step 2: Write the normalised integers into the new packed format - let mut new_data = Vec::new(); - let mut current_long: i64 = 0; - let mut bit_position = 0; - - for &value in &normalised_ints { - current_long |= (value as i64) << bit_position; - bit_position += new_bit_size; - - if bit_position >= 64 { - new_data.push(current_long); - current_long = (value as i64) >> (new_bit_size - (bit_position - 64)); - bit_position -= 64; - } - } - - // Push any remaining bits in the final long - if bit_position > 0 { - new_data.push(current_long); - } - - // Verify the size of the new data matches expectations - let expected_size = (4096 * new_bit_size).div_ceil(64); - if new_data.len() != expected_size { - return Err(WorldError::InvalidBlockStateData(format!( - "Expected packed data size of {}, but got {}", - expected_size, - new_data.len() - ))); - } - // Update the chunk with the new packed data and a bit size - self.block_data = PaletteType::Indirect { - bits_per_block: new_bit_size as u8, - data: new_data, - palette: palette.clone(), - } - } - _ => { - todo!("Implement resizing for direct palette") - } - }; - Ok(()) - } -} - -impl Chunk { - /// Sets the block at the specified coordinates to the specified block data. - /// If the block is the same as the old block, nothing happens. - /// If the block is not in the palette, it is added. - /// If the palette is in single block mode, it is converted to palette'd mode. - /// - /// # Arguments - /// - /// * `x` - The x-coordinate of the block. - /// * `y` - The y-coordinate of the block. - /// * `z` - The z-coordinate of the block. - /// * `block` - The block data to set the block to. - /// - /// # Returns - /// - /// * `Ok(())` - If the block was successfully set. - /// * `Err(WorldError)` - If an error occurs while setting the block. - /// - /// ### Note - /// The positions are modulo'd by 16 to get the block index in the section anyway, so converting - /// the coordinates to section coordinates isn't really necessary, but you should probably do it - /// anyway for readability's sake. - pub fn set_block( - &mut self, - x: i32, - y: i32, - z: i32, - block: impl Into, - ) -> Result<(), WorldError> { - let block = block.into(); - // Get old block - let old_block = self.get_block(x, y, z)?; - if old_block == block { - // debug!("Block is the same as the old block"); - return Ok(()); - } - // Get section - let section = self - .sections - .iter_mut() - .find(|section| section.y == (y >> 4) as i8) - .ok_or(WorldError::SectionOutOfBounds(y >> 4))?; - - let mut converted = false; - let mut new_contents = PaletteType::Indirect { - bits_per_block: 4, - data: vec![], - palette: vec![], - }; - - if let PaletteType::Single(val) = §ion.block_states.block_data { - new_contents = PaletteType::Indirect { - bits_per_block: 4, - data: vec![0; 256], - palette: vec![*val], - }; - converted = true; - } - - if converted { - section.block_states.block_data = new_contents; - } - - // Do different things based on the palette type - match &mut section.block_states.block_data { - PaletteType::Single(_val) => { - panic!("Single palette type should have been converted to indirect palette type"); - } - PaletteType::Indirect { - bits_per_block, - data, - palette, - } => { - // debug!("Indirect mode"); - match section.block_states.block_counts.entry(old_block) { - Entry::Occupied(mut occ_entry) => { - let count = occ_entry.get_mut(); - if *count <= 0 { - return match old_block.to_block_data() { - Some(block_data) => { - error!("Block count is zero for block: {:?}", block_data); - Err(WorldError::InvalidBlockStateData(format!( - "Block count is zero for block: {block_data:?}" - ))) - } - None => { - error!( - "Block count is zero for unknown block ID: {}", - old_block.0 - ); - Err(WorldError::InvalidBlockId(old_block.0)) - } - }; - } - *count -= 1; - } - Entry::Vacant(empty_entry) => { - warn!("Block not found in block counts: {:?}", old_block); - empty_entry.insert(0); - } - } - // Add new block - if let Some(e) = section.block_states.block_counts.get(&block) { - section.block_states.block_counts.insert(block, e + 1); - } else { - // debug!("Adding block to block counts"); - section.block_states.block_counts.insert(block, 1); - } - // let required_bits = max((palette.len() as f32).log2().ceil() as u8, 4); - // if *bits_per_block != required_bits { - // section.block_states.resize(required_bits as usize)?; - // } - // Get block index - let block_palette_index = palette - .iter() - .position(|p| *p == block.to_varint()) - .unwrap_or_else(|| { - // Add block to palette if it doesn't exist - let index = palette.len() as i16; - palette.push(block.to_varint()); - index as usize - }); - // Set block - let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; - let index = - ((y.abs() & 0xf) * 256 + (z.abs() & 0xf) * 16 + (x.abs() & 0xf)) as usize; - let i64_index = index / blocks_per_i64; - let packed_u64 = - data.get_mut(i64_index) - .ok_or(WorldError::InvalidBlockStateData(format!( - "Invalid block state data at index {i64_index}" - )))?; - let offset = (index % blocks_per_i64) * *bits_per_block as usize; - if let Err(e) = ferrumc_general_purpose::data_packing::u32::write_nbit_u32( - packed_u64, - offset as u32, - block_palette_index as u32, - *bits_per_block, - ) { - return Err(WorldError::InvalidBlockStateData(format!( - "Failed to write block: {e}" - ))); - } - } - PaletteType::Direct { .. } => { - todo!("Implement direct palette for set_block"); - } - } - - section.block_states.non_air_blocks = section - .block_states - .block_counts - .iter() - .filter(|(block, _)| { - // Air, void air and cave air respectively - ![0, 12958, 12959].contains(&block.0) - }) - .map(|(_, count)| *count as u16) - .sum(); - - self.sections - .iter_mut() - .for_each(|section| section.optimise().unwrap()); - Ok(()) - } - - /// Gets the block at the specified coordinates. - /// - /// # Arguments - /// - /// * `x` - The x-coordinate of the block. - /// * `y` - The y-coordinate of the block. - /// * `z` - The z-coordinate of the block. - /// - /// # Returns - /// - /// * `Ok(BlockData)` - The block data at the specified coordinates. - /// * `Err(WorldError)` - If an error occurs while retrieving the block data. - /// - /// ### Note - /// The positions are modulo'd by 16 to get the block index in the section anyway, so converting - /// the coordinates to section coordinates isn't really necessary, but you should probably do it - /// anyway for readability's sake. - pub fn get_block(&self, x: i32, y: i32, z: i32) -> Result { - let section = self - .sections - .iter() - .find(|section| section.y == (y / 16) as i8) - .ok_or(WorldError::SectionOutOfBounds(y >> 4))?; - match §ion.block_states.block_data { - PaletteType::Single(val) => Ok(BlockId::from_varint(*val)), - PaletteType::Indirect { - bits_per_block, - data, - palette, - } => { - if palette.len() == 1 || *bits_per_block == 0 { - return Ok(BlockId::from_varint(palette[0])); - } - let blocks_per_i64 = (64f64 / *bits_per_block as f64).floor() as usize; - let index = ((y & 0xf) * 256 + (z & 0xf) * 16 + (x & 0xf)) as usize; - let i64_index = index / blocks_per_i64; - let packed_u64 = data - .get(i64_index) - .ok_or(WorldError::InvalidBlockStateData(format!( - "Invalid block state data at index {i64_index}" - )))?; - let offset = (index % blocks_per_i64) * *bits_per_block as usize; - let id = ferrumc_general_purpose::data_packing::u32::read_nbit_u32( - packed_u64, - *bits_per_block, - offset as u32, - )?; - let palette_id = palette.get(id as usize).ok_or(WorldError::ChunkNotFound)?; - Ok(BlockId::from_varint(*palette_id)) - } - &PaletteType::Direct { .. } => todo!("Implement direct palette for get_block"), - } - } - - /// Sets the section at the specified index to the specified block data. - /// If the section is out of bounds, an error is returned. - /// - /// # Arguments - /// - /// * `section` - The index of the section to set. - /// * `block` - The block data to set the section to. - /// - /// # Returns - /// - /// * `Ok(())` - If the section was successfully set. - /// * `Err(WorldError)` - If an error occurs while setting the section. - pub fn set_section(&mut self, section_y: i8, block: BlockData) -> Result<(), WorldError> { - if let Some(section) = self - .sections - .iter_mut() - .find(|section| section.y == section_y) - { - section.fill(block.clone()) - } else { - Err(WorldError::SectionOutOfBounds(section_y as i32)) - } - } - - /// Fills the chunk with the specified block. - /// - /// # Arguments - /// - /// * `block` - The block data to fill the chunk with. - /// - /// # Returns - /// - /// * `Ok(())` - If the chunk was successfully filled. - /// * `Err(WorldError)` - If an error occurs while filling the chunk. - pub fn fill(&mut self, block: BlockData) -> Result<(), WorldError> { - for section in &mut self.sections { - section.fill(block.clone())?; - } - Ok(()) - } -} - -impl Section { - /// Fills the section with the specified block. - /// - /// # Arguments - /// - /// * `block` - The block data to fill the section with. - /// - /// # Returns - /// - /// * `Ok(())` - If the section was successfully filled. - /// * `Err(WorldError)` - If an error occurs while filling the section. - pub fn fill(&mut self, block: impl Into) -> Result<(), WorldError> { - let block = block.into(); - self.block_states.block_data = PaletteType::Single(block.to_varint()); - self.block_states.block_counts = HashMap::from([(block, 4096)]); - // Air, void air and cave air respectively - if [0, 12958, 12959].contains(&block.0) { - self.block_states.non_air_blocks = 0; - } else { - self.block_states.non_air_blocks = 4096; - } - Ok(()) - } - - /// This function trims out unnecessary data from the section. Primarily it does 2 things: - /// - /// 1. Removes any palette entries that are not used in the block states data. - /// - /// 2. If there is only one block in the palette, it converts the palette to single block mode. - pub fn optimise(&mut self) -> Result<(), WorldError> { - match &mut self.block_states.block_data { - PaletteType::Single(_) => { - // If the section is already in single block mode, there's nothing to optimise - return Ok(()); - } - PaletteType::Indirect { - bits_per_block, - data, - palette, - } => { - // Remove empty blocks from palette - let mut remove_indexes = Vec::new(); - for (block, count) in &self.block_states.block_counts { - if *count <= 0 { - let index = palette.iter().position(|p| *p == block.to_varint()); - if let Some(index) = index { - remove_indexes.push(index); - } else { - return Err(WorldError::InvalidBlockId(block.0)); - } - } - } - for index in remove_indexes { - // Decrement any data entries that are higher than the removed index - for data_point in &mut *data { - let mut i = 0; - while (i + *bits_per_block as usize) < 64 { - let block_index = - ferrumc_general_purpose::data_packing::u32::read_nbit_u32( - data_point, - *bits_per_block, - i as u32, - )?; - if block_index > index as u32 { - ferrumc_general_purpose::data_packing::u32::write_nbit_u32( - data_point, - i as u32, - block_index - 1, - *bits_per_block, - )?; - } - i += *bits_per_block as usize; - } - } - } - - { - // If there is only one block in the palette, convert to single block mode - if palette.len() == 1 { - let block = BlockId::from(palette[0]); - self.block_states.block_data = PaletteType::Single(palette[0]); - self.block_states.block_counts.clear(); - self.block_states.block_counts.insert(block, 4096); - } - } - } - PaletteType::Direct { .. } => { - todo!("Implement optimisation for direct palette"); - } - }; - - Ok(()) - } -} diff --git a/src/lib/world/src/lib.rs b/src/lib/world/src/lib.rs index 029f268ed..b07560300 100644 --- a/src/lib/world/src/lib.rs +++ b/src/lib/world/src/lib.rs @@ -1,14 +1,17 @@ pub mod block_id; pub mod chunk_format; +mod chunk_ops; mod db_functions; pub mod edit_batch; -pub mod edits; pub mod errors; mod importing; +pub mod section_ops; pub mod vanilla_chunk_format; +pub mod world_ops; use crate::chunk_format::Chunk; use crate::errors::WorldError; +use bevy_math::{IVec2, IVec3}; use deepsize::DeepSizeOf; use ferrumc_config::server_config::get_global_config; use ferrumc_general_purpose::paths::get_root_path; @@ -21,6 +24,18 @@ use std::sync::Arc; use std::time::Duration; use tracing::{error, trace, warn}; +/// Converts a global block position to an index in a chunk section (0-4095). +pub fn to_index(pos: IVec3) -> usize { + let x = (pos.x & 0xF) as usize; + let y = (pos.y & 0xF) as usize; + let z = (pos.z & 0xF) as usize; + (y << 8) | (z << 4) | x +} + +pub fn get_chunk_coordinates(input: IVec3) -> IVec2 { + IVec2::new(input.x.div_euclid(16), input.z.div_euclid(16)) +} + #[derive(Clone)] pub struct World { storage_backend: LmdbBackend, @@ -119,20 +134,34 @@ impl World { #[cfg(test)] mod tests { use super::*; + use bevy_math::IVec2; #[test] #[ignore] fn dump_chunk() { - let world = World::new( - std::env::current_dir() - .unwrap() - .join("../../../target/debug/world"), - ); - let chunk = world.load_chunk(1, 1, "overworld").expect( + let world = World::new(std::env::current_dir().unwrap().join("../../../world")); + let chunk = world.load_chunk(IVec2::new(0, 0), "overworld").expect( "Failed to load chunk. If it's a bitcode error, chances are the chunk format \ has changed since last generating a world so you'll need to regenerate", ); let encoded = bitcode::encode(&chunk); std::fs::write("../../../.etc/raw_chunk.dat", encoded).unwrap(); } + + #[test] + fn test_to_index_basic() { + // (x, y, z) = (0, 0, 0) should map to 0 + assert_eq!(to_index(IVec3::new(0, 0, 0)), 0); + + // (x, y, z) = (15, 15, 15) should map to the last index in a chunk section + assert_eq!(to_index(IVec3::new(15, 15, 15)), 4095); + + // (x, y, z) = (1, 2, 3) + let expected = (2 << 8) | (3 << 4) | 1; + assert_eq!(to_index(IVec3::new(1, 2, 3)), expected); + + // Values outside 0-15 should be masked + assert_eq!(to_index(IVec3::new(16, 16, 16)), 0); + assert_eq!(to_index(IVec3::new(17, 18, 19)), (2 << 8) | (3 << 4) | 1); + } } diff --git a/src/lib/world/src/section_ops.rs b/src/lib/world/src/section_ops.rs new file mode 100644 index 000000000..348bd2441 --- /dev/null +++ b/src/lib/world/src/section_ops.rs @@ -0,0 +1,250 @@ +use crate::block_id::BlockId; +use crate::chunk_format::{BiomeStates, Section}; +use crate::errors::WorldError; +use crate::to_index; +use bevy_math::IVec3; +use ferrumc_general_purpose::palette::Palette; +use ferrumc_net_codec::net_types::var_int::VarInt; +use tracing::trace; + +impl Section { + /// Creates a new empty section with the given Y level. + /// + /// # Arguments + /// + /// * `level` - The Y level of the section. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Section; + /// + /// let section = Section::new(0); + pub fn new(level: i8) -> Self { + Self { + y: level, + block_states: Palette::new(4096, BlockId::default(), 15), + // Add other fields as necessary + biome_states: BiomeStates { + bits_per_biome: 0, + data: vec![], + palette: vec![VarInt::from(0)], + }, + block_light: vec![255; 2048], + sky_light: vec![255; 2048], + } + } + /// Sets a block in the section at the given position. + /// + /// The position is relativized to the section (0-15) but you + /// should still convert to relative coordinates before calling this function for readability. + /// + /// # Arguments + /// + /// * `pos` - The position of the block to set, relative to the section. + /// * `block` - The BlockId to set at the given position. + /// # Errors + /// * Returns `WorldError::OutOfBounds` if the position is out of bounds. + /// # Examples + /// ```rust + /// use bevy_math::IVec3; + /// use ferrumc_world::chunk_format::Section; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut section = Section::new(0); + /// let pos = IVec3::new(1, 2, 3); + /// let block = BlockId(0); + /// section.set_block(pos, block)?; + /// Ok(()) + /// } + pub fn set_block(&mut self, pos: IVec3, block: BlockId) -> Result<(), WorldError> { + let index = to_index(pos); + self.block_states.set(index, block); + Ok(()) + } + + /// Gets a block in the section at the given position. + /// + /// The position is relativized to the section (0-15) but you + /// should still convert to relative coordinates before calling this function for readability. + /// + /// # Arguments + /// + /// * `pos` - The position of the block to get, relative to the section. + /// # Returns + /// * Returns the BlockId at the given position. If no block is found, returns BlockId::default(). + /// # Examples + /// ```rust + /// use bevy_math::IVec3; + /// use ferrumc_world::chunk_format::Section; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut section = Section::new(0); + /// let pos = IVec3::new(1, 2, 3); + /// let block = BlockId(0); + /// section.set_block(pos, block)?; + /// let retrieved_block = section.get_block(pos); + /// assert_eq!(block, retrieved_block); + /// Ok(()) + /// } + pub fn get_block(&self, pos: IVec3) -> BlockId { + let index = to_index(pos); + match self.block_states.get(index) { + Some(block) => *block, + None => { + trace!( + "Tried to get block but no block found at position: {:?}", + pos + ); + BlockId::default() + } + } + } + + /// Fills the entire section with the given block. + /// + /// # Arguments + /// + /// * `block` - The BlockId to fill the section with. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Section; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut section = Section::new(0); + /// let block = BlockId(0); + /// section.fill(block); + /// Ok(()) + /// } + pub fn fill(&mut self, block: BlockId) { + self.block_states = Palette::new(4096, block, 15); + } + + /// Optimises the section's block state palette. + /// + /// This should be called after a series of set operations to ensure the palette is as compact as possible. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Section; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// use bevy_math::IVec3; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut section = Section::new(0); + /// let block = BlockId(0); + /// section.fill(block); + /// section.set_block(IVec3::new(1, 2, 3), BlockId(0))?; + /// section.optimise(); + /// Ok(()) + /// } + pub fn optimise(&mut self) { + self.block_states.optimise() + } + + /// Gets the count of a specific block in the section. + /// + /// # Arguments + /// + /// * `block` - The BlockId to count in the section. + /// # Returns + /// * Returns the count of the specified block in the section. + /// # Examples + /// ```rust + /// use ferrumc_world::chunk_format::Section; + /// use ferrumc_world::block_id::BlockId; + /// use ferrumc_world::errors::WorldError; + /// + /// fn main() -> Result<(), WorldError> { + /// let mut section = Section::new(0); + /// let block = BlockId(0); + /// section.fill(block); + /// let count = section.get_count(&block); + /// assert_eq!(count, 4096); + /// Ok(()) + /// } + pub fn get_count(&self, block: &BlockId) -> usize { + self.block_states.get_count(block) + } +} + +#[cfg(test)] +mod tests { + use super::Section; + use crate::block_id::BlockId; + use crate::vanilla_chunk_format::BlockData; + use bevy_math::IVec3; + + #[test] + fn test_set_and_get_block() { + let mut section = Section::new(0); + let pos = IVec3::new(1, 2, 3); + let block = BlockId::from(BlockData { + name: "minecraft:stone".to_string(), + properties: None, + }); + section.set_block(pos, block).unwrap(); + assert_eq!(section.get_block(pos), block); + } + + #[test] + fn test_get_block_default() { + let section = Section::new(0); + let pos = IVec3::new(0, 0, 0); + assert_eq!(section.get_block(pos), BlockId::default()); + } + + #[test] + fn test_fill() { + let mut section = Section::new(0); + let block = BlockId::from(BlockData { + name: "minecraft:stone".to_string(), + properties: None, + }); + section.fill(block); + let pos = IVec3::new(5, 5, 5); + assert_eq!(section.get_block(pos), block); + } + + #[test] + fn test_optimise() { + let mut section = Section::new(0); + let block = BlockId::from(BlockData { + name: "minecraft:stone".to_string(), + properties: None, + }); + section.fill(block); + section + .set_block( + IVec3::new(1, 1, 1), + BlockId::from(BlockData { + name: "minecraft:dirt".to_string(), + properties: None, + }), + ) + .unwrap(); + section.optimise(); + // No assertion, just ensure no panic + } + + #[test] + fn test_get_count() { + let mut section = Section::new(0); + let stone = BlockId::from(BlockData { + name: "minecraft:stone".to_string(), + properties: None, + }); + let dirt = BlockId::from(BlockData { + name: "minecraft:dirt".to_string(), + properties: None, + }); + section.fill(stone); + section.set_block(IVec3::new(1, 1, 1), dirt).unwrap(); + assert_eq!(section.get_count(&stone), 4095); + assert_eq!(section.get_count(&dirt), 1); + } +} diff --git a/src/lib/world/src/world_ops.rs b/src/lib/world/src/world_ops.rs new file mode 100644 index 000000000..599579b1d --- /dev/null +++ b/src/lib/world/src/world_ops.rs @@ -0,0 +1,31 @@ +use crate::World; +use bevy_math::{IVec2, IVec3}; +use std::sync::Arc; + +impl World { + pub fn get_block_and_fetch( + &self, + chunk: IVec2, + block: IVec3, + dimension: &str, + ) -> Option { + if let Ok(chunk) = self.load_chunk(chunk, dimension) { + Some(chunk.get_block(block)) + } else { + None + } + } + + pub fn set_block_and_save( + &mut self, + chunk: IVec2, + block: IVec3, + dimension: &str, + block_id: crate::block_id::BlockId, + ) -> Result<(), crate::errors::WorldError> { + let mut chunk = self.load_chunk_owned(chunk, dimension)?; + chunk.set_block(block, block_id)?; + self.save_chunk(Arc::new(chunk))?; + Ok(()) + } +} diff --git a/src/lib/world_gen/Cargo.toml b/src/lib/world_gen/Cargo.toml index afcf3ab54..266f89790 100644 --- a/src/lib/world_gen/Cargo.toml +++ b/src/lib/world_gen/Cargo.toml @@ -8,6 +8,9 @@ ferrumc-world = { workspace = true } thiserror = { workspace = true } noise = { workspace = true } rand = { workspace = true } +bevy_math = { workspace = true } [lints] workspace = true + +[dev-dependencies] diff --git a/src/lib/world_gen/src/biomes/plains.rs b/src/lib/world_gen/src/biomes/plains.rs index f993f1f22..316c27222 100644 --- a/src/lib/world_gen/src/biomes/plains.rs +++ b/src/lib/world_gen/src/biomes/plains.rs @@ -1,7 +1,7 @@ use crate::errors::WorldGenError; use crate::{BiomeGenerator, NoiseGenerator}; +use bevy_math::{IVec2, IVec3}; use ferrumc_world::chunk_format::Chunk; -use ferrumc_world::edit_batch::EditBatch; use ferrumc_world::vanilla_chunk_format::BlockData; use std::collections::BTreeMap; @@ -16,35 +16,32 @@ impl BiomeGenerator for PlainsBiome { "plains".to_string() } - fn generate_chunk( - &self, - x: i32, - z: i32, - noise: &NoiseGenerator, - ) -> Result { - let mut chunk = Chunk::new(x, z, "overworld".to_string()); + fn generate_chunk(&self, pos: IVec2, noise: &NoiseGenerator) -> Result { + let mut chunk = Chunk::new(pos, "overworld".to_string()); let mut heights = vec![]; let stone = BlockData { name: "minecraft:stone".to_string(), properties: None, - }; + } + .to_block_id(); // Fill with water first for section_y in -4..4 { - chunk.set_section( + chunk.fill_section( section_y as i8, BlockData { name: "minecraft:water".to_string(), properties: Some(BTreeMap::from([("level".to_string(), "0".to_string())])), - }, + } + .to_block_id(), )?; } // Then generate some heights for chunk_x in 0..16i64 { for chunk_z in 0..16i64 { - let global_x = i64::from(x) * 16 + chunk_x; - let global_z = i64::from(z) * 16 + chunk_z; + let global_x = i64::from(pos.x) * 16 + chunk_x; + let global_z = i64::from(pos.y) * 16 + chunk_z; let height = noise.get_noise(global_x as f64, global_z as f64); let height = (height * 64.0) as i32 + 64; heights.push((global_x, global_z, height)); @@ -56,44 +53,47 @@ impl BiomeGenerator for PlainsBiome { let y_min = heights.iter().min_by(|a, b| a.2.cmp(&b.2)).unwrap().2; let highest_full_section = y_min / 16; for section_y in -4..highest_full_section { - chunk.set_section(section_y as i8, stone.clone())?; + chunk.fill_section(section_y as i8, stone)?; } - let mut batch = EditBatch::new(&mut chunk); let above_filled_sections = (highest_full_section * 16) - 1; for (global_x, global_z, height) in heights { if height > above_filled_sections { let height = height - above_filled_sections; for y in 0..height { if y + above_filled_sections <= 64 { - batch.set_block( - global_x as i32 & 0xF, - y + above_filled_sections, - global_z as i32 & 0xF, + chunk.set_block( + IVec3::new( + global_x as i32 & 0xF, + y + above_filled_sections, + global_z as i32 & 0xF, + ), BlockData { name: "minecraft:sand".to_string(), properties: None, - }, - ); + } + .to_block_id(), + )?; } else { - batch.set_block( - global_x as i32 & 0xF, - y + above_filled_sections, - global_z as i32 & 0xF, + chunk.set_block( + IVec3::new( + global_x as i32 & 0xF, + y + above_filled_sections, + global_z as i32 & 0xF, + ), BlockData { name: "minecraft:grass_block".to_string(), properties: Some(BTreeMap::from([( "snowy".to_string(), "false".to_string(), )])), - }, - ); + } + .to_block_id(), + )?; } } } } - batch.apply()?; - Ok(chunk) } } @@ -106,7 +106,7 @@ mod test { fn test_is_ok() { let generator = PlainsBiome {}; let noise = NoiseGenerator::new(0); - assert!(generator.generate_chunk(0, 0, &noise).is_ok()); + assert!(generator.generate_chunk(IVec2::new(0, 0), &noise).is_ok()); } #[test] @@ -116,7 +116,7 @@ mod test { for _ in 0..100 { let x = rand::random::(); let z = rand::random::(); - assert!(generator.generate_chunk(x, z, &noise).is_ok()); + assert!(generator.generate_chunk(IVec2::new(x, z), &noise).is_ok()); } } @@ -126,12 +126,12 @@ mod test { let noise = NoiseGenerator::new(0); assert!( generator - .generate_chunk(1610612735, 1610612735, &noise) + .generate_chunk(IVec2::new(1610612735, 1610612735), &noise) .is_ok() ); assert!( generator - .generate_chunk(-1610612735, -1610612735, &noise) + .generate_chunk(IVec2::new(-1610612735, -1610612735), &noise) .is_ok() ); } @@ -142,7 +142,7 @@ mod test { let generator = PlainsBiome {}; let seed = rand::random::(); let noise = NoiseGenerator::new(seed); - assert!(generator.generate_chunk(0, 0, &noise).is_ok()); + assert!(generator.generate_chunk(IVec2::new(0, 0), &noise).is_ok()); } } } diff --git a/src/lib/world_gen/src/lib.rs b/src/lib/world_gen/src/lib.rs index 30bbbb61e..9c3a697f2 100644 --- a/src/lib/world_gen/src/lib.rs +++ b/src/lib/world_gen/src/lib.rs @@ -2,6 +2,7 @@ mod biomes; pub mod errors; use crate::errors::WorldGenError; +use bevy_math::IVec2; use ferrumc_world::chunk_format::Chunk; use noise::{Clamp, NoiseFn, OpenSimplex}; @@ -11,12 +12,7 @@ use noise::{Clamp, NoiseFn, OpenSimplex}; pub(crate) trait BiomeGenerator { fn _biome_id(&self) -> u8; fn _biome_name(&self) -> String; - fn generate_chunk( - &self, - x: i32, - z: i32, - noise: &NoiseGenerator, - ) -> Result; + fn generate_chunk(&self, pos: IVec2, noise: &NoiseGenerator) -> Result; } pub(crate) struct NoiseGenerator { @@ -57,13 +53,13 @@ impl WorldGenerator { } } - fn get_biome(&self, _x: i32, _z: i32) -> Box { + fn get_biome(&self, _pos: IVec2) -> Box { // Implement biome selection here Box::new(biomes::plains::PlainsBiome) } - pub fn generate_chunk(&self, x: i32, z: i32) -> Result { - let biome = self.get_biome(x, z); - biome.generate_chunk(x, z, &self.noise_generator) + pub fn generate_chunk(&self, pos: IVec2) -> Result { + let biome = self.get_biome(pos); + biome.generate_chunk(pos, &self.noise_generator) } }