diff options
| author | Kai Stevenson <kai@kaistevenson.com> | 2026-08-14 00:59:05 -0700 |
|---|---|---|
| committer | Kai Stevenson <kai@kaistevenson.com> | 2026-08-14 00:59:05 -0700 |
| commit | ab2d7e3150eb28f9a8fc4ce83c6477753bdbcee3 (patch) | |
| tree | 6407a935237b37f903101f7c00addd22920a8a0f /src | |
| parent | b0d9202a97d2843477c5abbaa491f298c70a55a3 (diff) | |
pixel pipeline working
Diffstat (limited to 'src')
| -rw-r--r-- | src/camera.rs | 9 | ||||
| -rw-r--r-- | src/main.rs | 629 | ||||
| -rw-r--r-- | src/shader.wgsl | 25 | ||||
| -rw-r--r-- | src/sim/materials/mod.rs | 2 |
4 files changed, 491 insertions, 174 deletions
diff --git a/src/camera.rs b/src/camera.rs index df6a023..edb7f29 100644 --- a/src/camera.rs +++ b/src/camera.rs @@ -56,6 +56,15 @@ impl Camera { pub fn write_frame_view(&self, frame: &mut [u8], world: &World) { puffin::profile_function!(); + for i in 0..frame.len() / 4 { + let idx = i * 4; + frame[idx] = 0xFF; + frame[idx + 1] = 0xAA; + frame[idx + 2] = 0xAA; + frame[idx + 3] = 0xFF; + } + return; + const BG: [u8; 4] = [0x00, 0x00, 0x00, 0xFF]; for px in frame.chunks_exact_mut(4) { px.copy_from_slice(&BG); diff --git a/src/main.rs b/src/main.rs index 313a7a8..b5c19e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,9 +6,12 @@ mod ui; use egui::Id; use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; -use pixels::{Pixels, ScalingMode, SurfaceTexture}; -use rand::random_range; -use std::time::{Duration, Instant}; +use futures::executor; +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; +use wgpu::{Origin3d, TextureUsages}; use winit::{ application::ApplicationHandler, event::{ @@ -22,8 +25,8 @@ use winit::{ use crate::{ camera::Camera, - config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE}, - sim::{cell::Cell, materials::MaterialId, sim::sim_tick, world::World}, + config::WINDOW_TITLE, + sim::{materials::MaterialId, sim::sim_tick, world::World}, ui::draw_egui, }; @@ -53,13 +56,394 @@ struct Diagnostics { fps: f32, } +struct RendererState { + window: Arc<Window>, + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + is_surface_configured: bool, + + // egui + egui_context: egui::Context, + egui_state: egui_winit::State, + egui_renderer: egui_wgpu::Renderer, + + pixels_pipeline: wgpu::RenderPipeline, + pixels_bind_group: wgpu::BindGroup, + texture: wgpu::Texture, + // TODO remove me + frame_view: Vec<u8>, +} + +impl RendererState { + // https://sotrh.github.io/learn-wgpu/beginner/tutorial1-window + pub async fn new(window: Arc<Window>) -> Self { + let size = window.inner_size(); + println!("Got window! ({}x{})", size.width, size.height); + + let instance = wgpu::Instance::default(); + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + compatible_surface: Some(&surface), + ..wgpu::RequestAdapterOptions::default() + }) + .await + .unwrap(); + + println!( + "Initialized adapter, using GPU '{}'", + adapter.get_info().name + ); + + let (device, queue) = adapter + .request_device(&wgpu::DeviceDescriptor::default()) + .await + .unwrap(); + + 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, + height: size.height, + present_mode: wgpu::PresentMode::AutoVsync, + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + + let egui_context = Context::default(); + let egui_state = egui_winit::State::new( + egui_context.clone(), + egui_context.viewport_id(), + &window, + None, + None, + None, + ); + let egui_renderer = egui_wgpu::Renderer::new(&device, surface_format, { + RendererOptions { + msaa_samples: 1, + ..RendererOptions::default() + } + }); + + let texture = device.create_texture(&wgpu::TextureDescriptor { + label: None, + mip_level_count: 1, + sample_count: 1, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + format: surface_format, + size: wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }, + dimension: wgpu::TextureDimension::D2, + view_formats: &[], + }); + + let pixels_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + entries: &[wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Texture { + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + view_dimension: wgpu::TextureViewDimension::D2, + multisampled: false, + }, + binding: 0, + count: None, + visibility: wgpu::ShaderStages::FRAGMENT, + }], + }); + + let pixels_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + immediate_size: 0, + bind_group_layouts: &[Some(&pixels_bind_group_layout)], + }); + + let pixels_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &pixels_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&texture.create_view( + &wgpu::TextureViewDescriptor { + dimension: Some(wgpu::TextureViewDimension::D2), + usage: Some( + wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + ), + ..wgpu::TextureViewDescriptor::default() + }, + )), + }], + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let pixels_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pixels_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: Some("vs_main"), + buffers: &[], + compilation_options: wgpu::PipelineCompilationOptions::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: None, + polygon_mode: wgpu::PolygonMode::Fill, + unclipped_depth: false, + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview_mask: None, + cache: None, + }); + + RendererState { + window, + surface, + device, + queue, + config, + is_surface_configured: false, + + egui_context, + egui_state, + egui_renderer, + + texture, + frame_view: vec![0; size.width as usize * size.height as usize * 4], + pixels_pipeline, + pixels_bind_group, + } + } + + 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); + self.is_surface_configured = true; + } + } + + pub fn render( + &mut self, + config: &mut Config, + camera: &mut Camera, + diagnostics: &Diagnostics, + input: &Input, + ) { + puffin::profile_function!(); + + self.window.request_redraw(); + + if !self.is_surface_configured { + return; + } + + let output = match self.surface.get_current_texture() { + wgpu::CurrentSurfaceTexture::Success(surface_texture) => surface_texture, + wgpu::CurrentSurfaceTexture::Suboptimal(surface_texture) => surface_texture, + wgpu::CurrentSurfaceTexture::Timeout + | wgpu::CurrentSurfaceTexture::Occluded + | wgpu::CurrentSurfaceTexture::Validation => { + // Skip this frame + return; + } + wgpu::CurrentSurfaceTexture::Outdated => { + self.surface.configure(&self.device, &self.config); + return; + } + wgpu::CurrentSurfaceTexture::Lost => { + panic!("Lost device?"); + } + }; + + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + let raw_input = self.egui_state.take_egui_input(&self.window); + let full_output = self.egui_context.run_ui(raw_input, |ui| { + let right_panel = egui::Panel::right(Id::new("right_panel")); + right_panel + .resizable(false) + // TODO collapse button + .show_collapsible(ui, &mut true, |panel_ui| { + draw_egui(panel_ui, config, camera, diagnostics, input) + }); + }); + + self.egui_state + .handle_platform_output(&self.window, full_output.platform_output); + + let clipped_primitives = self + .egui_context + .tessellate(full_output.shapes, full_output.pixels_per_point); + + let pixels_per_point = full_output.pixels_per_point; + + let size = self.window.inner_size(); + let screen_descriptor = ScreenDescriptor { + size_in_pixels: [size.width, size.height], + pixels_per_point, + }; + + for (id, delta) in &full_output.textures_delta.set { + self.egui_renderer + .update_texture(&self.device, &self.queue, *id, delta); + } + + self.egui_renderer.update_buffers( + &self.device, + &self.queue, + &mut encoder, + &clipped_primitives, + &screen_descriptor, + ); + + // let 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.2, + // b: 0.3, + // a: 1.0, + // }), + // store: wgpu::StoreOp::Store, + // }, + // })], + // depth_stencil_attachment: None, + // occlusion_query_set: None, + // timestamp_writes: None, + // multiview_mask: None, + // }); + + // drop(render_pass); + + self.queue.write_texture( + wgpu::TexelCopyTextureInfo { + texture: &self.texture, + aspect: wgpu::TextureAspect::All, + mip_level: 0, + origin: Origin3d::ZERO, + }, + &self.frame_view, + wgpu::TexelCopyBufferLayout { + bytes_per_row: Some(size.width * 4), + offset: 0, + rows_per_image: Some(size.height), + }, + wgpu::Extent3d { + width: size.width, + height: size.height, + depth_or_array_layers: 1, + }, + ); + + 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.2, + b: 0.3, + 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(&self.pixels_pipeline); + render_pass.set_bind_group(0, &self.pixels_bind_group, &[]); + render_pass.draw(0..3, 0..1); + + drop(render_pass); + + let mut egui_pass = encoder + .begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("egui pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + depth_slice: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + multiview_mask: None, + }) + .forget_lifetime(); + + self.egui_renderer + .render(&mut egui_pass, &clipped_primitives, &screen_descriptor); + drop(egui_pass); + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + } +} + struct App { - // core - egui_renderer: Option<egui_wgpu::Renderer>, - egui_state: Option<egui_winit::State>, - egui_context: Option<egui::Context>, - window: Option<&'static Window>, - pixels: Option<Pixels<'static>>, + window: Option<Arc<Window>>, + renderer_state: Option<RendererState>, input: Input, @@ -85,11 +469,8 @@ struct App { impl Default for App { fn default() -> Self { Self { - egui_renderer: None, - egui_state: None, - egui_context: None, window: None, - pixels: None, + renderer_state: None, input: Input { last_mouse_pos_on_screen: None, @@ -130,40 +511,20 @@ impl Default for App { impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { - let window = event_loop - .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) - .unwrap(); - - let size = window.inner_size(); - let window_ref: &'static Window = Box::leak(Box::new(window)); - let surface = SurfaceTexture::new(size.width, size.height, window_ref); + let window = Arc::new( + event_loop + .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) + .unwrap(), + ); - let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); + self.window = Some(window.clone()); + self.renderer_state = Some(executor::block_on(RendererState::new(window.clone()))); - pixels.set_scaling_mode(ScalingMode::Fill); + // let surface = SurfaceTexture::new(size.width, size.height, window_ref); - let egui_context = Context::default(); - let egui_state = egui_winit::State::new( - egui_context.clone(), - egui_context.viewport_id(), - window_ref, - None, - None, - None, - ); - let egui_renderer = - egui_wgpu::Renderer::new(pixels.device(), pixels.render_texture_format(), { - RendererOptions { - msaa_samples: 1, - ..RendererOptions::default() - } - }); + // let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); - self.egui_context = Some(egui_context); - self.egui_state = Some(egui_state); - self.egui_renderer = Some(egui_renderer); - self.window = Some(window_ref); - self.pixels = Some(pixels); + // pixels.set_scaling_mode(ScalingMode::Fill); } fn window_event( @@ -172,15 +533,12 @@ impl ApplicationHandler for App { _: winit::window::WindowId, event: WindowEvent, ) { - if let Some(pixels) = &mut self.pixels - && let Some(window) = self.window - && let Some(egui_context) = &self.egui_context - && let Some(egui_state) = &mut self.egui_state - && let Some(egui_renderer) = &mut self.egui_renderer + if let Some(window) = &self.window + && let Some(renderer_state) = &mut self.renderer_state && let Some(world) = &mut self.world && let Some(camera) = &mut self.camera { - let egui_response = egui_state.on_window_event(window, &event); + let egui_response = renderer_state.egui_state.on_window_event(window, &event); // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks if egui_response.consumed { return; @@ -218,11 +576,11 @@ impl ApplicationHandler for App { } WindowEvent::CursorMoved { position, .. } => { self.input.last_mouse_pos_on_screen = Some((position.x, position.y)); - self.input.last_mouse_pos_on_board = pixels - .window_pos_to_pixel((position.x as f32, position.y as f32)) - .map(|v| camera.screen_position_to_world(v.0 as f64, v.1 as f64)) - .map(|v| (v.0 as i32, v.1 as i32)) - .ok(); + // self.input.last_mouse_pos_on_board = pixels + // .window_pos_to_pixel((position.x as f32, position.y as f32)) + // .map(|v| camera.screen_position_to_world(v.0 as f64, v.1 as f64)) + // .map(|v| (v.0 as i32, v.1 as i32)) + // .ok(); } WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left { @@ -231,7 +589,7 @@ impl ApplicationHandler for App { } WindowEvent::Resized(size) => { // Important: resize the surface when the window's size change - pixels.resize_surface(size.width, size.height).unwrap(); + renderer_state.resize(size.width, size.height); } WindowEvent::CloseRequested => { event_loop.exit(); @@ -252,40 +610,36 @@ impl ApplicationHandler for App { // apply inputs camera.handle_camera_input(&self.input, delta_time); - // pixels/camera logic - let frame = pixels.frame_mut(); - frame.fill(0); + // // --TEST DRAWING-- + // if self.input.is_lmb_pressed + // && let Some(lm) = self.input.last_mouse_pos_on_board + // { + // // start with the bounding box of the drawing brush circle + some margin + // // clamp the bounding box to the board sie + // // TODO better way to do this without unwrap? + // let bb_xl = lm.0 - self.config.brush_radius as i32; + // let bb_xu = lm.0 + self.config.brush_radius as i32; + // let bb_yl = lm.1 - self.config.brush_radius as i32; + // let bb_yu = lm.1 + self.config.brush_radius as i32; - // --TEST DRAWING-- - if self.input.is_lmb_pressed - && let Some(lm) = self.input.last_mouse_pos_on_board - { - // start with the bounding box of the drawing brush circle + some margin - // clamp the bounding box to the board sie - // TODO better way to do this without unwrap? - let bb_xl = lm.0 - self.config.brush_radius as i32; - let bb_xu = lm.0 + self.config.brush_radius as i32; - let bb_yl = lm.1 - self.config.brush_radius as i32; - let bb_yu = lm.1 + self.config.brush_radius as i32; - - // for each point, check if the distance is less than the brush size and write the pixel - for x in bb_xl..bb_xu { - for y in bb_yl..bb_yu { - let r = random_range(0.0..1.0); - if ((x - lm.0).pow(2) + (y - lm.1).pow(2)) - < (self.config.brush_radius as i32).pow(2) - && r > 0.9 - { - let mut cell = Cell::from_material(self.config.brush_material); - cell.flags = (self.sim_seqno as u8) & 0b1; - world.set_cell_from_game_position( - x, y, cell, // wake the chunk - false, - ); - } - } - } - } + // // for each point, check if the distance is less than the brush size and write the pixel + // for x in bb_xl..bb_xu { + // for y in bb_yl..bb_yu { + // let r = random_range(0.0..1.0); + // if ((x - lm.0).pow(2) + (y - lm.1).pow(2)) + // < (self.config.brush_radius as i32).pow(2) + // && r > 0.9 + // { + // let mut cell = Cell::from_material(self.config.brush_material); + // cell.flags = (self.sim_seqno as u8) & 0b1; + // world.set_cell_from_game_position( + // x, y, cell, // wake the chunk + // false, + // ); + // } + // } + // } + // } // TODO check if we need to run another sim tick given the sim speed + delta_time // SIM logic @@ -295,87 +649,14 @@ impl ApplicationHandler for App { self.ignore_pause_next_tick = false; } - // let get_overlay = - // create_compute_combined_overlay_offset(&world, &self.config, &self.input); - camera.write_frame_view(frame, &world); - - // egui logic - let raw_input = egui_state.take_egui_input(window); - let full_output = egui_context.run_ui(raw_input, |ui| { - let right_panel = egui::Panel::right(Id::new("right_panel")); - right_panel - .resizable(false) - // TODO collapse button - .show_collapsible(ui, &mut true, |panel_ui| { - draw_egui( - panel_ui, - &mut self.config, - camera, - &self.diagnostics, - &self.input, - ) - }); - }); - - egui_state.handle_platform_output(window, full_output.platform_output); - - let clipped_primitives = - egui_context.tessellate(full_output.shapes, full_output.pixels_per_point); - - let pixels_per_point = full_output.pixels_per_point; - - let size = window.inner_size(); - let screen_descriptor = ScreenDescriptor { - size_in_pixels: [size.width, size.height], - pixels_per_point, - }; - - let queue = pixels.queue(); - let device = pixels.device(); - - for (id, delta) in &full_output.textures_delta.set { - egui_renderer.update_texture(&device, &queue, *id, delta); - } - - let _ = pixels.render_with(|encoder, render_target, context| { - context.scaling_renderer.render(encoder, render_target); - - egui_renderer.update_buffers( - device, - &queue, - encoder, - &clipped_primitives, - &screen_descriptor, - ); + camera.write_frame_view(&mut renderer_state.frame_view, world); - let mut egui_pass = encoder - .begin_render_pass(&wgpu::RenderPassDescriptor { - label: Some("egui pass"), - color_attachments: &[Some(wgpu::RenderPassColorAttachment { - view: render_target, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Load, - store: wgpu::StoreOp::Store, - }, - depth_slice: None, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - multiview_mask: None, - }) - .forget_lifetime(); - - egui_renderer.render( - &mut egui_pass, - &clipped_primitives, - &screen_descriptor, - ); - drop(egui_pass); - - Ok(()) - }); + renderer_state.render( + &mut self.config, + camera, + &self.diagnostics, + &mut self.input, + ); } _ => {} } @@ -388,9 +669,11 @@ impl ApplicationHandler for App { // limit our internal redraw requests to (fps) if now - self.last_frame_requested >= frame_duration { self.last_frame_requested = now; - self.window - .expect("Bug - Window should exist") - .request_redraw(); + if let Some(window) = &self.window { + window.request_redraw(); + } else { + panic!("No window!") + } } } } diff --git a/src/shader.wgsl b/src/shader.wgsl new file mode 100644 index 0000000..e545dac --- /dev/null +++ b/src/shader.wgsl @@ -0,0 +1,25 @@ +@group(0) @binding(0) var tex: texture_2d<f32>; + +struct VertexOutput { + @builtin(position) clip_position: vec4<f32>, + @location(0) uv: vec2<f32>, +}; + +@vertex +fn vs_main( + @builtin(vertex_index) i: u32, +) -> VertexOutput { + var out: VertexOutput; + let uv = vec2f(f32((i << 1u) & 2u), f32(i & 2u)); + out.clip_position = vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, 0.0, 1.0); + out.uv = uv; + + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> { + let dims = vec2f(textureDimensions(tex)); + return textureLoad(tex, vec2i(in.uv * dims), 0); + // return vec4f(0.0, 1.0, 0.0, 1.0); +}
\ No newline at end of file diff --git a/src/sim/materials/mod.rs b/src/sim/materials/mod.rs index 0cba5ff..581ce31 100644 --- a/src/sim/materials/mod.rs +++ b/src/sim/materials/mod.rs @@ -24,7 +24,7 @@ pub struct MaterialDef { static MATERIALS: [MaterialDef; 5] = [ MaterialDef { name: "Void", - color: (0x00, 0x00, 0x00, 0xFF), + color: (0xFF, 0xFF, 0x00, 0xFF), density: 0, sim_update: None, }, |
