diff --git a/crates/voltex_math/src/lib.rs b/crates/voltex_math/src/lib.rs index f8b2b0a..1414e8b 100644 --- a/crates/voltex_math/src/lib.rs +++ b/crates/voltex_math/src/lib.rs @@ -1 +1,2 @@ -// Voltex Math Library - Phase 1 +pub mod vec3; +pub use vec3::Vec3; diff --git a/crates/voltex_math/src/vec3.rs b/crates/voltex_math/src/vec3.rs new file mode 100644 index 0000000..ca8cee4 --- /dev/null +++ b/crates/voltex_math/src/vec3.rs @@ -0,0 +1,158 @@ +use std::ops::{Add, Sub, Mul, Neg}; + +/// 3D vector (f32) +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Vec3 { + pub x: f32, + pub y: f32, + pub z: f32, +} + +impl Vec3 { + pub const ZERO: Self = Self { x: 0.0, y: 0.0, z: 0.0 }; + pub const ONE: Self = Self { x: 1.0, y: 1.0, z: 1.0 }; + pub const X: Self = Self { x: 1.0, y: 0.0, z: 0.0 }; + pub const Y: Self = Self { x: 0.0, y: 1.0, z: 0.0 }; + pub const Z: Self = Self { x: 0.0, y: 0.0, z: 1.0 }; + + pub const fn new(x: f32, y: f32, z: f32) -> Self { + Self { x, y, z } + } + + pub fn dot(self, rhs: Self) -> f32 { + self.x * rhs.x + self.y * rhs.y + self.z * rhs.z + } + + pub fn cross(self, rhs: Self) -> Self { + Self { + x: self.y * rhs.z - self.z * rhs.y, + y: self.z * rhs.x - self.x * rhs.z, + z: self.x * rhs.y - self.y * rhs.x, + } + } + + pub fn length_squared(self) -> f32 { + self.dot(self) + } + + pub fn length(self) -> f32 { + self.length_squared().sqrt() + } + + pub fn normalize(self) -> Self { + let len = self.length(); + Self { + x: self.x / len, + y: self.y / len, + z: self.z / len, + } + } +} + +impl Add for Vec3 { + type Output = Self; + fn add(self, rhs: Self) -> Self { + Self { x: self.x + rhs.x, y: self.y + rhs.y, z: self.z + rhs.z } + } +} + +impl Sub for Vec3 { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + Self { x: self.x - rhs.x, y: self.y - rhs.y, z: self.z - rhs.z } + } +} + +impl Mul for Vec3 { + type Output = Self; + fn mul(self, rhs: f32) -> Self { + Self { x: self.x * rhs, y: self.y * rhs, z: self.z * rhs } + } +} + +impl Neg for Vec3 { + type Output = Self; + fn neg(self) -> Self { + Self { x: -self.x, y: -self.y, z: -self.z } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new() { + let v = Vec3::new(1.0, 2.0, 3.0); + assert_eq!(v.x, 1.0); + assert_eq!(v.y, 2.0); + assert_eq!(v.z, 3.0); + } + + #[test] + fn test_zero() { + let v = Vec3::ZERO; + assert_eq!(v.x, 0.0); + assert_eq!(v.y, 0.0); + assert_eq!(v.z, 0.0); + } + + #[test] + fn test_add() { + let a = Vec3::new(1.0, 2.0, 3.0); + let b = Vec3::new(4.0, 5.0, 6.0); + let c = a + b; + assert_eq!(c, Vec3::new(5.0, 7.0, 9.0)); + } + + #[test] + fn test_sub() { + let a = Vec3::new(4.0, 5.0, 6.0); + let b = Vec3::new(1.0, 2.0, 3.0); + let c = a - b; + assert_eq!(c, Vec3::new(3.0, 3.0, 3.0)); + } + + #[test] + fn test_scalar_mul() { + let v = Vec3::new(1.0, 2.0, 3.0); + let r = v * 2.0; + assert_eq!(r, Vec3::new(2.0, 4.0, 6.0)); + } + + #[test] + fn test_dot() { + let a = Vec3::new(1.0, 2.0, 3.0); + let b = Vec3::new(4.0, 5.0, 6.0); + assert_eq!(a.dot(b), 32.0); + } + + #[test] + fn test_cross() { + let a = Vec3::new(1.0, 0.0, 0.0); + let b = Vec3::new(0.0, 1.0, 0.0); + let c = a.cross(b); + assert_eq!(c, Vec3::new(0.0, 0.0, 1.0)); + } + + #[test] + fn test_length() { + let v = Vec3::new(3.0, 4.0, 0.0); + assert!((v.length() - 5.0).abs() < f32::EPSILON); + } + + #[test] + fn test_normalize() { + let v = Vec3::new(3.0, 0.0, 0.0); + let n = v.normalize(); + assert!((n.length() - 1.0).abs() < 1e-6); + assert_eq!(n, Vec3::new(1.0, 0.0, 0.0)); + } + + #[test] + fn test_neg() { + let v = Vec3::new(1.0, -2.0, 3.0); + let n = -v; + assert_eq!(n, Vec3::new(-1.0, 2.0, -3.0)); + } +} diff --git a/crates/voltex_platform/src/game_loop.rs b/crates/voltex_platform/src/game_loop.rs new file mode 100644 index 0000000..215814a --- /dev/null +++ b/crates/voltex_platform/src/game_loop.rs @@ -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}"); + } +} diff --git a/crates/voltex_platform/src/input.rs b/crates/voltex_platform/src/input.rs new file mode 100644 index 0000000..a77b971 --- /dev/null +++ b/crates/voltex_platform/src/input.rs @@ -0,0 +1,106 @@ +use winit::keyboard::KeyCode; +use std::collections::HashSet; +use winit::event::MouseButton; + +pub struct InputState { + pressed: HashSet, + just_pressed: HashSet, + just_released: HashSet, + mouse_position: (f64, f64), + mouse_delta: (f64, f64), + mouse_buttons: HashSet, + mouse_buttons_just_pressed: HashSet, + mouse_buttons_just_released: HashSet, + 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; + } +} diff --git a/crates/voltex_platform/src/lib.rs b/crates/voltex_platform/src/lib.rs index c6a88ff..39fb4d3 100644 --- a/crates/voltex_platform/src/lib.rs +++ b/crates/voltex_platform/src/lib.rs @@ -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; diff --git a/crates/voltex_platform/src/window.rs b/crates/voltex_platform/src/window.rs new file mode 100644 index 0000000..611d894 --- /dev/null +++ b/crates/voltex_platform/src/window.rs @@ -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, + 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(); + } +} diff --git a/crates/voltex_renderer/src/gpu.rs b/crates/voltex_renderer/src/gpu.rs new file mode 100644 index 0000000..b8c0a3c --- /dev/null +++ b/crates/voltex_renderer/src/gpu.rs @@ -0,0 +1,83 @@ +use std::sync::Arc; +use winit::window::Window; + +pub struct GpuContext { + pub surface: wgpu::Surface<'static>, + pub device: wgpu::Device, + pub queue: wgpu::Queue, + pub config: wgpu::SurfaceConfiguration, + pub surface_format: wgpu::TextureFormat, +} + +impl GpuContext { + pub fn new(window: Arc) -> Self { + pollster::block_on(Self::new_async(window)) + } + + async fn new_async(window: Arc) -> Self { + let size = window.inner_size(); + + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }); + + let surface = instance.create_surface(window).expect("Failed to create surface"); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .expect("Failed to find a suitable GPU adapter"); + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + label: Some("Voltex Device"), + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + memory_hints: Default::default(), + ..Default::default() + }) + .await + .expect("Failed to create device"); + + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps + .formats + .iter() + .find(|f| f.is_srgb()) + .copied() + .unwrap_or(surface_caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width.max(1), + height: size.height.max(1), + present_mode: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &config); + + Self { + surface, + device, + queue, + config, + surface_format, + } + } + + pub fn resize(&mut self, width: u32, height: u32) { + if width > 0 && height > 0 { + self.config.width = width; + self.config.height = height; + self.surface.configure(&self.device, &self.config); + } + } +} diff --git a/crates/voltex_renderer/src/lib.rs b/crates/voltex_renderer/src/lib.rs index c1add99..b3d1710 100644 --- a/crates/voltex_renderer/src/lib.rs +++ b/crates/voltex_renderer/src/lib.rs @@ -1,2 +1,5 @@ -// Voltex Renderer - Phase 1 -// Modules will be added in Task 4 +pub mod gpu; +pub mod pipeline; +pub mod vertex; + +pub use gpu::GpuContext; diff --git a/crates/voltex_renderer/src/pipeline.rs b/crates/voltex_renderer/src/pipeline.rs new file mode 100644 index 0000000..f0eeeeb --- /dev/null +++ b/crates/voltex_renderer/src/pipeline.rs @@ -0,0 +1,55 @@ +use crate::vertex::Vertex; + +pub fn create_render_pipeline( + device: &wgpu::Device, + format: wgpu::TextureFormat, +) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Voltex Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[], + immediate_size: 0, + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[Vertex::LAYOUT], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview_mask: None, + cache: None, + }) +} diff --git a/crates/voltex_renderer/src/shader.wgsl b/crates/voltex_renderer/src/shader.wgsl new file mode 100644 index 0000000..d6332c2 --- /dev/null +++ b/crates/voltex_renderer/src/shader.wgsl @@ -0,0 +1,22 @@ +struct VertexInput { + @location(0) position: vec3, + @location(1) color: vec3, +}; + +struct VertexOutput { + @builtin(position) clip_position: vec4, + @location(0) color: vec3, +}; + +@vertex +fn vs_main(model: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.color = model.color; + out.clip_position = vec4(model.position, 1.0); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return vec4(in.color, 1.0); +} diff --git a/crates/voltex_renderer/src/vertex.rs b/crates/voltex_renderer/src/vertex.rs new file mode 100644 index 0000000..1cfe87f --- /dev/null +++ b/crates/voltex_renderer/src/vertex.rs @@ -0,0 +1,27 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Vertex { + pub position: [f32; 3], + pub color: [f32; 3], +} + +impl Vertex { + pub const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x3, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x3, + }, + ], + }; +} diff --git a/examples/triangle/src/main.rs b/examples/triangle/src/main.rs index 3a901df..b347e4e 100644 --- a/examples/triangle/src/main.rs +++ b/examples/triangle/src/main.rs @@ -1,4 +1,184 @@ -// examples/triangle/src/main.rs -fn main() { - println!("Voltex Triangle Demo"); +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{ActiveEventLoop, EventLoop}, + keyboard::{KeyCode, PhysicalKey}, + window::WindowId, +}; +use voltex_platform::{VoltexWindow, WindowConfig, InputState, GameTimer}; +use voltex_renderer::{GpuContext, pipeline, vertex::Vertex}; +use wgpu::util::DeviceExt; + +const TRIANGLE_VERTICES: &[Vertex] = &[ + Vertex { position: [0.0, 0.5, 0.0], color: [1.0, 0.0, 0.0] }, + Vertex { position: [-0.5, -0.5, 0.0], color: [0.0, 1.0, 0.0] }, + Vertex { position: [0.5, -0.5, 0.0], color: [0.0, 0.0, 1.0] }, +]; + +struct TriangleApp { + state: Option, +} + +struct AppState { + window: VoltexWindow, + gpu: GpuContext, + pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + input: InputState, + timer: GameTimer, +} + +impl ApplicationHandler for TriangleApp { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let config = WindowConfig { + title: "Voltex - Triangle".to_string(), + width: 1280, + height: 720, + ..Default::default() + }; + let window = VoltexWindow::new(event_loop, &config); + let gpu = GpuContext::new(window.handle.clone()); + let pipeline = pipeline::create_render_pipeline(&gpu.device, gpu.surface_format); + let vertex_buffer = gpu.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Triangle Vertex Buffer"), + contents: bytemuck::cast_slice(TRIANGLE_VERTICES), + usage: wgpu::BufferUsages::VERTEX, + }); + + self.state = Some(AppState { + window, + gpu, + pipeline, + vertex_buffer, + input: InputState::new(), + timer: GameTimer::new(60), + }); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + _window_id: WindowId, + event: WindowEvent, + ) { + let state = match &mut self.state { + Some(s) => s, + None => return, + }; + + match event { + WindowEvent::CloseRequested => event_loop.exit(), + + WindowEvent::KeyboardInput { + event: winit::event::KeyEvent { + physical_key: PhysicalKey::Code(key_code), + state: key_state, + .. + }, + .. + } => { + let pressed = key_state == winit::event::ElementState::Pressed; + state.input.process_key(key_code, pressed); + if key_code == KeyCode::Escape && pressed { + event_loop.exit(); + } + } + + WindowEvent::Resized(size) => { + state.gpu.resize(size.width, size.height); + } + + WindowEvent::CursorMoved { position, .. } => { + state.input.process_mouse_move(position.x, position.y); + } + + WindowEvent::MouseInput { state: btn_state, button, .. } => { + let pressed = btn_state == winit::event::ElementState::Pressed; + state.input.process_mouse_button(button, pressed); + } + + WindowEvent::MouseWheel { delta, .. } => { + let y = match delta { + winit::event::MouseScrollDelta::LineDelta(_, y) => y, + winit::event::MouseScrollDelta::PixelDelta(pos) => pos.y as f32, + }; + state.input.process_scroll(y); + } + + WindowEvent::RedrawRequested => { + state.timer.tick(); + state.input.begin_frame(); + + // Fixed update loop + while state.timer.should_fixed_update() { + let _fixed_dt = state.timer.fixed_dt(); + } + + let output = match state.gpu.surface.get_current_texture() { + Ok(t) => t, + Err(wgpu::SurfaceError::Lost) => { + let (w, h) = state.window.inner_size(); + state.gpu.resize(w, h); + return; + } + Err(wgpu::SurfaceError::OutOfMemory) => { + event_loop.exit(); + return; + } + Err(_) => return, + }; + + let view = output.texture.create_view(&wgpu::TextureViewDescriptor::default()); + let mut encoder = state.gpu.device.create_command_encoder( + &wgpu::CommandEncoderDescriptor { label: Some("Render Encoder") }, + ); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.1, + b: 0.15, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + multiview_mask: None, + }); + + render_pass.set_pipeline(&state.pipeline); + render_pass.set_vertex_buffer(0, state.vertex_buffer.slice(..)); + render_pass.draw(0..3, 0..1); + } + + state.gpu.queue.submit(std::iter::once(encoder.finish())); + output.present(); + } + + _ => {} + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(state) = &self.state { + state.window.request_redraw(); + } + } +} + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new().unwrap(); + let mut app = TriangleApp { state: None }; + event_loop.run_app(&mut app).unwrap(); }