- 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.
267 lines
8.2 KiB
Rust
267 lines
8.2 KiB
Rust
use crate::entity::item::ItemManager;
|
|
use crate::entity::EntityManager;
|
|
use crate::render::lighting;
|
|
use crate::ui::UiLayer;
|
|
use crate::world::cell::MaterialId;
|
|
use crate::world::grid::Grid;
|
|
use image::{ImageBuffer, RgbImage};
|
|
|
|
pub const CELL_SIZE: u32 = 8;
|
|
pub const UI_CELL_SIZE: u32 = 2;
|
|
|
|
pub fn capture_frame(
|
|
grid: &Grid,
|
|
entities: &EntityManager,
|
|
items: &ItemManager,
|
|
ui: &UiLayer,
|
|
cam_x: i32,
|
|
cam_y: i32,
|
|
view_w: u32,
|
|
view_h: u32,
|
|
lighting: Option<&lighting::LightGrid>,
|
|
) -> RgbImage {
|
|
let width = view_w * CELL_SIZE;
|
|
let height = view_h * CELL_SIZE;
|
|
let mut img: RgbImage = ImageBuffer::new(width, height);
|
|
|
|
let entity_positions = entity_positions(entities, cam_x, cam_y, view_w, view_h);
|
|
let shadow_positions = shadow_positions(&entity_positions, grid, cam_x, cam_y, view_w, view_h);
|
|
|
|
for vy in 0..view_h as i32 {
|
|
for vx in 0..view_w as i32 {
|
|
let wx = cam_x + vx;
|
|
let wy = cam_y + vy;
|
|
let light = lighting
|
|
.map(|l| l.get(vx, vy))
|
|
.unwrap_or_else(lighting::ambient_light);
|
|
|
|
let color = if let Some(c) = entity_positions.get(&(vx, vy)) {
|
|
lighting::apply_light(*c, light)
|
|
} else if let Some(c) = item_color_at(items, wx, wy) {
|
|
lighting::apply_light(c, light)
|
|
} else if shadow_positions.contains(&(vx, vy)) {
|
|
[0, 0, 0]
|
|
} else if !grid.in_bounds(wx, wy) {
|
|
lighting::apply_light([40, 40, 40], light)
|
|
} else {
|
|
let cell = grid.get(wx, wy);
|
|
if cell.is_empty() {
|
|
lighting::apply_light(background_color(wx, wy, vy, view_h as i32), light)
|
|
} else if cell.material == MaterialId::Lava {
|
|
let r = 200u8.saturating_add(cell.variant / 2);
|
|
lighting::apply_light([r, 60, 20], light)
|
|
} else {
|
|
lighting::apply_light([cell.fg[0], cell.fg[1], cell.fg[2]], light)
|
|
}
|
|
};
|
|
|
|
draw_cell(&mut img, vx as u32, vy as u32, color);
|
|
}
|
|
}
|
|
|
|
for (x, y) in ui.keys() {
|
|
let cell = ui.get(*x, *y).unwrap();
|
|
let px = (*x as u32) * UI_CELL_SIZE;
|
|
let py = (*y as u32) * UI_CELL_SIZE;
|
|
if px + UI_CELL_SIZE <= img.width() && py + UI_CELL_SIZE <= img.height() {
|
|
let alpha = cell.alpha as f32 / 255.0;
|
|
for dy in 0..UI_CELL_SIZE {
|
|
for dx in 0..UI_CELL_SIZE {
|
|
let p = img.get_pixel(px + dx, py + dy);
|
|
let r = (cell.fg[0] as f32 * alpha + p[0] as f32 * (1.0 - alpha)) as u8;
|
|
let g = (cell.fg[1] as f32 * alpha + p[1] as f32 * (1.0 - alpha)) as u8;
|
|
let b = (cell.fg[2] as f32 * alpha + p[2] as f32 * (1.0 - alpha)) as u8;
|
|
img.put_pixel(px + dx, py + dy, image::Rgb([r, g, b]));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
img
|
|
}
|
|
|
|
fn background_color(wx: i32, wy: i32, vy: i32, view_h: i32) -> [u8; 3] {
|
|
let t = (vy as f32 / view_h as f32).clamp(0.0, 1.0);
|
|
let base_r = (10.0 + t * 15.0) as u8;
|
|
let base_g = (10.0 + t * 25.0) as u8;
|
|
let base_b = (25.0 + t * 35.0) as u8;
|
|
|
|
let hash = ((wx.wrapping_mul(73856093)) ^ (wy.wrapping_mul(19349663))).abs();
|
|
if hash % 80 == 0 {
|
|
let brightness = (60 + (hash % 120) as u8).min(255);
|
|
return [brightness, brightness, brightness + 20];
|
|
}
|
|
|
|
[base_r, base_g, base_b]
|
|
}
|
|
|
|
fn entity_priority(kind: crate::entity::EntityKind) -> u32 {
|
|
use crate::entity::EntityKind;
|
|
match kind {
|
|
EntityKind::Player => 3,
|
|
EntityKind::Goblin => 2,
|
|
EntityKind::Slime => 1,
|
|
EntityKind::Corpse => 0,
|
|
}
|
|
}
|
|
|
|
fn entity_positions(
|
|
entities: &EntityManager,
|
|
cam_x: i32,
|
|
cam_y: i32,
|
|
view_w: u32,
|
|
view_h: u32,
|
|
) -> std::collections::HashMap<(i32, i32), [u8; 3]> {
|
|
let mut map: std::collections::HashMap<(i32, i32), (u32, [u8; 3])> =
|
|
std::collections::HashMap::new();
|
|
for e in entities.all() {
|
|
for b in &e.bodies {
|
|
if !b.alive {
|
|
continue;
|
|
}
|
|
let sx = b.x as i32 - cam_x;
|
|
let sy = b.y as i32 - cam_y;
|
|
if sx < 0 || sx >= view_w as i32 || sy < 0 || sy >= view_h as i32 {
|
|
continue;
|
|
}
|
|
let color = if e.on_fire {
|
|
let flicker = b.fire_timer % 4;
|
|
[255, 120 + flicker as u8 * 20, 20 + flicker as u8 * 10]
|
|
} else {
|
|
[b.color[0], b.color[1], b.color[2]]
|
|
};
|
|
let priority = entity_priority(e.kind);
|
|
if map
|
|
.get(&(sx, sy))
|
|
.map(|(p, _)| priority > *p)
|
|
.unwrap_or(true)
|
|
{
|
|
map.insert((sx, sy), (priority, color));
|
|
}
|
|
}
|
|
}
|
|
map.into_iter().map(|(k, (_, c))| (k, c)).collect()
|
|
}
|
|
|
|
fn shadow_positions(
|
|
entity_positions: &std::collections::HashMap<(i32, i32), [u8; 3]>,
|
|
grid: &Grid,
|
|
cam_x: i32,
|
|
cam_y: i32,
|
|
view_w: u32,
|
|
view_h: u32,
|
|
) -> std::collections::HashSet<(i32, i32)> {
|
|
let mut shadows = std::collections::HashSet::new();
|
|
for (vx, vy) in entity_positions.keys() {
|
|
for dy in -1..=1 {
|
|
for dx in -1..=1 {
|
|
if dx == 0 && dy == 0 {
|
|
continue;
|
|
}
|
|
let sx = vx + dx;
|
|
let sy = vy + dy;
|
|
if sx < 0 || sx >= view_w as i32 || sy < 0 || sy >= view_h as i32 {
|
|
continue;
|
|
}
|
|
if entity_positions.contains_key(&(sx, sy)) {
|
|
continue;
|
|
}
|
|
let wx = cam_x + sx;
|
|
let wy = cam_y + sy;
|
|
let empty = !grid.in_bounds(wx, wy) || grid.get(wx, wy).is_empty();
|
|
if empty {
|
|
shadows.insert((sx, sy));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
shadows
|
|
}
|
|
|
|
fn item_color_at(items: &ItemManager, wx: i32, wy: i32) -> Option<[u8; 3]> {
|
|
for item in items.all() {
|
|
if item.x == wx && item.y == wy {
|
|
return Some(item.color());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
fn draw_cell(img: &mut RgbImage, vx: u32, vy: u32, color: [u8; 3]) {
|
|
let base_x = vx * CELL_SIZE;
|
|
let base_y = vy * CELL_SIZE;
|
|
for dy in 0..CELL_SIZE {
|
|
for dx in 0..CELL_SIZE {
|
|
let px = base_x + dx;
|
|
let py = base_y + dy;
|
|
if px < img.width() && py < img.height() {
|
|
img.put_pixel(px, py, image::Rgb(color));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn save_capture(
|
|
path: &str,
|
|
grid: &Grid,
|
|
entities: &EntityManager,
|
|
items: &ItemManager,
|
|
ui: &UiLayer,
|
|
cam_x: i32,
|
|
cam_y: i32,
|
|
view_w: u32,
|
|
view_h: u32,
|
|
lighting: Option<&lighting::LightGrid>,
|
|
) -> Result<(), String> {
|
|
let img = capture_frame(
|
|
grid, entities, items, ui, cam_x, cam_y, view_w, view_h, lighting,
|
|
);
|
|
img.save(path).map_err(|e| format!("save capture: {e}"))?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn capture_from_state(
|
|
grid: &Grid,
|
|
entities: &EntityManager,
|
|
items: &ItemManager,
|
|
ui: &UiLayer,
|
|
cam_x: i32,
|
|
cam_y: i32,
|
|
path: &str,
|
|
lighting: Option<&lighting::LightGrid>,
|
|
) -> Result<(), String> {
|
|
let view_w = (grid.width as u32 / CELL_SIZE).min(256);
|
|
let view_h = (grid.height as u32 / CELL_SIZE).min(256);
|
|
save_capture(
|
|
path, grid, entities, items, ui, cam_x, cam_y, view_w, view_h, lighting,
|
|
)
|
|
}
|
|
|
|
pub fn capture_from_game(game: &crate::game::Game, path: &str) -> Result<(), String> {
|
|
let (px, py) = game.player.center(&game.entities);
|
|
let view_w = (game.grid.width as u32 / CELL_SIZE).min(256);
|
|
let view_h = (game.grid.height as u32 / CELL_SIZE).min(256);
|
|
let cam_x = px as i32 - (view_w as i32 / 2);
|
|
let cam_y = py as i32 - (view_h as i32 / 2);
|
|
let light = lighting::compute_lighting(
|
|
&game.grid,
|
|
cam_x,
|
|
cam_y,
|
|
view_w as usize,
|
|
view_h as usize,
|
|
lighting::ambient_light(),
|
|
);
|
|
save_capture(
|
|
path,
|
|
&game.grid,
|
|
&game.entities,
|
|
&game.items,
|
|
&game.ui,
|
|
cam_x,
|
|
cam_y,
|
|
view_w,
|
|
view_h,
|
|
Some(&light),
|
|
)
|
|
}
|