261 lines
8.7 KiB
Rust
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)
|
|
}
|