feat: implement Phase 1 foundation - triangle rendering
- voltex_math: Vec3 with arithmetic ops, dot, cross, length, normalize - voltex_platform: VoltexWindow (winit wrapper), InputState (keyboard/mouse), GameTimer (fixed timestep + variable render loop) - voltex_renderer: GpuContext (wgpu init), Vertex + buffer layout, WGSL shader, render pipeline - triangle example: colored triangle with ESC to exit All 13 tests passing. Window renders RGB triangle on dark background. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
85
crates/voltex_platform/src/game_loop.rs
Normal file
85
crates/voltex_platform/src/game_loop.rs
Normal file
@@ -0,0 +1,85 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub struct GameTimer {
|
||||
last_frame: Instant,
|
||||
accumulator: Duration,
|
||||
fixed_dt: Duration,
|
||||
frame_time: Duration,
|
||||
}
|
||||
|
||||
impl GameTimer {
|
||||
pub fn new(fixed_hz: u32) -> Self {
|
||||
Self {
|
||||
last_frame: Instant::now(),
|
||||
accumulator: Duration::ZERO,
|
||||
fixed_dt: Duration::from_secs_f64(1.0 / fixed_hz as f64),
|
||||
frame_time: Duration::ZERO,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
let now = Instant::now();
|
||||
self.frame_time = now - self.last_frame;
|
||||
if self.frame_time > Duration::from_millis(250) {
|
||||
self.frame_time = Duration::from_millis(250);
|
||||
}
|
||||
self.accumulator += self.frame_time;
|
||||
self.last_frame = now;
|
||||
}
|
||||
|
||||
pub fn should_fixed_update(&mut self) -> bool {
|
||||
if self.accumulator >= self.fixed_dt {
|
||||
self.accumulator -= self.fixed_dt;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fixed_dt(&self) -> f32 {
|
||||
self.fixed_dt.as_secs_f32()
|
||||
}
|
||||
|
||||
pub fn frame_dt(&self) -> f32 {
|
||||
self.frame_time.as_secs_f32()
|
||||
}
|
||||
|
||||
pub fn alpha(&self) -> f32 {
|
||||
self.accumulator.as_secs_f32() / self.fixed_dt.as_secs_f32()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn test_fixed_dt() {
|
||||
let timer = GameTimer::new(60);
|
||||
let expected = 1.0 / 60.0;
|
||||
assert!((timer.fixed_dt() - expected).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_fixed_update_accumulates() {
|
||||
let mut timer = GameTimer::new(60);
|
||||
thread::sleep(Duration::from_millis(100));
|
||||
timer.tick();
|
||||
let mut count = 0;
|
||||
while timer.should_fixed_update() {
|
||||
count += 1;
|
||||
}
|
||||
assert!(count >= 5 && count <= 7, "Expected ~6 fixed updates, got {count}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_alpha_range() {
|
||||
let mut timer = GameTimer::new(60);
|
||||
thread::sleep(Duration::from_millis(10));
|
||||
timer.tick();
|
||||
while timer.should_fixed_update() {}
|
||||
let alpha = timer.alpha();
|
||||
assert!(alpha >= 0.0 && alpha <= 1.0, "Alpha should be 0..1, got {alpha}");
|
||||
}
|
||||
}
|
||||
106
crates/voltex_platform/src/input.rs
Normal file
106
crates/voltex_platform/src/input.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use winit::keyboard::KeyCode;
|
||||
use std::collections::HashSet;
|
||||
use winit::event::MouseButton;
|
||||
|
||||
pub struct InputState {
|
||||
pressed: HashSet<KeyCode>,
|
||||
just_pressed: HashSet<KeyCode>,
|
||||
just_released: HashSet<KeyCode>,
|
||||
mouse_position: (f64, f64),
|
||||
mouse_delta: (f64, f64),
|
||||
mouse_buttons: HashSet<MouseButton>,
|
||||
mouse_buttons_just_pressed: HashSet<MouseButton>,
|
||||
mouse_buttons_just_released: HashSet<MouseButton>,
|
||||
mouse_scroll_delta: f32,
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
pressed: HashSet::new(),
|
||||
just_pressed: HashSet::new(),
|
||||
just_released: HashSet::new(),
|
||||
mouse_position: (0.0, 0.0),
|
||||
mouse_delta: (0.0, 0.0),
|
||||
mouse_buttons: HashSet::new(),
|
||||
mouse_buttons_just_pressed: HashSet::new(),
|
||||
mouse_buttons_just_released: HashSet::new(),
|
||||
mouse_scroll_delta: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_key_pressed(&self, key: KeyCode) -> bool {
|
||||
self.pressed.contains(&key)
|
||||
}
|
||||
|
||||
pub fn is_key_just_pressed(&self, key: KeyCode) -> bool {
|
||||
self.just_pressed.contains(&key)
|
||||
}
|
||||
|
||||
pub fn is_key_just_released(&self, key: KeyCode) -> bool {
|
||||
self.just_released.contains(&key)
|
||||
}
|
||||
|
||||
pub fn mouse_position(&self) -> (f64, f64) {
|
||||
self.mouse_position
|
||||
}
|
||||
|
||||
pub fn mouse_delta(&self) -> (f64, f64) {
|
||||
self.mouse_delta
|
||||
}
|
||||
|
||||
pub fn is_mouse_button_pressed(&self, button: MouseButton) -> bool {
|
||||
self.mouse_buttons.contains(&button)
|
||||
}
|
||||
|
||||
pub fn is_mouse_button_just_pressed(&self, button: MouseButton) -> bool {
|
||||
self.mouse_buttons_just_pressed.contains(&button)
|
||||
}
|
||||
|
||||
pub fn mouse_scroll(&self) -> f32 {
|
||||
self.mouse_scroll_delta
|
||||
}
|
||||
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.just_pressed.clear();
|
||||
self.just_released.clear();
|
||||
self.mouse_buttons_just_pressed.clear();
|
||||
self.mouse_buttons_just_released.clear();
|
||||
self.mouse_delta = (0.0, 0.0);
|
||||
self.mouse_scroll_delta = 0.0;
|
||||
}
|
||||
|
||||
pub fn process_key(&mut self, key: KeyCode, pressed: bool) {
|
||||
if pressed {
|
||||
if self.pressed.insert(key) {
|
||||
self.just_pressed.insert(key);
|
||||
}
|
||||
} else {
|
||||
if self.pressed.remove(&key) {
|
||||
self.just_released.insert(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_mouse_move(&mut self, x: f64, y: f64) {
|
||||
self.mouse_delta.0 += x - self.mouse_position.0;
|
||||
self.mouse_delta.1 += y - self.mouse_position.1;
|
||||
self.mouse_position = (x, y);
|
||||
}
|
||||
|
||||
pub fn process_mouse_button(&mut self, button: MouseButton, pressed: bool) {
|
||||
if pressed {
|
||||
if self.mouse_buttons.insert(button) {
|
||||
self.mouse_buttons_just_pressed.insert(button);
|
||||
}
|
||||
} else {
|
||||
if self.mouse_buttons.remove(&button) {
|
||||
self.mouse_buttons_just_released.insert(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_scroll(&mut self, delta: f32) {
|
||||
self.mouse_scroll_delta += delta;
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,7 @@
|
||||
// Voltex Platform - Phase 1
|
||||
// Modules will be added in Task 3
|
||||
pub mod window;
|
||||
pub mod input;
|
||||
pub mod game_loop;
|
||||
|
||||
pub use window::{VoltexWindow, WindowConfig};
|
||||
pub use input::InputState;
|
||||
pub use game_loop::GameTimer;
|
||||
|
||||
55
crates/voltex_platform/src/window.rs
Normal file
55
crates/voltex_platform/src/window.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use std::sync::Arc;
|
||||
use winit::event_loop::ActiveEventLoop;
|
||||
use winit::window::{Window as WinitWindow, WindowAttributes};
|
||||
|
||||
pub struct WindowConfig {
|
||||
pub title: String,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub fullscreen: bool,
|
||||
pub vsync: bool,
|
||||
}
|
||||
|
||||
impl Default for WindowConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
title: "Voltex Engine".to_string(),
|
||||
width: 1280,
|
||||
height: 720,
|
||||
fullscreen: false,
|
||||
vsync: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VoltexWindow {
|
||||
pub handle: Arc<WinitWindow>,
|
||||
pub vsync: bool,
|
||||
}
|
||||
|
||||
impl VoltexWindow {
|
||||
pub fn new(event_loop: &ActiveEventLoop, config: &WindowConfig) -> Self {
|
||||
let mut attrs = WindowAttributes::default()
|
||||
.with_title(&config.title)
|
||||
.with_inner_size(winit::dpi::LogicalSize::new(config.width, config.height));
|
||||
|
||||
if config.fullscreen {
|
||||
attrs = attrs.with_fullscreen(Some(winit::window::Fullscreen::Borderless(None)));
|
||||
}
|
||||
|
||||
let window = event_loop.create_window(attrs).expect("Failed to create window");
|
||||
Self {
|
||||
handle: Arc::new(window),
|
||||
vsync: config.vsync,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn inner_size(&self) -> (u32, u32) {
|
||||
let size = self.handle.inner_size();
|
||||
(size.width, size.height)
|
||||
}
|
||||
|
||||
pub fn request_redraw(&self) {
|
||||
self.handle.request_redraw();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user