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
This commit is contained in:
2026-03-21 16:06:41 +00:00
parent f03651e0cf
commit 5b563bad67
7 changed files with 124 additions and 59 deletions
Generated
+1
View File
@@ -2444,6 +2444,7 @@ dependencies = [
"rayon", "rayon",
"rustc-hash 2.1.1", "rustc-hash 2.1.1",
"serde", "serde",
"smallvec",
"toml", "toml",
] ]
+1
View File
@@ -14,6 +14,7 @@ serde = { version = "1.0", features = ["derive"] }
toml = "0.9.8" toml = "0.9.8"
rayon = "1.11.0" rayon = "1.11.0"
rustc-hash = "2.1.1" rustc-hash = "2.1.1"
smallvec = { version = "1", features = ["union"] }
ahash = "0.8.12" ahash = "0.8.12"
nohash-hasher = "0.2.0" nohash-hasher = "0.2.0"
futures-lite = "2.6.1" futures-lite = "2.6.1"
+15
View File
@@ -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 = []
+89 -32
View File
@@ -1,46 +1,50 @@
use crate::constants::SEED;
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab; use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
use bevy::prelude::*; 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<HashMap<String, DropTable>> = OnceLock::new();
#[derive(Clone, Copy, Debug)]
pub struct DropEntry { pub struct DropEntry {
pub prefab: MiscPrefab, pub prefab: MiscPrefab,
pub chance: f32, pub chance_pct: u8,
pub min_count: u32, pub min_count: u8,
pub max_count: u32, pub max_count: u8,
} }
impl DropEntry { impl DropEntry {
pub fn always(prefab: MiscPrefab) -> Self { pub fn always(prefab: MiscPrefab) -> Self {
Self { Self {
prefab, prefab,
chance: 1.0, chance_pct: 100,
min_count: 1, min_count: 1,
max_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 { Self {
prefab, prefab,
chance, chance_pct,
min_count, min_count,
max_count, max_count,
} }
} }
pub fn roll(&self, pos: IVec3) -> u32 { pub fn roll(&self, rng_val: u32) -> u8 {
if self.chance < 1.0 { if self.chance_pct < 100 && ((rng_val % 100) as u8) >= self.chance_pct {
let r = pseudo_rand_f32(pos);
if r >= self.chance {
return 0; return 0;
} }
} self.min_count + (((rng_val >> 8) % ((self.max_count - self.min_count + 1) as u32)) as u8)
pseudo_rand_u32(pos) % (self.max_count - self.min_count + 1) + self.min_count
} }
} }
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct DropTable(pub Vec<DropEntry>); pub struct DropTable(pub SmallVec<[DropEntry; 2]>);
impl DropTable { impl DropTable {
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@@ -48,26 +52,79 @@ impl DropTable {
} }
} }
fn hash3(pos: IVec3) -> u32 { pub fn dig_rng(pos: IVec3) -> u32 {
let mut h: u32 = 0; let mut h = (SEED as u64)
h = h.wrapping_mul(374761393).wrapping_add(pos.x as u32); .wrapping_add(pos.x as u64)
h = h.wrapping_mul(374761393).wrapping_add(pos.y as u32); .wrapping_mul(0x9e3779b97f4a7c15)
h = h.wrapping_mul(374761393).wrapping_add(pos.z as u32); ^ (pos.y as u64).wrapping_mul(0x6c62272e07bb0142)
h ^= h >> 13; ^ (pos.z as u64).wrapping_mul(0x94d049bb133111eb);
h = h.wrapping_mul(1274126177); let h32 = (h ^ (h >> 32)) as u32;
h ^= h >> 16; h32 ^ (h32 >> 16)
h
} }
fn pseudo_rand_u32(pos: IVec3) -> u32 { #[derive(Deserialize)]
let timestamp = std::time::SystemTime::now() struct DropEntryToml {
.duration_since(std::time::UNIX_EPOCH) pub prefab: String,
.unwrap() pub chance_pct: u8,
.as_nanos() as u32; pub min_count: u8,
hash3(pos) ^ timestamp #[serde(rename = "max_max")]
pub max_count: u8,
} }
fn pseudo_rand_f32(pos: IVec3) -> f32 { #[derive(Deserialize)]
let val = pseudo_rand_u32(pos); struct DropTableToml {
(val as f32) / (u32::MAX as f32) #[serde(default)]
pub drops: Vec<DropEntryToml>,
}
#[derive(Deserialize)]
struct DropTablesFile {
#[serde(flatten)]
pub tiles: HashMap<String, DropTableToml>,
}
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<String, DropTable>);
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()
}
} }
+6 -4
View File
@@ -1,11 +1,11 @@
use crate::constants::ITILE_SIZE; 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::entities::item::prefabs::misc::misc_prefabs::spawn_prefab;
use crate::world::chunks::{Z_ABOVE, Z_BELOW}; use crate::world::chunks::{Z_ABOVE, Z_BELOW};
use crate::world::tiles::tile_changed::TileChangedEvent; use crate::world::tiles::tile_changed::TileChangedEvent;
use crate::world::tiles::visibility::TileOcclusionEvent; use crate::world::tiles::visibility::TileOcclusionEvent;
use crate::world::tiles::TileMap; use crate::world::tiles::TileMap;
use bevy::prelude::*; use bevy::prelude::*;
use rand::RngExt;
#[derive(Component)] #[derive(Component)]
pub struct Digger { pub struct Digger {
@@ -52,6 +52,8 @@ pub fn dig_system(
continue; continue;
} }
let rng_val = dig_rng(below_pos);
let drop_table = tilemap let drop_table = tilemap
.floor_tiles .floor_tiles
.get(&below_pos) .get(&below_pos)
@@ -60,12 +62,12 @@ pub fn dig_system(
if let Some(_removed) = tilemap.dig_floor(&below_pos) { if let Some(_removed) = tilemap.dig_floor(&below_pos) {
if let Some(table) = drop_table { if let Some(table) = drop_table {
for entry in table.0 { for entry in table.0 {
let count = entry.roll(below_pos); let count = entry.roll(rng_val);
for _ in 0..count { for _ in 0..u32::from(count) {
spawn_prefab( spawn_prefab(
&mut commands, &mut commands,
&asset_server, &asset_server,
entry.prefab.clone(), entry.prefab,
below_pos.as_vec3(), below_pos.as_vec3(),
&mut tilemap, &mut tilemap,
); );
+7 -21
View File
@@ -4,8 +4,7 @@ use bevy_platform::time::Instant;
use noise::{NoiseFn, Perlin}; use noise::{NoiseFn, Perlin};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use crate::entities::item::drop_table::{DropEntry, DropTable}; use crate::entities::item::drop_table::DropTableRegistry;
use crate::entities::item::prefabs::misc::misc_prefabs::MiscPrefab;
use crate::{ use crate::{
config::TileRegistry, 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. /// Thread-safe storage for completed terrain blobs.
/// Uses type erasure to avoid Debug bounds on TerrainBlob. /// Uses type erasure to avoid Debug bounds on TerrainBlob.
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>; type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
@@ -130,7 +116,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent, tile.transparent,
tile.astar_weight, tile.astar_weight,
[0; 8], [0; 8],
drop_table_for("air"), DropTableRegistry::global_get("air"),
), ),
)); ));
chunk_data.set_floor_tile( chunk_data.set_floor_tile(
@@ -153,7 +139,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent, tile.transparent,
tile.astar_weight, tile.astar_weight,
[0; 8], [0; 8],
drop_table_for("rock"), DropTableRegistry::global_get("rock"),
), ),
)); ));
chunk_data.set_floor_tile( chunk_data.set_floor_tile(
@@ -176,7 +162,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent, tile.transparent,
tile.astar_weight, tile.astar_weight,
[0; 8], [0; 8],
drop_table_for("dirt"), DropTableRegistry::global_get("dirt"),
), ),
)); ));
chunk_data.set_floor_tile( chunk_data.set_floor_tile(
@@ -201,7 +187,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent, tile.transparent,
tile.astar_weight, tile.astar_weight,
[0; 8], [0; 8],
drop_table_for("grass"), DropTableRegistry::global_get("grass"),
), ),
)); ));
chunk_data.set_floor_tile( chunk_data.set_floor_tile(
@@ -225,7 +211,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent, tile.transparent,
tile.astar_weight, tile.astar_weight,
[0; 8], [0; 8],
drop_table_for("dirt"), DropTableRegistry::global_get("dirt"),
), ),
)); ));
chunk_data.set_floor_tile( chunk_data.set_floor_tile(
@@ -249,7 +235,7 @@ fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
tile.transparent, tile.transparent,
tile.astar_weight, tile.astar_weight,
[0; 8], [0; 8],
drop_table_for("air"), DropTableRegistry::global_get("air"),
), ),
)); ));
chunk_data.set_floor_tile( chunk_data.set_floor_tile(
+4 -1
View File
@@ -34,7 +34,10 @@ pub struct WorldPlugin;
impl Plugin for WorldPlugin { impl Plugin for WorldPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.init_resource::<ChunkMap>() 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::<ChunkMap>()
.init_resource::<TerrainBlobStorage>() .init_resource::<TerrainBlobStorage>()
.init_resource::<tiles::TilemapBenchmark>() .init_resource::<tiles::TilemapBenchmark>()
.init_resource::<tiles::TilemapChunkRegistry>() .init_resource::<tiles::TilemapChunkRegistry>()