commit dbda39b61ac7632525e8065f75ae018c75b82027 Author: Albin Chaboissier Date: Fri Dec 19 16:44:16 2025 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..c9e548b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wgpu-template" +version = "0.1.0" +edition = "2024" + +[dependencies] +anyhow = "1.0.100" +bytemuck = "1.24.0" +cgmath = "0.18.0" +crevice = {version = "0.18.0", features = ["cgmath"]} +egui = "0.33.3" +egui-wgpu = "0.33.3" +egui-winit = "0.33.3" +env_logger = "0.11.8" +pollster = "0.4.0" +wgpu = "27.0.1" +winit = "0.30.12" diff --git a/src/egui_renderer.rs b/src/egui_renderer.rs new file mode 100644 index 0000000..ccb37ee --- /dev/null +++ b/src/egui_renderer.rs @@ -0,0 +1,137 @@ +use egui::{Context, PaintCallbackInfo}; +use egui_wgpu::{CallbackResources, CallbackTrait, Renderer, RendererOptions, ScreenDescriptor}; +use egui_winit::State; +use wgpu::{CommandEncoder, Device, Queue, RenderPass, TextureFormat, TextureView}; +use winit::{event::WindowEvent, window::Window}; + +pub struct EguiState { + pub state: State, + pub renderer: Renderer, + pub frame_started: bool, + //pub msaa_texture: TextureView, + pub color_format: TextureFormat, +} + +impl EguiState { + pub fn context(&self) -> &Context { + self.state.egui_ctx() + } + + pub fn new(device: &Device, output_color_format: TextureFormat, window: &Window) -> EguiState { + let egui_context = Context::default(); + + let egui_state = egui_winit::State::new( + egui_context, + egui::ViewportId::ROOT, + &window, + Some(window.scale_factor() as f32), + None, + Some(2 * 1024), + ); + + let options = RendererOptions { + //msaa_samples: SAMPLE_COUNT, + //depth_stencil_format: Some(TextureFormat::Depth24PlusStencil8), + ..Default::default() + }; + let egui_renderer = Renderer::new(device, output_color_format, options); + + EguiState { + state: egui_state, + renderer: egui_renderer, + frame_started: false, + color_format: output_color_format, + } + } + + pub fn resize(&mut self, _device: &Device, _width: u32, _height: u32) {} + + pub fn handle_event(&mut self, window: &Window, event: &WindowEvent) { + let _ = self.state.on_window_event(window, event); + } + + pub fn begin_frame(&mut self, window: &Window) { + let input = self.state.take_egui_input(window); + self.state.egui_ctx().begin_pass(input); + self.frame_started = true; + } + + pub fn end_frame_and_draw( + &mut self, + device: &Device, + queue: &Queue, + encoder: &mut CommandEncoder, + window: &Window, + window_surface_view: &TextureView, + screen_descriptor: ScreenDescriptor, + ) { + if !self.frame_started { + panic!("begin_frame must be called before end_frame_and_draw can be called!"); + } + + //self.ppp(screen_descriptor.pixels_per_point); + + let full_output = self.state.egui_ctx().end_pass(); + + self.state + .handle_platform_output(window, full_output.platform_output); + + let tris = self + .state + .egui_ctx() + .tessellate(full_output.shapes, self.state.egui_ctx().pixels_per_point()); + for (id, image_delta) in &full_output.textures_delta.set { + self.renderer + .update_texture(device, queue, *id, image_delta); + } + self.renderer + .update_buffers(device, queue, encoder, &tris, &screen_descriptor); + let rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: window_surface_view, + resolve_target: None, + ops: egui_wgpu::wgpu::Operations { + load: egui_wgpu::wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + depth_slice: None, + })], + depth_stencil_attachment: None, + label: Some("egui main render pass"), + timestamp_writes: None, + occlusion_query_set: None, + }); + + self.renderer + .render(&mut rpass.forget_lifetime(), &tris, &screen_descriptor); + for x in &full_output.textures_delta.free { + self.renderer.free_texture(x) + } + + self.frame_started = false; + } +} + +pub struct CallbackFn

+where + P: Fn(PaintCallbackInfo, &mut RenderPass<'static>, &CallbackResources), +{ + //pub : FnOnce(&Device, &Queue, &ScreenDescriptor, &mut CommandEncoder, &mut CallbackResources) -> Vec<> + pub paint_fn: P, +} + +impl

CallbackTrait for CallbackFn

+where + P: Fn(PaintCallbackInfo, &mut RenderPass<'static>, &CallbackResources) + + std::marker::Sync + + std::marker::Send, +{ + fn paint( + &self, + info: egui::PaintCallbackInfo, + render_pass: &mut wgpu::RenderPass<'static>, + callback_resources: &egui_wgpu::CallbackResources, + ) { + (self.paint_fn)(info, render_pass, callback_resources) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..84abb7a --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,74 @@ +pub mod egui_renderer; +pub mod state; + +use std::sync::Arc; + +use winit::{ + application::ApplicationHandler, + event::WindowEvent, + event_loop::{self, EventLoop}, + window::Window, +}; + +use crate::state::State; + +pub fn run() -> anyhow::Result<()> { + env_logger::init(); + + let event_loop = EventLoop::with_user_event().build()?; + let mut app = App::default(); + event_loop.run_app(&mut app)?; + + Ok(()) +} + +// App struct +#[derive(Default)] +pub struct App { + state: Option, +} + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &event_loop::ActiveEventLoop) { + // Create window + let window = Arc::new( + event_loop + .create_window( + Window::default_attributes() + .with_title("Wgpu Template") + .with_resizable(true), + ) + .unwrap(), + ); + + let state = pollster::block_on(State::new(window.clone())); + self.state = Some(state); + + window.request_redraw(); + } + + fn window_event( + &mut self, + event_loop: &event_loop::ActiveEventLoop, + _window_id: winit::window::WindowId, + event: winit::event::WindowEvent, + ) { + let state = self.state.as_mut().unwrap(); + state.handle_event(&event); + + match event { + WindowEvent::CloseRequested => { + event_loop.exit(); + } + WindowEvent::RedrawRequested => { + state.render(); + state.get_window().request_redraw(); + } + + WindowEvent::Resized(size) => { + state.resize(size); + } + _ => {} + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..420bdcc --- /dev/null +++ b/src/main.rs @@ -0,0 +1,3 @@ +fn main() -> anyhow::Result<()> { + wgpu_template::run() +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..7b208c5 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,169 @@ +use std::sync::Arc; + +use cgmath::{Matrix4, Vector3}; +use crevice::std430::AsStd430; +use egui_wgpu::ScreenDescriptor; +use wgpu::{Features, FeaturesWGPU, FeaturesWebGPU}; +use winit::{event::WindowEvent, window::Window}; + +use crate::egui_renderer::EguiState; + +pub struct State { + window: Arc, + device: wgpu::Device, + queue: wgpu::Queue, + size: winit::dpi::PhysicalSize, + surface: wgpu::Surface<'static>, + surface_format: wgpu::TextureFormat, + egui_state: EguiState, +} + +impl State { + pub async fn new(window: Arc) -> State { + let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor::default()); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions::default()) + .await + .unwrap(); + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor { + required_features: Features { + features_wgpu: FeaturesWGPU::PUSH_CONSTANTS, + features_webgpu: FeaturesWebGPU::empty(), + }, + required_limits: wgpu::Limits { + max_push_constant_size: RayMarchingPushConstants::std430_size_static() as u32, + ..Default::default() + }, + ..Default::default() + }) + .await + .unwrap(); + + let size = window.inner_size(); + + let surface = instance.create_surface(window.clone()).unwrap(); + let cap = surface.get_capabilities(&adapter); + let surface_format = cap.formats[0]; + + let state = State { + egui_state: EguiState::new(&device, surface_format, &window), + + window, + device, + queue, + size, + surface, + surface_format, + }; + + // Configure surface for the first time + state.configure_surface(); + + state + } + + pub fn get_window(&self) -> &Window { + &self.window + } + + pub fn configure_surface(&self) { + let surface_config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: self.surface_format, + // Request compatibility with the sRGB-format texture view we‘re going to create later. + view_formats: vec![self.surface_format.add_srgb_suffix()], + alpha_mode: wgpu::CompositeAlphaMode::Auto, + width: self.size.width, + height: self.size.height, + desired_maximum_frame_latency: 2, + present_mode: wgpu::PresentMode::AutoVsync, + }; + self.surface.configure(&self.device, &surface_config); + } + + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { + self.size = new_size; + + // reconfigure the surface + self.configure_surface(); + } + + pub fn handle_event(&mut self, event: &WindowEvent) { + self.egui_state.handle_event(&self.window, event); + } + + pub fn render(&mut self) { + // Create texture view + let surface_texture = self + .surface + .get_current_texture() + .expect("failed to acquire next swapchain texture"); + let texture_view = surface_texture + .texture + .create_view(&wgpu::TextureViewDescriptor { + // Without add_srgb_suffix() the image we will be working with + // might not be "gamma correct". + format: Some(self.surface_format.add_srgb_suffix()), + ..Default::default() + }); + + // Renders a GREEN screen + let mut encoder = self.device.create_command_encoder(&Default::default()); + // Create the renderpass which will clear the screen. + let renderpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: None, + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &texture_view, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // End the renderpass. + drop(renderpass); + // Egui + { + let screen_descriptor = ScreenDescriptor { + size_in_pixels: [self.size.width, self.size.height], + pixels_per_point: 1., + }; + self.egui_state.begin_frame(&self.window); + + egui::Window::new("Hello Window").resizable(true).show( + self.egui_state.context(), + |ui| { + ui.label("Hello, world."); + }, + ); + + self.egui_state.end_frame_and_draw( + &self.device, + &self.queue, + &mut encoder, + &self.window, + &texture_view, + screen_descriptor, + ); + } + + // Submit the command in the queue to execute + self.queue.submit([encoder.finish()]); + self.window.pre_present_notify(); + surface_texture.present(); + } +} + +#[derive(AsStd430)] +pub struct RayMarchingPushConstants { + inverse_projection_matrix: Matrix4, + view_matrix: Matrix4, + camera_pos: Vector3, +}