summaryrefslogtreecommitdiff
path: root/rs/src/renderer.rs
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-10-03 20:28:33 -0400
committergrothedev <grothedev@gmail.com>2025-10-03 20:28:33 -0400
commit0d58ccc4e90b93fa1498a3fa97712697ac606775 (patch)
treec303ee4ff66fa0930869efe2160a49c561082770 /rs/src/renderer.rs
parentbcc93c8d058e6db05e32e262760d0903a721e402 (diff)
move rust proj into own fowdowh
Diffstat (limited to 'rs/src/renderer.rs')
-rw-r--r--rs/src/renderer.rs682
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(())
+ }
+}