summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/graph.rs122
-rw-r--r--src/main.rs89
-rw-r--r--src/metrics.rs290
-rw-r--r--src/renderer.rs682
-rw-r--r--src/shader.wgsl22
-rw-r--r--src/vertex.rs27
6 files changed, 0 insertions, 1232 deletions
diff --git a/src/graph.rs b/src/graph.rs
deleted file mode 100644
index 7426bee..0000000
--- a/src/graph.rs
+++ /dev/null
@@ -1,122 +0,0 @@
-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
deleted file mode 100644
index 917515d..0000000
--- a/src/main.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-mod vertex;
-mod graph;
-mod renderer;
-mod metrics;
-
-use winit::{
- event::*,
- event_loop::EventLoop,
- keyboard::{KeyCode, PhysicalKey},
-};
-
-use renderer::State;
-
-fn main() {
- env_logger::init();
- let event_loop = EventLoop::new().unwrap();
- let window = std::sync::Arc::new(
- event_loop
- .create_window(
- winit::window::Window::default_attributes()
- .with_title("TimePlot - Waterfall Display")
- .with_inner_size(winit::dpi::LogicalSize::new(1280, 720))
- )
- .unwrap(),
- );
-
- let mut state = pollster::block_on(State::new(window.clone()));
-
- event_loop
- .run(move |event, control_flow| match event {
- Event::WindowEvent {
- ref event,
- window_id,
- } if window_id == state.window.id() => match event {
- WindowEvent::CloseRequested => control_flow.exit(),
- WindowEvent::Resized(physical_size) => {
- state.resize(*physical_size);
- }
- WindowEvent::KeyboardInput {
- event: key_event,
- ..
- } => {
- if key_event.state == winit::event::ElementState::Pressed {
- if let PhysicalKey::Code(keycode) = key_event.physical_key {
- match keycode {
- KeyCode::KeyG => {
- println!("Grid toggle pressed");
- // Toggle grid for all graphs
- for graph in &mut state.graphs {
- graph.show_grid = !graph.show_grid;
- 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(),
- _ => {}
- }
- }
- }
- }
- WindowEvent::RedrawRequested => {
- state.update();
- match state.render() {
- Ok(_) => {}
- Err(wgpu::SurfaceError::Lost) => state.resize(state.size),
- Err(wgpu::SurfaceError::OutOfMemory) => control_flow.exit(),
- Err(e) => eprintln!("{:?}", e),
- }
- }
- _ => {}
- },
- Event::AboutToWait => {
- state.window.request_redraw();
- }
- _ => {}
- })
- .unwrap();
-}
diff --git a/src/metrics.rs b/src/metrics.rs
deleted file mode 100644
index 369aff8..0000000
--- a/src/metrics.rs
+++ /dev/null
@@ -1,290 +0,0 @@
-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
deleted file mode 100644
index 2b9c0d0..0000000
--- a/src/renderer.rs
+++ /dev/null
@@ -1,682 +0,0 @@
-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/shader.wgsl b/src/shader.wgsl
deleted file mode 100644
index 6655301..0000000
--- a/src/shader.wgsl
+++ /dev/null
@@ -1,22 +0,0 @@
-struct VertexInput {
- @location(0) position: vec2<f32>,
- @location(1) color: vec3<f32>,
-}
-
-struct VertexOutput {
- @builtin(position) clip_position: vec4<f32>,
- @location(0) color: vec3<f32>,
-}
-
-@vertex
-fn vs_main(in: VertexInput) -> VertexOutput {
- var out: VertexOutput;
- out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
- out.color = in.color;
- return out;
-}
-
-@fragment
-fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
- return vec4<f32>(in.color, 1.0);
-}
diff --git a/src/vertex.rs b/src/vertex.rs
deleted file mode 100644
index 8a25c30..0000000
--- a/src/vertex.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-#[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,
- },
- ],
- }
- }
-}