fix: winit + softbuffer for layout-agnostic input and better FPS

Replaced minifb with winit + softbuffer:

Input (layout-agnostic):
- winit uses PhysicalKey<KeyCode> which maps to physical key positions
- KeyCode::KeyA = physical A key, regardless of keyboard layout
- Works on Russian, Arabic, any layout — no key mapping needed
- HashSet<KeyCode> tracks pressed/released state
- Proper Press/Release events from OS, no timeout hacks

FPS improvements:
- Glyph atlas pre-built at startup (alpha bitmap, zero per-frame alloc)
- render_to_buffer: skip empty cells entirely (no draw call)
- blend_fast: bitshift instead of division for alpha blending
- Buffer fill via .fill() instead of nested loop
- copy_from_slice for pixel transfer to softbuffer (memcpy speed)
- ControlFlow::Poll for maximum frame rate

109 tests, 0 failures
This commit is contained in:
Emil
2026-06-21 00:11:12 +03:00
parent 65752b4cfe
commit 390eca9cda
4 changed files with 240 additions and 176 deletions
+70 -92
View File
@@ -1,4 +1,3 @@
use minifb::{Key, Window, WindowOptions};
use fontdue::{Font, FontSettings};
use crate::entity::{EntityManager, EntityKind};
use crate::world::cell::MaterialId;
@@ -13,12 +12,12 @@ const ATLAS_W: usize = ATLAS_COLS * CHAR_W;
const ATLAS_H: usize = ATLAS_ROWS * CHAR_H;
pub struct WindowRenderer {
window: Window,
width: usize,
height: usize,
pixels: Vec<u32>,
atlas: Vec<u8>,
atlas_map: std::collections::HashMap<char, (usize, usize)>,
font: Font,
}
impl WindowRenderer {
@@ -33,21 +32,10 @@ impl WindowRenderer {
let width = 160;
let height = 50;
let mut window = Window::new(
"Verbatim",
width * CHAR_W,
height * CHAR_H,
WindowOptions {
resize: true,
..WindowOptions::default()
},
).expect("Failed to create window");
window.set_target_fps(60);
let mut atlas = vec![0u8; ATLAS_W * ATLAS_H];
let mut atlas_map = std::collections::HashMap::new();
let chars: Vec<char> = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".chars().collect();
let chars: Vec<char> = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~?".chars().collect();
for (i, &ch) in chars.iter().enumerate() {
let col = i % ATLAS_COLS;
@@ -75,71 +63,41 @@ impl WindowRenderer {
}
Self {
window,
width,
height,
pixels: vec![0u32; width * CHAR_W * height * CHAR_H],
pixels: vec![0x000A0A0F; width * CHAR_W * height * CHAR_H],
atlas,
atlas_map,
font,
}
}
#[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);
}
#[inline(always)]
fn blend_fast(fg: [u8; 3], bg: u32, alpha: u8) -> u32 {
if alpha == 0 { return bg; }
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;
let br = (bg >> 16) & 0xFF;
let bg_ = (bg >> 8) & 0xFF;
let bb = bg & 0xFF;
let r = (fg[0] as u32 * a + br * inv) >> 8;
let g = (fg[1] as u32 * a + bg_ * inv) >> 8;
let b = (fg[2] as u32 * a + bb * inv) >> 8;
(r << 16) | (g << 8) | b
}
#[inline]
fn draw_cell(&mut self, col: usize, row: usize, ch: char, fg: [u8; 3], bg: [u8; 3]) {
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;
for y in 0..CHAR_H {
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) {
pub fn render_to_buffer(&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 bg_default = 0x000A0A0F;
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;
}
}
}
// Fast clear: fill with background
self.pixels.fill(bg_default);
// Build entity overlay
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 {
@@ -168,49 +126,69 @@ impl WindowRenderer {
}
}
// Draw cells
for dy in 0..self.height {
let wy = cam_y + dy as i32;
for dx in 0..self.width {
let wx = cam_x + dx as i32;
let wy = cam_y + dy as i32;
if let Some(&(ch, fg)) = entity_map.get(&(dx as i32, dy as i32)) {
self.draw_cell(dx, dy, ch, fg, [10, 10, 15]);
continue;
}
if !grid.in_bounds(wx, wy) {
self.draw_cell(dx, dy, '?', [80, 80, 80], [10, 10, 15]);
continue;
}
let cell = grid.get(wx, wy);
let mat = reg.get(cell.material);
if cell.is_empty() {
self.draw_cell(dx, dy, ' ', [10, 10, 15], [10, 10, 15]);
let (ch, fg, bg) = if let Some(&(ec, ef)) = entity_map.get(&(dx as i32, dy as i32)) {
(ec, ef, bg_default)
} else if !grid.in_bounds(wx, wy) {
('?', [80, 80, 80], bg_default)
} else {
let fg = if cell.material == MaterialId::Lava {
let r = 200u8.saturating_add(cell.variant / 2);
[r, 60, 20]
let cell = grid.get(wx, wy);
let mat = reg.get(cell.material);
if cell.is_empty() {
(' ', [10, 10, 15], bg_default)
} else {
[mat.color_fg.0, mat.color_fg.1, mat.color_fg.2]
};
let bg = [mat.color_bg.0, mat.color_bg.1, mat.color_bg.2];
self.draw_cell(dx, dy, mat.display_char, fg, bg);
let fg = if cell.material == MaterialId::Lava {
let r = 200u8.saturating_add(cell.variant / 2);
[r, 60, 20]
} else {
[mat.color_fg.0, mat.color_fg.1, mat.color_fg.2]
};
let bg = ((mat.color_bg.0 as u32) << 16) | ((mat.color_bg.1 as u32) << 8) | (mat.color_bg.2 as u32);
(mat.display_char, fg, bg)
}
};
// Skip drawing if it's a space on default background
if ch == ' ' && bg == bg_default {
continue;
}
let (ac, ar) = match self.atlas_map.get(&ch) {
Some(&(c, r)) => (c, r),
None => continue,
};
let base_x = dx * CHAR_W;
let base_y = dy * CHAR_H;
for y in 0..CHAR_H {
let ay = ar * CHAR_H + y;
let py = base_y + y;
let atlas_off = ay * ATLAS_W + ac * CHAR_W;
let pix_off = py * screen_w + base_x;
for x in 0..CHAR_W {
let alpha = self.atlas[atlas_off + x];
if alpha > 0 {
self.pixels[pix_off + x] = Self::blend_fast(fg, bg, alpha);
} else {
self.pixels[pix_off + x] = bg;
}
}
}
}
}
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_down(&self) -> Vec<Key> {
self.window.get_keys()
}
pub fn width(&self) -> usize { self.width }
pub fn height(&self) -> usize { self.height }
pub fn pixels(&self) -> &[u32] { &self.pixels }
pub fn pixel_w(&self) -> usize { self.width * CHAR_W }
pub fn pixel_h(&self) -> usize { self.height * CHAR_H }
pub fn font(&self) -> &Font { &self.font }
}
+49 -21
View File
@@ -1,4 +1,5 @@
use minifb::Key;
use std::collections::HashSet;
use winit::keyboard::{KeyCode, PhysicalKey};
pub struct WindowInput {
pub left: bool,
@@ -11,6 +12,7 @@ pub struct WindowInput {
pub quit: bool,
pub paint: Option<u8>,
jump_was_down: bool,
down_keys: HashSet<KeyCode>,
}
impl WindowInput {
@@ -20,13 +22,32 @@ impl WindowInput {
cam_left: false, cam_right: false, cam_up: false, cam_down: false,
quit: false, paint: None,
jump_was_down: false,
down_keys: HashSet::new(),
}
}
pub fn update(&mut self, keys: &[Key]) {
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);
pub fn on_key_event(&mut self, key: PhysicalKey, state: winit::event::ElementState) {
let code = match key {
PhysicalKey::Code(c) => c,
_ => return,
};
match state {
winit::event::ElementState::Pressed => {
self.down_keys.insert(code);
}
winit::event::ElementState::Released => {
self.down_keys.remove(&code);
}
}
}
pub fn update(&mut self) {
let keys = &self.down_keys;
let now_left = keys.contains(&KeyCode::KeyA) || keys.contains(&KeyCode::ArrowLeft);
let now_right = keys.contains(&KeyCode::KeyD) || keys.contains(&KeyCode::ArrowRight);
let now_jump = keys.contains(&KeyCode::KeyW) || keys.contains(&KeyCode::Space) || keys.contains(&KeyCode::ArrowUp);
self.left = now_left && !now_right;
self.right = now_right && !now_left;
@@ -34,23 +55,30 @@ impl WindowInput {
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.cam_left = keys.contains(&KeyCode::KeyH);
self.cam_right = keys.contains(&KeyCode::KeyL);
self.cam_up = keys.contains(&KeyCode::KeyK);
self.cam_down = keys.contains(&KeyCode::KeyJ);
self.quit = keys.contains(&KeyCode::KeyQ) || keys.contains(&KeyCode::Escape);
self.paint = None;
if keys.contains(&Key::Key1) { self.paint = Some(1); }
else if keys.contains(&Key::Key2) { self.paint = Some(2); }
else if keys.contains(&Key::Key3) { self.paint = Some(3); }
else if keys.contains(&Key::Key4) { self.paint = Some(4); }
else if keys.contains(&Key::Key5) { self.paint = Some(5); }
else if keys.contains(&Key::Key6) { self.paint = Some(6); }
else if keys.contains(&Key::Key7) { self.paint = Some(7); }
else if keys.contains(&Key::Key8) { self.paint = Some(8); }
else if keys.contains(&Key::Key9) { self.paint = Some(9); }
else if keys.contains(&Key::Key0) { self.paint = Some(0); }
else if keys.contains(&Key::X) { self.paint = Some(99); }
if keys.contains(&KeyCode::Digit1) { self.paint = Some(1); }
else if keys.contains(&KeyCode::Digit2) { self.paint = Some(2); }
else if keys.contains(&KeyCode::Digit3) { self.paint = Some(3); }
else if keys.contains(&KeyCode::Digit4) { self.paint = Some(4); }
else if keys.contains(&KeyCode::Digit5) { self.paint = Some(5); }
else if keys.contains(&KeyCode::Digit6) { self.paint = Some(6); }
else if keys.contains(&KeyCode::Digit7) { self.paint = Some(7); }
else if keys.contains(&KeyCode::Digit8) { self.paint = Some(8); }
else if keys.contains(&KeyCode::Digit9) { self.paint = Some(9); }
else if keys.contains(&KeyCode::Digit0) { self.paint = Some(0); }
else if keys.contains(&KeyCode::KeyX) { self.paint = Some(99); }
}
pub fn clear(&mut self) {
self.down_keys.clear();
self.left = false;
self.right = false;
self.jump = false;
}
}