use std::sync::Arc; use wgpu::util::DeviceExt; use winit::{ event::{Event, WindowEvent}, event_loop::EventLoop, window::Window, }; use std::sync::mpsc; use std::time::{Duration, Instant}; use serde::{Deserialize, Serialize}; use std::any::Any; mod data_sources; use data_sources::{DataEvent, DataSourceManager}; #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] struct Vertex { position: [f32; 2], tex_coords: [f32; 2], } #[derive(Clone, Copy)] pub struct Rect { pub x: f32, pub y: f32, pub width: f32, pub height: f32, } impl Rect { pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { Self { x, y, width, height } } pub fn contains(&self, px: f32, py: f32) -> bool { px >= self.x && px <= self.x + self.width && py >= self.y && py <= self.y + self.height } } pub trait Component { fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView, bounds: &Rect); fn update(&mut self, dt: f32); fn get_bounds(&self) -> &Rect; fn set_bounds(&mut self, bounds: Rect); fn set_window_size(&mut self, window_size: (u32, u32)); fn consume_data_events(&mut self, events: Vec); fn as_any_mut(&mut self) -> &mut dyn std::any::Any; } pub struct PlotComponent { bounds: Rect, data_texture: wgpu::Texture, bind_group: wgpu::BindGroup, render_pipeline: wgpu::RenderPipeline, vertex_buffer: wgpu::Buffer, gridlines_pipeline: wgpu::RenderPipeline, gridlines_vertex_buffer: wgpu::Buffer, time: f32, device: Arc, queue: Arc, window_size: (u32, u32), data_buffer: Vec>, // 2D buffer for waterfall data buffer_width: usize, buffer_height: usize, data_range: (f64, f64), // min, max values for normalization data_manager: DataSourceManager, } impl PlotComponent { pub fn new( device: Arc, queue: Arc, bounds: Rect, window_size: (u32, u32), surface_format: wgpu::TextureFormat, data_source_config: Option, ) -> Self { let data_texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("Plot Data Texture"), size: wgpu::Extent3d { width: 256, height: 256, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: wgpu::TextureFormat::Rgba8Unorm, usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, view_formats: &[], }); let sampler = device.create_sampler(&wgpu::SamplerDescriptor { address_mode_u: wgpu::AddressMode::Repeat, address_mode_v: wgpu::AddressMode::Repeat, mag_filter: wgpu::FilterMode::Linear, min_filter: wgpu::FilterMode::Linear, mipmap_filter: wgpu::FilterMode::Nearest, ..Default::default() }); let texture_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Texture { multisampled: false, view_dimension: wgpu::TextureViewDimension::D2, sample_type: wgpu::TextureSampleType::Float { filterable: true }, }, count: None, }, wgpu::BindGroupLayoutEntry { binding: 1, visibility: wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), count: None, }, ], label: Some("texture_bind_group_layout"), }); let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { layout: &texture_bind_group_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, resource: wgpu::BindingResource::TextureView( &data_texture.create_view(&wgpu::TextureViewDescriptor::default()) ), }, wgpu::BindGroupEntry { binding: 1, resource: wgpu::BindingResource::Sampler(&sampler), }, ], label: Some("plot_bind_group"), }); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("Plot Shader"), source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), }); let render_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Plot Render Pipeline Layout"), bind_group_layouts: &[&texture_bind_group_layout], push_constant_ranges: &[], }); let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Plot Render Pipeline"), layout: Some(&render_pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: "vs_main", buffers: &[Vertex::desc()], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_main", targets: &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::REPLACE), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleStrip, 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 gridlines_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Gridlines Pipeline Layout"), bind_group_layouts: &[], push_constant_ranges: &[], }); let gridlines_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("Gridlines Pipeline"), layout: Some(&gridlines_pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: "vs_main", buffers: &[Vertex::desc()], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: "fs_gridlines", targets: &[Some(wgpu::ColorTargetState { format: surface_format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::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, }); let vertices = Self::create_quad_vertices(&bounds, window_size); let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Plot Vertex Buffer"), contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); let gridlines_vertices = Self::create_gridlines(&bounds, window_size); let gridlines_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Gridlines Vertex Buffer"), contents: bytemuck::cast_slice(&gridlines_vertices), usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, }); let buffer_width = 256; let buffer_height = 256; let data_buffer = vec![vec![0.0; buffer_width]; buffer_height]; let mut data_manager = DataSourceManager::new(1000); // Add data source if provided if let Some(config) = data_source_config { use data_sources::{FileDataSource}; let source = Box::new(FileDataSource::new(config)); if data_manager.add_source(source).is_err() { eprintln!("Failed to add data source to plot component"); } } Self { bounds, data_texture, bind_group, render_pipeline, vertex_buffer, gridlines_pipeline, gridlines_vertex_buffer, time: 0.0, device, queue, window_size, data_buffer, buffer_width, buffer_height, data_range: (0.0, 1.0), data_manager, } } fn create_quad_vertices(bounds: &Rect, window_size: (u32, u32)) -> [Vertex; 4] { let left = (bounds.x / window_size.0 as f32) * 2.0 - 1.0; let right = ((bounds.x + bounds.width) / window_size.0 as f32) * 2.0 - 1.0; let top = 1.0 - (bounds.y / window_size.1 as f32) * 2.0; let bottom = 1.0 - ((bounds.y + bounds.height) / window_size.1 as f32) * 2.0; [ Vertex { position: [left, bottom], tex_coords: [0.0, 1.0] }, Vertex { position: [right, bottom], tex_coords: [1.0, 1.0] }, Vertex { position: [left, top], tex_coords: [0.0, 0.0] }, Vertex { position: [right, top], tex_coords: [1.0, 0.0] }, ] } fn create_gridlines(bounds: &Rect, window_size: (u32, u32)) -> Vec { let mut vertices = Vec::new(); let left = (bounds.x / window_size.0 as f32) * 2.0 - 1.0; let right = ((bounds.x + bounds.width) / window_size.0 as f32) * 2.0 - 1.0; let top = 1.0 - (bounds.y / window_size.1 as f32) * 2.0; let bottom = 1.0 - ((bounds.y + bounds.height) / window_size.1 as f32) * 2.0; let grid_lines = 10; for i in 0..=grid_lines { let t = i as f32 / grid_lines as f32; let x = left + (right - left) * t; vertices.push(Vertex { position: [x, top], tex_coords: [0.0, 0.0] }); vertices.push(Vertex { position: [x, bottom], tex_coords: [0.0, 0.0] }); let y = top + (bottom - top) * t; vertices.push(Vertex { position: [left, y], tex_coords: [0.0, 0.0] }); vertices.push(Vertex { position: [right, y], tex_coords: [0.0, 0.0] }); } vertices } fn update_vertices(&self) { let vertices = Self::create_quad_vertices(&self.bounds, self.window_size); self.queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(&vertices)); let gridlines_vertices = Self::create_gridlines(&self.bounds, self.window_size); self.queue.write_buffer(&self.gridlines_vertex_buffer, 0, bytemuck::cast_slice(&gridlines_vertices)); } pub fn add_data_events(&mut self, events: Vec) { if events.is_empty() { return; } // Update data range based on new events for event in &events { if event.value < self.data_range.0 { self.data_range.0 = event.value; } if event.value > self.data_range.1 { self.data_range.1 = event.value; } } // Shift existing data down (waterfall effect) for _ in 0..events.len() { if self.data_buffer.len() >= self.buffer_height { self.data_buffer.remove(0); } } // Add new data rows for event in events { let mut row = vec![0.0; self.buffer_width]; // For now, just fill the entire row with the event value // Later we can implement frequency domain or other representations for i in 0..self.buffer_width { row[i] = event.value; } self.data_buffer.push(row); } // Ensure we don't exceed buffer height while self.data_buffer.len() > self.buffer_height { self.data_buffer.remove(0); } self.update_texture(); } fn update_texture(&self) { let mut texture_data = vec![0u8; self.buffer_width * self.buffer_height * 4]; let range = self.data_range.1 - self.data_range.0; let range = if range == 0.0 { 1.0 } else { range }; for y in 0..self.buffer_height { for x in 0..self.buffer_width { let idx = (y * self.buffer_width + x) * 4; let value = if y < self.data_buffer.len() && x < self.data_buffer[y].len() { self.data_buffer[y][x] } else { 0.0 }; // Normalize value to 0-1 range let normalized = ((value - self.data_range.0) / range).clamp(0.0, 1.0); // Create a color gradient (blue to red) let intensity = (normalized * 255.0) as u8; texture_data[idx] = if normalized > 0.5 { ((normalized - 0.5) * 2.0 * 255.0) as u8 } else { 0 }; // Red texture_data[idx + 1] = if normalized > 0.3 && normalized < 0.7 { (((0.7 - (normalized - 0.3).abs()) / 0.4) * 255.0) as u8 } else { 0 }; // Green texture_data[idx + 2] = if normalized < 0.5 { ((0.5 - normalized) * 2.0 * 255.0) as u8 } else { intensity / 4 }; // Blue texture_data[idx + 3] = 255; // Alpha } } self.queue.write_texture( wgpu::ImageCopyTexture { texture: &self.data_texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, &texture_data, wgpu::ImageDataLayout { offset: 0, bytes_per_row: Some(self.buffer_width as u32 * 4), rows_per_image: Some(self.buffer_height as u32), }, wgpu::Extent3d { width: self.buffer_width as u32, height: self.buffer_height as u32, depth_or_array_layers: 1, }, ); } } impl Component for PlotComponent { fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView, _bounds: &Rect) { let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Plot Render Pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { 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, }); render_pass.set_pipeline(&self.render_pipeline); render_pass.set_bind_group(0, &self.bind_group, &[]); render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.draw(0..4, 0..1); render_pass.set_pipeline(&self.gridlines_pipeline); render_pass.set_vertex_buffer(0, self.gridlines_vertex_buffer.slice(..)); let gridlines_count = (11 * 4) as u32; render_pass.draw(0..gridlines_count, 0..1); } fn update(&mut self, dt: f32) { self.time += dt; // Pull data from this component's own data manager let events = self.data_manager.get_events(10); if !events.is_empty() { self.add_data_events(events); } } fn get_bounds(&self) -> &Rect { &self.bounds } fn set_bounds(&mut self, bounds: Rect) { self.bounds = bounds; self.update_vertices(); } fn set_window_size(&mut self, window_size: (u32, u32)) { self.window_size = window_size; self.update_vertices(); } fn consume_data_events(&mut self, events: Vec) { self.add_data_events(events); } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { self } } impl PlotComponent { pub async fn start_data_sources(&mut self) -> Result<(), Box> { self.data_manager.start_all().await } pub async fn stop_data_sources(&mut self) -> Result<(), Box> { self.data_manager.stop_all().await } } pub struct TileManager { pub components: Vec>, window_size: (u32, u32), } impl TileManager { pub fn new(window_size: (u32, u32)) -> Self { Self { components: Vec::new(), window_size, } } pub fn add_component(&mut self, component: Box) { self.components.push(component); } pub fn arrange_tiles(&mut self) { let count = self.components.len(); if count == 0 { return; } let cols = (count as f32).sqrt().ceil() as usize; let rows = (count + cols - 1) / cols; let tile_width = self.window_size.0 as f32 / cols as f32; let tile_height = self.window_size.1 as f32 / rows as f32; for (i, component) in self.components.iter_mut().enumerate() { let col = i % cols; let row = i / cols; let bounds = Rect::new( col as f32 * tile_width, row as f32 * tile_height, tile_width, tile_height, ); component.set_bounds(bounds); } } pub fn resize(&mut self, new_size: (u32, u32)) { self.window_size = new_size; for component in &mut self.components { component.set_window_size(new_size); } self.arrange_tiles(); } pub fn render(&mut self, encoder: &mut wgpu::CommandEncoder, view: &wgpu::TextureView) { for component in &mut self.components { let bounds = *component.get_bounds(); component.render(encoder, view, &bounds); } } pub fn update(&mut self, dt: f32) { for component in &mut self.components { component.update(dt); } } } impl Vertex { const ATTRIBUTES: [wgpu::VertexAttribute; 2] = [ 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::Float32x2, }, ]; fn desc() -> wgpu::VertexBufferLayout<'static> { wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as wgpu::BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &Self::ATTRIBUTES, } } } struct State { surface: wgpu::Surface<'static>, device: Arc, queue: Arc, config: wgpu::SurfaceConfiguration, size: winit::dpi::PhysicalSize, tile_manager: TileManager, } impl State { async fn new(window: Arc) -> Self { let size = window.inner_size(); let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { backends: wgpu::Backends::all(), ..Default::default() }); let surface = instance.create_surface(window.clone()).unwrap(); let adapter = instance .request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::default(), 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(), memory_hints: Default::default(), label: None, }, None, ) .await .unwrap(); let surface_caps = surface.get_capabilities(&adapter); let surface_format = surface_caps .formats .iter() .find(|f| f.is_srgb()) .copied() .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 device = Arc::new(device); let queue = Arc::new(queue); let mut tile_manager = TileManager::new((size.width, size.height)); // Create data source configurations for each plot use data_sources::{DataSourceConfig, FileFormat}; let data_configs = vec![ Some(DataSourceConfig::File { path: "data1.txt".to_string(), interval_ms: 300, format: FileFormat::PlainText, }), Some(DataSourceConfig::File { path: "data2.txt".to_string(), interval_ms: 400, format: FileFormat::PlainText, }), Some(DataSourceConfig::File { path: "data3.txt".to_string(), interval_ms: 200, format: FileFormat::PlainText, }), Some(DataSourceConfig::File { path: "data4.txt".to_string(), interval_ms: 600, format: FileFormat::Json, }), ]; for data_config in data_configs.into_iter() { let plot = PlotComponent::new( device.clone(), queue.clone(), Rect::new(0.0, 0.0, size.width as f32, size.height as f32), (size.width, size.height), config.format, data_config, ); tile_manager.add_component(Box::new(plot)); } tile_manager.arrange_tiles(); Self { surface, device, queue, config, size, tile_manager, } } pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { 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); self.tile_manager.resize((new_size.width, new_size.height)); } } fn update(&mut self) { self.tile_manager.update(0.016); } 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"), }); { let _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("Clear 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.2, b: 0.3, a: 1.0, }), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, occlusion_query_set: None, timestamp_writes: None, }); } self.tile_manager.render(&mut encoder, &view); self.queue.submit(std::iter::once(encoder.finish())); output.present(); Ok(()) } pub async fn setup_data_sources(&mut self) -> Result<(), Box> { // Start data sources for each plot component for component in &mut self.tile_manager.components { if let Some(plot) = component.as_any_mut().downcast_mut::() { if let Err(e) = plot.start_data_sources().await { eprintln!("Failed to start data sources for component: {}", e); // Continue with other components even if one fails } } } Ok(()) } } #[tokio::main] async fn main() -> anyhow::Result<()> { let event_loop = EventLoop::new()?; let window = Arc::new(event_loop.create_window(Window::default_attributes() .with_title("Timeplot - Simple Waterfall"))?); let mut state = pollster::block_on(State::new(window.clone())); // Setup data sources if let Err(e) = pollster::block_on(state.setup_data_sources()) { eprintln!("Failed to setup data sources: {}", e); // Continue running without data sources for now } #[allow(deprecated)] event_loop.run(move |event, elwt| { match event { Event::WindowEvent { event, window_id, } if window_id == window.id() => match event { WindowEvent::CloseRequested => elwt.exit(), WindowEvent::Resized(physical_size) => { state.resize(physical_size); } WindowEvent::RedrawRequested => { state.update(); match state.render() { Ok(_) => {} Err(wgpu::SurfaceError::Lost) => state.resize(state.size), Err(wgpu::SurfaceError::OutOfMemory) => elwt.exit(), Err(e) => eprintln!("{:?}", e), } } _ => {} }, Event::AboutToWait => { window.request_redraw(); } _ => {} } })?; Ok(()) }