feat(optimization): implement data-oriented chunk architecture
Phase 1: Bit-packed standability - Add ChunkData struct with 4 bitsets per chunk (stand_in/on for floor/fixture) - Replace 4 HashMap lookups per standability check with O(1) bit operations - Memory: ~2KB bitsets per chunk vs ~50KB HashMap overhead Phase 2: Reactive connectivity - Add dirty_chunks HashSet to ChunkMap for incremental updates - update_chunk_connectivity now O(d) where d = dirty chunks - Early exit when no changes, preventing O(N) full rebuilds Phase 3: Async terrain baking - Move terrain generation to AsyncComputeTaskPool - spawn_terrain_tasks: non-blocking task spawn (~34µs) - apply_terrain_blobs: batched entity spawn on main thread - Eliminates main-thread stutters during world generation
This commit is contained in:
+196
-138
@@ -1,18 +1,55 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy::tasks::AsyncComputeTaskPool;
|
||||
use bevy_platform::collections::HashMap;
|
||||
use bevy_platform::sync::Mutex;
|
||||
use bevy_platform::time::Instant;
|
||||
use noise::{NoiseFn, Perlin};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::{
|
||||
constants::{SEED, TILE_SIZE},
|
||||
world::{
|
||||
tiles::{FloorTileData, TileMap},
|
||||
tiles::{ChunkData, FloorTileData, TileMap, TerrainSpriteState, CurrentWorldSpriteState},
|
||||
ChunkForrestryEvent, ChunkTerrainEvent, FloorTilePrefab, TileOcclusionEvent, CHUNK_SIZE,
|
||||
Z_ABOVE, Z_BELOW,
|
||||
},
|
||||
};
|
||||
|
||||
/// Thread-safe storage for completed terrain blobs.
|
||||
/// Uses type erasure to avoid Debug bounds on TerrainBlob.
|
||||
type BlobStorage = Arc<Mutex<Box<dyn Send + Sync>>>;
|
||||
|
||||
/// Typed wrapper for terrain blob storage.
|
||||
#[derive(Resource)]
|
||||
pub struct TerrainBlobStorage {
|
||||
pub blobs: Arc<Mutex<Vec<TerrainBlob>>>,
|
||||
}
|
||||
|
||||
impl Default for TerrainBlobStorage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
blobs: Arc::new(Mutex::new(Vec::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for TerrainBlobStorage {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
blobs: self.blobs.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Result of async terrain generation for a single chunk.
|
||||
/// Contains all data needed to spawn entities and update TileMap on main thread.
|
||||
pub struct TerrainBlob {
|
||||
pub chunk_pos: IVec2,
|
||||
pub chunk_data: ChunkData,
|
||||
pub tile_updates: Vec<(IVec3, FloorTileData)>,
|
||||
pub surface_positions: Vec<(Vec3, String)>,
|
||||
pub tile_spawns: Vec<(Vec3, FloorTilePrefab)>,
|
||||
}
|
||||
|
||||
pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
|
||||
let noise = Perlin::new(SEED);
|
||||
let mut noise_value = 0.0;
|
||||
@@ -27,163 +64,184 @@ pub fn generate_surface_terrain(x: i32, y: i32) -> f32 {
|
||||
(noise_value * 2.5) as f32
|
||||
}
|
||||
|
||||
pub fn generate_chunk_terrain(
|
||||
commands: ParallelCommands<'_, '_>, // Use ParallelCommands for parallel spawning
|
||||
/// Async terrain generation - runs on AsyncComputeTaskPool.
|
||||
/// Computes all terrain data without ECS access, returns blob for main thread.
|
||||
fn generate_terrain_blob(chunk_pos: IVec2) -> TerrainBlob {
|
||||
let cave_noise = Perlin::new(SEED);
|
||||
let start_x = chunk_pos.x * CHUNK_SIZE;
|
||||
let start_y = chunk_pos.y * CHUNK_SIZE;
|
||||
|
||||
let mut chunk_data = ChunkData::new(chunk_pos);
|
||||
let mut tile_updates: Vec<(IVec3, FloorTileData)> = Vec::new();
|
||||
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
|
||||
let mut tile_spawns: Vec<(Vec3, FloorTilePrefab)> = Vec::new();
|
||||
|
||||
for local_y in 0..CHUNK_SIZE {
|
||||
for local_x in 0..CHUNK_SIZE {
|
||||
let world_x = start_x + local_x;
|
||||
let world_y = start_y + local_y;
|
||||
|
||||
let noise_position = Vec3::new(
|
||||
(world_x as f32 * TILE_SIZE).round(),
|
||||
(world_y as f32 * TILE_SIZE).round(),
|
||||
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(),
|
||||
);
|
||||
|
||||
for z in -Z_BELOW as isize..=Z_ABOVE as isize {
|
||||
let position = Vec3::new(
|
||||
(world_x as f32 * TILE_SIZE).round(),
|
||||
(world_y as f32 * TILE_SIZE).round(),
|
||||
(z as f32 * TILE_SIZE).round(),
|
||||
);
|
||||
let pos_ivec = position.as_ivec3();
|
||||
let local_z = z as i32;
|
||||
let surface_height =
|
||||
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round();
|
||||
|
||||
if z < -5 {
|
||||
let cave_value = cave_noise.get([
|
||||
world_x as f64 * 0.05,
|
||||
world_y as f64 * 0.05,
|
||||
z as f64 * 0.05,
|
||||
]);
|
||||
if cave_value < -0.75 {
|
||||
tile_spawns.push((position, FloorTilePrefab::air(position)));
|
||||
tile_updates.push((
|
||||
pos_ivec,
|
||||
FloorTileData::new(0, true, false, true, 0, [0; 8]),
|
||||
));
|
||||
chunk_data.set_floor_tile(local_x, local_y, local_z, 0, true, false);
|
||||
} else if cave_value < 0.8 {
|
||||
tile_spawns.push((position, FloorTilePrefab::rock(position)));
|
||||
tile_updates.push((
|
||||
pos_ivec,
|
||||
FloorTileData::new(2, false, true, false, 50, [0; 8]),
|
||||
));
|
||||
chunk_data.set_floor_tile(local_x, local_y, local_z, 2, false, true);
|
||||
} else {
|
||||
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
|
||||
tile_updates.push((
|
||||
pos_ivec,
|
||||
FloorTileData::new(1, false, true, false, 85, [0; 8]),
|
||||
));
|
||||
chunk_data.set_floor_tile(local_x, local_y, local_z, 1, false, true);
|
||||
}
|
||||
} else if noise_position.z > position.z {
|
||||
if surface_height <= position.z + TILE_SIZE {
|
||||
tile_spawns.push((position, FloorTilePrefab::grass(position)));
|
||||
tile_updates.push((
|
||||
pos_ivec,
|
||||
FloorTileData::new(1, false, true, false, 100, [0; 8]),
|
||||
));
|
||||
chunk_data.set_floor_tile(local_x, local_y, local_z, 1, false, true);
|
||||
surface_positions.push((position, "grass".to_string()));
|
||||
} else {
|
||||
tile_spawns.push((position, FloorTilePrefab::dirt(position)));
|
||||
tile_updates.push((
|
||||
pos_ivec,
|
||||
FloorTileData::new(1, false, true, false, 85, [0; 8]),
|
||||
));
|
||||
chunk_data.set_floor_tile(local_x, local_y, local_z, 1, false, true);
|
||||
}
|
||||
} else {
|
||||
tile_spawns.push((position, FloorTilePrefab::air(position)));
|
||||
tile_updates.push((
|
||||
pos_ivec,
|
||||
FloorTileData::new(0, true, false, true, 0, [0; 8]),
|
||||
));
|
||||
chunk_data.set_floor_tile(local_x, local_y, local_z, 0, true, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TerrainBlob {
|
||||
chunk_pos,
|
||||
chunk_data,
|
||||
tile_updates,
|
||||
surface_positions,
|
||||
tile_spawns,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawns async terrain generation tasks on AsyncComputeTaskPool.
|
||||
/// Fast - just reads events and spawns tasks.
|
||||
pub fn spawn_terrain_tasks(
|
||||
mut events: MessageReader<ChunkTerrainEvent>,
|
||||
mut cwss: ResMut<CurrentWorldSpriteState>,
|
||||
blob_storage: Res<TerrainBlobStorage>,
|
||||
) {
|
||||
let start = Instant::now();
|
||||
let count = events.len();
|
||||
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let task_pool = AsyncComputeTaskPool::get();
|
||||
let blobs = blob_storage.blobs.clone();
|
||||
|
||||
for event in events.read() {
|
||||
let chunk_pos = event.chunk_position;
|
||||
let blobs_clone = blobs.clone();
|
||||
task_pool.spawn(async move {
|
||||
let blob = generate_terrain_blob(chunk_pos);
|
||||
blobs_clone.lock().unwrap().push(blob);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
cwss.state = TerrainSpriteState::WaitingForRender;
|
||||
println!("{} terrain tasks spawned in {:.2?}", count, start.elapsed());
|
||||
}
|
||||
|
||||
/// Applies completed terrain blobs on main thread.
|
||||
/// Spawns entities, updates TileMap, sends occlusion events.
|
||||
pub fn apply_terrain_blobs(
|
||||
mut commands: Commands,
|
||||
blob_storage: Res<TerrainBlobStorage>,
|
||||
mut tilemap: ResMut<TileMap>,
|
||||
mut forrestry_event_writer: MessageWriter<ChunkForrestryEvent>,
|
||||
mut occlusion_event_writer: MessageWriter<TileOcclusionEvent>,
|
||||
) {
|
||||
let is_empty = events.is_empty();
|
||||
let start = Instant::now();
|
||||
let count: usize = events.len();
|
||||
let mut applied_count = 0;
|
||||
|
||||
let cave_noise = Perlin::new(SEED);
|
||||
let completed: Vec<TerrainBlob> = blob_storage.blobs.lock().unwrap().drain(..).collect();
|
||||
|
||||
// Create mutexes for our shared resources
|
||||
let tilemap_updates = Mutex::new(HashMap::new());
|
||||
let forrestry_events = Mutex::new(Vec::new());
|
||||
for blob in completed {
|
||||
let new_positions: Vec<IVec3> = blob
|
||||
.tile_updates
|
||||
.into_iter()
|
||||
.map(|(pos, data)| {
|
||||
tilemap.insert_floor(pos, data);
|
||||
pos
|
||||
})
|
||||
.collect();
|
||||
|
||||
events.par_read().for_each(|event| {
|
||||
let chunk_pos = event.chunk_position;
|
||||
tilemap.chunks.insert(blob.chunk_pos, blob.chunk_data);
|
||||
|
||||
let start_x = chunk_pos.x * CHUNK_SIZE;
|
||||
let start_y = chunk_pos.y * CHUNK_SIZE;
|
||||
|
||||
let mut surface_positions: Vec<(Vec3, String)> = Vec::new();
|
||||
let mut local_tilemap_updates: HashMap<IVec3, FloorTileData> = HashMap::new();
|
||||
|
||||
// Generate tiles for this chunk
|
||||
for local_y in 0..CHUNK_SIZE {
|
||||
for local_x in 0..CHUNK_SIZE {
|
||||
let world_x = start_x + local_x;
|
||||
let world_y = start_y + local_y;
|
||||
|
||||
let noise_position = Vec3::new(
|
||||
(world_x as f32 * TILE_SIZE).round(),
|
||||
(world_y as f32 * TILE_SIZE).round(),
|
||||
(generate_surface_terrain(world_x, world_y) * TILE_SIZE).round(),
|
||||
);
|
||||
|
||||
// Spawn tiles and add them to tilemap
|
||||
for z in -Z_BELOW as isize..=Z_ABOVE as isize {
|
||||
let position = Vec3::new(
|
||||
(world_x as f32 * TILE_SIZE).round(),
|
||||
(world_y as f32 * TILE_SIZE).round(),
|
||||
(z as f32 * TILE_SIZE).round(),
|
||||
);
|
||||
let pos_ivec = position.as_ivec3();
|
||||
|
||||
if z < -5 {
|
||||
let cave_value = cave_noise.get([
|
||||
world_x as f64 * 0.05,
|
||||
world_y as f64 * 0.05,
|
||||
z as f64 * 0.05,
|
||||
]);
|
||||
if cave_value < -0.75 {
|
||||
commands.command_scope(|mut cmd| {
|
||||
FloorTilePrefab::air(position).spawn(&mut cmd);
|
||||
});
|
||||
local_tilemap_updates.insert(
|
||||
pos_ivec,
|
||||
FloorTileData::new(0, true, false, true, 0, [0; 8]),
|
||||
);
|
||||
} else if cave_value < 0.8 {
|
||||
commands.command_scope(|mut cmd| {
|
||||
FloorTilePrefab::rock(position).spawn(&mut cmd);
|
||||
});
|
||||
local_tilemap_updates.insert(
|
||||
pos_ivec,
|
||||
FloorTileData::new(2, false, true, false, 50, [0; 8]),
|
||||
);
|
||||
} else {
|
||||
commands.command_scope(|mut cmd| {
|
||||
FloorTilePrefab::dirt(position).spawn(&mut cmd);
|
||||
});
|
||||
local_tilemap_updates.insert(
|
||||
pos_ivec,
|
||||
FloorTileData::new(1, false, true, false, 85, [0; 8]),
|
||||
);
|
||||
}
|
||||
} else if noise_position.z > position.z {
|
||||
if (generate_surface_terrain(world_x, world_y) * TILE_SIZE).round()
|
||||
<= position.z + TILE_SIZE
|
||||
{
|
||||
commands.command_scope(|mut cmd| {
|
||||
FloorTilePrefab::grass(position).spawn(&mut cmd);
|
||||
});
|
||||
local_tilemap_updates.insert(
|
||||
pos_ivec,
|
||||
FloorTileData::new(1, false, true, false, 100, [0; 8]),
|
||||
);
|
||||
surface_positions.push((position, "grass".to_string()));
|
||||
} else {
|
||||
commands.command_scope(|mut cmd| {
|
||||
FloorTilePrefab::dirt(position).spawn(&mut cmd);
|
||||
});
|
||||
local_tilemap_updates.insert(
|
||||
pos_ivec,
|
||||
FloorTileData::new(1, false, true, false, 85, [0; 8]),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
commands.command_scope(|mut cmd| {
|
||||
FloorTilePrefab::air(position).spawn(&mut cmd);
|
||||
});
|
||||
local_tilemap_updates.insert(
|
||||
pos_ivec,
|
||||
FloorTileData::new(0, true, false, true, 0, [0; 8]),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (_position, prefab) in blob.tile_spawns {
|
||||
prefab.spawn(&mut commands);
|
||||
}
|
||||
|
||||
// Add our local updates to the global mutexes
|
||||
{
|
||||
let mut tilemap_guard = tilemap_updates.lock().unwrap();
|
||||
for (pos, data) in local_tilemap_updates {
|
||||
tilemap_guard.insert(pos, data);
|
||||
}
|
||||
for pos in new_positions {
|
||||
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
|
||||
}
|
||||
|
||||
// Store forrestry event for this chunk
|
||||
forrestry_events.lock().unwrap().push(ChunkForrestryEvent {
|
||||
chunk_position: chunk_pos,
|
||||
floor_tiles: surface_positions,
|
||||
forrestry_event_writer.write(ChunkForrestryEvent {
|
||||
chunk_position: blob.chunk_pos,
|
||||
floor_tiles: blob.surface_positions,
|
||||
});
|
||||
});
|
||||
|
||||
let new_positions: Vec<IVec3> = tilemap_updates
|
||||
.into_inner()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|(pos, data)| {
|
||||
tilemap.insert_floor(pos, data);
|
||||
pos
|
||||
})
|
||||
.collect();
|
||||
|
||||
// All tiles now in tilemap — safe to calculate visibility
|
||||
for pos in new_positions {
|
||||
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
|
||||
applied_count += 1;
|
||||
}
|
||||
|
||||
// Send all forrestry events
|
||||
for event in forrestry_events.into_inner().unwrap() {
|
||||
forrestry_event_writer.write(event);
|
||||
}
|
||||
|
||||
if !is_empty {
|
||||
println!("{} terrain chunks loaded in {:.2?}", count, start.elapsed());
|
||||
if applied_count > 0 {
|
||||
println!("{} terrain blobs applied in {:.2?}", applied_count, start.elapsed());
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_chunk_weathering_and_precipitation(// mut commands: Commands,
|
||||
// mut events: EventReader<GenerateChunkEvent>,
|
||||
// mut chunk_map: ResMut<ChunkMap>,
|
||||
// mut tilemap: ResMut<TileMap>,
|
||||
) {
|
||||
pub fn generate_chunk_weathering_and_precipitation() {
|
||||
// TODO: Generate weathering and precipitation
|
||||
// Temperature and humidity
|
||||
// Erosion
|
||||
|
||||
Reference in New Issue
Block a user