From 93c0851ceed6f95159a3cd2ad70a3f6a1fb4b187 Mon Sep 17 00:00:00 2001 From: Lauda Carly Date: Sat, 18 Apr 2026 16:42:53 +0200 Subject: [PATCH 1/4] Add plot renderer --- Cargo.toml | 3 +- shaders/audio.comp | 7 +- shaders/audio_background.frag | 17 ++ shaders/audio_plot.frag | 17 ++ shaders/audio_plot.vert | 39 ++++ shaders/minmax_audio_pixels.comp | 48 +++++ src/main.rs | 104 ++++------ src/plot_renderer.rs | 344 +++++++++++++++++++++++++++++++ 8 files changed, 506 insertions(+), 73 deletions(-) create mode 100644 shaders/audio_background.frag create mode 100644 shaders/audio_plot.frag create mode 100644 shaders/audio_plot.vert create mode 100644 shaders/minmax_audio_pixels.comp create mode 100644 src/plot_renderer.rs diff --git a/Cargo.toml b/Cargo.toml index 39222a8..21b4308 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,8 @@ keywords = ["compute", "vulkan", "render", "engine", "synth"] categories = ["rendering::engine"] [dependencies] -cen = { git = "https://github.com/n-e-l/cen", rev = "62ed91da74058bb8401d1d3e03b45a1a4d658cb2" } +#cen = { git = "https://github.com/n-e-l/cen", rev = "62ed91da74058bb8401d1d3e03b45a1a4d658cb2" } +cen = { path = "../cen" } cpal = "0.13.4" bytemuck = { version = "1.17.0", features = ["derive"] } egui_plot = "0.34.1" diff --git a/shaders/audio.comp b/shaders/audio.comp index 893e4ad..ae9ad9e 100644 --- a/shaders/audio.comp +++ b/shaders/audio.comp @@ -9,7 +9,8 @@ layout( std430, binding = 0 ) buffer AudioBuffer { layout( push_constant ) uniform PushConstants { float time; - int samples; + int samples_per_second; + int total_samples; float frequency; float volume; float[4] options; @@ -20,6 +21,6 @@ audio_function void main() { int i = int( gl_GlobalInvocationID.x ); - if( i > constants.samples ) return; - audio_data.data[i] = audio(constants.time + float( i ) / constants.samples, constants.volume, constants.frequency, constants.options).r; + if( i > constants.total_samples ) return; + audio_data.data[i] = audio(constants.time + float( i ) / constants.samples_per_second, constants.volume, constants.frequency, constants.options).r; } diff --git a/shaders/audio_background.frag b/shaders/audio_background.frag new file mode 100644 index 0000000..014d42d --- /dev/null +++ b/shaders/audio_background.frag @@ -0,0 +1,17 @@ +#version 450 + +layout( push_constant ) uniform PushConstants +{ + uint samples; + uint pixels_x; + uint pixels_y; + float zoom; + uint offset; +} constants; + +layout(location = 0) out vec4 outColor; + +void main() +{ + outColor = vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/shaders/audio_plot.frag b/shaders/audio_plot.frag new file mode 100644 index 0000000..014d42d --- /dev/null +++ b/shaders/audio_plot.frag @@ -0,0 +1,17 @@ +#version 450 + +layout( push_constant ) uniform PushConstants +{ + uint samples; + uint pixels_x; + uint pixels_y; + float zoom; + uint offset; +} constants; + +layout(location = 0) out vec4 outColor; + +void main() +{ + outColor = vec4(1.0, 0.0, 0.0, 1.0); +} diff --git a/shaders/audio_plot.vert b/shaders/audio_plot.vert new file mode 100644 index 0000000..b6259ca --- /dev/null +++ b/shaders/audio_plot.vert @@ -0,0 +1,39 @@ +#version 450 + +layout( push_constant ) uniform PushConstants +{ + uint samples; + uint pixels_x; + uint pixels_y; + float zoom; + uint offset; +} constants; + +layout( std430, binding = 0 ) readonly buffer MinMaxBuffer { + vec2[] data; +} minmax_data; + +vec2 vertex[6] = vec2[]( + vec2( 0.0, 0.0), + vec2( 1.0, 0.0), + vec2( 0.0, 1.0), + + vec2( 1.0, 0.0), + vec2( 1.0, 1.0), + vec2( 0.0, 1.0) +); + +void main() +{ + float pixelwidth = 2. / float(constants.pixels_x); + float pixelheight = 2. / float(constants.pixels_y); + + float x = 2. * gl_InstanceIndex / constants.pixels_x - 1.; + + vec2 minmax = minmax_data.data[gl_InstanceIndex]; + + float height = max(minmax[1] - minmax[0], pixelheight * 1.); + vec2 pos = vec2(x, minmax[0]) + vertex[gl_VertexIndex] * vec2(pixelwidth, height); + + gl_Position = vec4(pos, 0.0, 1.0); +} diff --git a/shaders/minmax_audio_pixels.comp b/shaders/minmax_audio_pixels.comp new file mode 100644 index 0000000..4a6e0d0 --- /dev/null +++ b/shaders/minmax_audio_pixels.comp @@ -0,0 +1,48 @@ +#version 450 + +layout ( local_size_x = 64, local_size_y = 1, local_size_z = 1 ) in; + +layout( std430, binding = 0 ) buffer AudioBuffer { + float[] data; +} audio_data; + +layout( std430, binding = 1 ) buffer MinMaxBuffer { + vec2[] data; +} minmax_data; + +layout( push_constant ) uniform PushConstants +{ + uint samples; + uint pixels_x; + uint pixels_y; + float zoom; + uint offset; +} constants; + +void main() +{ + int pixel = int( gl_GlobalInvocationID.x ); + if( pixel > constants.pixels_x ) return; + + + float samples_per_pixel = constants.zoom * constants.samples / float(constants.pixels_x); + + int start_sample = int(floor(constants.offset - constants.samples * constants.zoom / 2. + pixel * samples_per_pixel)); + int end_sample = int(ceil(start_sample + samples_per_pixel + 1)); + + vec2 minmax = vec2(0.); + if( start_sample > 0 && end_sample < constants.samples ) { + minmax = vec2(100., -100.); + for (int i = start_sample; i < end_sample; i++) { + float s = audio_data.data[i]; + minmax[0] = min(minmax[0], s); + minmax[1] = max(minmax[1], s); + } + } + + // Always fill to zero + minmax[0] = min(minmax[0], 0.); + minmax[1] = max(minmax[1], 0.); + + minmax_data.data[ pixel ] = minmax; +} diff --git a/src/main.rs b/src/main.rs index 2cda693..cc7fde7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ pub mod app; +mod plot_renderer; use std::collections::HashMap; use std::fs; @@ -15,7 +16,7 @@ use cen::ash::vk::{BufferUsageFlags, DescriptorSetLayoutBinding, DescriptorType, use cen::egui; use cen::egui::{Context, Slider}; use cen::gpu_allocator::MemoryLocation; -use cen::graphics::pipeline_store::{PipelineConfig, PipelineKey}; +use cen::graphics::pipeline_store::{ComputePipelineConfig, PipelineKey}; use cen::graphics::renderer::RenderComponent; use cen::vulkan::{Buffer, DescriptorSetLayout}; use egui_plot::{Line, Plot, PlotPoints}; @@ -28,8 +29,10 @@ use ringbuf::consumer::Consumer; use ringbuf::producer::Producer; use ringbuf::traits::Split; use crate::app::cpal_wrapper::StreamFactory; +use crate::plot_renderer::PlotRenderer; -const BUFFER_SAMPLES: usize = 44800; +const SAMPLES_PER_SECOND: usize = 44800; +const BUFFER_SAMPLES: usize = 44800 * 8; struct AudioController { frequency: f32, @@ -49,7 +52,7 @@ impl AudioPlayer { fn new() -> Self { let sf = StreamFactory::default_factory().unwrap(); - let (producer, mut consumer) = ringbuf::HeapRb::::new(BUFFER_SAMPLES * 4).split(); + let (producer, mut consumer) = ringbuf::HeapRb::::new(SAMPLES_PER_SECOND * 4).split(); let routin = Box::new(move |len: usize| -> Vec { let mut out = vec![0.0; len]; @@ -81,15 +84,15 @@ struct App play: bool, repeat_play: bool, last_play: Instant, - graph_buf: Vec, - audio: Option<[f32; BUFFER_SAMPLES]>, + plot: PlotRenderer } #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct PushConstants { time: f32, - samples: u32, + samples_per_second: u32, + total_samples: u32, frequency: f32, volume: f32, a: f32, @@ -119,7 +122,7 @@ impl App { ); let mut macros = HashMap::new(); macros.insert("audio_function".to_string(), self.code.clone()); - let pipeline_config = PipelineConfig { + let pipeline_config = ComputePipelineConfig { shader_path: PathBuf::from("shaders/audio.comp"), descriptor_set_layouts: vec![ descriptor_set_layout ], push_constant_ranges: vec![ @@ -132,7 +135,7 @@ impl App { }; let pipeline = if let Some(pipeline) = self.pipeline { - ctx.pipeline_store.update(pipeline, pipeline_config) + ctx.pipeline_store.write(pipeline, pipeline_config) } else { ctx.pipeline_store.insert(pipeline_config) }; @@ -173,7 +176,7 @@ impl App { ); let mut macros = HashMap::new(); macros.insert("audio_function".to_string(), code.clone()); - let pipeline_config = PipelineConfig { + let pipeline_config = ComputePipelineConfig { shader_path: PathBuf::from("shaders/audio.comp"), descriptor_set_layouts: vec![ descriptor_set_layout ], push_constant_ranges: vec![ @@ -206,7 +209,7 @@ impl App { player, controller, pipeline, - buffer, + buffer: buffer.clone(), code, play: false, repeat_play: false, @@ -214,8 +217,7 @@ impl App { shader_errors, compile: false, file_path: Some(default_file.into()), - graph_buf: vec![Point::default(); BUFFER_SAMPLES], - audio: None, + plot: PlotRenderer::new(ctx, buffer) } } } @@ -230,7 +232,6 @@ impl RenderComponent for App { let binding = self.buffer.mapped().unwrap(); let gpu_data: &[f32] = cast_slice(binding.as_slice()); - self.audio = Some(gpu_data.try_into().unwrap()); if self.repeat_play { self.play = false; @@ -241,7 +242,8 @@ impl RenderComponent for App { } if self.play { - self.player.producer.push_slice(gpu_data); + let count = self.player.producer.push_slice(gpu_data); + println!("count {}", count); self.play = false; } @@ -249,12 +251,14 @@ impl RenderComponent for App { return; } + // Calculate audio let pipeline = ctx.pipeline_store.get(self.pipeline.unwrap()).unwrap(); - ctx.command_buffer.bind_pipeline(&pipeline); + ctx.command_buffer.bind_pipeline(pipeline); let push_constants = PushConstants { time: 0.0, - samples: BUFFER_SAMPLES as u32, + samples_per_second: SAMPLES_PER_SECOND as u32, + total_samples: BUFFER_SAMPLES as u32, frequency: self.controller.frequency, volume: self.controller.volume, a: self.controller.a, @@ -263,36 +267,33 @@ impl RenderComponent for App { d: self.controller.d, }; ctx.command_buffer.push_constants( - &pipeline, + pipeline, ShaderStageFlags::COMPUTE, 0, &bytemuck::cast_slice(std::slice::from_ref(&push_constants)) ); - let bindings = [vk::DescriptorBufferInfo::default() - .buffer(*self.buffer.handle()) - .offset(0) - .range(self.buffer.size()) - ]; - - let write_descriptor_set = WriteDescriptorSet::default() - .dst_binding(0) - .dst_array_element(0) - .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) - .buffer_info(&bindings); - ctx.command_buffer.push_descriptor_set( - &pipeline, + pipeline, 0, - &[write_descriptor_set] + &[ + WriteDescriptorSet::default() + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .buffer_info(&[self.buffer.binding()]) + ] ); ctx.command_buffer.dispatch(BUFFER_SAMPLES as u32 / 128, 1, 1); + + // Calculate plot + self.plot.render(ctx); } } impl GuiComponent for App { - fn gui(&mut self, _: &mut GuiHandler, ctx: &Context) { + fn gui(&mut self, gui_handler: &mut GuiHandler, ctx: &Context) { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { menu::MenuBar::new().ui(ui, |ui| { ui.menu_button("File", |ui| { @@ -321,42 +322,7 @@ impl GuiComponent for App { .default_width(520.0) .min_width(80.0) .show(ctx, |ui| { - if let Some(audio) = &self.audio { - Plot::new("audio_plot") - .view_aspect(2.0) - .show(ui, |plot_ui| { - - let bounds = plot_ui.plot_bounds(); - let x_min = (bounds.min()[0] as usize).clamp(0, BUFFER_SAMPLES); - let x_max = (bounds.max()[0] as usize).clamp(0, BUFFER_SAMPLES); - let range = if x_min < x_max { x_min..x_max } else { 0..BUFFER_SAMPLES }; - - let slice = &audio[range.clone()]; - let offset = range.start; - - self.graph_buf.clear(); - self.graph_buf.extend( - slice.iter().enumerate().map(|(i, &s)| Point::new((offset + i) as f64, s as f64)) - ); - - let decimated = if self.graph_buf.len() > 2000 { - LttbBuilder::new() - .method(LttbMethod::MinMax) - .threshold(2000) - .ratio(4) - .build() - .downsample(&self.graph_buf) - .unwrap() - .into_iter() - .map(|p| [p.x(), p.y()]) - .collect::>() - } else { - self.graph_buf.iter().map(|p| [p.x(), p.y()]).collect() - }; - - plot_ui.line(Line::new("audio", PlotPoints::new(decimated))); - }); - } + self.plot.ui(gui_handler, ui); ui.horizontal(|ui| { if ui.button("play").clicked() { @@ -415,7 +381,7 @@ fn main() { let cen_conf = cen::app::app::AppConfig::default() .width(1200) .height(800) - .vsync(true) + .vsync(false) .fullscreen(false) .resizable(true) .log_fps(true); diff --git a/src/plot_renderer.rs b/src/plot_renderer.rs new file mode 100644 index 0000000..4569a3f --- /dev/null +++ b/src/plot_renderer.rs @@ -0,0 +1,344 @@ +use std::collections::HashMap; +use std::path::PathBuf; +use bytemuck::{Pod, Zeroable}; +use cen::app::engine::InitContext; +use cen::app::gui::{GuiComponent, GuiHandler}; +use cen::ash::vk; +use cen::ash::vk::{AccessFlags, AttachmentLoadOp, AttachmentStoreOp, BufferUsageFlags, ClearColorValue, ClearValue, DescriptorBufferInfo, DescriptorSetLayoutBinding, DescriptorType, DeviceSize, Extent2D, Filter, Format, ImageLayout, ImageUsageFlags, Offset2D, PipelineStageFlags, PushConstantRange, Rect2D, RenderingAttachmentInfo, RenderingInfoKHR, ShaderStageFlags, Viewport, WriteDescriptorSet}; +use cen::egui; +use cen::egui::{Color32, Context, CornerRadius, Pos2, Rect, Sense, Stroke, StrokeKind, TextureId, Ui, Vec2, Widget}; +use cen::egui::load::SizedTexture; +use cen::gpu_allocator::MemoryLocation; +use cen::graphics::image_store::ImageKey; +use cen::graphics::pipeline_store::{ComputePipelineConfig, GraphicsPipelineConfig, PipelineKey}; +use cen::graphics::renderer::{RenderComponent, RenderContext}; +use cen::vulkan::{Buffer, ComputePipeline, DescriptorSetLayout, Image, ImageConfig, ImageTrait, Pipeline}; +use crate::{BUFFER_SAMPLES, SAMPLES_PER_SECOND}; + +pub struct PlotRenderer { + width: u32, + height: u32, + zoom: f32, + offset: f32, + image: Image, + audio_buffer: Buffer, + minmax_buffer: Buffer, + minmax_pipeline: PipelineKey, + texture: Option, + reset_texture: bool, + graph_pipeline: PipelineKey, +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct PushConstants { + samples: u32, + pixels_x: u32, + pixels_y: u32, + zoom: f32, + offset: u32, +} + +impl PlotRenderer { + pub fn new(ctx: &mut InitContext, audio_buffer: Buffer) -> Self { + + // Image + let width = 100; + let height = 100; + let image = Image::new( + ctx.device, + ctx.allocator, + ImageConfig::default() + .width(width) + .height(height) + .filter(Filter::LINEAR) + .image_usage_flags(ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED) + ); + + // Buffer + let minmax_buffer = Buffer::new( + ctx.device, + ctx.allocator, + MemoryLocation::GpuOnly, + (width as usize * size_of::() * 2) as DeviceSize, // Two floats (min, max) per pixel + BufferUsageFlags::STORAGE_BUFFER | BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::TRANSFER_SRC + ); + + // Pipelines + let minmax_pipeline = match ctx.pipeline_store.insert( + ComputePipelineConfig { + shader_path: PathBuf::from("shaders/minmax_audio_pixels.comp"), + descriptor_set_layouts: vec![ + DescriptorSetLayout::new_push_descriptor( + ctx.device, + &[ + DescriptorSetLayoutBinding::default() + .binding(0) + .descriptor_type(DescriptorType::STORAGE_BUFFER) + .descriptor_count(1) + .stage_flags(ShaderStageFlags::COMPUTE), + DescriptorSetLayoutBinding::default() + .binding(1) + .descriptor_type(DescriptorType::STORAGE_BUFFER) + .descriptor_count(1) + .stage_flags(ShaderStageFlags::COMPUTE), + ] + ) + ], + push_constant_ranges: vec![ + PushConstantRange::default() + .stage_flags(ShaderStageFlags::COMPUTE) + .offset(0) + .size(size_of::() as u32) + ], + macros: HashMap::new() + } + ) { + Ok(p) => { p } + Err(e) => { + panic!( "{}", e ) + } + }; + + let graph_pipeline = match ctx.pipeline_store.insert( + GraphicsPipelineConfig { + vertex_shader_path: PathBuf::from("shaders/audio_plot.vert"), + fragment_shader_path: PathBuf::from("shaders/audio_plot.frag"), + color_formats: vec![Format::R8G8B8A8_UNORM], + depth_format: None, + descriptor_set_layouts: vec![ + DescriptorSetLayout::new_push_descriptor( + ctx.device, + &[ + DescriptorSetLayoutBinding::default() + .binding(0) + .descriptor_type(DescriptorType::STORAGE_BUFFER) + .descriptor_count(1) + .stage_flags(ShaderStageFlags::VERTEX), + ] + ) + ], + push_constant_ranges: vec![ + PushConstantRange::default() + .stage_flags(ShaderStageFlags::VERTEX) + .offset(0) + .size(size_of::() as u32) + ], + macros: HashMap::new() + } + ) { + Ok(p) => { p } + Err(e) => { + panic!( "{}", e ) + } + }; + + Self { + audio_buffer, + image, + minmax_buffer, + minmax_pipeline, + graph_pipeline, + width, + height, + zoom: 1.0, + texture: None, + reset_texture: false, + offset: 0f32, + } + } + + fn resize_gpu_handles(&mut self, ctx: &mut RenderContext, width: u32, height: u32) { + self.image = Image::new( + ctx.device, + ctx.allocator, + self.image.config() + .width(width) + .height(height) + .image_usage_flags(ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED) + .filter(Filter::LINEAR) + ); + + self.minmax_buffer = Buffer::new( + ctx.device, + ctx.allocator, + MemoryLocation::GpuOnly, + (width as usize * size_of::() * 2) as DeviceSize, // Two floats (min, max) per pixel + BufferUsageFlags::STORAGE_BUFFER | BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::TRANSFER_SRC + ); + + self.reset_texture = true; + } +} + +impl RenderComponent for PlotRenderer { + fn render(&mut self, ctx: &mut RenderContext) { + + if( self.image.width() != self.width || self.image.height() != self.height ) { + self.resize_gpu_handles(ctx, self.width, self.height); + } + + // Compute per-pixel min-max values + let minmax_pipeline = ctx.pipeline_store.get(self.minmax_pipeline).unwrap(); + ctx.command_buffer.bind_pipeline(minmax_pipeline); + + let push_constants = PushConstants { + samples: BUFFER_SAMPLES as u32, + pixels_x: self.width, + pixels_y: self.height, + zoom: self.zoom, + offset: self.offset as u32, + }; + ctx.command_buffer.push_constants( + minmax_pipeline, + ShaderStageFlags::COMPUTE, + 0, + &bytemuck::cast_slice(std::slice::from_ref(&push_constants)) + ); + + // Manual track as push_descriptor doesn't have support yet for tracking + ctx.command_buffer.track(&self.audio_buffer); + ctx.command_buffer.track(&self.minmax_buffer); + ctx.command_buffer.push_descriptor_set( + minmax_pipeline, + 0, + &[ + WriteDescriptorSet::default() + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .buffer_info(&[self.audio_buffer.binding()]), + WriteDescriptorSet::default() + .dst_binding(1) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .buffer_info(&[self.minmax_buffer.binding()]) + ] + ); + + ctx.command_buffer.dispatch( (self.width + 63) / 64, 1, 1); + + // Draw the plot + ctx.command_buffer.image_barrier( + &self.image, + ImageLayout::UNDEFINED, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + PipelineStageFlags::TOP_OF_PIPE, + PipelineStageFlags::FRAGMENT_SHADER, + AccessFlags::NONE, + AccessFlags::SHADER_WRITE + ); + + ctx.command_buffer.set_viewport(Viewport{ x: 0f32, y: 0f32, width: self.width as f32, height: self.height as f32, min_depth: 0f32, max_depth: 0f32}); + ctx.command_buffer.set_scissor(Rect2D { offset: Offset2D::default(), extent: Extent2D { width: self.width, height: self.height }}); + + let color_attachments = vec![ + RenderingAttachmentInfo::default() + .image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .load_op(AttachmentLoadOp::CLEAR) + .store_op(AttachmentStoreOp::STORE) + .clear_value(ClearValue { color: ClearColorValue { float32: [0f32, 0f32, 0f32, 1f32] } }) + .image_view(self.image.image_view()) + ]; + let rendering_info = vk::RenderingInfoKHR::default() + .render_area(Rect2D { offset: Offset2D { x: 0, y: 0 }, extent: Extent2D { width: self.width, height: self.height } }) + .layer_count(1) + .view_mask(0) + .color_attachments(&color_attachments); + ctx.command_buffer.begin_rendering(&rendering_info); + { + let graph_pipeline = ctx.pipeline_store.get(self.graph_pipeline).unwrap(); + ctx.command_buffer.bind_pipeline(graph_pipeline); + + let push_constants = PushConstants { + samples: BUFFER_SAMPLES as u32, + pixels_x: self.width, + pixels_y: self.height, + zoom: self.zoom, + offset: self.offset as u32, + }; + ctx.command_buffer.push_constants( + graph_pipeline, + ShaderStageFlags::VERTEX, + 0, + &bytemuck::cast_slice(std::slice::from_ref(&push_constants)) + ); + + ctx.command_buffer.push_descriptor_set( + graph_pipeline, + 0, + &[ + WriteDescriptorSet::default() + .dst_binding(0) + .dst_array_element(0) + .descriptor_type(vk::DescriptorType::STORAGE_BUFFER) + .buffer_info(&[self.minmax_buffer.binding()]), + ] + ); + + ctx.command_buffer.draw(6, self.width, 0, 0); + } + ctx.command_buffer.end_rendering(); + + ctx.command_buffer.image_barrier( + &self.image, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + ImageLayout::SHADER_READ_ONLY_OPTIMAL, + PipelineStageFlags::FRAGMENT_SHADER, + PipelineStageFlags::BOTTOM_OF_PIPE, + AccessFlags::SHADER_WRITE, + AccessFlags::NONE + ); + } +} + +impl PlotRenderer { + pub fn ui(&mut self, gui: &mut GuiHandler, ui: &mut Ui) { + + if self.reset_texture { + gui.remove_texture(self.texture.unwrap()); + self.texture = None; + self.reset_texture = false; + } + if self.texture.is_none() { + self.texture = Some(gui.create_texture(&self.image)); + } + + let logical_width = ui.available_width() as u32; + let logical_height = u32::min( logical_width / 2, ui.available_height() as u32 ); + + let scale = ui.ctx().pixels_per_point(); + self.width = (logical_width as f32 * scale) as u32; + self.height = (logical_height as f32 * scale) as u32; + + let (response, painter) = ui.allocate_painter( + Vec2::new( + logical_width as f32, + logical_height as f32 + ), + Sense::click_and_drag(), + ); + + painter.image( + self.texture.unwrap(), + response.rect, + Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)), // full UV + Color32::WHITE, + ); + + // zoom toward cursor + if let Some(hover_pos) = response.hover_pos() { + let scroll = ui.input(|i| i.smooth_scroll_delta.y); + if scroll != 0.0 { + self.zoom *= 1.0 + scroll * -0.001; + } + } + + if response.dragged_by(egui::PointerButton::Primary) { + let delta = response.drag_delta() * ui.pixels_per_point(); + let samples_per_pixel = BUFFER_SAMPLES as f32 / self.width as f32 * self.zoom; + self.offset -= delta.x * samples_per_pixel; + } + + } +} From c18dfaae194bff4140f63838357b28e0b3beadba Mon Sep 17 00:00:00 2001 From: Lauda Carly Date: Sat, 18 Apr 2026 18:05:07 +0200 Subject: [PATCH 2/4] Cleanup and scrollbar --- audio/kick.glsl | 2 +- shaders/audio_background.frag | 38 +++++++++++++- shaders/audio_plot.frag | 9 +++- shaders/audio_plot.vert | 6 ++- shaders/fullscreen.vert | 13 +++++ shaders/minmax_audio_pixels.comp | 11 ++-- src/main.rs | 2 +- src/plot_renderer.rs | 88 ++++++++++++++++++++++++++++++-- 8 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 shaders/fullscreen.vert diff --git a/audio/kick.glsl b/audio/kick.glsl index cd16413..b807ea6 100644 --- a/audio/kick.glsl +++ b/audio/kick.glsl @@ -30,5 +30,5 @@ vec2 kick(float t, float q) { // Method called by the main shader vec2 audio(float t, float v, float f, float[4] option) { - return v * 0.5 * kick(t, option[0] * 0.001); + return v * kick(t, option[0] * 0.001); } \ No newline at end of file diff --git a/shaders/audio_background.frag b/shaders/audio_background.frag index 014d42d..6d2829e 100644 --- a/shaders/audio_background.frag +++ b/shaders/audio_background.frag @@ -2,16 +2,50 @@ layout( push_constant ) uniform PushConstants { - uint samples; + uint samples_per_second; + uint total_samples; uint pixels_x; uint pixels_y; float zoom; uint offset; } constants; +layout(location = 0) in vec2 uv; + layout(location = 0) out vec4 outColor; void main() { - outColor = vec4(1.0, 0.0, 0.0, 1.0); + vec3 color = vec3( 0 ); + + float offset = constants.offset / float(constants.total_samples); + + float p = offset + (uv.x - .5) * constants.zoom; + + float units_per_pixel = constants.zoom / constants.pixels_x; + + const int MINOR_GRID_LINES_PER_SECOND = 100; + const int MAJOR_GRID_LINES_PER_SECOND = 10; + + vec3 minor_grid_color = vec3(.02); + vec3 major_grid_color = vec3(.08); + if( fract(p * MINOR_GRID_LINES_PER_SECOND) / MINOR_GRID_LINES_PER_SECOND < units_per_pixel ) { + color = minor_grid_color; + } + + const int MINOR_GRID_LINES_Y = 4 * 2; // range is from -1 to 1 + if( fract(abs(uv.y - .5) * MINOR_GRID_LINES_Y ) / MINOR_GRID_LINES_Y < 2. / constants.pixels_y ) { + color = minor_grid_color; + } + + if( fract(p * MAJOR_GRID_LINES_PER_SECOND) / MAJOR_GRID_LINES_PER_SECOND < units_per_pixel ) { + color = major_grid_color; + } + if( fract(abs(uv.y - .5)) < 1. / constants.pixels_y ) { + color = major_grid_color; + } + + outColor = vec4(color, 1.0); + +// outColor = vec4(uv, 0., 1.); } diff --git a/shaders/audio_plot.frag b/shaders/audio_plot.frag index 014d42d..05cdfe2 100644 --- a/shaders/audio_plot.frag +++ b/shaders/audio_plot.frag @@ -2,7 +2,8 @@ layout( push_constant ) uniform PushConstants { - uint samples; + uint samples_per_second; + uint total_samples; uint pixels_x; uint pixels_y; float zoom; @@ -13,5 +14,9 @@ layout(location = 0) out vec4 outColor; void main() { - outColor = vec4(1.0, 0.0, 0.0, 1.0); + + vec3 color = vec3(241. / 255.0, 121. / 255., 25. / 255.); + color = pow(color, vec3(2.2)); + outColor = vec4(color, 1.0) +; } diff --git a/shaders/audio_plot.vert b/shaders/audio_plot.vert index b6259ca..b863bf6 100644 --- a/shaders/audio_plot.vert +++ b/shaders/audio_plot.vert @@ -2,7 +2,8 @@ layout( push_constant ) uniform PushConstants { - uint samples; + uint samples_per_second; + uint total_samples; uint pixels_x; uint pixels_y; float zoom; @@ -32,6 +33,9 @@ void main() vec2 minmax = minmax_data.data[gl_InstanceIndex]; + // Discard invalid samples + if(minmax[1] < minmax[0]) return; + float height = max(minmax[1] - minmax[0], pixelheight * 1.); vec2 pos = vec2(x, minmax[0]) + vertex[gl_VertexIndex] * vec2(pixelwidth, height); diff --git a/shaders/fullscreen.vert b/shaders/fullscreen.vert new file mode 100644 index 0000000..5c1e63c --- /dev/null +++ b/shaders/fullscreen.vert @@ -0,0 +1,13 @@ +#version 450 + +layout(location = 0) out vec2 uv; + +void main() +{ + vec2 pos = vec2( + float((gl_VertexIndex << 1) & 2) * 2.0 - 1.0, + float(gl_VertexIndex & 2) * 2.0 - 1.0 + ); + uv = pos * 0.5 + 0.5; + gl_Position = vec4(pos, 0.0, 1.0); +} \ No newline at end of file diff --git a/shaders/minmax_audio_pixels.comp b/shaders/minmax_audio_pixels.comp index 4a6e0d0..a237524 100644 --- a/shaders/minmax_audio_pixels.comp +++ b/shaders/minmax_audio_pixels.comp @@ -30,7 +30,9 @@ void main() int start_sample = int(floor(constants.offset - constants.samples * constants.zoom / 2. + pixel * samples_per_pixel)); int end_sample = int(ceil(start_sample + samples_per_pixel + 1)); - vec2 minmax = vec2(0.); + // Invalid sample as default + vec2 minmax = vec2(1., -1.); // Min bigger than max + if( start_sample > 0 && end_sample < constants.samples ) { minmax = vec2(100., -100.); for (int i = start_sample; i < end_sample; i++) { @@ -38,11 +40,12 @@ void main() minmax[0] = min(minmax[0], s); minmax[1] = max(minmax[1], s); } + + // Always fill to zero + minmax[0] = min(minmax[0], 0.); + minmax[1] = max(minmax[1], 0.); } - // Always fill to zero - minmax[0] = min(minmax[0], 0.); - minmax[1] = max(minmax[1], 0.); minmax_data.data[ pixel ] = minmax; } diff --git a/src/main.rs b/src/main.rs index cc7fde7..10da0de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,7 @@ use crate::app::cpal_wrapper::StreamFactory; use crate::plot_renderer::PlotRenderer; const SAMPLES_PER_SECOND: usize = 44800; -const BUFFER_SAMPLES: usize = 44800 * 8; +const BUFFER_SAMPLES: usize = 44800; struct AudioController { frequency: f32, diff --git a/src/plot_renderer.rs b/src/plot_renderer.rs index 4569a3f..66485b9 100644 --- a/src/plot_renderer.rs +++ b/src/plot_renderer.rs @@ -27,6 +27,7 @@ pub struct PlotRenderer { texture: Option, reset_texture: bool, graph_pipeline: PipelineKey, + background_pipeline: PipelineKey, } #[repr(C)] @@ -39,6 +40,17 @@ struct PushConstants { offset: u32, } +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct RenderPushConstants { + samples_per_seconds: u32, + total_samples: u32, + pixels_x: u32, + pixels_y: u32, + zoom: f32, + offset: u32, +} + impl PlotRenderer { pub fn new(ctx: &mut InitContext, audio_buffer: Buffer) -> Self { @@ -122,7 +134,29 @@ impl PlotRenderer { PushConstantRange::default() .stage_flags(ShaderStageFlags::VERTEX) .offset(0) - .size(size_of::() as u32) + .size(size_of::() as u32) + ], + macros: HashMap::new() + } + ) { + Ok(p) => { p } + Err(e) => { + panic!( "{}", e ) + } + }; + + let background_pipeline = match ctx.pipeline_store.insert( + GraphicsPipelineConfig { + vertex_shader_path: PathBuf::from("shaders/fullscreen.vert"), + fragment_shader_path: PathBuf::from("shaders/audio_background.frag"), + color_formats: vec![Format::R8G8B8A8_UNORM], + depth_format: None, + descriptor_set_layouts: vec![], + push_constant_ranges: vec![ + PushConstantRange::default() + .stage_flags(ShaderStageFlags::FRAGMENT) + .offset(0) + .size(size_of::() as u32) ], macros: HashMap::new() } @@ -139,6 +173,7 @@ impl PlotRenderer { minmax_buffer, minmax_pipeline, graph_pipeline, + background_pipeline, width, height, zoom: 1.0, @@ -247,16 +282,30 @@ impl RenderComponent for PlotRenderer { .color_attachments(&color_attachments); ctx.command_buffer.begin_rendering(&rendering_info); { - let graph_pipeline = ctx.pipeline_store.get(self.graph_pipeline).unwrap(); - ctx.command_buffer.bind_pipeline(graph_pipeline); + // Background + let background_pipeline = ctx.pipeline_store.get(self.background_pipeline).unwrap(); + ctx.command_buffer.bind_pipeline(background_pipeline); - let push_constants = PushConstants { - samples: BUFFER_SAMPLES as u32, + let push_constants = RenderPushConstants { + samples_per_seconds: SAMPLES_PER_SECOND as u32, + total_samples: BUFFER_SAMPLES as u32, pixels_x: self.width, pixels_y: self.height, zoom: self.zoom, offset: self.offset as u32, }; + ctx.command_buffer.push_constants( + background_pipeline, + ShaderStageFlags::FRAGMENT, + 0, + &bytemuck::cast_slice(std::slice::from_ref(&push_constants)) + ); + ctx.command_buffer.draw(6, self.width, 0, 0); + + // Graph + let graph_pipeline = ctx.pipeline_store.get(self.graph_pipeline).unwrap(); + ctx.command_buffer.bind_pipeline(graph_pipeline); + ctx.command_buffer.push_constants( graph_pipeline, ShaderStageFlags::VERTEX, @@ -340,5 +389,34 @@ impl PlotRenderer { self.offset -= delta.x * samples_per_pixel; } + // Scroll bar + let bar_rect = Rect::from_min_max( + Pos2::new(response.rect.min.x, response.rect.max.y - 6.0), + response.rect.max, + ); + + let thumb_frac = self.offset / BUFFER_SAMPLES as f32; + let thumb_width = (bar_rect.width() * self.zoom).clamp(8.0, bar_rect.width()); + let thumb_x = bar_rect.min.x + thumb_frac * (bar_rect.width() - thumb_width); + let thumb_rect = Rect::from_min_max( + Pos2::new(thumb_x, bar_rect.min.y), + Pos2::new(thumb_x + thumb_width, bar_rect.max.y), + ); + + if thumb_width < bar_rect.width() { + // Render + painter.rect_filled(bar_rect, 0.0, Color32::from_black_alpha(40)); + painter.rect_filled(thumb_rect, 3.0, Color32::from_white_alpha(100)); + + // Draggable + let thumb_response = ui.interact(thumb_rect, ui.id().with("scrollbar_thumb"), Sense::drag()); + if thumb_response.dragged() { + let delta = thumb_response.drag_delta().x; + let scroll_range = bar_rect.width() - thumb_width; + self.offset = (self.offset + delta / scroll_range * BUFFER_SAMPLES as f32) + .clamp(0.0, BUFFER_SAMPLES as f32); + } + } + } } From c06ed009af966f4a3a09194b04b69064b3b2a741 Mon Sep 17 00:00:00 2001 From: Lauda Carly Date: Sat, 18 Apr 2026 19:07:34 +0200 Subject: [PATCH 3/4] Fix scrollbar --- shaders/audio_background.frag | 6 ++-- shaders/audio_plot.frag | 7 ++-- shaders/audio_plot.vert | 4 ++- shaders/minmax_audio_pixels.comp | 13 ++++---- src/main.rs | 3 +- src/plot_renderer.rs | 56 +++++++++++++++----------------- 6 files changed, 46 insertions(+), 43 deletions(-) diff --git a/shaders/audio_background.frag b/shaders/audio_background.frag index 6d2829e..8d24ebc 100644 --- a/shaders/audio_background.frag +++ b/shaders/audio_background.frag @@ -18,7 +18,7 @@ void main() { vec3 color = vec3( 0 ); - float offset = constants.offset / float(constants.total_samples); + float offset = constants.offset / float(constants.samples_per_second); float p = offset + (uv.x - .5) * constants.zoom; @@ -27,8 +27,8 @@ void main() const int MINOR_GRID_LINES_PER_SECOND = 100; const int MAJOR_GRID_LINES_PER_SECOND = 10; - vec3 minor_grid_color = vec3(.02); - vec3 major_grid_color = vec3(.08); + vec3 minor_grid_color = vec3(.02) * min(1., 2. / constants.zoom ); + vec3 major_grid_color = vec3(.08) * min(1., 4. / constants.zoom ); if( fract(p * MINOR_GRID_LINES_PER_SECOND) / MINOR_GRID_LINES_PER_SECOND < units_per_pixel ) { color = minor_grid_color; } diff --git a/shaders/audio_plot.frag b/shaders/audio_plot.frag index 05cdfe2..6433d41 100644 --- a/shaders/audio_plot.frag +++ b/shaders/audio_plot.frag @@ -10,13 +10,14 @@ layout( push_constant ) uniform PushConstants uint offset; } constants; + layout(location = 0) out vec4 outColor; +layout(location = 0) in vec2 minmax; + void main() { - vec3 color = vec3(241. / 255.0, 121. / 255., 25. / 255.); color = pow(color, vec3(2.2)); - outColor = vec4(color, 1.0) -; + outColor = vec4(color, 1.0); } diff --git a/shaders/audio_plot.vert b/shaders/audio_plot.vert index b863bf6..cfb6240 100644 --- a/shaders/audio_plot.vert +++ b/shaders/audio_plot.vert @@ -24,6 +24,8 @@ vec2 vertex[6] = vec2[]( vec2( 0.0, 1.0) ); +layout(location = 0) out vec2 minmax; + void main() { float pixelwidth = 2. / float(constants.pixels_x); @@ -31,7 +33,7 @@ void main() float x = 2. * gl_InstanceIndex / constants.pixels_x - 1.; - vec2 minmax = minmax_data.data[gl_InstanceIndex]; + minmax = minmax_data.data[gl_InstanceIndex]; // Discard invalid samples if(minmax[1] < minmax[0]) return; diff --git a/shaders/minmax_audio_pixels.comp b/shaders/minmax_audio_pixels.comp index a237524..8641ac8 100644 --- a/shaders/minmax_audio_pixels.comp +++ b/shaders/minmax_audio_pixels.comp @@ -12,7 +12,8 @@ layout( std430, binding = 1 ) buffer MinMaxBuffer { layout( push_constant ) uniform PushConstants { - uint samples; + uint samples_per_second; + uint total_samples; uint pixels_x; uint pixels_y; float zoom; @@ -25,15 +26,15 @@ void main() if( pixel > constants.pixels_x ) return; - float samples_per_pixel = constants.zoom * constants.samples / float(constants.pixels_x); + float samples_per_pixel = constants.zoom * constants.samples_per_second / float(constants.pixels_x); - int start_sample = int(floor(constants.offset - constants.samples * constants.zoom / 2. + pixel * samples_per_pixel)); + int start_sample = int(floor(constants.offset - constants.samples_per_second * constants.zoom / 2. + pixel * samples_per_pixel)); int end_sample = int(ceil(start_sample + samples_per_pixel + 1)); // Invalid sample as default vec2 minmax = vec2(1., -1.); // Min bigger than max - if( start_sample > 0 && end_sample < constants.samples ) { + if( start_sample > 0 && end_sample < constants.total_samples ) { minmax = vec2(100., -100.); for (int i = start_sample; i < end_sample; i++) { float s = audio_data.data[i]; @@ -42,8 +43,8 @@ void main() } // Always fill to zero - minmax[0] = min(minmax[0], 0.); - minmax[1] = max(minmax[1], 0.); +// minmax[0] = min(minmax[0], 0.); +// minmax[1] = max(minmax[1], 0.); } diff --git a/src/main.rs b/src/main.rs index 10da0de..86f38c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,7 +32,8 @@ use crate::app::cpal_wrapper::StreamFactory; use crate::plot_renderer::PlotRenderer; const SAMPLES_PER_SECOND: usize = 44800; -const BUFFER_SAMPLES: usize = 44800; +const BUFFER_DURATION: f32 = 1f32; +const BUFFER_SAMPLES: usize = (SAMPLES_PER_SECOND as f32 * BUFFER_DURATION) as usize; struct AudioController { frequency: f32, diff --git a/src/plot_renderer.rs b/src/plot_renderer.rs index 66485b9..ef859b0 100644 --- a/src/plot_renderer.rs +++ b/src/plot_renderer.rs @@ -13,13 +13,13 @@ use cen::graphics::image_store::ImageKey; use cen::graphics::pipeline_store::{ComputePipelineConfig, GraphicsPipelineConfig, PipelineKey}; use cen::graphics::renderer::{RenderComponent, RenderContext}; use cen::vulkan::{Buffer, ComputePipeline, DescriptorSetLayout, Image, ImageConfig, ImageTrait, Pipeline}; -use crate::{BUFFER_SAMPLES, SAMPLES_PER_SECOND}; +use crate::{BUFFER_DURATION, BUFFER_SAMPLES, SAMPLES_PER_SECOND}; pub struct PlotRenderer { width: u32, height: u32, zoom: f32, - offset: f32, + sample_offset: f32, image: Image, audio_buffer: Buffer, minmax_buffer: Buffer, @@ -33,16 +33,6 @@ pub struct PlotRenderer { #[repr(C)] #[derive(Copy, Clone, Pod, Zeroable)] struct PushConstants { - samples: u32, - pixels_x: u32, - pixels_y: u32, - zoom: f32, - offset: u32, -} - -#[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] -struct RenderPushConstants { samples_per_seconds: u32, total_samples: u32, pixels_x: u32, @@ -132,9 +122,9 @@ impl PlotRenderer { ], push_constant_ranges: vec![ PushConstantRange::default() - .stage_flags(ShaderStageFlags::VERTEX) + .stage_flags(ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT) .offset(0) - .size(size_of::() as u32) + .size(size_of::() as u32) ], macros: HashMap::new() } @@ -156,7 +146,7 @@ impl PlotRenderer { PushConstantRange::default() .stage_flags(ShaderStageFlags::FRAGMENT) .offset(0) - .size(size_of::() as u32) + .size(size_of::() as u32) ], macros: HashMap::new() } @@ -179,7 +169,7 @@ impl PlotRenderer { zoom: 1.0, texture: None, reset_texture: false, - offset: 0f32, + sample_offset: 0f32, } } @@ -218,11 +208,12 @@ impl RenderComponent for PlotRenderer { ctx.command_buffer.bind_pipeline(minmax_pipeline); let push_constants = PushConstants { - samples: BUFFER_SAMPLES as u32, + samples_per_seconds: SAMPLES_PER_SECOND as u32, + total_samples: BUFFER_SAMPLES as u32, pixels_x: self.width, pixels_y: self.height, zoom: self.zoom, - offset: self.offset as u32, + offset: self.sample_offset as u32, }; ctx.command_buffer.push_constants( minmax_pipeline, @@ -286,13 +277,13 @@ impl RenderComponent for PlotRenderer { let background_pipeline = ctx.pipeline_store.get(self.background_pipeline).unwrap(); ctx.command_buffer.bind_pipeline(background_pipeline); - let push_constants = RenderPushConstants { + let push_constants = PushConstants { samples_per_seconds: SAMPLES_PER_SECOND as u32, total_samples: BUFFER_SAMPLES as u32, pixels_x: self.width, pixels_y: self.height, zoom: self.zoom, - offset: self.offset as u32, + offset: self.sample_offset as u32, }; ctx.command_buffer.push_constants( background_pipeline, @@ -308,7 +299,7 @@ impl RenderComponent for PlotRenderer { ctx.command_buffer.push_constants( graph_pipeline, - ShaderStageFlags::VERTEX, + ShaderStageFlags::VERTEX | ShaderStageFlags::FRAGMENT, 0, &bytemuck::cast_slice(std::slice::from_ref(&push_constants)) ); @@ -385,8 +376,8 @@ impl PlotRenderer { if response.dragged_by(egui::PointerButton::Primary) { let delta = response.drag_delta() * ui.pixels_per_point(); - let samples_per_pixel = BUFFER_SAMPLES as f32 / self.width as f32 * self.zoom; - self.offset -= delta.x * samples_per_pixel; + let samples_per_pixel = SAMPLES_PER_SECOND as f32 / self.width as f32 * self.zoom; + self.sample_offset -= delta.x * samples_per_pixel; } // Scroll bar @@ -395,8 +386,8 @@ impl PlotRenderer { response.rect.max, ); - let thumb_frac = self.offset / BUFFER_SAMPLES as f32; - let thumb_width = (bar_rect.width() * self.zoom).clamp(8.0, bar_rect.width()); + let thumb_frac = self.sample_offset / BUFFER_SAMPLES as f32; + let thumb_width = (bar_rect.width() * self.zoom / BUFFER_DURATION ).clamp(8.0, bar_rect.width()); let thumb_x = bar_rect.min.x + thumb_frac * (bar_rect.width() - thumb_width); let thumb_rect = Rect::from_min_max( Pos2::new(thumb_x, bar_rect.min.y), @@ -404,18 +395,25 @@ impl PlotRenderer { ); if thumb_width < bar_rect.width() { - // Render - painter.rect_filled(bar_rect, 0.0, Color32::from_black_alpha(40)); - painter.rect_filled(thumb_rect, 3.0, Color32::from_white_alpha(100)); // Draggable let thumb_response = ui.interact(thumb_rect, ui.id().with("scrollbar_thumb"), Sense::drag()); if thumb_response.dragged() { let delta = thumb_response.drag_delta().x; let scroll_range = bar_rect.width() - thumb_width; - self.offset = (self.offset + delta / scroll_range * BUFFER_SAMPLES as f32) + self.sample_offset = (self.sample_offset + delta / scroll_range * BUFFER_SAMPLES as f32) .clamp(0.0, BUFFER_SAMPLES as f32); } + + // Render + let thumb_color = if thumb_response.dragged() { + Color32::from_white_alpha(200) + } else if thumb_response.hovered() { + Color32::from_white_alpha(150) + } else { + Color32::from_white_alpha(100) + }; + painter.rect_filled(thumb_rect, 3.0, thumb_color); } } From 12b974fb9a477e79b5a2b8499c5e94133f29987d Mon Sep 17 00:00:00 2001 From: Lauda Carly Date: Sun, 19 Apr 2026 14:06:44 +0200 Subject: [PATCH 4/4] Add anti-aliasing --- Cargo.toml | 3 +- shaders/audio_plot.vert | 34 ++++++++-- shaders/minmax_audio_pixels.comp | 19 ++++-- src/main.rs | 8 +-- src/plot_renderer.rs | 107 ++++++++++++++++++++++++------- 5 files changed, 133 insertions(+), 38 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 21b4308..f52a0af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,8 +11,7 @@ keywords = ["compute", "vulkan", "render", "engine", "synth"] categories = ["rendering::engine"] [dependencies] -#cen = { git = "https://github.com/n-e-l/cen", rev = "62ed91da74058bb8401d1d3e03b45a1a4d658cb2" } -cen = { path = "../cen" } +cen = { git = "https://github.com/n-e-l/cen", rev = "77cd8f13a3ac80e143a56cb17472b5c5eeb14992" } cpal = "0.13.4" bytemuck = { version = "1.17.0", features = ["derive"] } egui_plot = "0.34.1" diff --git a/shaders/audio_plot.vert b/shaders/audio_plot.vert index cfb6240..c4c531e 100644 --- a/shaders/audio_plot.vert +++ b/shaders/audio_plot.vert @@ -10,8 +10,14 @@ layout( push_constant ) uniform PushConstants uint offset; } constants; +struct PixelData { + float minimum; + float maximum; + int direction; + int unused; +}; layout( std430, binding = 0 ) readonly buffer MinMaxBuffer { - vec2[] data; + PixelData[] data; } minmax_data; vec2 vertex[6] = vec2[]( @@ -33,13 +39,31 @@ void main() float x = 2. * gl_InstanceIndex / constants.pixels_x - 1.; - minmax = minmax_data.data[gl_InstanceIndex]; + PixelData minmax = minmax_data.data[gl_InstanceIndex]; + float minimum = minmax.minimum; + float maximum = minmax.maximum; // Discard invalid samples - if(minmax[1] < minmax[0]) return; + if(maximum < minimum) return; + + vec2 vertex_p = vertex[gl_VertexIndex]; + + // Shift x position in order to have aliased lines + float x_offset = 0.; + if( minmax.direction == 1 ) { + // Upward line, move the top vertices right by half a pixel + // Move the bottom pixel left by half a pixel + x_offset = (vertex_p.y * 2. - 1.) * pixelwidth / 2.; + } else if (minmax.direction == 2) { + // Downward line, move the top vertices left by half a pixel + // Move the bottom pixel right by half a pixel + x_offset = -(vertex_p.y * 2. - 1.) * pixelwidth / 2.; + } + x_offset *= 1.; + + float height = max(maximum - minimum, pixelheight * 1.); + vec2 pos = vec2(x + x_offset, minimum) + vertex_p * vec2(pixelwidth, height); - float height = max(minmax[1] - minmax[0], pixelheight * 1.); - vec2 pos = vec2(x, minmax[0]) + vertex[gl_VertexIndex] * vec2(pixelwidth, height); gl_Position = vec4(pos, 0.0, 1.0); } diff --git a/shaders/minmax_audio_pixels.comp b/shaders/minmax_audio_pixels.comp index 8641ac8..cbb1625 100644 --- a/shaders/minmax_audio_pixels.comp +++ b/shaders/minmax_audio_pixels.comp @@ -6,8 +6,14 @@ layout( std430, binding = 0 ) buffer AudioBuffer { float[] data; } audio_data; +struct PixelData { + float minimum; + float maximum; + int direction; + int unused; +}; layout( std430, binding = 1 ) buffer MinMaxBuffer { - vec2[] data; + PixelData[] data; } minmax_data; layout( push_constant ) uniform PushConstants @@ -25,11 +31,11 @@ void main() int pixel = int( gl_GlobalInvocationID.x ); if( pixel > constants.pixels_x ) return; - float samples_per_pixel = constants.zoom * constants.samples_per_second / float(constants.pixels_x); + float pixel_size_y = 2.0 / constants.pixels_y; int start_sample = int(floor(constants.offset - constants.samples_per_second * constants.zoom / 2. + pixel * samples_per_pixel)); - int end_sample = int(ceil(start_sample + samples_per_pixel + 1)); + int end_sample = int(ceil(start_sample + samples_per_pixel) + 1); // Add 1 to not have any gaps // Invalid sample as default vec2 minmax = vec2(1., -1.); // Min bigger than max @@ -47,6 +53,11 @@ void main() // minmax[1] = max(minmax[1], 0.); } + float a = audio_data.data[start_sample]; + float b = audio_data.data[end_sample]; + int direction = 0; + if( a < b ) direction = 1; + if( a > b ) direction = 2; - minmax_data.data[ pixel ] = minmax; + minmax_data.data[ pixel ] = PixelData(minmax[0], minmax[1], direction, 0); } diff --git a/src/main.rs b/src/main.rs index 86f38c7..e022407 100644 --- a/src/main.rs +++ b/src/main.rs @@ -16,9 +16,9 @@ use cen::ash::vk::{BufferUsageFlags, DescriptorSetLayoutBinding, DescriptorType, use cen::egui; use cen::egui::{Context, Slider}; use cen::gpu_allocator::MemoryLocation; -use cen::graphics::pipeline_store::{ComputePipelineConfig, PipelineKey}; +use cen::graphics::pipeline_store::{PipelineKey}; use cen::graphics::renderer::RenderComponent; -use cen::vulkan::{Buffer, DescriptorSetLayout}; +use cen::vulkan::{Buffer, ComputePipelineConfig, DescriptorSetLayout}; use egui_plot::{Line, Plot, PlotPoints}; use egui::containers::menu; use cpal::{Stream}; @@ -124,7 +124,7 @@ impl App { let mut macros = HashMap::new(); macros.insert("audio_function".to_string(), self.code.clone()); let pipeline_config = ComputePipelineConfig { - shader_path: PathBuf::from("shaders/audio.comp"), + shader_source: PathBuf::from("shaders/audio.comp"), descriptor_set_layouts: vec![ descriptor_set_layout ], push_constant_ranges: vec![ PushConstantRange::default() @@ -178,7 +178,7 @@ impl App { let mut macros = HashMap::new(); macros.insert("audio_function".to_string(), code.clone()); let pipeline_config = ComputePipelineConfig { - shader_path: PathBuf::from("shaders/audio.comp"), + shader_source: PathBuf::from("shaders/audio.comp"), descriptor_set_layouts: vec![ descriptor_set_layout ], push_constant_ranges: vec![ PushConstantRange::default() diff --git a/src/plot_renderer.rs b/src/plot_renderer.rs index ef859b0..a9456d9 100644 --- a/src/plot_renderer.rs +++ b/src/plot_renderer.rs @@ -1,18 +1,19 @@ use std::collections::HashMap; +use std::os::raw::c_void; use std::path::PathBuf; use bytemuck::{Pod, Zeroable}; use cen::app::engine::InitContext; use cen::app::gui::{GuiComponent, GuiHandler}; use cen::ash::vk; -use cen::ash::vk::{AccessFlags, AttachmentLoadOp, AttachmentStoreOp, BufferUsageFlags, ClearColorValue, ClearValue, DescriptorBufferInfo, DescriptorSetLayoutBinding, DescriptorType, DeviceSize, Extent2D, Filter, Format, ImageLayout, ImageUsageFlags, Offset2D, PipelineStageFlags, PushConstantRange, Rect2D, RenderingAttachmentInfo, RenderingInfoKHR, ShaderStageFlags, Viewport, WriteDescriptorSet}; +use cen::ash::vk::{AccessFlags, AttachmentLoadOp, AttachmentStoreOp, BufferUsageFlags, ClearColorValue, ClearValue, DescriptorBufferInfo, DescriptorSetLayoutBinding, DescriptorType, DeviceSize, Extent2D, Extent3D, Filter, Format, ImageLayout, ImageUsageFlags, Offset2D, PipelineStageFlags, PushConstantRange, Rect2D, RenderingAttachmentInfo, RenderingInfoKHR, ResolveModeFlags, SampleCountFlags, ShaderStageFlags, Viewport, WriteDescriptorSet}; use cen::egui; use cen::egui::{Color32, Context, CornerRadius, Pos2, Rect, Sense, Stroke, StrokeKind, TextureId, Ui, Vec2, Widget}; use cen::egui::load::SizedTexture; use cen::gpu_allocator::MemoryLocation; use cen::graphics::image_store::ImageKey; -use cen::graphics::pipeline_store::{ComputePipelineConfig, GraphicsPipelineConfig, PipelineKey}; +use cen::graphics::pipeline_store::{PipelineKey}; use cen::graphics::renderer::{RenderComponent, RenderContext}; -use cen::vulkan::{Buffer, ComputePipeline, DescriptorSetLayout, Image, ImageConfig, ImageTrait, Pipeline}; +use cen::vulkan::{Buffer, ComputePipeline, ComputePipelineConfig, DescriptorSetLayout, GraphicsPipelineConfig, Image, ImageConfig, ImageTrait, Pipeline}; use crate::{BUFFER_DURATION, BUFFER_SAMPLES, SAMPLES_PER_SECOND}; pub struct PlotRenderer { @@ -20,6 +21,7 @@ pub struct PlotRenderer { height: u32, zoom: f32, sample_offset: f32, + ms_image: Image, image: Image, audio_buffer: Buffer, minmax_buffer: Buffer, @@ -50,11 +52,33 @@ impl PlotRenderer { let image = Image::new( ctx.device, ctx.allocator, - ImageConfig::default() - .width(width) - .height(height) - .filter(Filter::LINEAR) - .image_usage_flags(ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED) + ImageConfig { + extent: Extent3D { + width, + height, + depth: 1 + }, + samples: SampleCountFlags::TYPE_1, + filter: Filter::LINEAR, + image_usage_flags: ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED, + ..Default::default() + } + ); + + let ms_image = Image::new( + ctx.device, + ctx.allocator, + ImageConfig { + extent: Extent3D { + width, + height, + depth: 1 + }, + samples: SampleCountFlags::TYPE_4, + filter: Filter::LINEAR, + image_usage_flags: ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED, + ..Default::default() + } ); // Buffer @@ -62,14 +86,14 @@ impl PlotRenderer { ctx.device, ctx.allocator, MemoryLocation::GpuOnly, - (width as usize * size_of::() * 2) as DeviceSize, // Two floats (min, max) per pixel + (width as usize * size_of::() * 4) as DeviceSize, BufferUsageFlags::STORAGE_BUFFER | BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::TRANSFER_SRC ); // Pipelines let minmax_pipeline = match ctx.pipeline_store.insert( ComputePipelineConfig { - shader_path: PathBuf::from("shaders/minmax_audio_pixels.comp"), + shader_source: PathBuf::from("shaders/minmax_audio_pixels.comp"), descriptor_set_layouts: vec![ DescriptorSetLayout::new_push_descriptor( ctx.device, @@ -104,10 +128,11 @@ impl PlotRenderer { let graph_pipeline = match ctx.pipeline_store.insert( GraphicsPipelineConfig { - vertex_shader_path: PathBuf::from("shaders/audio_plot.vert"), - fragment_shader_path: PathBuf::from("shaders/audio_plot.frag"), + vertex_shader_source: PathBuf::from("shaders/audio_plot.vert"), + fragment_shader_source: PathBuf::from("shaders/audio_plot.frag"), color_formats: vec![Format::R8G8B8A8_UNORM], depth_format: None, + sample_count: SampleCountFlags::TYPE_4, descriptor_set_layouts: vec![ DescriptorSetLayout::new_push_descriptor( ctx.device, @@ -137,10 +162,11 @@ impl PlotRenderer { let background_pipeline = match ctx.pipeline_store.insert( GraphicsPipelineConfig { - vertex_shader_path: PathBuf::from("shaders/fullscreen.vert"), - fragment_shader_path: PathBuf::from("shaders/audio_background.frag"), + vertex_shader_source: PathBuf::from("shaders/fullscreen.vert"), + fragment_shader_source: PathBuf::from("shaders/audio_background.frag"), color_formats: vec![Format::R8G8B8A8_UNORM], depth_format: None, + sample_count: SampleCountFlags::TYPE_4, descriptor_set_layouts: vec![], push_constant_ranges: vec![ PushConstantRange::default() @@ -160,6 +186,7 @@ impl PlotRenderer { Self { audio_buffer, image, + ms_image, minmax_buffer, minmax_pipeline, graph_pipeline, @@ -177,18 +204,40 @@ impl PlotRenderer { self.image = Image::new( ctx.device, ctx.allocator, - self.image.config() - .width(width) - .height(height) - .image_usage_flags(ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED) - .filter(Filter::LINEAR) + ImageConfig { + extent: Extent3D { + width, + height, + depth: 1 + }, + image_usage_flags: ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED, + samples: SampleCountFlags::TYPE_1, + filter: Filter::LINEAR, + ..Default::default() + } + ); + + self.ms_image = Image::new( + ctx.device, + ctx.allocator, + ImageConfig { + extent: Extent3D { + width, + height, + depth: 1 + }, + image_usage_flags: ImageUsageFlags::TRANSFER_DST | ImageUsageFlags::COLOR_ATTACHMENT | ImageUsageFlags::SAMPLED, + samples: SampleCountFlags::TYPE_4, + filter: Filter::LINEAR, + ..Default::default() + } ); self.minmax_buffer = Buffer::new( ctx.device, ctx.allocator, MemoryLocation::GpuOnly, - (width as usize * size_of::() * 2) as DeviceSize, // Two floats (min, max) per pixel + (width as usize * size_of::() * 4) as DeviceSize, // Two floats (min, max) per pixel BufferUsageFlags::STORAGE_BUFFER | BufferUsageFlags::TRANSFER_DST | BufferUsageFlags::TRANSFER_SRC ); @@ -254,17 +303,29 @@ impl RenderComponent for PlotRenderer { AccessFlags::NONE, AccessFlags::SHADER_WRITE ); + ctx.command_buffer.image_barrier( + &self.ms_image, + ImageLayout::UNDEFINED, + ImageLayout::COLOR_ATTACHMENT_OPTIMAL, + PipelineStageFlags::TOP_OF_PIPE, + PipelineStageFlags::FRAGMENT_SHADER, + AccessFlags::NONE, + AccessFlags::SHADER_WRITE + ); ctx.command_buffer.set_viewport(Viewport{ x: 0f32, y: 0f32, width: self.width as f32, height: self.height as f32, min_depth: 0f32, max_depth: 0f32}); ctx.command_buffer.set_scissor(Rect2D { offset: Offset2D::default(), extent: Extent2D { width: self.width, height: self.height }}); - let color_attachments = vec![ + let mut color_attachments = vec![ RenderingAttachmentInfo::default() .image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) .load_op(AttachmentLoadOp::CLEAR) .store_op(AttachmentStoreOp::STORE) .clear_value(ClearValue { color: ClearColorValue { float32: [0f32, 0f32, 0f32, 1f32] } }) - .image_view(self.image.image_view()) + .image_view(self.ms_image.image_view()) + .resolve_image_layout(ImageLayout::COLOR_ATTACHMENT_OPTIMAL) + .resolve_image_view(self.image.image_view()) + .resolve_mode(ResolveModeFlags::AVERAGE) ]; let rendering_info = vk::RenderingInfoKHR::default() .render_area(Rect2D { offset: Offset2D { x: 0, y: 0 }, extent: Extent2D { width: self.width, height: self.height } }) @@ -291,7 +352,7 @@ impl RenderComponent for PlotRenderer { 0, &bytemuck::cast_slice(std::slice::from_ref(&push_constants)) ); - ctx.command_buffer.draw(6, self.width, 0, 0); + ctx.command_buffer.draw(6, 1, 0, 0); // Graph let graph_pipeline = ctx.pipeline_store.get(self.graph_pipeline).unwrap();