YrXtals/src/visualizer/pip.rs

261 lines
8.7 KiB
Rust

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<Vec<FrameData>>,
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::<u32> {
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)
}