├── .gitignore ├── Cargo.toml └── src ├── winit_backend.rs ├── yew_backend.rs └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "wgpu-runner" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | console_log = "1.0.0" 10 | env_logger = "0.11.8" 11 | glam = { version = "0.30.8", features = ["bytemuck"] } 12 | gloo = "0.11.0" 13 | instant = { version = "0.1.13", features = [] } 14 | log = "0.4.28" 15 | pollster = "0.4.0" 16 | wasm-bindgen = "0.2.104" 17 | wasm-bindgen-futures = "0.4.54" 18 | web-sys = { version = "0.3.81", features = ["HtmlCanvasElement", "MouseEvent"] } 19 | wgpu = { version = "27.0.1", features = ["webgl"] } 20 | # wgpu = { git = "https://github.com/gfx-rs/wgpu.git" } #, features = ["webgl"] } 21 | winit = { version = "0.30.12"} 22 | yew = { version = "0.21.0", features = ["csr"] } 23 | 24 | rand = { version = "0.8.5" } 25 | getrandom = { version = "0.2", features = ["js"] } 26 | -------------------------------------------------------------------------------- /src/winit_backend.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use crate::Props; 4 | use crate::Renderer; 5 | use crate::RendererState; 6 | 7 | use glam::Vec2; 8 | use winit::application::ApplicationHandler; 9 | use winit::dpi::LogicalSize; 10 | use winit::event_loop::ActiveEventLoop; 11 | use winit::keyboard::Key; 12 | use winit::keyboard::NamedKey; 13 | use winit::window::Window; 14 | use winit::{event::*, event_loop::EventLoop}; 15 | 16 | struct App { 17 | props: Props, 18 | state: Option, 19 | renderer: Option, 20 | } 21 | 22 | impl App { 23 | fn new(props: Props) -> Self { 24 | Self { 25 | props, 26 | state: None, 27 | renderer: None, 28 | } 29 | } 30 | } 31 | 32 | impl ApplicationHandler for App { 33 | fn resumed(&mut self, event_loop: &ActiveEventLoop) { 34 | dbg!(); 35 | let window = Arc::new( 36 | event_loop 37 | .create_window( 38 | Window::default_attributes().with_inner_size(LogicalSize::new(1920, 1080)), 39 | ) 40 | .unwrap(), 41 | ); 42 | dbg!(); 43 | 44 | if self.props.capture_cursor { 45 | let _ = window.set_cursor_grab(winit::window::CursorGrabMode::Confined); 46 | window.set_cursor_visible(false); 47 | } 48 | 49 | let (state, renderer) = pollster::block_on(async { 50 | let state = RendererState::init_winit(window.clone()).await; 51 | let renderer = R::init(&state).await; 52 | (state, renderer) 53 | }); 54 | 55 | self.state = Some(state); 56 | self.renderer = Some(renderer); 57 | 58 | dbg!(); 59 | window.request_redraw(); 60 | } 61 | 62 | fn device_event( 63 | &mut self, 64 | _event_loop: &ActiveEventLoop, 65 | _device_id: DeviceId, 66 | event: DeviceEvent, 67 | ) { 68 | let state = self.state.as_mut().unwrap(); 69 | let renderer = self.renderer.as_mut().unwrap(); 70 | 71 | renderer.on_device_event(state, &event); 72 | } 73 | 74 | fn window_event( 75 | &mut self, 76 | event_loop: &ActiveEventLoop, 77 | _window_id: winit::window::WindowId, 78 | event: WindowEvent, 79 | ) { 80 | let state = self.state.as_mut().unwrap(); 81 | let renderer = self.renderer.as_mut().unwrap(); 82 | 83 | if let WindowEvent::KeyboardInput { event, .. } = &event { 84 | if event.state.is_pressed() { 85 | state.pressed_keys.insert(event.physical_key); 86 | } else { 87 | state.pressed_keys.remove(&event.physical_key); 88 | } 89 | } 90 | 91 | match event { 92 | WindowEvent::CloseRequested 93 | | WindowEvent::KeyboardInput { 94 | event: 95 | KeyEvent { 96 | state: ElementState::Pressed, 97 | logical_key: Key::Named(NamedKey::Escape), 98 | .. 99 | }, 100 | .. 101 | } => event_loop.exit(), 102 | WindowEvent::Focused(false) => { 103 | state.pressed_keys.clear(); 104 | } 105 | WindowEvent::Resized(new_size) => { 106 | if new_size.width > 0 && new_size.height > 0 { 107 | state.width = new_size.width; 108 | state.height = new_size.height; 109 | state.config.width = new_size.width; 110 | state.config.height = new_size.height; 111 | state.surface.configure(&state.device, &state.config); 112 | renderer.on_resize(&state); 113 | } 114 | } 115 | WindowEvent::CursorMoved { position, .. } => { 116 | state.cursor.position = Vec2::new( 117 | position.x as f32 / state.width as f32 * 2.0 - 1.0, 118 | -position.y as f32 / state.height as f32 * 2.0 + 1.0, 119 | ); 120 | } 121 | WindowEvent::MouseInput { 122 | state: button_state, 123 | button: MouseButton::Left | MouseButton::Middle | MouseButton::Right, 124 | .. 125 | } => { 126 | if button_state == ElementState::Pressed { 127 | if state.cursor.dragging_from == None { 128 | state.cursor.dragging_from = Some(state.cursor.position); 129 | } 130 | } else { 131 | state.cursor.dragging_from = None; 132 | } 133 | } 134 | WindowEvent::RedrawRequested => { 135 | renderer.render(&state); 136 | state.window.request_redraw(); 137 | } 138 | _ => {} 139 | }; 140 | renderer.on_window_event(&state, &event); 141 | } 142 | } 143 | 144 | pub fn start(props: Props) 145 | where 146 | R: Renderer, 147 | { 148 | env_logger::init(); 149 | 150 | let event_loop = EventLoop::new().unwrap(); 151 | 152 | event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll); 153 | 154 | event_loop.run_app(&mut App::::new(props)).unwrap(); 155 | /* 156 | event_loop 157 | .run(move |event, elwt| match event { 158 | Event::DeviceEvent { event, .. } => { 159 | app.on_device_event(&state, &event); 160 | } 161 | _ => {} 162 | }) 163 | .unwrap(); 164 | */ 165 | } 166 | -------------------------------------------------------------------------------- /src/yew_backend.rs: -------------------------------------------------------------------------------- 1 | use crate::Props; 2 | use std::sync::RwLock; 3 | use std::{cell::RefCell, rc::Rc}; 4 | use wasm_bindgen::prelude::*; 5 | use web_sys::{Element, HtmlCanvasElement, HtmlElement, MouseEvent}; 6 | use winit::event::DeviceEvent; 7 | use winit::keyboard::{KeyCode, PhysicalKey}; 8 | use yew::prelude::*; 9 | use yew::suspense::use_future_with_deps; 10 | 11 | use crate::RendererState; 12 | 13 | const WIDTH: u32 = 1024; 14 | const HEIGHT: u32 = 1024; 15 | const RENDER_LOOP: bool = true; 16 | 17 | #[function_component] 18 | fn App(props: &Props) -> Html { 19 | let canvas_ref = use_node_ref(); 20 | let start_render_loop = use_state(|| false); 21 | let renderer_state = use_state(|| None); 22 | let renderer = use_state(|| None); 23 | { 24 | let renderer = renderer.clone(); 25 | let renderer_state = renderer_state.clone(); 26 | let start_render_loop = start_render_loop.clone(); 27 | let canvas_ref = canvas_ref.clone(); 28 | let canvas_ref2 = canvas_ref.clone(); 29 | use_future_with_deps( 30 | |_| async move { 31 | if let Some(canvas) = canvas_ref.cast::() { 32 | let state = RendererState::init_web(canvas).await; 33 | let mut real_renderer = R::init(&state).await; 34 | real_renderer.render(&state); 35 | 36 | renderer_state.set(Some(RwLock::new(state))); 37 | renderer.set(Some(RwLock::new(real_renderer))); 38 | start_render_loop.set(RENDER_LOOP); 39 | } 40 | }, 41 | canvas_ref2.clone(), 42 | ); 43 | } 44 | 45 | { 46 | let renderer = renderer.clone(); 47 | let renderer_state = renderer_state.clone(); 48 | use_effect_with_deps( 49 | move |start_render_loop| { 50 | if !**start_render_loop { 51 | return; 52 | } 53 | let window = web_sys::window().unwrap(); 54 | let window2 = window.clone(); 55 | let f = Rc::new(RefCell::>>::new(None)); 56 | let g = f.clone(); 57 | *g.borrow_mut() = Some(Closure::new(move || { 58 | renderer 59 | .as_ref() 60 | .unwrap() 61 | .write() 62 | .unwrap() 63 | .render(&renderer_state.as_ref().unwrap().read().unwrap()); 64 | window2 65 | .request_animation_frame( 66 | f.borrow().as_ref().unwrap().as_ref().unchecked_ref(), 67 | ) 68 | .unwrap(); 69 | })); 70 | 71 | window 72 | .request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref()) 73 | .unwrap(); 74 | }, 75 | start_render_loop.clone(), 76 | ); 77 | } 78 | 79 | let mousemove = { 80 | let renderer = renderer.clone(); 81 | let renderer_state = renderer_state.clone(); 82 | Callback::from(move |e: MouseEvent| { 83 | let element = e.target().unwrap().dyn_into::().unwrap(); 84 | if gloo::utils::document().pointer_lock_element() != Some(element) { 85 | return; 86 | } 87 | let Some(state) = &renderer_state.as_ref().map(|s| s.read().unwrap()) else { 88 | return; 89 | }; 90 | renderer.as_ref().unwrap().write().unwrap().on_device_event( 91 | &state, 92 | &DeviceEvent::MouseMotion { 93 | delta: (2.0 * e.movement_x() as f64, 2.0 * e.movement_y() as f64), 94 | }, 95 | ); 96 | }) 97 | }; 98 | 99 | let keydown = { 100 | let renderer_state = renderer_state.clone(); 101 | Callback::from(move |e: KeyboardEvent| { 102 | let Some(state) = &mut renderer_state.as_ref().map(|s| s.write().unwrap()) else { 103 | return; 104 | }; 105 | gloo::console::warn!("HAA down"); 106 | if let Some(keycode) = key_to_keycode(&e.key()) { 107 | state.pressed_keys.insert(PhysicalKey::Code(keycode)); 108 | } 109 | }) 110 | }; 111 | 112 | let keyup = { 113 | let renderer_state = renderer_state.clone(); 114 | Callback::from(move |e: KeyboardEvent| { 115 | let Some(state) = &mut renderer_state.as_ref().map(|s| s.write().unwrap()) else { 116 | return; 117 | }; 118 | gloo::console::warn!("HAA up"); 119 | if let Some(keycode) = key_to_keycode(&e.key()) { 120 | state.pressed_keys.remove(&PhysicalKey::Code(keycode)); 121 | } 122 | }) 123 | }; 124 | 125 | use_effect(move || { 126 | Box::leak(Box::new(gloo::events::EventListener::new( 127 | &web_sys::window().unwrap(), 128 | "resize", 129 | move |e| { 130 | gloo::console::warn!("resize"); 131 | let canvas = gloo::utils::document() 132 | .get_element_by_id("main-canvas") 133 | .unwrap(); 134 | gloo::console::warn!("resize 2"); 135 | let canvas = canvas.dyn_into::().unwrap(); 136 | gloo::console::warn!("resize 3"); 137 | let Some(state) = &mut renderer_state.as_ref().map(|s| s.write().unwrap()) else { 138 | gloo::console::warn!("reszize 4"); 139 | return; 140 | }; 141 | gloo::console::warn!("RESIZE", canvas.client_width(), canvas.client_height()); 142 | gloo::console::warn!("RESIZE", canvas.width(), canvas.height()); 143 | 144 | let width = canvas.client_width() as u32; 145 | let height = canvas.client_height() as u32; 146 | canvas.set_width(width); 147 | canvas.set_height(height); 148 | state.width = width; 149 | state.height = height; 150 | state.config.width = width; 151 | state.config.height = height; 152 | state.surface.configure(&state.device, &state.config); 153 | 154 | renderer 155 | .as_ref() 156 | .unwrap() 157 | .write() 158 | .unwrap() 159 | .on_resize(&state); 160 | }, 161 | ))); 162 | }); 163 | 164 | html! { 165 |
166 |

{&props.title}

167 | ().unwrap(); 175 | element.focus(); 176 | element.request_pointer_lock(); 177 | })} 178 | onmousemove={mousemove} 179 | onkeydown={keydown} 180 | onkeyup={keyup} 181 | /> 182 |
183 | } 184 | } 185 | 186 | pub fn start(props: Props) { 187 | yew::Renderer::>::with_props(props).render(); 188 | } 189 | 190 | fn key_to_keycode(key: &str) -> Option { 191 | let key = match &*key.to_lowercase() { 192 | "shift" => KeyCode::ShiftLeft, 193 | "w" => KeyCode::KeyW, 194 | "a" => KeyCode::KeyA, 195 | "s" => KeyCode::KeyS, 196 | "d" => KeyCode::KeyD, 197 | k => { 198 | gloo::console::warn!("Unknown key: ", k); 199 | return None; 200 | } 201 | }; 202 | Some(key) 203 | } 204 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use glam; 2 | 3 | use glam::Vec2; 4 | use instant::Instant; 5 | use std::collections::BTreeSet; 6 | use std::fs::File; 7 | use std::io::{Read, Seek}; 8 | use std::path::PathBuf; 9 | use std::sync::Arc; 10 | use wgpu::{ExperimentalFeatures, MemoryHints}; 11 | use winit::keyboard::PhysicalKey; 12 | use winit::{ 13 | event::{DeviceEvent, WindowEvent}, 14 | window::Window, 15 | }; 16 | use yew::prelude::*; 17 | 18 | pub use rand; 19 | pub use wgpu; 20 | pub use winit; 21 | 22 | #[cfg(target_arch = "wasm32")] 23 | pub mod yew_backend; 24 | 25 | #[cfg(not(target_arch = "wasm32"))] 26 | pub mod winit_backend; 27 | 28 | pub trait Renderer: 'static + Sized { 29 | fn init(state: &RendererState) -> impl std::future::Future; 30 | fn on_window_event(&mut self, state: &RendererState, event: &WindowEvent); 31 | fn on_device_event(&mut self, state: &RendererState, event: &DeviceEvent); 32 | fn on_resize(&mut self, state: &RendererState); 33 | fn render(&mut self, state: &RendererState); 34 | } 35 | 36 | #[derive(PartialEq, Properties)] 37 | pub struct Props { 38 | pub title: String, 39 | pub capture_cursor: bool, 40 | } 41 | impl Default for Props { 42 | fn default() -> Self { 43 | Self { 44 | title: "Default title!".to_owned(), 45 | capture_cursor: false, 46 | } 47 | } 48 | } 49 | 50 | pub struct CursorState { 51 | pub position: Vec2, 52 | pub dragging_from: Option, 53 | } 54 | 55 | pub struct RendererState { 56 | pub width: u32, 57 | pub height: u32, 58 | pub window: Arc, 59 | pub device: wgpu::Device, 60 | pub surface: wgpu::Surface<'static>, 61 | pub config: wgpu::SurfaceConfiguration, 62 | pub pressed_keys: BTreeSet, 63 | pub cursor: CursorState, 64 | pub queue: wgpu::Queue, 65 | pub start: Instant, 66 | } 67 | 68 | impl RendererState { 69 | #[cfg(target_arch = "wasm32")] 70 | async fn init_web(canvas: web_sys::HtmlCanvasElement) -> Self { 71 | let width = canvas.width(); 72 | let height = canvas.height(); 73 | 74 | let instance = wgpu::Instance::default(); 75 | let surface: wgpu::Surface<'static> = instance 76 | .create_surface(wgpu::SurfaceTarget::Canvas(canvas)) 77 | .unwrap(); 78 | let adapter = instance 79 | .request_adapter(&wgpu::RequestAdapterOptions { 80 | power_preference: wgpu::PowerPreference::default(), 81 | compatible_surface: Some(&surface), 82 | force_fallback_adapter: false, 83 | }) 84 | .await 85 | .unwrap(); 86 | let (device, queue) = adapter 87 | .request_device( 88 | &wgpu::DeviceDescriptor { 89 | required_features: wgpu::Features::empty(), 90 | required_limits: wgpu::Limits { 91 | max_uniform_buffer_binding_size: 65536, 92 | max_storage_buffer_binding_size: 128 << 23, 93 | max_texture_array_layers: 256 * 3, 94 | max_texture_dimension_2d: 8192, 95 | max_storage_buffers_per_shader_stage: 6, 96 | ..wgpu::Limits::downlevel_defaults() 97 | }, 98 | label: None, 99 | }, 100 | None, 101 | ) 102 | .await 103 | .unwrap(); 104 | 105 | let swapchain_capabilities = surface.get_capabilities(&adapter); 106 | let swapchain_format = swapchain_capabilities.formats[0]; 107 | 108 | let config = wgpu::SurfaceConfiguration { 109 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 110 | format: swapchain_format, 111 | width, 112 | height, 113 | present_mode: wgpu::PresentMode::Fifo, 114 | alpha_mode: swapchain_capabilities.alpha_modes[0], 115 | view_formats: vec![], 116 | desired_maximum_frame_latency: 2, 117 | }; 118 | 119 | surface.configure(&device, &config); 120 | 121 | RendererState { 122 | width, 123 | height, 124 | device, 125 | surface, 126 | config, 127 | queue, 128 | pressed_keys: BTreeSet::new(), 129 | cursor: CursorState { 130 | position: Vec2::ZERO, 131 | dragging_from: None, 132 | }, 133 | start: Instant::now(), 134 | } 135 | } 136 | 137 | async fn init_winit(window: Arc) -> Self { 138 | let instance = wgpu::Instance::default(); 139 | 140 | let size = window.inner_size(); 141 | let width = size.width; 142 | let height = size.height; 143 | 144 | let surface = instance.create_surface(window.clone()).unwrap(); 145 | 146 | let adapter = instance 147 | .request_adapter(&wgpu::RequestAdapterOptions { 148 | power_preference: wgpu::PowerPreference::HighPerformance, 149 | compatible_surface: Some(&surface), 150 | force_fallback_adapter: false, 151 | }) 152 | .await 153 | .unwrap(); 154 | let (device, queue) = adapter 155 | .request_device(&wgpu::DeviceDescriptor { 156 | required_features: wgpu::Features::VERTEX_WRITABLE_STORAGE 157 | | wgpu::Features::PUSH_CONSTANTS, 158 | required_limits: wgpu::Limits { 159 | max_uniform_buffer_binding_size: 65536, 160 | max_storage_buffer_binding_size: 128 << 23, 161 | max_texture_array_layers: 256 * 3, 162 | max_texture_dimension_2d: 8192, 163 | max_storage_buffers_per_shader_stage: 6, 164 | max_push_constant_size: 128, 165 | max_buffer_size: 128 << 22, 166 | ..wgpu::Limits::downlevel_defaults() 167 | }, 168 | label: None, 169 | experimental_features: ExperimentalFeatures::disabled(), 170 | memory_hints: MemoryHints::Performance, 171 | trace: wgpu::Trace::Off, 172 | }) 173 | .await 174 | .unwrap(); 175 | 176 | let swapchain_capabilities = surface.get_capabilities(&adapter); 177 | let swapchain_format = swapchain_capabilities.formats[0]; 178 | 179 | let config = wgpu::SurfaceConfiguration { 180 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 181 | format: swapchain_format, 182 | width, 183 | height, 184 | present_mode: wgpu::PresentMode::Fifo, 185 | alpha_mode: swapchain_capabilities.alpha_modes[0], 186 | view_formats: vec![], 187 | desired_maximum_frame_latency: 2, 188 | }; 189 | 190 | surface.configure(&device, &config); 191 | 192 | RendererState { 193 | width, 194 | height, 195 | window, 196 | device, 197 | surface, 198 | config, 199 | queue, 200 | pressed_keys: BTreeSet::new(), 201 | cursor: CursorState { 202 | position: Vec2::ZERO, 203 | dragging_from: None, 204 | }, 205 | start: Instant::now(), 206 | } 207 | } 208 | } 209 | 210 | pub fn start(props: Props) { 211 | #[cfg(target_arch = "wasm32")] 212 | { 213 | console_log::init_with_level(log::Level::Debug); 214 | println!("Starting wasm!"); 215 | yew_backend::start::(props); 216 | } 217 | 218 | #[cfg(not(target_arch = "wasm32"))] 219 | { 220 | println!("Starting native!"); 221 | winit_backend::start::(props); 222 | } 223 | 224 | println!("Done!"); 225 | } 226 | 227 | pub async fn file_open(rel_path: &str) -> Option { 228 | #[cfg(not(target_arch = "wasm32"))] 229 | { 230 | let mut full_path = PathBuf::from("assets/"); 231 | full_path.push(&rel_path); 232 | File::open(full_path).ok() 233 | } 234 | 235 | #[cfg(target_arch = "wasm32")] 236 | { 237 | let path = "assets/".to_owned() + rel_path; 238 | Request::get(&path) 239 | .send() 240 | .await 241 | .ok()? 242 | .binary() 243 | .await 244 | .ok() 245 | .filter(|file| !file.starts_with(b"")) 246 | .map(Cursor::new) 247 | } 248 | } 249 | --------------------------------------------------------------------------------