From 65752b4cfe0105803b2580ff67a4ea5a804f92df Mon Sep 17 00:00:00 2001 From: Emil Date: Sun, 21 Jun 2026 00:01:28 +0300 Subject: [PATCH] fix: window input uses get_keys() for held state, glyph atlas for FPS Input fix: get_keys_pressed(No) only fires on initial press, not held. Switched to get_keys() which returns all currently-down keys every frame. Jump still edge-triggered via jump_was_down flag. Performance fix: pre-build glyph atlas at startup (all ASCII chars rasterized once into 128x256 alpha bitmap). No per-frame cloning. draw_cell reads atlas alpha + blends fg/bg inline. Zero allocations in render loop. 109 tests, 0 warnings --- src/main.rs | 2 +- src/render/window.rs | 133 ++++++++++++++++++++++--------------- src/render/window_input.rs | 24 +++---- 3 files changed, 90 insertions(+), 69 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3fa9bd0..ea94195 100644 --- a/src/main.rs +++ b/src/main.rs @@ -226,7 +226,7 @@ fn run_window_mode() { accumulator -= fixed_dt; } - let keys = renderer.get_pressed_keys(); + let keys = renderer.get_keys_down(); input.update(&keys); if input.quit { diff --git a/src/render/window.rs b/src/render/window.rs index de90050..9caed1a 100644 --- a/src/render/window.rs +++ b/src/render/window.rs @@ -1,4 +1,4 @@ -use minifb::{Key, KeyRepeat, Window, WindowOptions}; +use minifb::{Key, Window, WindowOptions}; use fontdue::{Font, FontSettings}; use crate::entity::{EntityManager, EntityKind}; use crate::world::cell::MaterialId; @@ -7,14 +7,18 @@ use crate::world::material::MaterialRegistry; const CHAR_W: usize = 8; const CHAR_H: usize = 16; +const ATLAS_COLS: usize = 16; +const ATLAS_ROWS: usize = 16; +const ATLAS_W: usize = ATLAS_COLS * CHAR_W; +const ATLAS_H: usize = ATLAS_ROWS * CHAR_H; pub struct WindowRenderer { window: Window, - font: Font, - glyph_cache: std::collections::HashMap<(char, [u8; 3]), Vec>, width: usize, height: usize, pixels: Vec, + atlas: Vec, + atlas_map: std::collections::HashMap, } impl WindowRenderer { @@ -29,7 +33,7 @@ impl WindowRenderer { let width = 160; let height = 50; - let window = Window::new( + let mut window = Window::new( "Verbatim", width * CHAR_W, height * CHAR_H, @@ -38,79 +42,105 @@ impl WindowRenderer { ..WindowOptions::default() }, ).expect("Failed to create window"); + window.set_target_fps(60); - Self { - window, - font, - glyph_cache: std::collections::HashMap::new(), - width, - height, - pixels: vec![0u32; width * CHAR_W * height * CHAR_H], - } - } + let mut atlas = vec![0u8; ATLAS_W * ATLAS_H]; + let mut atlas_map = std::collections::HashMap::new(); - fn rgb_to_u32(r: u8, g: u8, b: u8) -> u32 { - ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) - } + let chars: Vec = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".chars().collect(); - fn get_glyph(&mut self, ch: char, fg: [u8; 3]) -> Vec { - let key = (ch, fg); - if !self.glyph_cache.contains_key(&key) { - let (metrics, bitmap) = self.font.rasterize(ch, CHAR_H as f32); + for (i, &ch) in chars.iter().enumerate() { + let col = i % ATLAS_COLS; + let row = i / ATLAS_COLS; + atlas_map.insert(ch, (col, row)); + + let (metrics, bitmap) = font.rasterize(ch, CHAR_H as f32); let gw = metrics.width; let gh = metrics.height; - let mut pixels = vec![0u32; CHAR_W * CHAR_H]; + for y in 0..gh.min(CHAR_H) { for x in 0..gw.min(CHAR_W) { - let alpha = bitmap[y * gw + x] as f32 / 255.0; - if alpha > 0.01 { + let alpha = bitmap[y * gw + x]; + if alpha > 0 { let px = (x as i32 + metrics.xmin).max(0) as usize; let py = (y as i32 + CHAR_H as i32 - gh as i32 - metrics.ymin).max(0) as usize; if px < CHAR_W && py < CHAR_H { - pixels[py * CHAR_W + px] = Self::rgb_to_u32( - (fg[0] as f32 * alpha) as u8, - (fg[1] as f32 * alpha) as u8, - (fg[2] as f32 * alpha) as u8, - ); + let ax = col * CHAR_W + px; + let ay = row * CHAR_H + py; + atlas[ay * ATLAS_W + ax] = alpha; } } } } - self.glyph_cache.insert(key, pixels); } - self.glyph_cache[&key].clone() + + Self { + window, + width, + height, + pixels: vec![0u32; width * CHAR_W * height * CHAR_H], + atlas, + atlas_map, + } } + #[inline] + fn blend(fg: [u8; 3], bg: [u8; 3], alpha: u8) -> u32 { + if alpha == 0 { + return ((bg[0] as u32) << 16) | ((bg[1] as u32) << 8) | (bg[2] as u32); + } + if alpha == 255 { + return ((fg[0] as u32) << 16) | ((fg[1] as u32) << 8) | (fg[2] as u32); + } + let a = alpha as u32; + let inv = 255 - a; + let r = (fg[0] as u32 * a + bg[0] as u32 * inv) / 255; + let g = (fg[1] as u32 * a + bg[1] as u32 * inv) / 255; + let b = (fg[2] as u32 * a + bg[2] as u32 * inv) / 255; + (r << 16) | (g << 8) | b + } + + #[inline] fn draw_cell(&mut self, col: usize, row: usize, ch: char, fg: [u8; 3], bg: [u8; 3]) { - let bg_u32 = Self::rgb_to_u32(bg[0], bg[1], bg[2]); - let glyph = self.get_glyph(ch, fg); + let (ac, ar) = match self.atlas_map.get(&ch) { + Some(&(c, r)) => (c, r), + None => return, + }; let base_x = col * CHAR_W; let base_y = row * CHAR_H; let screen_w = self.width * CHAR_W; - let screen_h = self.height * CHAR_H; for y in 0..CHAR_H { - for x in 0..CHAR_W { - let px = base_x + x; - let py = base_y + y; - if px >= screen_w || py >= screen_h { - continue; - } - let gp = glyph[y * CHAR_W + x]; - if gp != 0 { - self.pixels[py * screen_w + px] = gp; - } else { - self.pixels[py * screen_w + px] = bg_u32; - } + let ay = ar * CHAR_H + y; + let py = base_y + y; + if py >= self.height * CHAR_H { break; } + + let atlas_row = &self.atlas[ay * ATLAS_W + ac * CHAR_W..ay * ATLAS_W + ac * CHAR_W + CHAR_W]; + let pixel_row = &mut self.pixels[py * screen_w + base_x..py * screen_w + base_x + CHAR_W.min(screen_w - base_x)]; + + for x in 0..pixel_row.len() { + pixel_row[x] = Self::blend(fg, bg, atlas_row[x]); } } } pub fn render(&mut self, grid: &Grid, entities: &EntityManager, cam_x: i32, cam_y: i32) { let reg = MaterialRegistry::instance(); + let screen_w = self.width * CHAR_W; + let screen_h = self.height * CHAR_H; - let mut entity_map = std::collections::HashMap::new(); + for py in (0..screen_h).step_by(CHAR_H) { + for px in (0..screen_w).step_by(CHAR_W) { + let base = py * screen_w + px; + let end = base + CHAR_W.min(screen_w - px); + for p in base..end { + self.pixels[p] = 0x000A0A0F; + } + } + } + + let mut entity_map: std::collections::HashMap<(i32, i32), (char, [u8; 3])> = std::collections::HashMap::new(); for e in entities.all() { for b in &e.bodies { if !b.alive { continue; } @@ -156,7 +186,7 @@ impl WindowRenderer { let cell = grid.get(wx, wy); let mat = reg.get(cell.material); if cell.is_empty() { - self.draw_cell(dx, dy, ' ', [mat.color_fg.0, mat.color_fg.1, mat.color_fg.2], [10, 10, 15]); + self.draw_cell(dx, dy, ' ', [10, 10, 15], [10, 10, 15]); } else { let fg = if cell.material == MaterialId::Lava { let r = 200u8.saturating_add(cell.variant / 2); @@ -170,22 +200,17 @@ impl WindowRenderer { } } - self.window.update_with_buffer(&self.pixels, self.width * CHAR_W, self.height * CHAR_H) - .expect("update_with_buffer failed"); + let _ = self.window.update_with_buffer(&self.pixels, screen_w, screen_h); } pub fn is_open(&self) -> bool { self.window.is_open() } - pub fn get_keys(&self) -> Vec { + pub fn get_keys_down(&self) -> Vec { self.window.get_keys() } - pub fn get_pressed_keys(&self) -> Vec { - self.window.get_keys_pressed(KeyRepeat::No) - } - pub fn width(&self) -> usize { self.width } pub fn height(&self) -> usize { self.height } } diff --git a/src/render/window_input.rs b/src/render/window_input.rs index 43244f8..da9f5ea 100644 --- a/src/render/window_input.rs +++ b/src/render/window_input.rs @@ -10,6 +10,7 @@ pub struct WindowInput { pub cam_down: bool, pub quit: bool, pub paint: Option, + jump_was_down: bool, } impl WindowInput { @@ -18,30 +19,25 @@ impl WindowInput { left: false, right: false, jump: false, cam_left: false, cam_right: false, cam_up: false, cam_down: false, quit: false, paint: None, + jump_was_down: false, } } pub fn update(&mut self, keys: &[Key]) { - let was_jump = self.jump; let now_left = keys.contains(&Key::A) || keys.contains(&Key::Left); let now_right = keys.contains(&Key::D) || keys.contains(&Key::Right); let now_jump = keys.contains(&Key::W) || keys.contains(&Key::Space) || keys.contains(&Key::Up); - let now_cam_left = keys.contains(&Key::H); - let now_cam_right = keys.contains(&Key::L); - let now_cam_up = keys.contains(&Key::K); - let now_cam_down = keys.contains(&Key::J); self.left = now_left && !now_right; self.right = now_right && !now_left; - if now_left && now_right { - self.left = false; - self.right = false; - } - self.jump = now_jump && !was_jump; - self.cam_left = now_cam_left; - self.cam_right = now_cam_right; - self.cam_up = now_cam_up; - self.cam_down = now_cam_down; + + self.jump = now_jump && !self.jump_was_down; + self.jump_was_down = now_jump; + + self.cam_left = keys.contains(&Key::H); + self.cam_right = keys.contains(&Key::L); + self.cam_up = keys.contains(&Key::K); + self.cam_down = keys.contains(&Key::J); self.quit = keys.contains(&Key::Q) || keys.contains(&Key::Escape); self.paint = None;