perf: UiLayer HashMap → flat Vec array — 62 FPS → 95 FPS

Replaced HashMap<(i32,i32), UiCell> with Vec<Option<UiCell>> indexed
by y*width+x. dirty_keys Vec tracks non-None cells for iteration.
- set/set_alpha: O(1) array write (was O(1) amortized HashMap insert
  with hashing overhead)
- get: O(1) array index (was HashMap lookup)
- keys(): iterate dirty_keys Vec (was HashMap keys iterator)
- clear(): memset None over flat array (was HashMap::clear)
- resize() called in build_ui to size array to viewport
- All 185 tests + 14 scenarios pass
- Benchmark: 95.1 FPS (was 61.9 — 53% improvement)
This commit is contained in:
Emil
2026-06-22 13:32:47 +03:00
parent a16645d2d8
commit b52798c634
7 changed files with 59 additions and 24 deletions
+11 -11
View File
@@ -1,17 +1,17 @@
{
"mode": "graphics",
"ticks": 300,
"total_time_ms": 4845.5,
"avg_fps": 61.9,
"avg_frame_time_ms": 16.12,
"p99_frame_time_ms": 145.95,
"min_frame_time_ms": 8.19,
"total_time_ms": 3153.2,
"avg_fps": 95.1,
"avg_frame_time_ms": 10.48,
"p99_frame_time_ms": 65.20,
"min_frame_time_ms": 6.55,
"subsystems": {
"ca_step_avg_us": 8153,
"ca_step_p99_us": 137703,
"ca_step_min_us": 1174,
"render_avg_us": 4433,
"render_p99_us": 6450,
"render_min_us": 3956
"ca_step_avg_us": 3386,
"ca_step_p99_us": 58075,
"ca_step_min_us": 231,
"render_avg_us": 3770,
"render_p99_us": 5334,
"render_min_us": 3320
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 640 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

+1
View File
@@ -357,6 +357,7 @@ impl Game {
let ui_h = (vh as i32) * crate::ui::UI_SCALE;
let fs = ((ui_h / 200).max(2)).min(6) as i32;
self.ui.set_font_scale(fs);
self.ui.resize(ui_w as usize, ui_h as usize);
let player = self.player.entity(&self.entities).cloned();
let brush = self
+45 -13
View File
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use crate::entity::entity::{Entity, EntityKind};
use crate::world::cell::MaterialId;
use crate::world::chunked_grid::ChunkedGrid;
@@ -17,7 +15,10 @@ pub struct UiCell {
}
pub struct UiLayer {
cells: HashMap<(i32, i32), UiCell>,
cells: Vec<Option<UiCell>>,
width: usize,
height: usize,
dirty_keys: Vec<(i32, i32)>,
messages: Vec<(String, u32)>,
damage_numbers: Vec<DamageNumber>,
font_scale: i32,
@@ -35,7 +36,10 @@ pub struct DamageNumber {
impl UiLayer {
pub fn new() -> Self {
Self {
cells: HashMap::new(),
cells: Vec::new(),
width: 0,
height: 0,
dirty_keys: Vec::new(),
messages: Vec::new(),
damage_numbers: Vec::new(),
font_scale: 2,
@@ -61,7 +65,10 @@ impl UiLayer {
}
pub fn clear(&mut self) {
self.cells.clear();
for c in self.cells.iter_mut() {
*c = None;
}
self.dirty_keys.clear();
self.damage_numbers.retain(|d| d.life > 0);
for d in &mut self.damage_numbers {
d.y -= 0.1;
@@ -69,28 +76,53 @@ impl UiLayer {
}
}
pub fn resize(&mut self, w: usize, h: usize) {
self.width = w;
self.height = h;
self.cells.clear();
self.cells.resize(w * h, None);
self.dirty_keys.clear();
}
#[inline]
fn idx(&self, x: i32, y: i32) -> Option<usize> {
if x < 0 || y < 0 || x as usize >= self.width || y as usize >= self.height {
return None;
}
Some(y as usize * self.width + x as usize)
}
pub fn set(&mut self, x: i32, y: i32, ch: char, fg: [u8; 3], bg: [u8; 3]) {
self.cells.insert(
(x, y),
UiCell {
if let Some(i) = self.idx(x, y) {
let was_none = self.cells[i].is_none();
self.cells[i] = Some(UiCell {
ch,
fg,
bg,
alpha: 255,
},
);
});
if was_none {
self.dirty_keys.push((x, y));
}
}
}
pub fn set_alpha(&mut self, x: i32, y: i32, ch: char, fg: [u8; 3], bg: [u8; 3], alpha: u8) {
self.cells.insert((x, y), UiCell { ch, fg, bg, alpha });
if let Some(i) = self.idx(x, y) {
let was_none = self.cells[i].is_none();
self.cells[i] = Some(UiCell { ch, fg, bg, alpha });
if was_none {
self.dirty_keys.push((x, y));
}
}
}
pub fn get(&self, x: i32, y: i32) -> Option<&UiCell> {
self.cells.get(&(x, y))
self.idx(x, y).and_then(|i| self.cells[i].as_ref())
}
pub fn keys(&self) -> impl Iterator<Item = &(i32, i32)> {
self.cells.keys()
self.dirty_keys.iter()
}
pub fn add_message(&mut self, text: &str) {
+1
View File
@@ -53,6 +53,7 @@ fn depth_shown_in_hud() {
let mut ui = verbatim::ui::UiLayer::new();
let screen_w = 320;
let screen_h = 100;
ui.resize(screen_w, screen_h);
ui.draw_hud(
screen_w,
screen_h,
+1
View File
@@ -172,6 +172,7 @@ fn ui_hud_shows_player_hp() {
let mut s = setup();
let mut ui = verbatim::ui::UiLayer::new();
let player = s.game.entities.all()[0].clone();
ui.resize(80, 25);
ui.draw_hud(
80,
25,