use crate::autointensity::AutoIntensityResources;
use crate::gradient::{Gradient, RgbColor};
use crate::intermediate_texture::IntermediateTextureResources;
use crate::lut_texture::LutTextureResources;
use crate::pipeline::PipelineResources;
use crate::transform::AffineTransform;
use crate::types::UniformData;
use crate::uniform_buffer::UniformBufferResources;
use crate::waveform_resources::{WaveformMode, WaveformResources};
use cgmath::{Vector2, vec2};
use wgpu::{
CommandBuffer, Device, Queue, RenderPass, ShaderModule, ShaderModuleDescriptor, TextureFormat,
};
#[repr(C)]
#[derive(Copy, Clone, Default, Debug, PartialOrd, PartialEq, Ord, Eq)]
pub struct Size<T = u32> {
pub width: T,
pub height: T,
}
impl<T> From<(T, T)> for Size<T> {
fn from((width, height): (T, T)) -> Size<T> {
Size { width, height }
}
}
impl<T> From<[T; 2]> for Size<T> {
fn from([width, height]: [T; 2]) -> Size<T> {
Size { width, height }
}
}
#[derive(Debug, Copy, Clone)]
pub struct Viewport {
pub size: Size<f32>,
pub origin: Vector2<f32>,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("width and height must both greater than zero, got ({0}, {1})")]
ZeroSize(u32, u32),
#[error("no waveform")]
NoWaveform,
#[error("`render_intermediate()` must be called before to allocate necessary resource ({0})")]
MissingResources(&'static str),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
struct RenderConfig {
#[cfg(feature = "auto-intensity")]
calc_auto_intensity: bool,
gamma: f32,
intensity: f32,
beam_radius: f32,
decay: Decay,
viewport: Viewport,
}
#[derive(Debug)]
pub struct Renderer {
size: Size,
device: Device,
texture_format: TextureFormat,
uniform_resources: UniformBufferResources,
intermediate_texture_resources: Option<IntermediateTextureResources>,
lut_texture_resources: LutTextureResources,
waveform_resources: Option<WaveformResources>,
pipeline_resources: Option<PipelineResources>,
auto_intensity_resources: Option<AutoIntensityResources>,
render_shader: ShaderModule,
post_fx_shader: ShaderModule,
decay_shader: ShaderModule,
#[cfg(feature = "auto-intensity")]
auto_intensity_shader: ShaderModule,
render_config: RenderConfig,
}
impl Renderer {
pub fn new(device: &Device, texture_format: &TextureFormat, size: Size) -> Renderer {
let uniform_data = UniformBufferResources::new(device);
let render_shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Render Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("render.wgsl").into()),
});
let post_fx_shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Post-processing Shader"),
source: wgpu::ShaderSource::Wgsl(
include_str!(concat!(env!("OUT_DIR"), "/postfx.wgsl")).into(),
),
});
let decay_shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Decay Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("decay.wgsl").into()),
});
#[cfg(feature = "auto-intensity")]
let auto_intensity_shader = device.create_shader_module(ShaderModuleDescriptor {
label: Some("Auto Intensity Shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("auto_intensity.wgsl").into()),
});
let viewport = Viewport {
origin: vec2(0., -1.),
size: [1., 2.].into(),
};
Renderer {
device: device.clone(),
size,
intermediate_texture_resources: None,
lut_texture_resources: LutTextureResources::new(device),
waveform_resources: None,
auto_intensity_resources: None,
uniform_resources: uniform_data,
render_config: RenderConfig {
viewport,
beam_radius: 6.,
intensity: 1.,
gamma: 1.0,
decay: Decay::instant_redraw(),
#[cfg(feature = "auto-intensity")]
calc_auto_intensity: false,
},
texture_format: *texture_format,
pipeline_resources: None,
render_shader,
post_fx_shader,
decay_shader,
#[cfg(feature = "auto-intensity")]
auto_intensity_shader,
}
}
}
impl Renderer {
pub fn size(&self) -> Size {
self.size
}
pub fn resize(&mut self, new_size: Size) -> Result<()> {
if new_size.width == 0 || new_size.height == 0 {
return Err(Error::ZeroSize(new_size.width, new_size.height));
}
if self.size != new_size {
log::info!("Setting new size: {new_size:?}");
self.size = new_size;
self.intermediate_texture_resources = None;
}
Ok(())
}
pub fn set_xlim(&mut self, left: f32, right: f32) {
self.render_config.viewport.origin.x = f32::min(left, right);
self.render_config.viewport.size.width = (right - left).abs();
}
pub fn set_ylim(&mut self, lower: f32, upper: f32) {
self.render_config.viewport.origin.y = f32::min(lower, upper);
self.render_config.viewport.size.height = (upper - lower).abs();
}
pub fn set_intensity(&mut self, value: f32) {
self.render_config.intensity = value;
}
pub fn set_gamma(&mut self, value: f32) {
self.render_config.gamma = value;
}
pub fn set_beam_width(&mut self, width: f32) {
self.render_config.beam_radius = width / 2.;
}
pub fn set_waveform_yt(&mut self, waveform: &[f32]) {
log::info!("Setting new YT waveform ({} samples)", waveform.len());
if self.waveform_resources.as_ref().map(|wf| wf.mode()) != Some(WaveformMode::YT) {
self.pipeline_resources = None;
}
self.waveform_resources = Some(WaveformResources::new(
waveform,
&self.device,
WaveformMode::YT,
))
}
pub fn set_waveform_xy(&mut self, waveform: &[[f32; 2]]) {
log::info!("Setting new XY waveform ({} samples)", waveform.len());
if self.waveform_resources.as_ref().map(|wf| wf.mode()) != Some(WaveformMode::XY) {
self.pipeline_resources = None;
}
self.waveform_resources = Some(WaveformResources::new(
bytemuck::cast_slice(waveform),
&self.device,
WaveformMode::XY,
))
}
pub fn set_lut(&mut self, lut: impl IntoIterator<Item = (f32, RgbColor<f32>)>) {
self.lut_texture_resources.update_lut(Gradient::new(lut));
}
}
#[derive(Debug, Copy, Clone)]
pub struct Decay {
screen_decay: f32,
sample_decay: f32,
}
impl Decay {
pub fn new(mut screen_decay: f32, mut sample_decay: f32) -> Decay {
screen_decay = screen_decay.max(0.);
sample_decay = sample_decay.clamp(0., 1.);
Decay {
screen_decay,
sample_decay,
}
}
pub fn with_sample_decay(mut self, sample_decay: f32) -> Decay {
self.sample_decay = sample_decay.clamp(0., 1.);
self
}
pub fn with_screen_decay(mut self, screen_decay: f32) -> Decay {
self.screen_decay = screen_decay.max(0.);
self
}
}
impl Decay {
pub fn no_decay() -> Decay {
Decay {
sample_decay: 0.,
screen_decay: 0.,
}
}
pub fn instant_redraw() -> Decay {
Decay {
screen_decay: f32::INFINITY,
sample_decay: 0.,
}
}
pub fn continuous_update(
time_since_last_trace: f32,
persistence: f32,
trace_length: usize,
) -> Decay {
Decay::new(
time_since_last_trace / persistence,
time_since_last_trace / persistence / (trace_length as f32),
)
}
}
impl Default for Decay {
fn default() -> Self {
Decay::instant_redraw()
}
}
impl Renderer {
pub fn set_decay(&mut self, decay: Decay) {
self.render_config.decay = decay
}
}
impl Renderer {
fn prepare(&mut self, queue: &Queue) -> Result<()> {
self.lut_texture_resources.prepare(queue);
if self.intermediate_texture_resources.is_none() {
self.intermediate_texture_resources =
Some(IntermediateTextureResources::new(self.size, &self.device));
self.pipeline_resources = None;
}
let intermediate_texture_resources = self.intermediate_texture_resources.as_ref().unwrap();
if self.auto_intensity_resources.is_none() {
let auto_intensity_data = AutoIntensityResources::new(&self.device);
self.auto_intensity_resources = Some(auto_intensity_data);
#[cfg(feature = "auto-intensity")]
self.reset_auto_intensity(queue)?;
}
let Some(waveform_resources) = self.waveform_resources.as_ref() else {
return Err(Error::NoWaveform);
};
let auto_intensity_data = self.auto_intensity_resources.as_ref().unwrap();
if self.pipeline_resources.is_none() {
self.pipeline_resources = Some(PipelineResources::new(
&self.render_shader,
&self.decay_shader,
&self.post_fx_shader,
#[cfg(feature = "auto-intensity")]
&self.auto_intensity_shader,
&self.device,
&self.texture_format,
intermediate_texture_resources,
&self.lut_texture_resources,
&self.uniform_resources,
waveform_resources,
auto_intensity_data,
));
}
self.update_uniform_buffer(queue);
#[cfg(feature = "auto-intensity")]
if self.render_config.calc_auto_intensity {
self.set_auto_intensity_value(queue, 1)?;
}
Ok(())
}
fn update_uniform_buffer(&mut self, queue: &Queue) {
let transform = AffineTransform::new()
.translate(
-self.render_config.viewport.origin.x,
-self.render_config.viewport.origin.y,
)
.scale(
1. / self.render_config.viewport.size.width,
1. / self.render_config.viewport.size.height,
)
.scale(self.size.width as f32, self.size.height as f32)
.wgpu_mat3x3();
let uniform_data = UniformData {
resolution: [self.size.width, self.size.height],
transform,
beam_radius: self.render_config.beam_radius,
intensity: self.render_config.intensity,
gamma: self.render_config.gamma,
decay_per_sample: self.render_config.decay.sample_decay,
num_samples: self
.waveform_resources
.as_ref()
.map(|wf| wf.num_samples())
.unwrap_or(0) as u32,
screen_decay: self.render_config.decay.screen_decay,
};
self.uniform_resources.update_data(queue, uniform_data);
}
pub fn render_intermediate(&mut self, queue: &Queue) -> Result<CommandBuffer> {
self.prepare(queue)?;
let texture_data = self
.intermediate_texture_resources
.as_ref()
.expect("texture_data is missing");
let pipelines = self
.pipeline_resources
.as_ref()
.expect("pipeline_data is missing");
let Some(waveform_resources) = &self.waveform_resources else {
return Err(Error::NoWaveform);
};
let mut encoder = self
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Encoder"),
});
{
let load_op = if self.render_config.decay.screen_decay == f32::INFINITY
|| !texture_data.is_texture_initialized()
{
texture_data.assume_texture_initialized();
wgpu::LoadOp::Clear(wgpu::Color::BLACK)
} else {
wgpu::LoadOp::Load
};
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: texture_data.texture_view(),
resolve_target: None,
ops: wgpu::Operations {
load: load_op,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
occlusion_query_set: None,
timestamp_writes: None,
});
if self.render_config.decay.screen_decay > 0.0
&& self.render_config.decay.screen_decay != f32::INFINITY
{
render_pass.set_pipeline(pipelines.decay_pipeline());
render_pass.set_bind_group(0, self.uniform_resources.bind_group(), &[]);
render_pass.draw(0..3, 0..1); }
render_pass.set_pipeline(pipelines.render_pipeline());
render_pass.set_bind_group(0, self.uniform_resources.bind_group(), &[]);
if waveform_resources.num_samples() >= 2 {
let (start_index, end_index) = match waveform_resources.mode() {
WaveformMode::YT => {
let start_index: u32 =
(self.render_config.viewport.origin.x.floor().max(0.) as i32)
.min(waveform_resources.num_samples() as i32 - 1)
.try_into()
.unwrap();
let end_index: u32 = ((self.render_config.viewport.origin.x
+ self.render_config.viewport.size.width)
.ceil()
.max(0.) as i32)
.min(waveform_resources.num_samples() as i32 - 1)
.try_into()
.unwrap();
(start_index, end_index)
}
WaveformMode::XY => (0, waveform_resources.num_samples() as u32 - 1),
};
let span = end_index - start_index;
render_pass.set_vertex_buffer(0, waveform_resources.buffer().slice(..));
render_pass.draw(0..4, (start_index / 2)..(start_index + span).div_ceil(2));
let stride = waveform_resources.samples_size_bytes() as u64;
render_pass.set_vertex_buffer(0, waveform_resources.buffer().slice(stride..));
render_pass.draw(4..8, (start_index / 2)..(start_index + span) / 2);
}
}
#[cfg(feature = "auto-intensity")]
if self.render_config.calc_auto_intensity {
let auto_intensity_data = self
.auto_intensity_resources
.as_ref()
.expect("auto_intensity_data is missing");
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("Auto Intensity Compute Pass"),
timestamp_writes: None,
});
compute_pass.set_pipeline(pipelines.auto_intensity_pipeline());
compute_pass.set_bind_group(0, texture_data.bind_group(), &[]);
compute_pass.set_bind_group(1, auto_intensity_data.bind_group_compute(), &[]);
compute_pass.dispatch_workgroups(
self.size.width.div_ceil(16),
self.size.height.div_ceil(16),
1,
);
}
Ok(encoder.finish())
}
pub fn render(&self, render_pass: &mut RenderPass) -> Result<()> {
let Some(ref intermediate_texture_data) = self.intermediate_texture_resources else {
return Err(Error::MissingResources("intermediate_texture_data"));
};
let Some(auto_intensity_data) = &self.auto_intensity_resources else {
return Err(Error::MissingResources("auto_intensity_data"));
};
let Some(ref pipelines) = self.pipeline_resources else {
return Err(Error::MissingResources("pipeline_data"));
};
render_pass.set_pipeline(pipelines.post_fx_pipeline());
render_pass.set_bind_group(0, intermediate_texture_data.bind_group(), &[]);
render_pass.set_bind_group(1, self.lut_texture_resources.bind_group(), &[]);
render_pass.set_bind_group(2, auto_intensity_data.bind_group_fragment(), &[]);
render_pass.set_bind_group(3, self.uniform_resources.bind_group(), &[]);
render_pass.draw(0..3, 0..1);
Ok(())
}
}
#[cfg(feature = "auto-intensity")]
impl Renderer {
pub fn enable_auto_intensity(&mut self, value: bool) {
if value != self.render_config.calc_auto_intensity {
log::info!("Auto-intensity enabled: {value:?}");
self.render_config.calc_auto_intensity = value
}
}
pub fn reset_auto_intensity(&self, queue: &Queue) -> Result<()> {
log::info!("Resetting auto-intensity value to default");
self.set_auto_intensity_value(queue, 1 << 16)
}
fn set_auto_intensity_value(&self, queue: &Queue, value: u32) -> Result<()> {
let Some(ref auto_intensity_data) = self.auto_intensity_resources else {
return Err(Error::MissingResources("auto_intensity_data"));
};
log::debug!("Setting auto-intensity value to {value}");
queue.write_buffer(
auto_intensity_data.max_value_buffer(),
0,
bytemuck::cast_slice(&[value]),
);
Ok(())
}
}