From 4ad69d966f0640daae34c677eead5b689cbfac2e Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Mon, 17 Aug 2026 19:31:31 -0700 Subject: debug physics --- src/main.rs | 6 +- src/renderer/mod.rs | 145 ++++++++++++++++++++++++++++++++++++++++- src/renderer/ui.rs | 24 ++++++- src/shader/debug.wgsl | 28 ++++++++ src/sim/rb_sim/debug_render.rs | 71 ++++++++++++++++++++ src/sim/rb_sim/mod.rs | 37 ++++++++++- 6 files changed, 306 insertions(+), 5 deletions(-) create mode 100644 src/shader/debug.wgsl create mode 100644 src/sim/rb_sim/debug_render.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 043d125..3452065 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,7 @@ use crate::{ sim::{ cell::{cell::Cell, materials::MaterialId}, cell_sim::{sim::sim_tick, world::World}, - rb_sim::RbSimManager, + rb_sim::{DebugRenderMode, RbSimManager}, write_rb_entity_to_world, }, }; @@ -38,6 +38,8 @@ struct Config { brush_material: MaterialId, dropper_material: MaterialId, use_threading: bool, + debug_render: bool, + debug_render_mode: DebugRenderMode, } struct Input { @@ -230,6 +232,8 @@ impl Default for App { brush_radius: 10.0, brush_material: MaterialId::Sand, dropper_material: MaterialId::Sand, + debug_render: true, + debug_render_mode: DebugRenderMode::default(), }, diagnostics: Diagnostics { fps: 0.0, diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 9bdbe46..7dd1802 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -10,7 +10,11 @@ use crate::{ camera::Camera, config::{CELLS_IN_CHUNK, CHUNK_SIZE}, renderer::ui::draw_egui, - sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::RbSimManager}, + sim::{ + cell::materials::MaterialId, + cell_sim::world::World, + rb_sim::{RbSimManager, debug_render::DebugVertex}, + }, }; // TODO derive from shared chunk config @@ -18,6 +22,8 @@ const CHUNK_SLOTS: usize = 11 * 200; const RB_ENTITY_SLOTS: usize = 64; const CELL_SLOTS: usize = CHUNK_SLOTS + RB_ENTITY_SLOTS; +const INITIAL_DEBUG_VERTEX_CAPACITY: usize = 4096; + #[repr(C)] #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct Instance { @@ -38,6 +44,18 @@ fn srgb_to_linear(channel: u8) -> f32 { } } +const DEBUG_VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] = + wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4]; + +fn create_debug_vertex_buffer(device: &wgpu::Device, capacity: usize) -> wgpu::Buffer { + device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Debug vertex buffer"), + size: (capacity * size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }) +} + pub struct RendererState { window: Arc, surface: wgpu::Surface<'static>, @@ -59,6 +77,12 @@ pub struct RendererState { cell_buffer: wgpu::Buffer, pixels_bind_group: wgpu::BindGroup, + // physics debug lines + debug_pipeline: wgpu::RenderPipeline, + debug_bind_group: wgpu::BindGroup, + debug_vertex_buffer: wgpu::Buffer, + debug_vertex_capacity: usize, + renderer_rb_entities: FxHashMap, } @@ -293,6 +317,86 @@ impl RendererState { ], }); + let debug_bind_group_layout = + device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("Debug bind group layout"), + entries: &[ + // camera uniform + wgpu::BindGroupLayoutEntry { + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + binding: 0, + count: None, + visibility: wgpu::ShaderStages::VERTEX, + }, + ], + }); + + let debug_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("Debug bind group"), + layout: &debug_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: camera_uniform_buffer.as_entire_binding(), + }], + }); + + let debug_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Debug shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("../shader/debug.wgsl").into()), + }); + + let debug_pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Debug pipeline layout"), + immediate_size: 0, + bind_group_layouts: &[Some(&debug_bind_group_layout)], + }); + + let debug_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Debug pipeline"), + layout: Some(&debug_pipeline_layout), + vertex: wgpu::VertexState { + module: &debug_shader, + entry_point: Some("vs_main"), + buffers: &[wgpu::VertexBufferLayout { + array_stride: size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &DEBUG_VERTEX_ATTRIBUTES, + }], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &debug_shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::LineList, + 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, + }); + + let debug_vertex_capacity = INITIAL_DEBUG_VERTEX_CAPACITY; + let debug_vertex_buffer = create_debug_vertex_buffer(&device, debug_vertex_capacity); + RendererState { window, surface, @@ -311,6 +415,11 @@ impl RendererState { cell_buffer, pixels_bind_group, + debug_pipeline, + debug_bind_group, + debug_vertex_buffer, + debug_vertex_capacity, + renderer_rb_entities: FxHashMap::default(), } } @@ -327,7 +436,7 @@ impl RendererState { pub fn render( &mut self, world: &mut World, - rb_sim_manager: &RbSimManager, + rb_sim_manager: &mut RbSimManager, camera: &mut Camera, config: &mut Config, diagnostics: &Diagnostics, @@ -510,6 +619,31 @@ impl RendererState { .write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(&instances)); } + let debug_vertex_count = if config.debug_render { + puffin::profile_scope!("Build physics debug lines"); + let vertices = rb_sim_manager.debug_render(config.debug_render_mode); + + if vertices.is_empty() { + 0 + } else { + if vertices.len() > self.debug_vertex_capacity { + self.debug_vertex_capacity = vertices.len().next_power_of_two(); + self.debug_vertex_buffer = + create_debug_vertex_buffer(&self.device, self.debug_vertex_capacity); + } + + self.queue.write_buffer( + &self.debug_vertex_buffer, + 0, + bytemuck::cast_slice(vertices), + ); + + vertices.len() as u32 + } + } else { + 0 + }; + { puffin::profile_scope!("Main render pass"); let mut render_pass: wgpu::RenderPass<'_> = @@ -538,6 +672,13 @@ impl RendererState { render_pass.set_pipeline(&self.pixels_pipeline); render_pass.set_bind_group(0, &self.pixels_bind_group, &[]); render_pass.draw(0..4, 0..instances.len() as u32); + + if debug_vertex_count > 0 { + render_pass.set_pipeline(&self.debug_pipeline); + render_pass.set_bind_group(0, &self.debug_bind_group, &[]); + render_pass.set_vertex_buffer(0, self.debug_vertex_buffer.slice(..)); + render_pass.draw(0..debug_vertex_count, 0..1); + } } { diff --git a/src/renderer/ui.rs b/src/renderer/ui.rs index bf51ab4..a1ac31f 100644 --- a/src/renderer/ui.rs +++ b/src/renderer/ui.rs @@ -2,9 +2,18 @@ use egui::{Color32, Stroke, Ui, epaint::CircleShape}; use crate::{ Camera, Config, Diagnostics, Input, - sim::{cell::materials::MaterialId, cell_sim::world::World}, + sim::{cell::materials::MaterialId, cell_sim::world::World, rb_sim::DebugRenderMode}, }; +const DEBUG_RENDER_MODES: [(&str, DebugRenderMode); 6] = [ + ("Collider shapes", DebugRenderMode::COLLIDER_SHAPES), + ("Collider AABBs", DebugRenderMode::COLLIDER_AABBS), + ("Rigid body axes", DebugRenderMode::RIGID_BODY_AXES), + ("Joints", DebugRenderMode::JOINTS), + ("Contacts", DebugRenderMode::CONTACTS), + ("Solver contacts", DebugRenderMode::SOLVER_CONTACTS), +]; + pub fn draw_egui<'a>( ui: &mut Ui, config: &mut Config, @@ -73,6 +82,19 @@ pub fn draw_egui<'a>( }); ui.checkbox(&mut config.use_threading, "Use multithreading"); + ui.checkbox(&mut config.debug_render, "Draw physics debug lines"); + + if config.debug_render { + ui.indent("debug_render_modes", |ui| { + for (name, flag) in DEBUG_RENDER_MODES { + let mut enabled = config.debug_render_mode.contains(flag); + if ui.checkbox(&mut enabled, name).changed() { + config.debug_render_mode.set(flag, enabled); + } + } + }); + } + ui.label(format!("Real FPS: {}", diagnostics.fps)); ui.heading("Camera"); ui.add(egui::Slider::new(&mut camera.zoom, 0.0..=10.0).text("Zoom")); diff --git a/src/shader/debug.wgsl b/src/shader/debug.wgsl new file mode 100644 index 0000000..256b969 --- /dev/null +++ b/src/shader/debug.wgsl @@ -0,0 +1,28 @@ +struct Camera { + scale: vec2f, + centre: vec2f, +}; + +@group(0) @binding(0) var camera: Camera; + +struct VertexOutput { + @builtin(position) clip_position: vec4f, + @location(0) color: vec4f, +}; + +@vertex +fn vs_main( + @location(0) world: vec2f, + @location(1) color: vec4f, +) -> VertexOutput { + var out: VertexOutput; + out.clip_position = vec4f((world - camera.centre) * camera.scale, 0.0, 1.0); + out.color = color; + + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4f { + return in.color; +} diff --git a/src/sim/rb_sim/debug_render.rs b/src/sim/rb_sim/debug_render.rs new file mode 100644 index 0000000..342fc5b --- /dev/null +++ b/src/sim/rb_sim/debug_render.rs @@ -0,0 +1,71 @@ +use rapier2d::pipeline::{DebugColor, DebugRenderBackend, DebugRenderObject}; + +use crate::config::PIXELS_TO_METRES; + +#[repr(C)] +#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] +pub struct DebugVertex { + pub position: [f32; 2], + pub color: [f32; 4], +} + +#[derive(Default)] +pub struct DebugLineBuffer { + pub vertices: Vec, +} + +impl DebugRenderBackend for DebugLineBuffer { + fn draw_line( + &mut self, + _object: DebugRenderObject, + a: rapier2d::math::Vector, + b: rapier2d::math::Vector, + color: DebugColor, + ) { + let color = hsla_to_linear_rgba(color); + self.vertices.push(DebugVertex { + position: [a.x * PIXELS_TO_METRES, a.y * PIXELS_TO_METRES], + color, + }); + self.vertices.push(DebugVertex { + position: [b.x * PIXELS_TO_METRES, b.y * PIXELS_TO_METRES], + color, + }); + } +} + +fn hsla_to_linear_rgba(hsla: DebugColor) -> [f32; 4] { + let [hue, saturation, lightness, alpha] = hsla; + + let hue = hue.rem_euclid(360.0) / 60.0; + let saturation = saturation.clamp(0.0, 1.0); + let lightness = lightness.clamp(0.0, 1.0); + + let chroma = (1.0 - (2.0 * lightness - 1.0).abs()) * saturation; + let second = chroma * (1.0 - (hue % 2.0 - 1.0).abs()); + let (r, g, b) = match hue as u32 { + 0 => (chroma, second, 0.0), + 1 => (second, chroma, 0.0), + 2 => (0.0, chroma, second), + 3 => (0.0, second, chroma), + 4 => (second, 0.0, chroma), + _ => (chroma, 0.0, second), + }; + let m = lightness - chroma / 2.0; + + [ + srgb_to_linear(r + m), + srgb_to_linear(g + m), + srgb_to_linear(b + m), + alpha.clamp(0.0, 1.0), + ] +} + +fn srgb_to_linear(channel: f32) -> f32 { + let channel = channel.clamp(0.0, 1.0); + if channel <= 0.04045 { + channel / 12.92 + } else { + ((channel + 0.055) / 1.055).powf(2.4) + } +} diff --git a/src/sim/rb_sim/mod.rs b/src/sim/rb_sim/mod.rs index 8101afd..48e4282 100644 --- a/src/sim/rb_sim/mod.rs +++ b/src/sim/rb_sim/mod.rs @@ -1,3 +1,4 @@ +pub mod debug_render; pub mod rb_entity; use fxhash::FxHashMap; @@ -12,10 +13,15 @@ use crate::{ config::{CELLS_IN_CHUNK, CHUNK_SIZE, PHYSICS_DELTA_TIME, PIXELS_TO_METRES}, sim::{ cell::{cell::Cell, materials::MaterialId}, - rb_sim::rb_entity::RbEntity, + rb_sim::{ + debug_render::{DebugLineBuffer, DebugVertex}, + rb_entity::RbEntity, + }, }, }; +pub use rapier2d::pipeline::DebugRenderMode; + pub struct PhysicsManager { rigid_body_set: prelude::RigidBodySet, collider_set: prelude::ColliderSet, @@ -27,6 +33,7 @@ pub struct PhysicsManager { impulse_joint_set: prelude::ImpulseJointSet, multibody_joint_set: prelude::MultibodyJointSet, ccd_solver: prelude::CCDSolver, + debug_render_pipeline: prelude::DebugRenderPipeline, } impl PhysicsManager { @@ -47,6 +54,14 @@ impl PhysicsManager { impulse_joint_set: prelude::ImpulseJointSet::new(), multibody_joint_set: prelude::MultibodyJointSet::new(), ccd_solver: prelude::CCDSolver::new(), + debug_render_pipeline: prelude::DebugRenderPipeline::new( + prelude::DebugRenderStyle { + sleep_color_multiplier: [1.0; 4], + sleep_eligible_color_multiplier: [1.0; 4], + ..prelude::DebugRenderStyle::default() + }, + prelude::DebugRenderMode::default(), + ), } } } @@ -55,6 +70,7 @@ pub struct RbSimManager { physics_manager: PhysicsManager, pub rb_entities: FxHashMap, next_id: u32, + debug_line_buffer: DebugLineBuffer, } impl RbSimManager { @@ -77,6 +93,24 @@ impl RbSimManager { ); } + pub fn debug_render(&mut self, mode: DebugRenderMode) -> &[DebugVertex] { + puffin::profile_function!(); + + let physics = &mut self.physics_manager; + self.debug_line_buffer.vertices.clear(); + physics.debug_render_pipeline.mode = mode; + physics.debug_render_pipeline.render( + &mut self.debug_line_buffer, + &physics.rigid_body_set, + &physics.collider_set, + &physics.impulse_joint_set, + &physics.multibody_joint_set, + &physics.narrow_phase, + ); + + &self.debug_line_buffer.vertices + } + pub fn create_rb_entity( &mut self, cells: Box<[Cell; CELLS_IN_CHUNK]>, @@ -175,6 +209,7 @@ impl RbSimManager { physics_manager: PhysicsManager::new(), rb_entities: FxHashMap::default(), next_id: 0, + debug_line_buffer: DebugLineBuffer::default(), } } } -- cgit v1.3.1