From 9b5382762228b034a029972ed9cfd8dc95edd6cc Mon Sep 17 00:00:00 2001 From: Verox001 Date: Mon, 5 May 2025 15:33:22 +0200 Subject: [PATCH] Fixed timewarp - restructured project into solar_engine --- simulator/Cargo.toml | 3 - simulator/src/main.rs | 670 ++------------------ solar_engine/Cargo.toml | 9 +- solar_engine/src/application.rs | 76 +++ solar_engine/src/lib.rs | 9 +- solar_engine/src/render.rs | 109 ++++ {simulator => solar_engine}/src/shader.wgsl | 0 solar_engine/src/simulator.rs | 49 +- solar_engine/src/state.rs | 404 ++++++++++++ 9 files changed, 694 insertions(+), 635 deletions(-) create mode 100644 solar_engine/src/application.rs create mode 100644 solar_engine/src/render.rs rename {simulator => solar_engine}/src/shader.wgsl (100%) create mode 100644 solar_engine/src/state.rs diff --git a/simulator/Cargo.toml b/simulator/Cargo.toml index d4904cd..124fe6b 100644 --- a/simulator/Cargo.toml +++ b/simulator/Cargo.toml @@ -5,11 +5,8 @@ edition = "2021" [dependencies] solar_engine = { path = "../solar_engine" } -pixels = "0.15.0" winit = "0.30.10" log = "0.4" env_logger = "0.11.8" -bytemuck = "1.23.0" -wgpu = "25.0" pollster = "0.4.0" cgmath = "0.18.0" \ No newline at end of file diff --git a/simulator/src/main.rs b/simulator/src/main.rs index 0a5c00d..50d4ad4 100644 --- a/simulator/src/main.rs +++ b/simulator/src/main.rs @@ -1,665 +1,109 @@ -use std::cmp::max; -use std::collections::HashMap; +use cgmath::Rotation3; +use solar_engine::{Application, Body, Simulator}; use std::sync::{Arc, RwLock}; use std::thread; -use std::time::{Duration, Instant}; -use cgmath::num_traits::{pow, ToPrimitive}; -use cgmath::Rotation3; -use log::info; -use pollster::FutureExt; -use wgpu::util::DeviceExt; -use wgpu::{Adapter, Device, Instance, PresentMode, Queue, Surface, SurfaceCapabilities, SurfaceConfiguration}; -use winit::application::ApplicationHandler; -use winit::dpi::PhysicalSize; -use winit::event::{ElementState, WindowEvent}; +use winit::event::ElementState; use winit::event::WindowEvent::KeyboardInput; -use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::event_loop::EventLoop; use winit::keyboard::Key; use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; -use winit::window::{Window, WindowId}; -use solar_engine::{Body, Simulator}; pub async fn run() { let event_loop = EventLoop::new().unwrap(); - let mut window_state = StateApplication::new(); - let _ = event_loop.run_app(&mut window_state); -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct Globals { - aspect_ratio: f32, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum Shape { - Polygon, - Circle, -} - -struct Geometry { - vertex_buffer: wgpu::Buffer, - index_buffer: wgpu::Buffer, - index_count: u32, -} - -struct RenderInstance { - position: cgmath::Vector3, - rotation: cgmath::Quaternion, - color: [f32; 3], - scale: f32, - shape: Shape, -} - -impl RenderInstance { - fn to_raw(&self) -> InstanceRaw { - let model = cgmath::Matrix4::from_translation(self.position) - * cgmath::Matrix4::from(self.rotation) - * cgmath::Matrix4::from_scale(self.scale); - InstanceRaw { - model: model.into(), - color: self.color, - } - } -} - -#[repr(C)] -#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] -struct InstanceRaw { - model: [[f32; 4]; 4], - color: [f32; 3], -} - -impl InstanceRaw { - fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Instance, - attributes: &[ - wgpu::VertexAttribute { offset: 0, shader_location: 5, format: wgpu::VertexFormat::Float32x4 }, - wgpu::VertexAttribute { offset: 16, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, - wgpu::VertexAttribute { offset: 32, shader_location: 7, format: wgpu::VertexFormat::Float32x4 }, - wgpu::VertexAttribute { offset: 48, shader_location: 8, format: wgpu::VertexFormat::Float32x4 }, - wgpu::VertexAttribute { offset: 64, shader_location: 9, format: wgpu::VertexFormat::Float32x3 }, - ], - } - } -} - -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - position: [f32; 3], - color: [f32; 3], -} - -impl Vertex { - const ATTRIBS: [wgpu::VertexAttribute; 2] = - wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3]; - - fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: size_of::() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &Self::ATTRIBS, - } - } -} - -struct StateApplication<'a> { - state: Option>, -} - -impl<'a> StateApplication<'a> { - pub fn new() -> Self { - Self { - state: None, - } - } -} - -impl<'a> ApplicationHandler for StateApplication<'a>{ - fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let window = event_loop.create_window(Window::default_attributes().with_title("Hello!")).unwrap(); - self.state = Some(State::new(window)); - } - - fn window_event(&mut self, event_loop: &ActiveEventLoop, window_id: WindowId, event: WindowEvent) { - let window = self.state.as_ref().unwrap().window(); - - if window.id() == window_id { - match event { - WindowEvent::CloseRequested => { - event_loop.exit(); - } - WindowEvent::Resized(physical_size) => { - self.state.as_mut().unwrap().resize(physical_size); - } - WindowEvent::RedrawRequested => { - self.state.as_mut().unwrap().update(); - self.state.as_mut().unwrap().render().unwrap(); - } - WindowEvent::KeyboardInput { .. } => { - if let Some(state) = self.state.as_mut() { - if state.input(&event) { - return; - } - } - } - _ => {} - } - } - } - - fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { - let window = self.state.as_ref().unwrap().window(); - window.request_redraw(); - } -} - -fn create_circle_vertices(segment_count: usize, radius: f32, color: [f32; 3]) -> (Vec, Vec) { - let mut vertices = vec![Vertex { position: [0.0, 0.0, 0.0], color }]; - let mut indices = vec![]; - - for i in 0..=segment_count { - let theta = (i as f32) / (segment_count as f32) * std::f32::consts::TAU; - let x = radius * theta.cos(); - let y = radius * theta.sin(); - vertices.push(Vertex { position: [x, y, 0.0], color }); - } - - for i in 1..=segment_count { - indices.push(0); - indices.push(i as u16); - indices.push((i % segment_count + 1) as u16); - } - - (vertices, indices) -} - -struct SampleCount(u32); - -impl SampleCount { - pub fn get(&self) -> u32 { - self.0 - } -} - -struct State<'a> { - surface: Surface<'a>, - device: Device, - queue: Queue, - config: SurfaceConfiguration, - sample_count: SampleCount, - - size: PhysicalSize, - window: Arc, - - render_pipeline: wgpu::RenderPipeline, - - instances: Vec, - instance_buffer: wgpu::Buffer, - - geometries: HashMap, - global_bind_group: wgpu::BindGroup, - global_buffer: wgpu::Buffer, - - simulator: Arc>, -} - -impl<'a> State<'a> { - pub fn new (window: Window) -> Self { - let window_arc = Arc::new(window); - let size = window_arc.inner_size(); - let instance = Self::create_gpu_instance(); - let surface = instance.create_surface(window_arc.clone()).unwrap(); - let adapter = Self::create_adapter(instance, &surface); - let (device, queue) = Self::create_device(&adapter); - let surface_caps = surface.get_capabilities(&adapter); - let config = Self::create_surface_config(size, surface_caps); - surface.configure(&device, &config); - - let sample_count = Self::probe_msaa_support(&device, &config); - info!("MSAA sample count: {}", sample_count); - - let globals = Globals { - aspect_ratio: config.width as f32 / config.height as f32, - }; - let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Global Uniform Buffer"), - contents: bytemuck::cast_slice(&[globals]), - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); - let global_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("Global Bind Group Layout"), - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - }); - let global_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &global_bind_group_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: global_buffer.as_entire_binding(), - }], - label: Some("Global Bind Group"), - }); - - let render_pipeline = Self::create_render_pipeline(&device, &config, sample_count, &global_bind_group_layout); - - let mut geometries = HashMap::new(); - let polygon_vertices = vec![ - Vertex { position: [-0.0868241, 0.49240386, 0.0], color: [0.5, 0.0, 0.5] }, - Vertex { position: [-0.49513406, 0.06958647, 0.0], color: [0.5, 0.0, 0.5] }, - Vertex { position: [-0.21918549, -0.44939706, 0.0], color: [0.5, 0.0, 0.5] }, - Vertex { position: [0.35966998, -0.3473291, 0.0], color: [0.5, 0.0, 0.5] }, - Vertex { position: [0.44147372, 0.2347359, 0.0], color: [0.5, 0.0, 0.5] }, - ]; - let polygon_indices = vec![0, 1, 4, 1, 2, 4, 2, 3, 4]; - - let polygon_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Polygon Vertex Buffer"), - contents: bytemuck::cast_slice(&polygon_vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - - let polygon_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Polygon Index Buffer"), - contents: bytemuck::cast_slice(&polygon_indices), - usage: wgpu::BufferUsages::INDEX, - }); - - geometries.insert(Shape::Polygon, Geometry { - vertex_buffer: polygon_vertex_buffer, - index_buffer: polygon_index_buffer, - index_count: polygon_indices.len() as u32, - }); - - let (circle_vertices, circle_indices) = create_circle_vertices(512, 0.5, [0.5, 0.5, 0.5]); - let circle_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Circle Vertex Buffer"), - contents: bytemuck::cast_slice(&circle_vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - let circle_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Circle Index Buffer"), - contents: bytemuck::cast_slice(&circle_indices), - usage: wgpu::BufferUsages::INDEX, - }); - geometries.insert(Shape::Circle, Geometry { - vertex_buffer: circle_vertex_buffer, - index_buffer: circle_index_buffer, - index_count: circle_indices.len() as u32, - }); - - let mut sim = Simulator::new(); + let simulator = Arc::new(RwLock::new(Simulator::new())); + { + let mut sim = simulator.write().unwrap(); sim.add_body(Body { - name: "Sun".to_string(), + name: "Sun".into(), position: [0.0, 0.0], velocity: [0.0, 0.0], mass: 1.989e30, }); sim.add_body(Body { - name: "Earth".to_string(), + name: "Earth".into(), position: [1.496e11, 0.0], velocity: [0.0, 29780.0], mass: 5.972e24, }); + } - let instances = { - let sun_pos = sim.bodies[0].position; - sim.bodies.iter().enumerate().map(|(i, b)| RenderInstance { - position: cgmath::Vector3::new( - ((b.position[0] - sun_pos[0]) / 1.496e11) as f32, - ((b.position[1] - sun_pos[1]) / 1.496e11) as f32, - 0.0, - ), - rotation: cgmath::Quaternion::from_angle_z(cgmath::Deg(0.0)), - color: match i { - 0 => [1.0, 1.0, 0.0], - 1 => [0.0, 0.0, 1.0], - _ => [0.5, 0.5, 0.5], - }, - scale: 0.1, - shape: Shape::Circle - }).collect::>() - }; + let sim_clone = simulator.clone(); + thread::spawn(move || { + use std::time::{Duration, Instant}; + let mut last = Instant::now(); + loop { + let now = Instant::now(); + let dt = now.duration_since(last).as_secs_f64(); + last = now; - let instance_data: Vec = instances.iter().map(RenderInstance::to_raw).collect(); - let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("Instance Buffer"), - contents: bytemuck::cast_slice(&instance_data), - usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - }); - - let simulator = Arc::new(RwLock::new(sim)); - let simulator_clone = simulator.clone(); - thread::spawn(move || { - let mut last = Instant::now(); - loop { - let now = Instant::now(); - let dt = now.duration_since(last).as_secs_f64(); - last = now; - - { - let mut sim = simulator_clone.write().unwrap(); - let timewarp = sim.get_timewarp(); - sim.step(dt * timewarp as f64); - } - - thread::sleep(Duration::from_millis(1)); + { + let mut sim = sim_clone.write().unwrap(); + let timewarp = sim.get_timewarp(); + sim.step(dt * timewarp as f64); } - }); - Self { - surface, - device, - queue, - config, - size, - window: window_arc, - sample_count: SampleCount(sample_count), - - global_bind_group, - global_buffer, - - render_pipeline, - geometries, - - instances, - instance_buffer, - - simulator, + thread::sleep(Duration::from_millis(1)); } - } + }); - fn probe_msaa_support(device: &Device, config: &SurfaceConfiguration) -> u32 { - pollster::block_on(async { - for &count in &[16, 8, 4, 2] { - device.push_error_scope(wgpu::ErrorFilter::Validation); + let simulator_clone = simulator.clone(); - let _ = device.create_texture(&wgpu::TextureDescriptor { - label: Some("MSAA Probe"), - size: wgpu::Extent3d { - width: 4, - height: 4, - depth_or_array_layers: 1, + let mut window_state = Application::new().on_update(move |state| { + let sim = simulator_clone.read().unwrap(); + let bodies = &sim.bodies; + + let instances = bodies + .iter() + .enumerate() + .map(|(i, b)| { + solar_engine::RenderInstance { + position: cgmath::Vector3::new( + (b.position[0] / 1.496e11) as f32, + (b.position[1] / 1.496e11) as f32, + 0.0, + ), + rotation: cgmath::Quaternion::from_angle_z(cgmath::Deg(0.0)), + color: match i { + 0 => [1.0, 1.0, 0.0], // Sun + 1 => [0.0, 0.0, 1.0], // Earth + _ => [0.5, 0.5, 0.5], }, - mip_level_count: 1, - sample_count: count, - dimension: wgpu::TextureDimension::D2, - format: config.format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - - if device.pop_error_scope().await.is_none() { - return count; + scale: 0.05, + shape: solar_engine::Shape::Circle, } - } + }) + .collect(); - 1 // fallback - }) - } - - pub fn input(&mut self, event: &WindowEvent) -> bool { + state.set_instances(instances); + }).on_input(move |state, event| { if let KeyboardInput { event, .. } = event { if event.state == ElementState::Pressed { return match event.key_without_modifiers().as_ref() { Key::Character(".") => { - let mut sim = self.simulator.write().unwrap(); + let mut sim = simulator.write().unwrap(); sim.increase_timewarp(); println!("Timewarp: {}", sim.get_timewarp()); - true } Key::Character(",") => { - let mut sim = self.simulator.write().unwrap(); + let mut sim = simulator.write().unwrap(); sim.decrease_timewarp(); println!("Timewarp: {}", sim.get_timewarp()); - true } Key::Character("-") => { - let mut sim = self.simulator.write().unwrap(); + let mut sim = simulator.write().unwrap(); sim.reset_timewarp(); println!("Timewarp: {}", sim.get_timewarp()); - true - } - _ => { - false } + _ => {} } } } - - false - } - - fn update(&mut self) { - let sim = self.simulator.read().unwrap(); - - let updated_instances: Vec = { - sim.bodies.iter().enumerate().map(|(i, b)| RenderInstance { - position: cgmath::Vector3::new((b.position[0] / 1.496e11) as f32, (b.position[1] / 1.496e11) as f32, 0.0), - rotation: cgmath::Quaternion::from_angle_z(cgmath::Deg(0.0)), - color: match i { - 0 => [1.0, 1.0, 0.0], - 1 => [0.0, 0.0, 1.0], - _ => [0.5, 0.5, 0.5], - }, - scale: 0.5, - shape: Shape::Circle - }).collect() - }; - - let instance_data: Vec = updated_instances.iter().map(RenderInstance::to_raw).collect(); - self.queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instance_data)); - self.instances = updated_instances; - } + }); - fn create_render_pipeline(device: &Device, config: &SurfaceConfiguration, sample_count: u32, global_bind_group_layout: &wgpu::BindGroupLayout) -> wgpu::RenderPipeline { - let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { - label: Some("Shader"), - source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), - }); - - let render_pipeline_layout = - device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: Some("Render Pipeline Layout"), - bind_group_layouts: &[&global_bind_group_layout], - push_constant_ranges: &[], - }); - - device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: Some("Render Pipeline"), - layout: Some(&render_pipeline_layout), - vertex: wgpu::VertexState { - module: &shader, - entry_point: Some("vs_main"), - buffers: &[Vertex::desc(), InstanceRaw::desc()], - compilation_options: Default::default(), - }, - fragment: Some(wgpu::FragmentState { - module: &shader, - entry_point: Some("fs_main"), - targets: &[Some(wgpu::ColorTargetState { - format: config.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), - // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE - polygon_mode: wgpu::PolygonMode::Fill, - // Requires Features::DEPTH_CLIP_CONTROL - unclipped_depth: false, - // Requires Features::CONSERVATIVE_RASTERIZATION - conservative: false, - }, - depth_stencil: None, - multisample: wgpu::MultisampleState { - count: sample_count, - mask: !0, - alpha_to_coverage_enabled: false, - }, - multiview: None, - cache: None, - }) - } - - fn create_surface_config(size: PhysicalSize, capabilities: SurfaceCapabilities) -> wgpu::SurfaceConfiguration { - let surface_format = capabilities.formats.iter() - .find(|f| f.is_srgb()) - .copied() - .unwrap_or(capabilities.formats[0]); - - SurfaceConfiguration { - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - format: surface_format, - width: size.width, - height: size.height, - present_mode: PresentMode::AutoVsync, - alpha_mode: capabilities.alpha_modes[0], - view_formats: vec![], - desired_maximum_frame_latency: 2, - } - } - - fn create_device(adapter: &Adapter) -> (Device, Queue) { - adapter.request_device( - &wgpu::DeviceDescriptor { - required_features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, - required_limits: wgpu::Limits::default(), - memory_hints: Default::default(), - label: None, - trace: Default::default(), - }).block_on().unwrap() - } - - fn create_adapter(instance: Instance, surface: &Surface) -> Adapter { - instance.request_adapter( - &wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - compatible_surface: Some(&surface), - force_fallback_adapter: false, - } - ).block_on().unwrap() - } - - fn create_gpu_instance() -> Instance { - Instance::new(&wgpu::InstanceDescriptor { - backends: wgpu::Backends::PRIMARY, - ..Default::default() - }) - } - - pub fn resize(&mut self, new_size: PhysicalSize) { - self.size = new_size; - - self.config.width = max(new_size.width, 1); - self.config.height = max(new_size.height, 1); - - self.surface.configure(&self.device, &self.config); - - let new_globals = Globals { - aspect_ratio: self.config.width as f32 / self.config.height as f32, - }; - self.queue.write_buffer(&self.global_buffer, 0, bytemuck::cast_slice(&[new_globals])); - - println!("Resized to {:?} from state!", new_size); - } - - pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> { - let output = self.surface.get_current_texture()?; - - let multisampled_texture = self.device.create_texture(&wgpu::TextureDescriptor { - label: Some("Multisampled Render Target"), - size: wgpu::Extent3d { - width: self.config.width, - height: self.config.height, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: self.sample_count.get(), - dimension: wgpu::TextureDimension::D2, - format: self.config.format, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - view_formats: &[], - }); - let multisampled_view = multisampled_texture.create_view(&Default::default()); - - let mut encoder = self.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: &multisampled_view, - resolve_target: Some(&output.texture.create_view(&Default::default())), - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color { - r: 1.0, - g: 0.2, - b: 0.3, - a: 1.0, - }), - store: wgpu::StoreOp::Store, - } - })], - depth_stencil_attachment: None, - occlusion_query_set: None, - timestamp_writes: None, - }); - - render_pass.set_pipeline(&self.render_pipeline); - render_pass.set_bind_group(0, &self.global_bind_group, &[]); - - for shape in [Shape::Polygon, Shape::Circle] { - let geometry = &self.geometries[&shape]; - - let relevant_instances: Vec<_> = self.instances - .iter() - .enumerate() - .filter(|(_, inst)| inst.shape == shape) - .map(|(i, _)| i as u32) - .collect(); - - if relevant_instances.is_empty() { - continue; - } - - render_pass.set_vertex_buffer(0, geometry.vertex_buffer.slice(..)); - render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); - render_pass.set_index_buffer(geometry.index_buffer.slice(..), wgpu::IndexFormat::Uint16); - render_pass.draw_indexed(0..geometry.index_count, 0, 0..relevant_instances.len() as u32); - } - } - - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); - - Ok(()) - } - - pub fn window(&self) -> &Window { - &self.window - } + let _ = event_loop.run_app(&mut window_state); } fn main() { pollster::block_on(run()); -} \ No newline at end of file +} diff --git a/solar_engine/Cargo.toml b/solar_engine/Cargo.toml index cca4314..2d69e65 100644 --- a/solar_engine/Cargo.toml +++ b/solar_engine/Cargo.toml @@ -4,4 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] -rayon = "1.8" \ No newline at end of file +rayon = "1.8" +winit = "0.30.10" +log = "0.4" +env_logger = "0.11.8" +bytemuck = "1.23.0" +wgpu = "25.0" +pollster = "0.4.0" +cgmath = "0.18.0" \ No newline at end of file diff --git a/solar_engine/src/application.rs b/solar_engine/src/application.rs new file mode 100644 index 0000000..4b0e408 --- /dev/null +++ b/solar_engine/src/application.rs @@ -0,0 +1,76 @@ +use winit::application::ApplicationHandler; +use winit::event::WindowEvent; +use winit::event_loop::ActiveEventLoop; +use winit::window::{Window, WindowId}; + +pub struct StateApplication<'a> { + state: Option>, + update_fn: Option) + 'a>>, + input_fn: Option, &WindowEvent) + 'a>>, +} + +impl<'a> StateApplication<'a> { + pub fn new() -> Self { + Self { state: None, update_fn: None, input_fn: None } + } + + pub fn on_update) + 'a>(mut self, func: F) -> Self { + self.update_fn = Some(Box::new(func)); + self + } + + pub fn on_input, &WindowEvent) + 'a>(mut self, func: F) -> Self { + self.input_fn = Some(Box::new(func)); + self + } +} + +impl<'a> ApplicationHandler for StateApplication<'a> { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window = event_loop + .create_window(Window::default_attributes().with_title("Solar Engine")) + .unwrap(); + self.state = Some(crate::state::State::new(window)); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let window = self.state.as_ref().unwrap().window(); + + if window.id() == window_id { + match event { + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::Resized(physical_size) => { + self.state.as_mut().unwrap().resize(physical_size); + } + WindowEvent::RedrawRequested => { + if let (Some(state), Some(update_fn)) = (self.state.as_mut(), self.update_fn.as_mut()) { + update_fn(state); + } + + self.state.as_mut().unwrap().render().unwrap(); + } + WindowEvent::KeyboardInput { .. } => { + if let Some(state) = self.state.as_mut() { + if let Some(input_fn) = self.input_fn.as_mut() { + input_fn(state, &event); + } + } + } + _ => {} + } + } + } + + fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) { + if let Some(state) = self.state.as_ref() { + state.window().request_redraw(); + } + } +} diff --git a/solar_engine/src/lib.rs b/solar_engine/src/lib.rs index 96c2a8b..d5c35d9 100644 --- a/solar_engine/src/lib.rs +++ b/solar_engine/src/lib.rs @@ -1,6 +1,13 @@ mod body; mod simulator; +mod state; +mod render; +mod application; pub use body::Body; pub use simulator::Simulator; -pub use simulator::distance_squared; \ No newline at end of file +pub use simulator::distance_squared; +pub use application::StateApplication as Application; +pub use render::RenderInstance; +pub use render::Shape; +pub use state::State; \ No newline at end of file diff --git a/solar_engine/src/render.rs b/solar_engine/src/render.rs new file mode 100644 index 0000000..3c7eef4 --- /dev/null +++ b/solar_engine/src/render.rs @@ -0,0 +1,109 @@ +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Globals { + pub(crate) aspect_ratio: f32, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Shape { + Polygon, + Circle, +} + +pub struct Geometry { + pub(crate) vertex_buffer: wgpu::Buffer, + pub(crate) index_buffer: wgpu::Buffer, + pub(crate) index_count: u32, +} + +pub struct RenderInstance { + pub position: cgmath::Vector3, + pub rotation: cgmath::Quaternion, + pub color: [f32; 3], + pub scale: f32, + pub shape: Shape, +} + +impl RenderInstance { + pub fn to_raw(&self) -> InstanceRaw { + let model = cgmath::Matrix4::from_translation(self.position) + * cgmath::Matrix4::from(self.rotation) + * cgmath::Matrix4::from_scale(self.scale); + InstanceRaw { + model: model.into(), + color: self.color, + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct InstanceRaw { + model: [[f32; 4]; 4], + color: [f32; 3], +} + +impl InstanceRaw { + pub(crate) fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { offset: 0, shader_location: 5, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 16, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 32, shader_location: 7, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 48, shader_location: 8, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 64, shader_location: 9, format: wgpu::VertexFormat::Float32x3 }, + ], + } + } +} + + +pub struct SampleCount(pub u32); + +impl SampleCount { + pub fn get(&self) -> u32 { + self.0 + } +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vertex { + pub(crate) position: [f32; 3], + pub(crate) color: [f32; 3], +} + +impl Vertex { + const ATTRIBS: [wgpu::VertexAttribute; 2] = + wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x3]; + + pub(crate) fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBS, + } + } +} + +pub fn create_circle_vertices(segment_count: usize, radius: f32, color: [f32; 3]) -> (Vec, Vec) { + let mut vertices = vec![Vertex { position: [0.0, 0.0, 0.0], color }]; + let mut indices = vec![]; + + for i in 0..=segment_count { + let theta = (i as f32) / (segment_count as f32) * std::f32::consts::TAU; + let x = radius * theta.cos(); + let y = radius * theta.sin(); + vertices.push(Vertex { position: [x, y, 0.0], color }); + } + + for i in 1..=segment_count { + indices.push(0); + indices.push(i as u16); + indices.push((i % segment_count + 1) as u16); + } + + (vertices, indices) +} \ No newline at end of file diff --git a/simulator/src/shader.wgsl b/solar_engine/src/shader.wgsl similarity index 100% rename from simulator/src/shader.wgsl rename to solar_engine/src/shader.wgsl diff --git a/solar_engine/src/simulator.rs b/solar_engine/src/simulator.rs index 32b1b9b..fc6b404 100644 --- a/solar_engine/src/simulator.rs +++ b/solar_engine/src/simulator.rs @@ -8,7 +8,7 @@ const G: f64 = 6.67430e-11; pub struct Simulator { pub bodies: Vec, pub time: f64, - timewarp: AtomicUsize + timewarp: u32 } pub fn distance_squared(a: [f64; 2], b: [f64; 2]) -> f64 { @@ -17,12 +17,14 @@ pub fn distance_squared(a: [f64; 2], b: [f64; 2]) -> f64 { dx * dx + dy * dy } +const MAX_TIMEWARP: u32 = 536870912; + impl Simulator { pub fn new() -> Self { Self { bodies: Vec::new(), time: 0.0, - timewarp: AtomicUsize::new(1), + timewarp: 1, } } @@ -145,24 +147,37 @@ impl Simulator { self.time += dt; } - - pub fn increase_timewarp(&self) { - let current_timewarp = self.timewarp.load(std::sync::atomic::Ordering::SeqCst); - self.timewarp.store(current_timewarp * 2, std::sync::atomic::Ordering::SeqCst); - } - - pub fn decrease_timewarp(&self) { - let current_timewarp = self.timewarp.load(std::sync::atomic::Ordering::SeqCst); - if current_timewarp > 1 { - self.timewarp.store(current_timewarp / 2, std::sync::atomic::Ordering::SeqCst); + + pub fn increase_timewarp(&mut self) { + if let Some(new) = self.timewarp.checked_mul(2) { + if new <= MAX_TIMEWARP { + self.timewarp = new; + } else { + println!("Timewarp is already at maximum ({}).", MAX_TIMEWARP); + } + } else { + println!("Timewarp multiplication would overflow."); } } - - pub fn reset_timewarp(&self) { - self.timewarp.store(1, std::sync::atomic::Ordering::SeqCst); + + pub fn decrease_timewarp(&mut self) { + if let Some(new) = self.timewarp.checked_div(2) { + if new >= 1 { + self.timewarp = new; + } else { + println!("Timewarp is already at minimum."); + } + } else { + println!("Timewarp is already at minimum."); + } + } + + + pub fn reset_timewarp(&mut self) { + self.timewarp = 1; } - pub fn get_timewarp(&self) -> usize { - self.timewarp.load(std::sync::atomic::Ordering::SeqCst) + pub fn get_timewarp(&self) -> u32 { + self.timewarp } } diff --git a/solar_engine/src/state.rs b/solar_engine/src/state.rs new file mode 100644 index 0000000..4061257 --- /dev/null +++ b/solar_engine/src/state.rs @@ -0,0 +1,404 @@ +use std::cmp::max; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::thread; +use std::time::{Duration, Instant}; +use cgmath::num_traits::{pow, ToPrimitive}; +use cgmath::Rotation3; +use log::info; +use pollster::FutureExt; +use wgpu::util::DeviceExt; +use wgpu::{Adapter, Device, Instance, PresentMode, Queue, Surface, SurfaceCapabilities, SurfaceConfiguration}; +use winit::application::ApplicationHandler; +use winit::dpi::PhysicalSize; +use winit::event::{ElementState, WindowEvent}; +use winit::event::WindowEvent::KeyboardInput; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::keyboard::Key; +use winit::platform::modifier_supplement::KeyEventExtModifierSupplement; +use winit::window::{Window, WindowId}; +use crate::render::{create_circle_vertices, Geometry, Globals, InstanceRaw, RenderInstance, SampleCount, Shape, Vertex}; + +pub struct State<'a> { + surface: Surface<'a>, + device: Device, + queue: Queue, + config: SurfaceConfiguration, + sample_count: SampleCount, + + size: PhysicalSize, + window: Arc, + + render_pipeline: wgpu::RenderPipeline, + + instances: Vec, + instance_buffer: wgpu::Buffer, + + geometries: HashMap, + global_bind_group: wgpu::BindGroup, + global_buffer: wgpu::Buffer, +} + +impl<'a> State<'a> { + pub(crate) fn new(window: Window) -> Self { + let window_arc = Arc::new(window); + let size = window_arc.inner_size(); + let instance = Self::create_gpu_instance(); + let surface = instance.create_surface(window_arc.clone()).unwrap(); + let adapter = Self::create_adapter(instance, &surface); + let (device, queue) = Self::create_device(&adapter); + let surface_caps = surface.get_capabilities(&adapter); + let config = Self::create_surface_config(size, surface_caps); + surface.configure(&device, &config); + + let sample_count = SampleCount(Self::probe_msaa_support(&device, &config)); + info!("MSAA sample count: {}", sample_count.0); + + let globals = Globals { + aspect_ratio: config.width as f32 / config.height as f32, + }; + let global_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Global Uniform Buffer"), + contents: bytemuck::cast_slice(&[globals]), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let global_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Global Bind Group Layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let global_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &global_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: global_buffer.as_entire_binding(), + }], + label: Some("Global Bind Group"), + }); + + let render_pipeline = Self::create_render_pipeline(&device, &config, sample_count.0, &global_bind_group_layout); + let geometries = Self::create_geometries(&device); + + let instances = vec![]; + let instance_data: Vec = instances.iter().map(RenderInstance::to_raw).collect(); + let instance_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Instance Buffer"), + contents: bytemuck::cast_slice(&instance_data), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + + Self { + surface, + device, + queue, + config, + sample_count, + size, + window: window_arc, + render_pipeline, + geometries, + global_bind_group, + global_buffer, + instances, + instance_buffer, + } + } + + fn create_geometries(device: &Device) -> HashMap { + let mut geometries = HashMap::new(); + + let polygon_vertices = vec![ + Vertex { position: [-0.0868241, 0.49240386, 0.0], color: [0.5, 0.0, 0.5] }, + Vertex { position: [-0.49513406, 0.06958647, 0.0], color: [0.5, 0.0, 0.5] }, + Vertex { position: [-0.21918549, -0.44939706, 0.0], color: [0.5, 0.0, 0.5] }, + Vertex { position: [0.35966998, -0.3473291, 0.0], color: [0.5, 0.0, 0.5] }, + Vertex { position: [0.44147372, 0.2347359, 0.0], color: [0.5, 0.0, 0.5] }, + ]; + let polygon_indices = vec![0, 1, 4, 1, 2, 4, 2, 3, 4]; + + let polygon_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Polygon Vertex Buffer"), + contents: bytemuck::cast_slice(&polygon_vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let polygon_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Polygon Index Buffer"), + contents: bytemuck::cast_slice(&polygon_indices), + usage: wgpu::BufferUsages::INDEX, + }); + geometries.insert(Shape::Polygon, Geometry { + vertex_buffer: polygon_vertex_buffer, + index_buffer: polygon_index_buffer, + index_count: polygon_indices.len() as u32, + }); + + let (circle_vertices, circle_indices) = create_circle_vertices(512, 0.5, [0.5, 0.5, 0.5]); + let circle_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Circle Vertex Buffer"), + contents: bytemuck::cast_slice(&circle_vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let circle_index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Circle Index Buffer"), + contents: bytemuck::cast_slice(&circle_indices), + usage: wgpu::BufferUsages::INDEX, + }); + geometries.insert(Shape::Circle, Geometry { + vertex_buffer: circle_vertex_buffer, + index_buffer: circle_index_buffer, + index_count: circle_indices.len() as u32, + }); + + geometries + } + + fn probe_msaa_support(device: &Device, config: &SurfaceConfiguration) -> u32 { + pollster::block_on(async { + for &count in &[16, 8, 4, 2] { + device.push_error_scope(wgpu::ErrorFilter::Validation); + + let _ = device.create_texture(&wgpu::TextureDescriptor { + label: Some("MSAA Probe"), + size: wgpu::Extent3d { + width: 4, + height: 4, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: count, + dimension: wgpu::TextureDimension::D2, + format: config.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + + if device.pop_error_scope().await.is_none() { + return count; + } + } + + 1 // fallback + }) + } + + fn create_render_pipeline(device: &Device, config: &SurfaceConfiguration, sample_count: u32, global_bind_group_layout: &wgpu::BindGroupLayout) -> wgpu::RenderPipeline { + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let render_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[&global_bind_group_layout], + push_constant_ranges: &[], + }); + + device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[Vertex::desc(), InstanceRaw::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: config.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), + // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE + polygon_mode: wgpu::PolygonMode::Fill, + // Requires Features::DEPTH_CLIP_CONTROL + unclipped_depth: false, + // Requires Features::CONSERVATIVE_RASTERIZATION + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: sample_count, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }) + } + + fn create_surface_config(size: PhysicalSize, capabilities: SurfaceCapabilities) -> wgpu::SurfaceConfiguration { + let surface_format = capabilities.formats.iter() + .find(|f| f.is_srgb()) + .copied() + .unwrap_or(capabilities.formats[0]); + + SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: surface_format, + width: size.width, + height: size.height, + present_mode: PresentMode::AutoVsync, + alpha_mode: capabilities.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + } + } + + fn create_device(adapter: &Adapter) -> (Device, Queue) { + adapter.request_device( + &wgpu::DeviceDescriptor { + required_features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, + required_limits: wgpu::Limits::default(), + memory_hints: Default::default(), + label: None, + trace: Default::default(), + }).block_on().unwrap() + } + + fn create_adapter(instance: Instance, surface: &Surface) -> Adapter { + instance.request_adapter( + &wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + } + ).block_on().unwrap() + } + + fn create_gpu_instance() -> Instance { + Instance::new(&wgpu::InstanceDescriptor { + backends: wgpu::Backends::PRIMARY, + ..Default::default() + }) + } + + pub(crate) fn resize(&mut self, new_size: PhysicalSize) { + self.size = new_size; + + self.config.width = max(new_size.width, 1); + self.config.height = max(new_size.height, 1); + + self.surface.configure(&self.device, &self.config); + + let new_globals = Globals { + aspect_ratio: self.config.width as f32 / self.config.height as f32, + }; + self.queue.write_buffer(&self.global_buffer, 0, bytemuck::cast_slice(&[new_globals])); + + println!("Resized to {:?} from state!", new_size); + } + + pub(crate) fn render(&mut self) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + + let multisampled_texture = self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("Multisampled Render Target"), + size: wgpu::Extent3d { + width: self.config.width, + height: self.config.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: self.sample_count.get(), + dimension: wgpu::TextureDimension::D2, + format: self.config.format, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + view_formats: &[], + }); + let multisampled_view = multisampled_texture.create_view(&Default::default()); + + let mut encoder = self.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: &multisampled_view, + resolve_target: Some(&output.texture.create_view(&Default::default())), + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 1.0, + g: 0.2, + b: 0.3, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + } + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + render_pass.set_pipeline(&self.render_pipeline); + render_pass.set_bind_group(0, &self.global_bind_group, &[]); + + for shape in [Shape::Polygon, Shape::Circle] { + let geometry = &self.geometries[&shape]; + + let relevant_instances: Vec<_> = self.instances + .iter() + .enumerate() + .filter(|(_, inst)| inst.shape == shape) + .map(|(i, _)| i as u32) + .collect(); + + if relevant_instances.is_empty() { + continue; + } + + render_pass.set_vertex_buffer(0, geometry.vertex_buffer.slice(..)); + render_pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); + render_pass.set_index_buffer(geometry.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed(0..geometry.index_count, 0, 0..relevant_instances.len() as u32); + } + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } + + pub fn set_instances(&mut self, instances: Vec) { + let raw_data: Vec = instances.iter().map(RenderInstance::to_raw).collect(); + let byte_len = (raw_data.len() * size_of::()) as wgpu::BufferAddress; + + // Resize the buffer if necessary + if byte_len > self.instance_buffer.size() { + self.instance_buffer = self.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Instance Buffer (resized)"), + contents: bytemuck::cast_slice(&raw_data), + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + }); + } else { + self.queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&raw_data)); + } + + self.instances = instances; + } + + pub fn window(&self) -> &Window { + &self.window + } +} \ No newline at end of file