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",
"rustc-hash 2.1.1",
"serde",
"smallvec",
"toml",
]
+1
View File
@@ -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"
+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 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 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 {
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<DropEntry>);
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<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::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,
);
+7 -21
View File
@@ -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<Mutex<Box<dyn Send + Sync>>>;
@@ -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(
+4 -1
View File
@@ -34,7 +34,10 @@ pub struct WorldPlugin;
impl Plugin for WorldPlugin {
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::<tiles::TilemapBenchmark>()
.init_resource::<tiles::TilemapChunkRegistry>()