From 5b563bad6733dcbf8c2c95ca8018f2510cf0217b Mon Sep 17 00:00:00 2001 From: popertots Date: Sat, 21 Mar 2026 16:06:41 +0000 Subject: [PATCH] perf: SmallVec inline storage, TOML drop tables, seeded deterministic RNG - SmallVec<[DropEntry; 2]> replaces Vec in DropTable: zero heap allocation for all current tiles (0 or 1 entries), spills to heap only at 3+ - DropEntry fields packed to u8 (chance_pct, min_count, max_count): ~12 bytes -> 4 bytes; derives Copy so no extra clones - Replaced SystemTime pseudo-RNG with dig_rng(pos): PCG-style hash of world SEED + tile position. Deterministic per world, same dig = same roll every time - TOML-driven drop tables: assets/drop_tables.toml (grass=5%/1-2, rock=10%/1, dirt/air=none). TOML parsed at startup into DropTableRegistry resource - OnceLock global map for async terrain generation tasks to access drop tables without Bevy resource borrowing - terrain.rs: DropTableRegistry::global_get("tile") replaces hardcoded drop_table_for --- Cargo.lock | 1 + Cargo.toml | 1 + assets/drop_tables.toml | 15 +++ src/entities/item/drop_table.rs | 123 ++++++++++++++++++------- src/entities/shared_systems/digging.rs | 10 +- src/world/generation/terrain.rs | 28 ++---- src/world/mod.rs | 5 +- 7 files changed, 124 insertions(+), 59 deletions(-) create mode 100644 assets/drop_tables.toml diff --git a/Cargo.lock b/Cargo.lock index a8e57fe..b1c19f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2444,6 +2444,7 @@ dependencies = [ "rayon", "rustc-hash 2.1.1", "serde", + "smallvec", "toml", ] diff --git a/Cargo.toml b/Cargo.toml index 19ee719..240f54b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ serde = { version = "1.0", features = ["derive"] } toml = "0.9.8" rayon = "1.11.0" rustc-hash = "2.1.1" +smallvec = { version = "1", features = ["union"] } ahash = "0.8.12" nohash-hasher = "0.2.0" futures-lite = "2.6.1" diff --git a/assets/drop_tables.toml b/assets/drop_tables.toml new file mode 100644 index 0000000..26cd69e --- /dev/null +++ b/assets/drop_tables.toml @@ -0,0 +1,15 @@ +[grass] +drops = [ + { prefab = "Coin", chance_pct = 5, min_count = 1, max_count = 2 }, +] + +[dirt] +drops = [] + +[rock] +drops = [ + { prefab = "Coin", chance_pct = 10, min_count = 1, max_max = 1 }, +] + +[air] +drops = [] diff --git a/src/entities/item/drop_table.rs b/src/entities/item/drop_table.rs index a73d518..7b2d4e7 100644 --- a/src/entities/item/drop_table.rs +++ b/src/entities/item/drop_table.rs @@ -1,46 +1,50 @@ +use crate::constants::SEED; use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab; use bevy::prelude::*; +use serde::Deserialize; +use smallvec::SmallVec; +use std::collections::HashMap; +use std::sync::OnceLock; -#[derive(Clone, Debug)] +static DROP_TABLE_GLOBAL: OnceLock> = OnceLock::new(); + +#[derive(Clone, Copy, Debug)] pub struct DropEntry { pub prefab: MiscPrefab, - pub chance: f32, - pub min_count: u32, - pub max_count: u32, + pub chance_pct: u8, + pub min_count: u8, + pub max_count: u8, } impl DropEntry { pub fn always(prefab: MiscPrefab) -> Self { Self { prefab, - chance: 1.0, + chance_pct: 100, min_count: 1, max_count: 1, } } - pub fn chance(prefab: MiscPrefab, chance: f32, min_count: u32, max_count: u32) -> Self { + pub fn chance(prefab: MiscPrefab, chance_pct: u8, min_count: u8, max_count: u8) -> Self { Self { prefab, - chance, + chance_pct, min_count, max_count, } } - pub fn roll(&self, pos: IVec3) -> u32 { - if self.chance < 1.0 { - let r = pseudo_rand_f32(pos); - if r >= self.chance { - return 0; - } + pub fn roll(&self, rng_val: u32) -> u8 { + if self.chance_pct < 100 && ((rng_val % 100) as u8) >= self.chance_pct { + return 0; } - pseudo_rand_u32(pos) % (self.max_count - self.min_count + 1) + self.min_count + self.min_count + (((rng_val >> 8) % ((self.max_count - self.min_count + 1) as u32)) as u8) } } #[derive(Clone, Debug, Default)] -pub struct DropTable(pub Vec); +pub struct DropTable(pub SmallVec<[DropEntry; 2]>); impl DropTable { pub fn is_empty(&self) -> bool { @@ -48,26 +52,79 @@ impl DropTable { } } -fn hash3(pos: IVec3) -> u32 { - let mut h: u32 = 0; - h = h.wrapping_mul(374761393).wrapping_add(pos.x as u32); - h = h.wrapping_mul(374761393).wrapping_add(pos.y as u32); - h = h.wrapping_mul(374761393).wrapping_add(pos.z as u32); - h ^= h >> 13; - h = h.wrapping_mul(1274126177); - h ^= h >> 16; - h +pub fn dig_rng(pos: IVec3) -> u32 { + let mut h = (SEED as u64) + .wrapping_add(pos.x as u64) + .wrapping_mul(0x9e3779b97f4a7c15) + ^ (pos.y as u64).wrapping_mul(0x6c62272e07bb0142) + ^ (pos.z as u64).wrapping_mul(0x94d049bb133111eb); + let h32 = (h ^ (h >> 32)) as u32; + h32 ^ (h32 >> 16) } -fn pseudo_rand_u32(pos: IVec3) -> u32 { - let timestamp = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() as u32; - hash3(pos) ^ timestamp +#[derive(Deserialize)] +struct DropEntryToml { + pub prefab: String, + pub chance_pct: u8, + pub min_count: u8, + #[serde(rename = "max_max")] + pub max_count: u8, } -fn pseudo_rand_f32(pos: IVec3) -> f32 { - let val = pseudo_rand_u32(pos); - (val as f32) / (u32::MAX as f32) +#[derive(Deserialize)] +struct DropTableToml { + #[serde(default)] + pub drops: Vec, +} + +#[derive(Deserialize)] +struct DropTablesFile { + #[serde(flatten)] + pub tiles: HashMap, +} + +fn prefab_from_str(s: &str) -> MiscPrefab { + match s { + "Coin" => MiscPrefab::Coin, + "RawMeat" => MiscPrefab::RawMeat, + other => panic!("Unknown prefab in drop_tables.toml: \"{}\"", other), + } +} + +#[derive(Resource)] +pub struct DropTableRegistry(pub HashMap); + +impl DropTableRegistry { + pub fn load() -> Self { + let src = std::fs::read_to_string("assets/drop_tables.toml") + .expect("assets/drop_tables.toml not found"); + let file: DropTablesFile = + toml::from_str(&src).expect("Failed to parse assets/drop_tables.toml"); + let mut map = HashMap::new(); + for (tile_name, table_toml) in file.tiles { + let entries: SmallVec<[DropEntry; 2]> = table_toml + .drops + .iter() + .map(|e| DropEntry { + prefab: prefab_from_str(&e.prefab), + chance_pct: e.chance_pct, + min_count: e.min_count, + max_count: e.max_count, + }) + .collect(); + map.insert(tile_name, DropTable(entries)); + } + Self(map) + } + + pub fn init_global(registry: &DropTableRegistry) { + DROP_TABLE_GLOBAL.get_or_init(|| registry.0.clone()); + } + + pub fn global_get(tile_name: &str) -> DropTable { + DROP_TABLE_GLOBAL + .get() + .and_then(|m| m.get(tile_name).cloned()) + .unwrap_or_default() + } } diff --git a/src/entities/shared_systems/digging.rs b/src/entities/shared_systems/digging.rs index 1c6901e..9f232fd 100644 --- a/src/entities/shared_systems/digging.rs +++ b/src/entities/shared_systems/digging.rs @@ -1,11 +1,11 @@ use crate::constants::ITILE_SIZE; +use crate::entities::item::drop_table::dig_rng; use crate::entities::item::prefabs::misc::misc_prefabs::spawn_prefab; use crate::world::chunks::{Z_ABOVE, Z_BELOW}; use crate::world::tiles::tile_changed::TileChangedEvent; use crate::world::tiles::visibility::TileOcclusionEvent; use crate::world::tiles::TileMap; use bevy::prelude::*; -use rand::RngExt; #[derive(Component)] pub struct Digger { @@ -52,6 +52,8 @@ pub fn dig_system( continue; } + let rng_val = dig_rng(below_pos); + let drop_table = tilemap .floor_tiles .get(&below_pos) @@ -60,12 +62,12 @@ pub fn dig_system( if let Some(_removed) = tilemap.dig_floor(&below_pos) { if let Some(table) = drop_table { for entry in table.0 { - let count = entry.roll(below_pos); - for _ in 0..count { + let count = entry.roll(rng_val); + for _ in 0..u32::from(count) { spawn_prefab( &mut commands, &asset_server, - entry.prefab.clone(), + entry.prefab, below_pos.as_vec3(), &mut tilemap, ); diff --git a/src/world/generation/terrain.rs b/src/world/generation/terrain.rs index 820e6df..b7a1f79 100644 --- a/src/world/generation/terrain.rs +++ b/src/world/generation/terrain.rs @@ -4,8 +4,7 @@ use bevy_platform::time::Instant; use noise::{NoiseFn, Perlin}; use std::sync::{Arc, Mutex}; -use crate::entities::item::drop_table::{DropEntry, DropTable}; -use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab; +use crate::entities::item::drop_table::DropTableRegistry; use crate::{ config::TileRegistry, @@ -17,19 +16,6 @@ use crate::{ }, }; -fn drop_table_for(tile_name: &str) -> DropTable { - match tile_name { - "grass" => DropTable(vec![ - DropEntry::chance(MiscPrefab::Coin, 0.05, 1, 2), - ]), - "dirt" => DropTable::default(), - "rock" => DropTable(vec![ - DropEntry::chance(MiscPrefab::Coin, 0.1, 1, 1), - ]), - _ => DropTable::default(), - } -} - /// Thread-safe storage for completed terrain blobs. /// Uses type erasure to avoid Debug bounds on TerrainBlob. type BlobStorage = Arc>>; @@ -130,7 +116,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob { tile.transparent, tile.astar_weight, [0; 8], - drop_table_for("air"), + DropTableRegistry::global_get("air"), ), )); chunk_data.set_floor_tile( @@ -153,7 +139,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob { tile.transparent, tile.astar_weight, [0; 8], - drop_table_for("rock"), + DropTableRegistry::global_get("rock"), ), )); chunk_data.set_floor_tile( @@ -176,7 +162,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob { tile.transparent, tile.astar_weight, [0; 8], - drop_table_for("dirt"), + DropTableRegistry::global_get("dirt"), ), )); chunk_data.set_floor_tile( @@ -201,7 +187,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob { tile.transparent, tile.astar_weight, [0; 8], - drop_table_for("grass"), + DropTableRegistry::global_get("grass"), ), )); chunk_data.set_floor_tile( @@ -225,7 +211,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob { tile.transparent, tile.astar_weight, [0; 8], - drop_table_for("dirt"), + DropTableRegistry::global_get("dirt"), ), )); chunk_data.set_floor_tile( @@ -249,7 +235,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob { tile.transparent, tile.astar_weight, [0; 8], - drop_table_for("air"), + DropTableRegistry::global_get("air"), ), )); chunk_data.set_floor_tile( diff --git a/src/world/mod.rs b/src/world/mod.rs index d3b20a5..6c236ff 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -34,7 +34,10 @@ pub struct WorldPlugin; impl Plugin for WorldPlugin { fn build(&self, app: &mut App) { - app.init_resource::() + let drop_registry = crate::entities::item::drop_table::DropTableRegistry::load(); + crate::entities::item::drop_table::DropTableRegistry::init_global(&drop_registry); + app.insert_resource(drop_registry) + .init_resource::() .init_resource::() .init_resource::() .init_resource::()