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
+50
View File
@@ -0,0 +1,50 @@
use image::{GenericImageView, ImageBuffer, Rgba};
const TILE_PX: u32 = 16;
const ROWS: &[(&str, u8)] = &[
("assets/sky.png", 0), // air (transparent)
("assets/grass_floor.png", 1), // grass
("assets/dirt_floor.png", 2), // dirt
("assets/rock_floor.png", 3), // rock
("assets/bedrock_floor.png", 4), // bedrock
("assets/sky.png", 5), // sky (transparent)
("assets/dirt_wall.png", 6), // dirt_wall fixture
("assets/rock_wall.png", 7), // rock_wall fixture
("assets/bedrock_wall.png", 8), // bedrock_wall fixture
("assets/log.png", 9), // log fixture
("assets/leaves.png", 10), // leaves fixture
];
fn main() {
let rows = ROWS.len() as u32;
let mut combined: ImageBuffer<Rgba<u8>, Vec<u8>> =
ImageBuffer::from_pixel(TILE_PX, TILE_PX * rows, Rgba([0, 0, 0, 0]));
for (i, (path, _id)) in ROWS.iter().enumerate() {
let y = i as u32 * TILE_PX;
match image::open(path) {
Ok(img) => {
image::imageops::overlay(&mut combined, &img, 0, y as i64);
println!("cargo:rerun-if-changed={}", path);
}
Err(e) => {
eprintln!(
"Warning: could not open {} — leaving row {} blank ({})",
path, i, e
);
let mut row_pixels = combined.rows_mut();
if let Some(row) = row_pixels.nth(i) {
for px in row {
*px = Rgba([255, 0, 255, 255]);
}
}
}
}
}
combined
.save("assets/tileset.png")
.expect("Failed to write assets/tileset.png");
println!("cargo:rerun-if-changed=build.rs");
}