feat: migrate quilter rendering to Bevy TilemapChunk

Replaces the CPU pixel-baking quilter system (QuiltCache, TerrainSprite,
pixel_buffers) with Bevy's native TilemapChunk GPU-index-lookup API.

Architecture:
- TilemapChunk entities replace TerrainSprite entities per (chunk, z, layer)
- Tileset PNG stacked vertically as texture_2d_array (11 rows: 6 floor + 5 fixture)
- populate_chunk_tiles reads TileMap HashMap directly; no ECS FloorTile entities
- Chunk spawn deferred to apply_terrain_blobs (after TileMap data exists)
- Dirty key system triggers repopulate on occlusion/camera-z changes

Bug fixes:
- populate_chunk_tiles: add chunk_pos offset to world tile lookups (was always (0,0))
- spawn_tilemap_chunks: offset Transform by -TILE_SIZE/2 (tile-centre vs bottom-left)
- spawn_tilemap_chunks: AlphaMode2d::Blend (was Opaque, blocking DF fade)
- update_tilemap_chunk_visibility: actually mutate Visibility (was read-only)
- handle_tile_occlusion_updates: mark dirty keys inline (was double-reading events)
- TilemapChunkSpawner: deduplicate pending queue and guard stale registry

Performance:
- ~115K FloorTile ECS entities eliminated
- GPU memory: O(tile_data) instead of O(CHUNK_TILES² × z_levels × pixel_bytes)
- Z-scroll: only TilemapChunkTileData repopulates (no entity re-spawn)
- Benchmarks via TilemapBenchmark resource (F9 to report)
This commit is contained in:
2026-03-20 14:02:04 +00:00
parent 0eb2fcfbb3
commit f9a74ed432
14 changed files with 609 additions and 556 deletions
-47
View File
@@ -182,30 +182,11 @@ pub struct TileMap {
}
impl TileMap {
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn get_floor(&self, pos: &IVec3) -> Option<&FloorTileData> {
self.floor_tiles.get(pos)
}
#[inline]
pub fn get_fixture(&self, pos: &IVec3) -> Option<&FixtureTileData> {
self.fixture_tiles.get(pos)
}
#[inline]
pub fn has_floor(&self, pos: &IVec3) -> bool {
self.floor_tiles.contains_key(pos)
}
#[inline]
pub fn has_fixture(&self, pos: &IVec3) -> bool {
self.fixture_tiles.contains_key(pos)
}
#[inline]
pub fn insert_floor(&mut self, pos: IVec3, tile: FloorTileData) {
self.floor_tiles.insert(pos, tile);
@@ -242,34 +223,6 @@ impl TileMap {
chunk.is_standable(local_x, local_y, z)
}
/// 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)
}
/// Get A* pathfinding weight for a tile position.
/// Returns 100 (default) if tile not found. Lower is better.
pub fn get_astar_weight(&self, world_pos: IVec3) -> u8 {