refactor: --mode window is now Vulkan, remove softbuffer fallback

- --mode window = Vulkan renderer (was softbuffer CPU)
- --mode vulkan removed (merged into --mode window)
- softbuffer dependency removed
- log dependency removed (unused)
- Old window.rs (softbuffer CPU renderer) deleted
- Fallback: if Vulkan init fails, falls back to --mode terminal
- 109 tests, 0 failures
This commit is contained in:
Emil
2026-06-21 00:53:20 +03:00
parent 8b79b99ab2
commit 020914ab99
4 changed files with 5 additions and 355 deletions
+1 -3
View File
@@ -10,12 +10,10 @@ serde = { version = "1", features = ["derive"] }
serde_json = "1"
fontdue = "0.9"
winit = "0.30"
softbuffer = "0.4"
log = "0.4"
ash = "0.38"
ash-window = "0.13"
raw-window-handle = "0.6"
bytemuck = "1"
bytemuck = { version = "1", features = ["derive"] }
[profile.release]
opt-level = 3
+4 -157
View File
@@ -1,7 +1,6 @@
use clap::Parser;
use verbatim::game::Game;
use verbatim::render::terminal::TerminalRenderer;
use verbatim::render::window::WindowRenderer;
use verbatim::render::window_input::WindowInput;
use verbatim::world::cell::MaterialId;
use verbatim::ai;
@@ -31,9 +30,6 @@ fn main() {
match cli.mode.as_str() {
"window" => {
run_window_mode();
}
"vulkan" => {
run_vulkan_mode();
}
"terminal" => {
@@ -204,156 +200,6 @@ fn player_info(game: &Game) -> String {
}
}
fn run_window_mode() {
use winit::event::{Event, WindowEvent};
use winit::event_loop::{EventLoop, ControlFlow};
use winit::window::Window;
use std::time::{Duration, Instant};
use std::sync::Arc;
use std::num::NonZeroU32;
let event_loop = EventLoop::new().expect("Failed to create event loop");
let window = event_loop.create_window(
Window::default_attributes()
.with_title("Verbatim")
.with_inner_size(winit::dpi::LogicalSize::new(160 * 8, 50 * 16))
).expect("Failed to create window");
let window = Arc::new(window);
let display_handle = event_loop.owned_display_handle();
let context = softbuffer::Context::new(display_handle)
.expect("Failed to create softbuffer context");
let mut surface = softbuffer::Surface::new(&context, Arc::clone(&window))
.expect("Failed to create surface");
let pw = 160 * 8;
let ph = 50 * 16;
surface.resize(NonZeroU32::new(pw).unwrap(), NonZeroU32::new(ph).unwrap())
.expect("Failed to resize surface");
let mut renderer = WindowRenderer::new();
let mut game = Game::new();
game.init_world();
let mut input = WindowInput::new();
let vw = renderer.width();
let vh = renderer.height();
let fixed_dt = Duration::from_millis(16);
let mut last_time = Instant::now();
let mut accumulator = Duration::ZERO;
let mut running = true;
event_loop.run(|event, ctrl| {
ctrl.set_control_flow(ControlFlow::Poll);
match event {
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::CloseRequested => {
running = false;
ctrl.exit();
}
WindowEvent::KeyboardInput { event: key_event, .. } => {
input.on_key_event(key_event.physical_key, key_event.state);
}
_ => {}
}
}
Event::AboutToWait => {
if !running {
ctrl.exit();
return;
}
let now = Instant::now();
let frame_time = now.duration_since(last_time);
last_time = now;
accumulator += frame_time;
let mut steps = 0;
while accumulator >= fixed_dt && steps < 5 {
game.fixed_update();
accumulator -= fixed_dt;
steps += 1;
}
input.update();
if input.quit {
running = false;
ctrl.exit();
return;
}
if input.jump {
let on_ground = game.check_on_ground();
game.player.jump(&mut game.entities, on_ground);
}
if input.left {
game.player.move_left(&mut game.entities);
} else if input.right {
game.player.move_right(&mut game.entities);
} else {
game.player.stop_horizontal(&mut game.entities);
}
if input.cam_left { game.cam_x -= 3; }
if input.cam_right { game.cam_x += 3; }
if input.cam_up { game.cam_y -= 3; }
if input.cam_down { game.cam_y += 3; }
if let Some(brush_id) = input.paint {
let mat = match brush_id {
1 => MaterialId::Sand,
2 => MaterialId::Water,
3 => MaterialId::Stone,
4 => MaterialId::Lava,
5 => MaterialId::Wood,
6 => MaterialId::Acid,
7 => MaterialId::Grass,
8 => MaterialId::Dirt,
9 => MaterialId::Fire,
0 => MaterialId::Flesh,
99 => MaterialId::Empty,
_ => MaterialId::Empty,
};
let cx = game.cam_x + (vw as i32 / 2);
let cy = game.cam_y + (vh as i32 / 2);
let r = 2;
for dy in -r..=r {
for dx in -r..=r {
if dx * dx + dy * dy <= r * r + 1 {
if mat == MaterialId::Empty {
game.grid.set(cx + dx, cy + dy, verbatim::world::cell::Cell::empty());
} else {
game.grid.set_material(cx + dx, cy + dy, mat);
}
}
}
}
}
let (px, py) = game.player.center(&game.entities);
game.cam_x = px as i32 - (vw as i32 / 2);
game.cam_y = py as i32 - (vh as i32 / 2);
renderer.render_to_buffer(&game.grid, &game.entities, game.cam_x, game.cam_y);
let mut buffer = surface.buffer_mut().expect("buffer");
let pixels = renderer.pixels();
let len = buffer.len().min(pixels.len());
buffer[..len].copy_from_slice(&pixels[..len]);
buffer.present().expect("present");
}
_ => {}
}
}).expect("event loop error");
}
fn run_vulkan_mode() {
use verbatim::render::vulkan::VulkanRenderer;
@@ -375,9 +221,10 @@ fn run_vulkan_mode() {
Ok(r) => r,
Err(e) => {
eprintln!("Vulkan init failed: {e}");
eprintln!("Vulkan init failed: {e}");
eprintln!("Falling back to softbuffer window mode...");
run_window_mode();
eprintln!("Falling back to terminal mode...");
let mut renderer = TerminalRenderer::new();
let mut game = Game::new();
game.run(&mut renderer);
return;
}
};
-1
View File
@@ -1,5 +1,4 @@
pub mod terminal;
pub mod window;
pub mod window_input;
pub mod vulkan;
-194
View File
@@ -1,194 +0,0 @@
use fontdue::{Font, FontSettings};
use crate::entity::{EntityManager, EntityKind};
use crate::world::cell::MaterialId;
use crate::world::grid::Grid;
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 {
width: usize,
height: usize,
pixels: Vec<u32>,
atlas: Vec<u8>,
atlas_map: std::collections::HashMap<char, (usize, usize)>,
font: Font,
}
impl WindowRenderer {
pub fn new() -> Self {
let font_bytes: &[u8] = include_bytes!("../../assets/DejaVuSansMono.ttf");
let font = Font::from_bytes(font_bytes, FontSettings {
collection_index: 0,
scale: CHAR_H as f32,
load_substitutions: false,
}).expect("Failed to load font");
let width = 160;
let height = 50;
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();
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;
for y in 0..gh.min(CHAR_H) {
for x in 0..gw.min(CHAR_W) {
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 {
let ax = col * CHAR_W + px;
let ay = row * CHAR_H + py;
atlas[ay * ATLAS_W + ax] = alpha;
}
}
}
}
}
Self {
width,
height,
pixels: vec![0x000A0A0F; width * CHAR_W * height * CHAR_H],
atlas,
atlas_map,
font,
}
}
#[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 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
}
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 bg_default = 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 {
if !b.alive { continue; }
let sx = b.x as i32 - cam_x;
let sy = b.y as i32 - cam_y;
if sx >= 0 && sx < self.width as i32 && sy >= 0 && sy < self.height as i32 {
let ch = match e.kind {
EntityKind::Player if e.alive => '@',
EntityKind::Goblin if e.alive => 'g',
_ => '%',
};
let fg = if e.on_fire {
[255, 160, 40]
} else if !e.alive {
[100, 60, 60]
} else {
match e.kind {
EntityKind::Player => [255, 255, 100],
EntityKind::Goblin => [100, 220, 100],
_ => [180, 50, 50],
}
};
entity_map.insert((sx, sy), (ch, fg));
}
}
}
// 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 (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 cell = grid.get(wx, wy);
let mat = reg.get(cell.material);
if cell.is_empty() {
(' ', [10, 10, 15], bg_default)
} else {
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;
}
}
}
}
}
}
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 }
}