diff options
Diffstat (limited to 'rs/src/renderer.rs')
| -rw-r--r-- | rs/src/renderer.rs | 682 |
1 files changed, 682 insertions, 0 deletions
diff --git a/rs/src/renderer.rs b/rs/src/renderer.rs new file mode 100644 index 0000000..2b9c0d0 --- /dev/null +++ b/rs/src/renderer.rs @@ -0,0 +1,682 @@ +use glyphon::{ + Attrs, Buffer, Color as TextColor, Family, FontSystem, Metrics, Resolution, Shaping, + SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport +}; +use crate::vertex::Vertex; +use crate::graph::{GraphView, SubView}; +use crate::metrics::PerformanceMetrics; + +pub struct State { + surface: wgpu::Surface<'static>, + device: wgpu::Device, + queue: wgpu::Queue, + config: wgpu::SurfaceConfiguration, + pub size: winit::dpi::PhysicalSize<u32>, + pub window: std::sync::Arc<winit::window::Window>, + line_pipeline: wgpu::RenderPipeline, + line_list_pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + time: f32, + pub graphs: Vec<GraphView>, + font_system: FontSystem, + swash_cache: SwashCache, + text_atlas: TextAtlas, + text_renderer: TextRenderer, + viewport: Viewport, + metrics: PerformanceMetrics, + show_metrics: bool, +} + +impl State { + pub async fn new(window: std::sync::Arc<winit::window::Window>) -> Self { + let size = window.inner_size(); + + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::VULKAN, + ..Default::default() + }); + + let surface = instance.create_surface(window.clone()).unwrap(); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + required_features: wgpu::Features::empty(), + required_limits: wgpu::Limits::default(), + label: None, + memory_hints: Default::default(), + }, + None, + ) + .await + .unwrap(); + + let surface_caps = surface.get_capabilities(&adapter); + let surface_format = surface_caps + .formats + .iter() + .copied() + .find(|f| f.is_srgb()) + .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: surface_caps.present_modes[0], + alpha_mode: surface_caps.alpha_modes[0], + view_formats: vec![], + desired_maximum_frame_latency: 2, + }; + surface.configure(&device, &config); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Waterfall 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: &[], + push_constant_ranges: &[], + }); + + let line_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Line Strip Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::LineStrip, + 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 { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + let line_list_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Line List Pipeline"), + layout: Some(&render_pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::REPLACE), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::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 { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + cache: None, + }); + + // Create initial empty buffer (will be updated each frame) + let vertex_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Vertex Buffer"), + size: (std::mem::size_of::<Vertex>() * 100 * 100) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Create 2 graph views side-by-side with header area + // Reserve top 60px for header + let header_height = 60.0 / size.height as f32; + let graph_area_height = 1.0 - header_height; + + let mut graphs = vec![ + GraphView::new( + SubView { + x: 0.0, + y: header_height, + width: 0.5, + height: graph_area_height + }, + "Frequency vs Time".to_string(), + "Time (s)".to_string(), + "Frequency (Hz)".to_string() + ), + GraphView::new( + SubView { + x: 0.5, + y: header_height, + width: 0.5, + height: graph_area_height + }, + "Position vs Time".to_string(), + "Time (s)".to_string(), + "Position (m)".to_string() + ), + ]; + + // Add legend items + graphs[0].add_legend_item("Signal A".to_string(), [1.0, 0.3, 0.3]); + graphs[0].add_legend_item("Signal B".to_string(), [0.3, 1.0, 0.3]); + graphs[1].add_legend_item("Object 1".to_string(), [0.3, 0.6, 1.0]); + graphs[1].add_legend_item("Object 2".to_string(), [1.0, 0.8, 0.3]); + + // Initialize text rendering + let font_system = FontSystem::new(); + let swash_cache = SwashCache::new(); + let cache = glyphon::Cache::new(&device); + let mut text_atlas = TextAtlas::new(&device, &queue, &cache, config.format); + let text_renderer = TextRenderer::new( + &mut text_atlas, + &device, + wgpu::MultisampleState::default(), + None, + ); + + let mut viewport = Viewport::new(&device, &cache); + + // Initialize viewport with current resolution + viewport.update( + &queue, + Resolution { + width: size.width, + height: size.height, + }, + ); + + Self { + surface, + device, + queue, + config, + size, + window, + line_pipeline, + line_list_pipeline, + vertex_buffer, + time: 0.0, + graphs, + font_system, + swash_cache, + text_atlas, + text_renderer, + viewport, + metrics: PerformanceMetrics::new(60, 10000), // 60 frame rolling avg, 10k frame history + show_metrics: true, // Show metrics by default + } + } + + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) { + if new_size.width > 0 && new_size.height > 0 { + self.size = new_size; + self.config.width = new_size.width; + self.config.height = new_size.height; + self.surface.configure(&self.device, &self.config); + + // Update text viewport resolution + self.viewport.update( + &self.queue, + Resolution { + width: new_size.width, + height: new_size.height, + }, + ); + } + } + + pub fn update(&mut self) { + self.metrics.begin_update(); + + self.time += 0.016; // ~60fps + + // Update each graph independently + for (graph_idx, graph) in self.graphs.iter_mut().enumerate() { + graph.update(self.time, graph_idx); + } + } + + pub fn toggle_metrics(&mut self) { + self.show_metrics = !self.show_metrics; + println!("Metrics display: {}", if self.show_metrics { "ON" } else { "OFF" }); + } + + pub fn export_metrics(&self, path: &str) -> std::io::Result<()> { + self.metrics.export_to_csv(path) + } + + pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> { + self.metrics.begin_frame(); + + // End update timing from previous update() call + let update_ms = self.metrics.end_update(); + + self.metrics.begin_render(); + + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + // Collect all vertex data for all graphs + struct DrawData { + viewport: SubView, + border_offset: usize, + border_count: usize, + grid_offset: usize, + grid_count: usize, + show_grid: bool, + lines_offset: usize, + lines_count: usize, + num_lines: usize, + } + + let mut all_vertices = Vec::new(); + let mut draw_data = Vec::new(); + let mut total_line_count = 0; + + for graph in &self.graphs { + total_line_count += graph.lines.len(); + let border_vertices = graph.generate_border(); + let border_offset = all_vertices.len(); + all_vertices.extend_from_slice(&border_vertices); + let border_count = border_vertices.len(); + + let grid_offset = all_vertices.len(); + let grid_vertices = graph.generate_grid_lines(); + all_vertices.extend_from_slice(&grid_vertices); + let grid_count = grid_vertices.len(); + + let lines_offset = all_vertices.len(); + let mut line_vertices = Vec::new(); + for line in &graph.lines { + line_vertices.extend_from_slice(line); + } + all_vertices.extend_from_slice(&line_vertices); + let lines_count = line_vertices.len(); + + draw_data.push(DrawData { + viewport: graph.viewport.clone(), + border_offset, + border_count, + grid_offset, + grid_count, + show_grid: graph.show_grid, + lines_offset, + lines_count, + num_lines: graph.lines.len(), + }); + } + + // Write all vertices at once + if !all_vertices.is_empty() { + self.queue.write_buffer( + &self.vertex_buffer, + 0, + bytemuck::cast_slice(&all_vertices), + ); + } + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.1, + g: 0.1, + b: 0.15, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + // Render each graph view + for data in &draw_data { + // Set viewport for this graph + render_pass.set_viewport( + data.viewport.x * self.size.width as f32, + data.viewport.y * self.size.height as f32, + data.viewport.width * self.size.width as f32, + data.viewport.height * self.size.height as f32, + 0.0, + 1.0, + ); + + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + + // Draw border + render_pass.set_pipeline(&self.line_list_pipeline); + render_pass.draw( + data.border_offset as u32..(data.border_offset + data.border_count) as u32, + 0..1, + ); + + // Draw grid if enabled + if data.show_grid { + render_pass.set_pipeline(&self.line_list_pipeline); + render_pass.draw( + data.grid_offset as u32..(data.grid_offset + data.grid_count) as u32, + 0..1, + ); + } + + // Draw waterfall lines + if data.lines_count > 0 { + render_pass.set_pipeline(&self.line_pipeline); + let points_per_line = 100; + for i in 0..data.num_lines { + let start = (data.lines_offset + i * points_per_line) as u32; + let end = start + points_per_line as u32; + render_pass.draw(start..end, 0..1); + } + } + } + } + + // Render text (header and labels) + self.viewport.update( + &self.queue, + Resolution { + width: self.size.width, + height: self.size.height, + }, + ); + + let mut text_areas = Vec::new(); + + // Main header + let mut header_buffer = Buffer::new(&mut self.font_system, Metrics::new(24.0, 32.0)); + header_buffer.set_size(&mut self.font_system, Some(500.0), Some(40.0)); + header_buffer.set_text( + &mut self.font_system, + "TimePlot - Waterfall Display", + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + + // Performance metrics (if enabled) + let mut metrics_buffer = Buffer::new(&mut self.font_system, Metrics::new(11.0, 14.0)); + if self.show_metrics { + let metrics_text = self.metrics.format_summary(); + metrics_buffer.set_size(&mut self.font_system, Some(self.size.width as f32 - 520.0), Some(40.0)); + metrics_buffer.set_text( + &mut self.font_system, + &metrics_text, + Attrs::new().family(Family::Monospace), + Shaping::Advanced, + ); + } + + // Graph titles and labels - create all buffers first + let mut graph_buffers = Vec::new(); + let mut x_axis_buffers = Vec::new(); + let mut y_axis_buffers = Vec::new(); + let mut legend_buffers = Vec::new(); + + for graph in &self.graphs { + let width = graph.viewport.width * self.size.width as f32; + let height = graph.viewport.height * self.size.height as f32; + + // Title + let mut title_buffer = Buffer::new(&mut self.font_system, Metrics::new(18.0, 24.0)); + title_buffer.set_size(&mut self.font_system, Some(width), Some(30.0)); + title_buffer.set_text( + &mut self.font_system, + &graph.title, + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + graph_buffers.push(title_buffer); + + // X-axis label + let mut x_buffer = Buffer::new(&mut self.font_system, Metrics::new(14.0, 18.0)); + x_buffer.set_size(&mut self.font_system, Some(width), Some(20.0)); + x_buffer.set_text( + &mut self.font_system, + &graph.x_axis_label, + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + x_axis_buffers.push(x_buffer); + + // Y-axis label + let mut y_buffer = Buffer::new(&mut self.font_system, Metrics::new(14.0, 18.0)); + y_buffer.set_size(&mut self.font_system, Some(height * 0.5), Some(20.0)); + y_buffer.set_text( + &mut self.font_system, + &graph.y_axis_label, + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + y_axis_buffers.push(y_buffer); + + // Legend + let mut legend_text = String::new(); + for (i, item) in graph.legend_items.iter().enumerate() { + if i > 0 { legend_text.push_str(" "); } + legend_text.push_str(&format!("● {}", item.label)); + } + let mut legend_buffer = Buffer::new(&mut self.font_system, Metrics::new(12.0, 16.0)); + legend_buffer.set_size(&mut self.font_system, Some(width * 0.8), Some(20.0)); + legend_buffer.set_text( + &mut self.font_system, + &legend_text, + Attrs::new().family(Family::SansSerif), + Shaping::Advanced, + ); + legend_buffers.push(legend_buffer); + } + + // Now create text areas with references to the buffers + text_areas.push(TextArea { + buffer: &header_buffer, + left: 10.0, + top: 15.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 500, + bottom: 40, + }, + default_color: TextColor::rgb(255, 255, 255), + custom_glyphs: &[] + }); + + // Metrics display + if self.show_metrics { + text_areas.push(TextArea { + buffer: &metrics_buffer, + left: 520.0, + top: 18.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: (self.size.width as i32 - 520).max(0), + bottom: 40, + }, + default_color: TextColor::rgb(100, 255, 100), + custom_glyphs: &[] + }); + } + + for (i, graph) in self.graphs.iter().enumerate() { + let x_offset = graph.viewport.x * self.size.width as f32; + let y_offset = graph.viewport.y * self.size.height as f32; + let width = graph.viewport.width * self.size.width as f32; + let height = graph.viewport.height * self.size.height as f32; + + // Graph title + text_areas.push(TextArea { + buffer: &graph_buffers[i], + left: x_offset + 10.0, + top: y_offset + 5.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: width as i32, + bottom: 30, + }, + default_color: TextColor::rgb(230, 230, 230), + custom_glyphs: &[] + }); + + // X-axis label (bottom center) + text_areas.push(TextArea { + buffer: &x_axis_buffers[i], + left: x_offset + width * 0.5 - 50.0, + top: y_offset + height - 20.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 200, + bottom: 20, + }, + default_color: TextColor::rgb(200, 200, 200), + custom_glyphs: &[] + }); + + // Y-axis label (left center, rotated appearance via positioning) + text_areas.push(TextArea { + buffer: &y_axis_buffers[i], + left: x_offset + 5.0, + top: y_offset + height * 0.3, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: 150, + bottom: 20, + }, + default_color: TextColor::rgb(200, 200, 200), + custom_glyphs: &[] + }); + + // Legend (top right) + text_areas.push(TextArea { + buffer: &legend_buffers[i], + left: x_offset + width * 0.3, + top: y_offset + 30.0, + scale: 1.0, + bounds: TextBounds { + left: 0, + top: 0, + right: (width * 0.7) as i32, + bottom: 20, + }, + default_color: TextColor::rgb(220, 220, 220), + custom_glyphs: &[] + }); + } + + self.text_renderer + .prepare( + &self.device, + &self.queue, + &mut self.font_system, + &mut self.text_atlas, + &self.viewport, + text_areas, + &mut self.swash_cache, + ) + .expect("Failed to prepare text"); + + { + let mut text_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Text Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + occlusion_query_set: None, + timestamp_writes: None, + }); + + self.text_renderer + .render(&self.text_atlas, &self.viewport, &mut text_pass) + .expect("Failed to render text"); + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + // Finalize metrics + let render_ms = self.metrics.end_render(); + let vertex_count = all_vertices.len(); + self.metrics.end_frame(update_ms, render_ms, vertex_count, total_line_count); + + Ok(()) + } +} |
