use std::sync::Arc; use crate::analyzer::FrameData; use crate::visualizer::pipeline::{ GlobalsGpu, VisPipeline, FLAG_GLASS, FLAG_INVERTED, FLAG_MIRRORED, FLAG_STEREO, }; use crate::visualizer::{build, VizParams}; const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Bgra8Unorm; const BYTES_PER_PIXEL: u32 = 4; const COPY_BYTES_PER_ROW_ALIGNMENT: u32 = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT; /// off-screen BGRA visualizer renderer. pub struct PipRenderer { device: wgpu::Device, queue: wgpu::Queue, pipeline: VisPipeline, texture: wgpu::Texture, view: wgpu::TextureView, staging: wgpu::Buffer, width: u32, height: u32, padded_bytes_per_row: u32, } impl PipRenderer { /// allocates the off-screen texture, staging readback buffer, and a fresh BGRA-format visualizer pipeline. pub fn new(device: wgpu::Device, queue: wgpu::Queue, width: u32, height: u32) -> Self { let pipeline = VisPipeline::for_format(&device, &queue, FORMAT); let (texture, view, staging, padded_bytes_per_row) = allocate_targets(&device, width, height); Self { device, queue, pipeline, texture, view, staging, width, height, padded_bytes_per_row, } } /// reallocates the texture and staging buffer when the requested size changes. fn ensure_size(&mut self, width: u32, height: u32) { if self.width == width && self.height == height { return; } let (texture, view, staging, padded_bytes_per_row) = allocate_targets(&self.device, width, height); self.texture = texture; self.view = view; self.staging = staging; self.width = width; self.height = height; self.padded_bytes_per_row = padded_bytes_per_row; } /// renders one frame of the visualizer at the requested resolution and writes the BGRA pixels into dst, packed at dst_stride bytes per row. pub fn render_into_bgra( &mut self, frames: &Arc>, params: &VizParams, palette: Option<&[[f32; 3]]>, width: u32, height: u32, dst: *mut u8, dst_stride: u32, ) { if width == 0 || height == 0 || dst.is_null() { return; } self.ensure_size(width, height); if !frames.is_empty() { let frames_id = Arc::as_ptr(frames) as usize; self.pipeline.state.ingest(frames, frames_id, params, palette); } let stereo = self.pipeline.state.channels.len() > 1; let num_channels = self.pipeline.state.channels.len() as u32; let num_bins = self .pipeline .state .channels .first() .map(|c| c.bins.len() as u32) .unwrap_or(0); let w_px = width as f32; let h_px = height as f32; let (base_w, base_h, instances) = if params.mirrored { (w_px * 0.55, h_px * 0.5, 4u32) } else { (w_px, h_px, 1u32) }; let mut flags = 0u32; if params.glass { flags |= FLAG_GLASS; } if params.mirrored { flags |= FLAG_MIRRORED; } if params.inverted { flags |= FLAG_INVERTED; } if stereo { flags |= FLAG_STEREO; } let uc = self.pipeline.state.unified_color; let globals = GlobalsGpu { bounds: [w_px, h_px], base: [base_w, base_h], num_bins, num_channels, flags, fade_bins: if params.mirrored { 4 } else { 0 }, hue_param: params.hue, contrast: params.contrast, brightness: params.brightness, _pad0: 0.0, unified_hue: uc[0], unified_sat: uc[1], unified_val: uc[2], _pad1: 0.0, }; let mut scratch_bins = std::mem::take(&mut self.pipeline.scratch_bins); let mut scratch_cep = std::mem::take(&mut self.pipeline.scratch_cep); self.pipeline.state.pack_bins(frames, stereo, &mut scratch_bins); scratch_cep.clear(); if params.mirrored { build::build_cepstrum(&mut scratch_cep, &self.pipeline.state, w_px, h_px); } self.pipeline.scratch_bins = scratch_bins; self.pipeline.scratch_cep = scratch_cep; self.pipeline .upload(&self.device, &self.queue, &globals, num_channels, num_bins, instances); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("yr_crystals.pip.encoder"), }); { let _ = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("yr_crystals.pip.clear"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &self.view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, }); } let clip = iced_wgpu::core::Rectangle:: { x: 0, y: 0, width: self.width, height: self.height, }; self.pipeline.render_into(&mut encoder, &self.view, &clip); encoder.copy_texture_to_buffer( wgpu::TexelCopyTextureInfo { texture: &self.texture, mip_level: 0, origin: wgpu::Origin3d::ZERO, aspect: wgpu::TextureAspect::All, }, wgpu::TexelCopyBufferInfo { buffer: &self.staging, layout: wgpu::TexelCopyBufferLayout { offset: 0, bytes_per_row: Some(self.padded_bytes_per_row), rows_per_image: Some(self.height), }, }, wgpu::Extent3d { width: self.width, height: self.height, depth_or_array_layers: 1, }, ); self.queue.submit(std::iter::once(encoder.finish())); let slice = self.staging.slice(..); let (tx, rx) = std::sync::mpsc::channel(); slice.map_async(wgpu::MapMode::Read, move |r| { let _ = tx.send(r); }); let _ = self.device.poll(wgpu::PollType::wait_indefinitely()); if rx.recv().ok().and_then(|r| r.ok()).is_none() { return; } { let view = slice.get_mapped_range(); let row_bytes = (self.width * BYTES_PER_PIXEL) as usize; let padded = self.padded_bytes_per_row as usize; let dst_step = dst_stride as usize; for y in 0..self.height as usize { let src_row = &view[y * padded..y * padded + row_bytes]; unsafe { let dst_row = dst.add(y * dst_step); std::ptr::copy_nonoverlapping(src_row.as_ptr(), dst_row, row_bytes); } } } self.staging.unmap(); } } /// allocates the BGRA render texture, a matching view, and the row-aligned staging readback buffer. fn allocate_targets( device: &wgpu::Device, width: u32, height: u32, ) -> (wgpu::Texture, wgpu::TextureView, wgpu::Buffer, u32) { let texture = device.create_texture(&wgpu::TextureDescriptor { label: Some("yr_crystals.pip.texture"), size: wgpu::Extent3d { width: width.max(1), height: height.max(1), depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgpu::TextureDimension::D2, format: FORMAT, usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC, view_formats: &[], }); let view = texture.create_view(&wgpu::TextureViewDescriptor::default()); let unpadded = width * BYTES_PER_PIXEL; let padded_bytes_per_row = unpadded.div_ceil(COPY_BYTES_PER_ROW_ALIGNMENT) * COPY_BYTES_PER_ROW_ALIGNMENT; let staging = device.create_buffer(&wgpu::BufferDescriptor { label: Some("yr_crystals.pip.staging"), size: (padded_bytes_per_row * height.max(1)) as u64, usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ, mapped_at_creation: false, }); (texture, view, staging, padded_bytes_per_row) }