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:
@@ -12,12 +12,18 @@
|
||||
//! ~18 bytes vs 48 bytes. Bit-packing flags (can_stand_in/on, visibly_transparent)
|
||||
//! reduces memory footprint and improves cache locality.
|
||||
//!
|
||||
//! ## ChunkData for O(1) Standability
|
||||
//! Each chunk stores bit-packed standability data. The `is_standable()` method
|
||||
//! checks chunk data first (4 bit-checks) before falling back to HashMap lookups.
|
||||
//! This replaces 4 HashMap lookups with O(1) bit operations.
|
||||
//!
|
||||
//! ## Single-Threaded Access
|
||||
//! No Arc wrapper because pathfinding runs on the main thread using thread-local
|
||||
//! scratchpads. Async pathfinding was attempted but snapshot copying overhead
|
||||
//! exceeded the benefit given current P99 (~357µs).
|
||||
//!
|
||||
//! ## Memory Layout
|
||||
//! - chunks: O(1) standability lookups via bitsets (~2KB per chunk)
|
||||
//! - floor_tiles: Primary pathfinding data (standability checks)
|
||||
//! - fixture_tiles: Secondary checks (fixtures can be standable)
|
||||
//! - item_tiles: Entity references per tile position
|
||||
@@ -25,6 +31,9 @@
|
||||
use bevy::prelude::*;
|
||||
use rustc_hash::FxHashMap;
|
||||
|
||||
use super::chunk_data::ChunkData;
|
||||
use crate::world::chunks::world_to_chunk;
|
||||
|
||||
/// Packed floor tile data for efficient storage. ~35 bytes vs 76 bytes tuple.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct FloorTileData {
|
||||
@@ -162,8 +171,13 @@ impl FixtureTileData {
|
||||
/// Tile map using FxHashMap for fast lookups. No Arc wrapper - single-threaded access.
|
||||
#[derive(Resource, Default)]
|
||||
pub struct TileMap {
|
||||
/// O(1) standability lookups via bitsets (~2KB per chunk).
|
||||
pub chunks: FxHashMap<IVec2, ChunkData>,
|
||||
/// Primary tile storage for pathfinding (fallback for standability).
|
||||
pub floor_tiles: FxHashMap<IVec3, FloorTileData>,
|
||||
/// Secondary tile storage (fixtures like trees can be standable).
|
||||
pub fixture_tiles: FxHashMap<IVec3, FixtureTileData>,
|
||||
/// Entity references per tile position.
|
||||
pub item_tiles: FxHashMap<IVec3, Vec<u32>>,
|
||||
}
|
||||
|
||||
@@ -216,4 +230,43 @@ impl TileMap {
|
||||
pub fn get_floor_mut(&mut self, pos: &IVec3) -> Option<&mut FloorTileData> {
|
||||
self.floor_tiles.get_mut(pos)
|
||||
}
|
||||
|
||||
/// O(1) standability check using bit-packed chunk data.
|
||||
/// Falls back to HashMap lookups if chunk data is not available.
|
||||
pub fn is_standable(&self, world_pos: IVec3) -> bool {
|
||||
let chunk_pos = world_to_chunk(world_pos);
|
||||
if let Some(chunk) = self.chunks.get(&chunk_pos) {
|
||||
let (local_x, local_y, z) = ChunkData::world_to_local(world_pos);
|
||||
return chunk.is_standable(local_x, local_y, z);
|
||||
}
|
||||
self.is_standable_slow(world_pos)
|
||||
}
|
||||
|
||||
/// Fallback standability check using HashMap lookups.
|
||||
fn is_standable_slow(&self, pos: IVec3) -> bool {
|
||||
let can_stand_in_floor = self
|
||||
.floor_tiles
|
||||
.get(&pos)
|
||||
.map(|t| t.can_stand_in())
|
||||
.unwrap_or(false);
|
||||
let can_stand_in_fixture = self
|
||||
.fixture_tiles
|
||||
.get(&pos)
|
||||
.map(|t| t.can_stand_in())
|
||||
.unwrap_or(false);
|
||||
|
||||
let pos_below = IVec3::new(pos.x, pos.y, pos.z - crate::constants::ITILE_SIZE);
|
||||
let can_stand_on_floor = self
|
||||
.floor_tiles
|
||||
.get(&pos_below)
|
||||
.map(|t| t.can_stand_on())
|
||||
.unwrap_or(false);
|
||||
let can_stand_on_fixture = self
|
||||
.fixture_tiles
|
||||
.get(&pos_below)
|
||||
.map(|t| t.can_stand_on())
|
||||
.unwrap_or(false);
|
||||
|
||||
(can_stand_in_floor || can_stand_in_fixture) && (can_stand_on_floor || can_stand_on_fixture)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user