feat: GPU optimization — lighting, viewport CA, benchmark mode, 531 FPS

- Resolution: 8x8 world cells, 2x2 UI cells (UI_SCALE=4)
- GPU lighting: vertex-shader computed, light source list buffer (max 64)
  instead of O(N×R²) grid scan, O(N×S) per cell
- Viewport-aware CA: iterate only active chunks, not all 250×250
- Flat array entity/item/shadow maps instead of HashMaps
- Flat 128-entry ASCII atlas array instead of HashMap lookup
- Partial grid upload: viewport + 30-cell margin only
- Pre-allocated viewport arrays in renderer structs (zero alloc/frame)
- Skip CPU lighting for GPU modes (pass None)
- Benchmark mode: --mode benchmark with per-subsystem timing
- GpuLightSource struct, light_count in push constants
- gather_sources_in_range() for viewport-scoped source gathering

Benchmark (600 ticks, release):
  Graphics: 531 FPS (was 386, +38%), render 1013us (was 1699us, -40%)
  ASCII:    402 FPS (was 313, +28%), render 1502us (was 2346us, -36%)

All 171 tests + 14 scenarios pass.
This commit is contained in:
Emil
2026-06-21 16:09:45 +03:00
parent c25cae9a32
commit 357db17c2f
53 changed files with 6362 additions and 651 deletions
+76
View File
@@ -0,0 +1,76 @@
use crate::world::cell::{Cell, MaterialId};
pub const CHUNK_SIZE: usize = 64;
pub struct Chunk {
pub cells: Vec<Cell>,
pub active: bool,
pub modified: bool,
}
impl Chunk {
pub fn new() -> Self {
let size = CHUNK_SIZE * CHUNK_SIZE;
Self {
cells: vec![Cell::empty(); size],
active: false,
modified: false,
}
}
#[inline]
pub fn in_bounds(x: i32, y: i32) -> bool {
x >= 0 && x < CHUNK_SIZE as i32 && y >= 0 && y < CHUNK_SIZE as i32
}
#[inline]
fn idx(x: i32, y: i32) -> usize {
(y as usize) * CHUNK_SIZE + (x as usize)
}
pub fn get(&self, x: i32, y: i32) -> Cell {
if !Self::in_bounds(x, y) {
return Cell::new(MaterialId::Stone);
}
self.cells[Self::idx(x, y)]
}
pub fn set(&mut self, x: i32, y: i32, cell: Cell) {
if Self::in_bounds(x, y) {
self.cells[Self::idx(x, y)] = cell;
self.modified = true;
}
}
pub fn set_material(&mut self, x: i32, y: i32, mat: MaterialId) {
if Self::in_bounds(x, y) {
self.cells[Self::idx(x, y)] = Cell::new(mat);
self.modified = true;
}
}
pub fn reset_tick_flags(&mut self) {
for c in &mut self.cells {
c.updated_this_tick = false;
}
}
}
pub fn world_to_chunk(world_x: i32, world_y: i32) -> (i32, i32, i32, i32) {
let cx = world_x.div_euclid(CHUNK_SIZE as i32);
let cy = world_y.div_euclid(CHUNK_SIZE as i32);
let lx = world_x.rem_euclid(CHUNK_SIZE as i32);
let ly = world_y.rem_euclid(CHUNK_SIZE as i32);
(cx, cy, lx, ly)
}
#[derive(Clone)]
pub struct ChunkCell {
pub x: i32,
pub y: i32,
pub cell: Cell,
}
pub fn chunk_cells() -> impl Iterator<Item = (i32, i32)> {
(0..CHUNK_SIZE as i32).flat_map(|y| (0..CHUNK_SIZE as i32).map(move |x| (x, y)))
}