summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-10-02 10:25:04 -0400
committergrothedev <grothedev@gmail.com>2025-10-02 10:25:04 -0400
commit836459dce3f50767d41978be4a2f7ac788e6a9ba (patch)
tree9fd17f4f7e0bb808f8467a14932355e3e72875ef /src
parenta162c98ce54159e3e7dbe867d908ce3276b7f633 (diff)
added metrics for rust impl. having trouble with c++ atm
Diffstat (limited to 'src')
-rw-r--r--src/graph.rs122
-rw-r--r--src/main.rs640
-rw-r--r--src/metrics.rs290
-rw-r--r--src/renderer.rs682
-rw-r--r--src/vertex.rs27
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,
+ },
+ ],
+ }
+ }
+}