diff options
| author | grothedev <grothedev@gmail.com> | 2025-10-02 10:25:04 -0400 |
|---|---|---|
| committer | grothedev <grothedev@gmail.com> | 2025-10-02 10:25:04 -0400 |
| commit | 836459dce3f50767d41978be4a2f7ac788e6a9ba (patch) | |
| tree | 9fd17f4f7e0bb808f8467a14932355e3e72875ef /src | |
| parent | a162c98ce54159e3e7dbe867d908ce3276b7f633 (diff) | |
added metrics for rust impl. having trouble with c++ atm
Diffstat (limited to 'src')
| -rw-r--r-- | src/graph.rs | 122 | ||||
| -rw-r--r-- | src/main.rs | 640 | ||||
| -rw-r--r-- | src/metrics.rs | 290 | ||||
| -rw-r--r-- | src/renderer.rs | 682 | ||||
| -rw-r--r-- | src/vertex.rs | 27 |
5 files changed, 1139 insertions, 622 deletions
diff --git a/src/graph.rs b/src/graph.rs new file mode 100644 index 0000000..7426bee --- /dev/null +++ b/src/graph.rs @@ -0,0 +1,122 @@ +use crate::vertex::Vertex; + +#[derive(Clone)] +pub struct SubView { + pub x: f32, + pub y: f32, + pub width: f32, + pub height: f32, +} + +pub struct GraphView { + pub viewport: SubView, + pub lines: Vec<Vec<Vertex>>, + pub show_grid: bool, + pub title: String, + pub x_axis_label: String, + pub y_axis_label: String, + pub legend_items: Vec<LegendItem>, +} + +#[derive(Clone)] +pub struct LegendItem { + pub label: String, + pub color: [f32; 3], +} + +impl GraphView { + pub fn new(viewport: SubView, title: String, x_axis_label: String, y_axis_label: String) -> Self { + Self { + viewport, + lines: Vec::new(), + show_grid: true, + title, + x_axis_label, + y_axis_label, + legend_items: Vec::new(), + } + } + + pub fn add_legend_item(&mut self, label: String, color: [f32; 3]) { + self.legend_items.push(LegendItem { label, color }); + } + + pub fn generate_grid_lines(&self) -> Vec<Vertex> { + let mut vertices = Vec::new(); + let grid_color = [0.3, 0.7, 0.9]; + + // Vertical grid lines (10 divisions) + for i in 0..=10 { + let x = -1.0 + (i as f32 / 10.0) * 2.0; + vertices.push(Vertex { position: [x, -1.0], color: grid_color }); + vertices.push(Vertex { position: [x, 1.0], color: grid_color }); + } + + // Horizontal grid lines (10 divisions) + for i in 0..=10 { + let y = -1.0 + (i as f32 / 10.0) * 2.0; + vertices.push(Vertex { position: [-1.0, y], color: grid_color }); + vertices.push(Vertex { position: [1.0, y], color: grid_color }); + } + + vertices + } + + pub fn generate_border(&self) -> Vec<Vertex> { + let border_color = [0.6, 0.7, 0.7]; + vec![ + // Top border + Vertex { position: [-1.0, 1.0], color: border_color }, + Vertex { position: [1.0, 1.0], color: border_color }, + // Right border + Vertex { position: [1.0, 1.0], color: border_color }, + Vertex { position: [1.0, -1.0], color: border_color }, + // Bottom border + Vertex { position: [1.0, -1.0], color: border_color }, + Vertex { position: [-1.0, -1.0], color: border_color }, + // Left border + Vertex { position: [-1.0, -1.0], color: border_color }, + Vertex { position: [-1.0, 1.0], color: border_color }, + ] + } + + pub fn update(&mut self, time: f32, graph_idx: usize) { + // Add new line every 10 frames + if (time * 60.0) as u32 % 10 == 0 && self.lines.len() < 50 { + let mut line = Vec::new(); + let phase = time + (graph_idx as f32 * 2.0); + let freq = 2.0 + (time * 0.5 + graph_idx as f32).sin() * 1.0; + + for i in 0..100 { + let x = (i as f32 / 100.0) * 2.0 - 1.0; + let y = ((i as f32) * 0.1 * freq + phase).sin() * 0.3; + + // Different color per graph + let hue = (time * 0.1 + graph_idx as f32 * 0.5) % 1.0; + let color = [ + (hue * 6.0).sin().abs(), + ((hue + 0.33) * 6.0).sin().abs(), + ((hue + 0.66) * 6.0).sin().abs(), + ]; + + line.push(Vertex { + position: [x, y], + color, + }); + } + self.lines.push(line); + } + + // Scroll lines down + for line in self.lines.iter_mut() { + for vertex in line.iter_mut() { + vertex.position[1] -= 0.01; + } + } + + // Remove lines that have scrolled off screen + self.lines.retain(|line| { + line.first().map(|v| v.position[1] > -1.1).unwrap_or(false) + }); + } +} diff --git a/src/main.rs b/src/main.rs index b74ee29..917515d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,631 +1,15 @@ +mod vertex; +mod graph; +mod renderer; +mod metrics; + use winit::{ event::*, event_loop::EventLoop, keyboard::{KeyCode, PhysicalKey}, }; -use wgpu::util::DeviceExt; -use glyphon::{ - Attrs, Buffer, Color as TextColor, Family, FontSystem, Metrics, Resolution, Shaping, - SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport -}; - -#[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] -struct Vertex { - position: [f32; 2], - color: [f32; 3], -} - -impl Vertex { - fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x2, - }, - wgpu::VertexAttribute { - offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x3, - }, - ], - } - } -} - -#[derive(Clone)] -struct SubView { - x: f32, - y: f32, - width: f32, - height: f32, -} - -struct GraphView { - viewport: SubView, - lines: Vec<Vec<Vertex>>, - show_grid: bool, - title: String, -} - -impl GraphView { - fn new(viewport: SubView, title: String) -> Self { - Self { - viewport, - lines: Vec::new(), - show_grid: true, - title, - } - } - - fn generate_grid_lines(&self) -> Vec<Vertex> { - let mut vertices = Vec::new(); - let grid_color = [0.3, 0.7, 0.9]; - - // Vertical grid lines (10 divisions) - for i in 0..=10 { - let x = -1.0 + (i as f32 / 10.0) * 2.0; - vertices.push(Vertex { position: [x, -1.0], color: grid_color }); - vertices.push(Vertex { position: [x, 1.0], color: grid_color }); - } - - // Horizontal grid lines (10 divisions) - for i in 0..=10 { - let y = -1.0 + (i as f32 / 10.0) * 2.0; - vertices.push(Vertex { position: [-1.0, y], color: grid_color }); - vertices.push(Vertex { position: [1.0, y], color: grid_color }); - } - - vertices - } - - fn generate_border(&self) -> Vec<Vertex> { - let border_color = [0.6, 0.7, 0.7]; - vec![ - // Top border - Vertex { position: [-1.0, 1.0], color: border_color }, - Vertex { position: [1.0, 1.0], color: border_color }, - // Right border - Vertex { position: [1.0, 1.0], color: border_color }, - Vertex { position: [1.0, -1.0], color: border_color }, - // Bottom border - Vertex { position: [1.0, -1.0], color: border_color }, - Vertex { position: [-1.0, -1.0], color: border_color }, - // Left border - Vertex { position: [-1.0, -1.0], color: border_color }, - Vertex { position: [-1.0, 1.0], color: border_color }, - ] - } -} - -struct State { - surface: wgpu::Surface<'static>, - device: wgpu::Device, - queue: wgpu::Queue, - config: wgpu::SurfaceConfiguration, - size: winit::dpi::PhysicalSize<u32>, - window: std::sync::Arc<winit::window::Window>, - line_pipeline: wgpu::RenderPipeline, - line_list_pipeline: wgpu::RenderPipeline, - vertex_buffer: wgpu::Buffer, - time: f32, - graphs: Vec<GraphView>, - font_system: FontSystem, - swash_cache: SwashCache, - text_atlas: TextAtlas, - text_renderer: TextRenderer, -} - -impl State { - 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, // Larger buffer for multiple views - 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 graphs = vec![ - GraphView::new( - SubView { - x: 0.0, - y: header_height, - width: 0.5, - height: graph_area_height - }, - "Frequency vs Time".to_string() - ), - GraphView::new( - SubView { - x: 0.5, - y: header_height, - width: 0.5, - height: graph_area_height - }, - "Position vs Time".to_string() - ), - ]; - - // Initialize text rendering - let mut 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, - ); - - 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, - } - } - - 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); - } - } - - fn update(&mut self) { - self.time += 0.016; // ~60fps - - // Update each graph independently - for (graph_idx, graph) in self.graphs.iter_mut().enumerate() { - // Add new line every 10 frames - if (self.time * 60.0) as u32 % 10 == 0 && graph.lines.len() < 50 { - let mut line = Vec::new(); - let phase = self.time + (graph_idx as f32 * 2.0); - let freq = 2.0 + (self.time * 0.5 + graph_idx as f32).sin() * 1.0; - - for i in 0..100 { - let x = (i as f32 / 100.0) * 2.0 - 1.0; - let y = ((i as f32) * 0.1 * freq + phase).sin() * 0.3; - // Different color per graph - let hue = (self.time * 0.1 + graph_idx as f32 * 0.5) % 1.0; - let color = [ - (hue * 6.0).sin().abs(), - ((hue + 0.33) * 6.0).sin().abs(), - ((hue + 0.66) * 6.0).sin().abs(), - ]; - - line.push(Vertex { - position: [x, y], - color, - }); - } - graph.lines.push(line); - } - - // Scroll lines down - for line in graph.lines.iter_mut() { - for vertex in line.iter_mut() { - vertex.position[1] -= 0.01; - } - } - - // Remove lines that have scrolled off screen - graph.lines.retain(|line| { - line.first().map(|v| v.position[1] > -1.1).unwrap_or(false) - }); - } - } - - fn render(&mut self) -> Result<(), wgpu::SurfaceError> { - 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(); - - for graph in &self.graphs { - 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) - let mut text_areas = Vec::new(); - - // Main header - let mut header_buffer = Buffer::new(&mut self.font_system, Metrics::new(32.0, 40.0)); - header_buffer.set_size(&mut self.font_system, Some(800.0), Some(50.0)); - header_buffer.set_text( - &mut self.font_system, - "TimePlot - Waterfall Display", - Attrs::new().family(Family::SansSerif), - Shaping::Advanced, - ); - - // Graph titles - create all buffers first - let mut graph_buffers = Vec::new(); - for graph in &self.graphs { - 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 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); - } - - // 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: 800, - bottom: 50, - }, - default_color: TextColor::rgb(255, 255, 255), - 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; - - 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: &[] - }); - } - - self.text_renderer - .prepare( - &self.device, - &self.queue, - &mut self.font_system, - &mut self.text_atlas, - &Viewport::new( - &self.device, - &glyphon::Cache::new(&self.device), - ), - 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, &Viewport::new( - &self.device, - &glyphon::Cache::new(&self.device), - ), &mut text_pass) - .expect("Failed to render text"); - } - - self.queue.submit(std::iter::once(encoder.finish())); - output.present(); - - Ok(()) - } -} +use renderer::State; fn main() { env_logger::init(); @@ -667,6 +51,18 @@ fn main() { println!("Grid now: {}", graph.show_grid); } } + KeyCode::KeyM => { + // Toggle metrics display + state.toggle_metrics(); + } + KeyCode::KeyE => { + // Export metrics to CSV + if let Err(e) = state.export_metrics("metrics.csv") { + eprintln!("Failed to export metrics: {}", e); + } else { + println!("Metrics exported to metrics.csv"); + } + } KeyCode::Escape => control_flow.exit(), _ => {} } diff --git a/src/metrics.rs b/src/metrics.rs new file mode 100644 index 0000000..369aff8 --- /dev/null +++ b/src/metrics.rs @@ -0,0 +1,290 @@ +use std::time::{Duration, Instant}; +use std::collections::VecDeque; + +/// Rolling average calculator for smooth metric display +pub struct RollingAverage { + values: VecDeque<f64>, + capacity: usize, + sum: f64, +} + +impl RollingAverage { + pub fn new(capacity: usize) -> Self { + Self { + values: VecDeque::with_capacity(capacity), + capacity, + sum: 0.0, + } + } + + pub fn push(&mut self, value: f64) { + if self.values.len() >= self.capacity { + if let Some(old) = self.values.pop_front() { + self.sum -= old; + } + } + self.values.push_back(value); + self.sum += value; + } + + pub fn average(&self) -> f64 { + if self.values.is_empty() { + 0.0 + } else { + self.sum / self.values.len() as f64 + } + } + + pub fn min(&self) -> f64 { + self.values.iter().copied().fold(f64::INFINITY, f64::min) + } + + pub fn max(&self) -> f64 { + self.values.iter().copied().fold(f64::NEG_INFINITY, f64::max) + } + + pub fn latest(&self) -> f64 { + self.values.back().copied().unwrap_or(0.0) + } +} + +/// Frame timing breakdown +#[derive(Debug, Clone)] +pub struct FrameTiming { + pub total_ms: f64, + pub update_ms: f64, + pub render_ms: f64, + pub vertex_count: usize, + pub line_count: usize, +} + +/// Performance metrics collector with rolling averages +pub struct PerformanceMetrics { + // Rolling averages (default: 60 frames) + frame_time: RollingAverage, + update_time: RollingAverage, + render_time: RollingAverage, + vertex_count: RollingAverage, + line_count: RollingAverage, + + // Session-wide statistics + pub total_frames: u64, + session_start: Instant, + + // Current frame timing + frame_start: Option<Instant>, + update_start: Option<Instant>, + render_start: Option<Instant>, + + // Historical data for export + history: VecDeque<FrameTiming>, + history_capacity: usize, +} + +impl PerformanceMetrics { + pub fn new(rolling_window: usize, history_capacity: usize) -> Self { + Self { + frame_time: RollingAverage::new(rolling_window), + update_time: RollingAverage::new(rolling_window), + render_time: RollingAverage::new(rolling_window), + vertex_count: RollingAverage::new(rolling_window), + line_count: RollingAverage::new(rolling_window), + total_frames: 0, + session_start: Instant::now(), + frame_start: None, + update_start: None, + render_start: None, + history: VecDeque::with_capacity(history_capacity), + history_capacity, + } + } + + // Frame timing markers + pub fn begin_frame(&mut self) { + self.frame_start = Some(Instant::now()); + } + + pub fn begin_update(&mut self) { + self.update_start = Some(Instant::now()); + } + + pub fn end_update(&mut self) -> f64 { + if let Some(start) = self.update_start.take() { + let duration = start.elapsed(); + duration.as_secs_f64() * 1000.0 + } else { + 0.0 + } + } + + pub fn begin_render(&mut self) { + self.render_start = Some(Instant::now()); + } + + pub fn end_render(&mut self) -> f64 { + if let Some(start) = self.render_start.take() { + let duration = start.elapsed(); + duration.as_secs_f64() * 1000.0 + } else { + 0.0 + } + } + + pub fn end_frame(&mut self, update_ms: f64, render_ms: f64, vertex_count: usize, line_count: usize) { + if let Some(start) = self.frame_start.take() { + let total_ms = start.elapsed().as_secs_f64() * 1000.0; + + // Update rolling averages + self.frame_time.push(total_ms); + self.update_time.push(update_ms); + self.render_time.push(render_ms); + self.vertex_count.push(vertex_count as f64); + self.line_count.push(line_count as f64); + + // Record to history + let timing = FrameTiming { + total_ms, + update_ms, + render_ms, + vertex_count, + line_count, + }; + + if self.history.len() >= self.history_capacity { + self.history.pop_front(); + } + self.history.push_back(timing); + + self.total_frames += 1; + } + } + + // Getters for current metrics + pub fn fps(&self) -> f64 { + let avg_frame_time = self.frame_time.average(); + if avg_frame_time > 0.0 { + 1000.0 / avg_frame_time + } else { + 0.0 + } + } + + pub fn avg_frame_time_ms(&self) -> f64 { + self.frame_time.average() + } + + pub fn avg_update_time_ms(&self) -> f64 { + self.update_time.average() + } + + pub fn avg_render_time_ms(&self) -> f64 { + self.render_time.average() + } + + pub fn avg_vertex_count(&self) -> f64 { + self.vertex_count.average() + } + + pub fn avg_line_count(&self) -> f64 { + self.line_count.average() + } + + pub fn min_fps(&self) -> f64 { + let max_frame_time = self.frame_time.max(); + if max_frame_time > 0.0 && max_frame_time.is_finite() { + 1000.0 / max_frame_time + } else { + 0.0 + } + } + + pub fn max_fps(&self) -> f64 { + let min_frame_time = self.frame_time.min(); + if min_frame_time > 0.0 && min_frame_time.is_finite() { + 1000.0 / min_frame_time + } else { + 0.0 + } + } + + pub fn session_duration(&self) -> Duration { + self.session_start.elapsed() + } + + // Export functionality + pub fn export_to_csv(&self, path: &str) -> std::io::Result<()> { + use std::io::Write; + let mut file = std::fs::File::create(path)?; + + // Write header + writeln!(file, "frame,total_ms,update_ms,render_ms,vertex_count,line_count,fps")?; + + // Write data + for (i, timing) in self.history.iter().enumerate() { + let fps = if timing.total_ms > 0.0 { + 1000.0 / timing.total_ms + } else { + 0.0 + }; + writeln!( + file, + "{},{},{},{},{},{},{}", + i, + timing.total_ms, + timing.update_ms, + timing.render_ms, + timing.vertex_count, + timing.line_count, + fps + )?; + } + + Ok(()) + } + + pub fn format_summary(&self) -> String { + format!( + "FPS: {:.1} (min: {:.1}, max: {:.1}) | Frame: {:.2}ms | Update: {:.2}ms | Render: {:.2}ms | Vertices: {:.0} | Lines: {:.0}", + self.fps(), + self.min_fps(), + self.max_fps(), + self.avg_frame_time_ms(), + self.avg_update_time_ms(), + self.avg_render_time_ms(), + self.avg_vertex_count(), + self.avg_line_count() + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rolling_average() { + let mut avg = RollingAverage::new(3); + avg.push(10.0); + avg.push(20.0); + avg.push(30.0); + assert_eq!(avg.average(), 20.0); + + avg.push(40.0); + assert_eq!(avg.average(), 30.0); // (20 + 30 + 40) / 3 + } + + #[test] + fn test_metrics_lifecycle() { + let mut metrics = PerformanceMetrics::new(60, 1000); + + metrics.begin_frame(); + metrics.begin_update(); + let update_ms = metrics.end_update(); + metrics.begin_render(); + let render_ms = metrics.end_render(); + metrics.end_frame(update_ms, render_ms, 1000, 10); + + assert_eq!(metrics.total_frames, 1); + assert!(metrics.fps() > 0.0); + } +} diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..2b9c0d0 --- /dev/null +++ b/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(()) + } +} diff --git a/src/vertex.rs b/src/vertex.rs new file mode 100644 index 0000000..8a25c30 --- /dev/null +++ b/src/vertex.rs @@ -0,0 +1,27 @@ +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct Vertex { + pub position: [f32; 2], + pub color: [f32; 3], +} + +impl Vertex { + pub fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 1, + format: wgpu::VertexFormat::Float32x3, + }, + ], + } + } +} |
