summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs640
1 files changed, 18 insertions, 622 deletions
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(),
_ => {}
}