Each cell now stores its own fg/bg color inline, instead of looking
up from MaterialRegistry every frame.
Cell struct:
- fg: [u8; 3] — foreground color (for rendering)
- bg: [u8; 3] — background color (for rendering)
- temp: f32 — temperature (already there)
- material: MaterialId — for physics property lookup
- variant: u8 — visual/behavioral variant
- updated_this_tick: bool — CA flag
Colors are copied from MaterialRegistry at Cell::new() time.
This allows per-cell color variation in the future (water depth
shading, lava gradient, damaged materials, etc.) without registry
changes.
MaterialRegistry still stores physics properties (density, solid,
liquid, flammable, etc.) — looked up only during CA/physics steps,
not during rendering.
Renderers (terminal, ascii, graphics) updated to use cell.fg/cell.bg
directly — no more registry lookup in render path.
Cell size: ~16 bytes (was ~12). 250x250 grid = 1MB (was 750KB).
126 tests, 0 failures
Wayland surface always reports current_extent as 0xFFFFFFFF (undefined),
meaning the swapchain size is determined by the application, not the
surface. Previous code returned early when this happened, so resize
never worked — cells stretched to fill the window.
Fix: when current_extent is 0xFFFFFFFF, use winit's window.inner_size()
to get the actual pixel dimensions. This works on both X11 and Wayland.
Both renderers (ascii + graphics) updated.
109 tests, 0 failures
Both renderers (ascii + graphics) now support dynamic window resize:
- check_resize() called at start of every render() frame
- Compares surface capabilities with current swapchain extent
- If changed: recreates swapchain, image views, framebuffers, command buffers
- Recalculates grid_w/grid_h from new extent / cell_size (16x16)
- Reallocates instance buffer if grid cell count changed
- Old swapchain properly destroyed after new one created
- Handles window minimize (skip if extent = 0)
- physical_device field added back to both renderers (needed for surface caps query)
main.rs: vw/vh queried every frame from renderer.grid_w()/grid_h()
instead of cached once at startup. Camera center uses dynamic dimensions.
Result: resize window → more/fewer cells visible, cells stay 16x16 pixels.
109 tests, 0 failures
Square cells:
- CHAR_W and CHAR_H both 16 (was 8x16, non-square)
- Window size updated to 160*16 x 50*16 = 2560x800
- Applies to both ascii and graphics renderers
Slope stepping collision:
- Before resolving X, check if new position overlaps solid
- If overlap, try stepping up 1 cell — if clear, snap up (walk up slope)
- If can't step up, resolve X normally (wall block)
- aabb_overlaps_solid() helper for fast overlap check
- Player can now walk up 1-cell-high steps and slopes
109 tests, 0 failures
Remove unused fields from VulkanRenderer: pixel_w, pixel_h,
physical_device, queue_family, swapchain_format. Mark unused
device param in create_swapchain as _device.
Only winit deprecation warnings remain (create_window, run).
Three render modes:
- --mode terminal: pure ANSI ASCII in terminal
- --mode ascii: Vulkan window with ASCII characters (glyph atlas)
- --mode graphics: Vulkan window with colored cells (no glyphs, each
material = unique base color, no lighting yet)
Graphics renderer:
- Same Vulkan pipeline as ASCII but without glyph atlas
- Simpler shaders: graphics.vert/frag just output instance color
- No descriptor set, no texture sampling
- Each cell = colored quad, material color fills entire cell
- Entities rendered as colored shapes (player=yellow, goblin=green)
Default mode changed to --mode ascii
109 tests, 0 failures
Use ash_window::enumerate_required_extensions() instead of hardcoded
VK_KHR_xlib_surface / VK_KHR_wayland_surface. This automatically
selects the correct surface extension per platform:
- Linux X11: VK_KHR_xlib_surface
- Linux Wayland: VK_KHR_wayland_surface
- Windows: VK_KHR_win32_surface
- macOS: VK_EXT_metal_surface (via MoltenVK)
No platform-specific code in the renderer — fully cross-platform.
109 tests, 0 failures
- Fix: Y coordinate was inverted (1.0 - 2.0*y → 2.0*y - 1.0)
Vulkan clip space Y is already down-up, no need to flip
- Fix: dynamic viewport/scissor with viewport_count(1) + scissor_count(1)
Required by validation layer even with dynamic state
- Clean: removed debug eprintlns, restored default clear color
- Shaders recompiled to SPIR-V
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
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
New --mode window (now default):
- minifb creates a real OS window (no terminal needed)
- fontdue rasterizes DejaVu Sans Mono glyphs to pixel buffer
- ASCII characters rendered as colored pixels — same aesthetic
- Keyboard polled via get_pressed_keys() every frame:
- Instant response, no terminal delay
- Proper multi-key support (W+A strafing works)
- No Release event hacks needed
- No key repeat timeout hacks
- Instant stop when key released
Controls: WASD/arrows, 1-9/0 paint, X erase, HJKL camera, Q/Esc quit
--mode terminal still available as fallback
109 tests, 0 warnings, 0 failures
Root cause of both issues: terminals don't send Release events by
default. When pressing W while holding A, terminal stops repeating A
(but doesn't send Release), so A times out and strafing breaks.
On key release, 400ms timeout means 400ms of extra movement.
Fix: PushKeyboardEnhancementFlags(REPORT_EVENT_TYPES) in terminal init.
This asks the terminal to send proper Press/Release/Repeat events for
ALL keys, not just special ones.
With Release events:
- Key release is instant (no 400ms timeout delay)
- Pressing W doesn't cancel A's held state (terminal sends Release
only when A is actually released)
- A/D strafing works while W is held
Fallback: if terminal doesn't support enhancement flags (old xterm),
got_release flag stays false and 150ms timeout is used. Once any
Release event is received, all keys switch to Release-based mode
(infinite timeout, rely on actual Release events).
109 tests, 0 warnings, 0 failures