Skip to main content

phosphor/
renderer.rs

1use crate::autointensity::AutoIntensityResources;
2use crate::gradient::{Gradient, RgbColor};
3use crate::intermediate_texture::IntermediateTextureResources;
4use crate::lut_texture::LutTextureResources;
5use crate::pipeline::PipelineResources;
6use crate::transform::AffineTransform;
7use crate::types::UniformData;
8use crate::uniform_buffer::UniformBufferResources;
9use crate::waveform_resources::{WaveformMode, WaveformResources};
10use cgmath::{Vector2, vec2};
11use wgpu::{
12    CommandBuffer, Device, Queue, RenderPass, ShaderModule, ShaderModuleDescriptor, TextureFormat,
13};
14
15#[repr(C)]
16#[derive(Copy, Clone, Default, Debug, PartialOrd, PartialEq, Ord, Eq)]
17pub struct Size<T = u32> {
18    pub width: T,
19    pub height: T,
20}
21
22impl<T> From<(T, T)> for Size<T> {
23    fn from((width, height): (T, T)) -> Size<T> {
24        Size { width, height }
25    }
26}
27
28impl<T> From<[T; 2]> for Size<T> {
29    fn from([width, height]: [T; 2]) -> Size<T> {
30        Size { width, height }
31    }
32}
33
34#[derive(Debug, Copy, Clone)]
35pub struct Viewport {
36    pub size: Size<f32>,
37    pub origin: Vector2<f32>,
38}
39
40#[derive(thiserror::Error, Debug)]
41pub enum Error {
42    #[error("width and height must both greater than zero, got ({0}, {1})")]
43    ZeroSize(u32, u32),
44    #[error("no waveform")]
45    NoWaveform,
46    #[error("`render_intermediate()` must be called before to allocate necessary resource ({0})")]
47    MissingResources(&'static str),
48}
49
50pub type Result<T> = std::result::Result<T, Error>;
51
52#[derive(Debug)]
53struct RenderConfig {
54    #[cfg(feature = "auto-intensity")]
55    calc_auto_intensity: bool,
56    gamma: f32,
57    intensity: f32,
58    beam_radius: f32,
59    decay: Decay,
60    viewport: Viewport,
61}
62
63/// Renderer context.
64///
65/// The `Renderer` is the main entry point. It uses a multi-pass rendering pipeline:
66///
67/// 1. **Intermediate rendering**: Waveform segments are rendered to a high-dynamic-range texture.
68/// 2. (optional) **Auto-intensity**: The maximum value of the texture is determined for normalization.
69/// 3. **Post-processing**: The intermediate texture is drawn into the scene while color mapping is applied using customizable gradient LUT.
70///
71/// # Example
72///
73/// ```rust
74/// use phosphor::{Renderer, gradient::{Gradient, RgbColor}};
75/// use wgpu::{Device, Queue, RenderPass, TextureFormat};
76///
77/// # fn example(device: &Device, queue: &Queue, render_pass: &mut RenderPass) -> Result<(), Box<dyn std::error::Error>> {
78/// // Create a renderer for 800x600 output
79/// let mut renderer = Renderer::new(device, &TextureFormat::Bgra8Unorm, (800, 600).into());
80///
81/// // Set waveform data (Vec<[f32; 2]> of (x, y) coordinates)
82/// let waveform: Vec<[f32;2]> = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 0.5]];
83/// renderer.set_waveform_xy(&waveform);
84///
85/// // Configure appearance
86/// renderer.set_intensity(2.0);
87/// renderer.set_xlim(0.0, 2.0); // Set X-axis range
88/// renderer.set_ylim(-1.0, 1.0); // Set Y-axis range
89///
90/// // Set color gradient (black to green)
91/// let gradient = Gradient::new(vec![
92///     (0.0, RgbColor { r: 0.0, g: 0.0, b: 0.0 }),
93///     (1.0, RgbColor { r: 0.0, g: 1.0, b: 0.0 }),
94/// ]);
95/// renderer.set_lut(gradient);
96///
97/// // Render intermediate texture
98/// let intermediate_cmd = renderer.render_intermediate(queue)?;
99/// queue.submit([intermediate_cmd]);
100/// // The final render pass just paints the previously computed texture into your scene.
101/// // For performance, it is advisable to re-use the same `RenderPass` as you use for
102/// // drawing the rest of your scene, so we expect you to provide your own render pass.
103/// renderer.render(render_pass)?;
104/// # Ok(())
105/// # }
106/// ```
107#[derive(Debug)]
108pub struct Renderer {
109    size: Size,
110    device: Device,
111    texture_format: TextureFormat,
112
113    uniform_resources: UniformBufferResources,
114    intermediate_texture_resources: Option<IntermediateTextureResources>,
115    lut_texture_resources: LutTextureResources,
116    waveform_resources: Option<WaveformResources>,
117    pipeline_resources: Option<PipelineResources>,
118    auto_intensity_resources: Option<AutoIntensityResources>,
119
120    render_shader: ShaderModule,
121    post_fx_shader: ShaderModule,
122    decay_shader: ShaderModule,
123    #[cfg(feature = "auto-intensity")]
124    auto_intensity_shader: ShaderModule,
125
126    render_config: RenderConfig,
127}
128
129impl Renderer {
130    /// Creates a new waveform renderer.
131    ///
132    /// # Arguments
133    ///
134    /// * `device` - WebGPU device for GPU resource creation
135    /// * `texture_format` - Target texture format for final rendering (e.g., `TextureFormat::Bgra8Unorm`)
136    /// * `size` - Initial render target dimensions in pixels.
137    ///
138    /// # Example
139    ///
140    /// ```rust
141    /// use phosphor::Renderer;
142    /// use wgpu::{Device, TextureFormat};
143    ///
144    /// # fn example(device: &Device) {
145    /// let renderer = Renderer::new(device, &TextureFormat::Bgra8Unorm, (1920, 1080).into());
146    /// # }
147    /// ```
148    pub fn new(device: &Device, texture_format: &TextureFormat, size: Size) -> Renderer {
149        let uniform_data = UniformBufferResources::new(device);
150
151        let render_shader = device.create_shader_module(ShaderModuleDescriptor {
152            label: Some("Render Shader"),
153            source: wgpu::ShaderSource::Wgsl(include_str!("render.wgsl").into()),
154        });
155        let post_fx_shader = device.create_shader_module(ShaderModuleDescriptor {
156            label: Some("Post-processing Shader"),
157            source: wgpu::ShaderSource::Wgsl(
158                include_str!(concat!(env!("OUT_DIR"), "/postfx.wgsl")).into(),
159            ),
160        });
161        let decay_shader = device.create_shader_module(ShaderModuleDescriptor {
162            label: Some("Decay Shader"),
163            source: wgpu::ShaderSource::Wgsl(include_str!("decay.wgsl").into()),
164        });
165
166        #[cfg(feature = "auto-intensity")]
167        let auto_intensity_shader = device.create_shader_module(ShaderModuleDescriptor {
168            label: Some("Auto Intensity Shader"),
169            source: wgpu::ShaderSource::Wgsl(include_str!("auto_intensity.wgsl").into()),
170        });
171
172        let viewport = Viewport {
173            origin: vec2(0., -1.),
174            size: [1., 2.].into(),
175        };
176
177        Renderer {
178            device: device.clone(),
179            size,
180            intermediate_texture_resources: None,
181            lut_texture_resources: LutTextureResources::new(device),
182            waveform_resources: None,
183            auto_intensity_resources: None,
184            uniform_resources: uniform_data,
185            render_config: RenderConfig {
186                viewport,
187                beam_radius: 6.,
188                intensity: 1.,
189                gamma: 1.0,
190                decay: Decay::instant_redraw(),
191                #[cfg(feature = "auto-intensity")]
192                calc_auto_intensity: false,
193            },
194            texture_format: *texture_format,
195            pipeline_resources: None,
196            render_shader,
197            post_fx_shader,
198            decay_shader,
199            #[cfg(feature = "auto-intensity")]
200            auto_intensity_shader,
201        }
202    }
203}
204
205impl Renderer {
206    /// Returns the current render target size in pixels.
207    pub fn size(&self) -> Size {
208        self.size
209    }
210
211    /// Resizes the render target and recreates internal resources.
212    ///
213    /// This updates the intermediate texture size and affects the apparent beam width.
214    /// Must be called when the target texture dimensions change. If it mismatches the
215    /// size of the final render target, then artifacts (distorted aspect ratio, ...)
216    /// might be visible.
217    ///
218    /// # Arguments
219    ///
220    /// * `new_size` - New render target dimensions in pixels
221    ///
222    /// # Errors
223    ///
224    /// Returns `Error::ZeroSize` if either width or height is zero.
225    ///
226    /// # Example
227    ///
228    /// ```rust
229    /// # use phosphor::Renderer;
230    /// # use wgpu::{Device, TextureFormat};
231    /// # fn example(device: &Device) -> Result<(), phosphor::renderer::Error> {
232    /// let mut renderer = Renderer::new(device, &TextureFormat::Bgra8Unorm, (800, 600).into());
233    /// renderer.resize((1920, 1080).into())?;
234    /// # Ok(())
235    /// # }
236    /// ```
237    pub fn resize(&mut self, new_size: Size) -> Result<()> {
238        if new_size.width == 0 || new_size.height == 0 {
239            return Err(Error::ZeroSize(new_size.width, new_size.height));
240        }
241        if self.size != new_size {
242            log::info!("Setting new size: {new_size:?}");
243            self.size = new_size;
244            self.intermediate_texture_resources = None;
245        }
246        Ok(())
247    }
248
249    /// Sets the horizontal axis range (X-axis limits).
250    ///
251    /// Defines the range of X coordinates that will be visible in the rendered output.
252    /// Values outside this range will be clipped.
253    ///
254    /// # Example
255    ///
256    /// ```rust
257    /// # use phosphor::Renderer;
258    /// # use wgpu::{Device, TextureFormat};
259    /// # fn example(device: &Device) {
260    /// let mut renderer = Renderer::new(device, &TextureFormat::Bgra8Unorm, (800, 600).into());
261    /// renderer.set_xlim(-10.0, 10.0); // Show X range from -10 to +10
262    /// # }
263    /// ```
264    pub fn set_xlim(&mut self, left: f32, right: f32) {
265        self.render_config.viewport.origin.x = f32::min(left, right);
266        self.render_config.viewport.size.width = (right - left).abs();
267    }
268
269    /// Sets the vertical axis range (Y-axis limits).
270    ///
271    /// Defines the range of Y coordinates that will be visible in the rendered output.
272    /// Values outside this range will be clipped.
273    ///
274    /// # Example
275    ///
276    /// ```rust
277    /// # use phosphor::Renderer;
278    /// # use wgpu::{Device, TextureFormat};
279    /// # fn example(device: &Device) {
280    /// let mut renderer = Renderer::new(device, &TextureFormat::Bgra8Unorm, (800, 600).into());
281    /// renderer.set_ylim(-1.0, 1.0); // Show Y range from -1 to +1
282    /// # }
283    /// ```
284    pub fn set_ylim(&mut self, lower: f32, upper: f32) {
285        self.render_config.viewport.origin.y = f32::min(lower, upper);
286        self.render_config.viewport.size.height = (upper - lower).abs();
287    }
288
289    /// Sets the intensity (brightness multiplier) for the rendered waveform.
290    ///
291    /// Controls the overall brightness of the rendered trace. When auto-intensity is enabled
292    /// (see [`Renderer::enable_auto_intensity`]), this value is multiplied with the
293    /// automatically calculated intensity.
294    pub fn set_intensity(&mut self, value: f32) {
295        self.render_config.intensity = value;
296    }
297
298    /// Set the gamma correction value for the waveform rendering.
299    pub fn set_gamma(&mut self, value: f32) {
300        self.render_config.gamma = value;
301    }
302
303    /// Set the beam width (in pixels) for the waveform rendering.
304    pub fn set_beam_width(&mut self, width: f32) {
305        self.render_config.beam_radius = width / 2.;
306    }
307
308    /// Load the samples to draw in the next call to [`Renderer::render_intermediate()`]
309    ///
310    /// The x-coordinates of the samples are not given; instead they are implicitly assumed
311    /// to be 0, 1, 2, ..., N-1, where N is the number of samples. This is useful for drawing
312    /// a waveform where the beam sweeps uniformly from the left to the right of the screen.
313    /// Use [`Renderer::set_waveform_xy()`] if you need to specify arbitrary x and y coordinates.
314    ///
315    /// Loading a new waveform invalidates the GPU buffer that holds the waveform data.
316    pub fn set_waveform_yt(&mut self, waveform: &[f32]) {
317        log::info!("Setting new YT waveform ({} samples)", waveform.len());
318        if self.waveform_resources.as_ref().map(|wf| wf.mode()) != Some(WaveformMode::YT) {
319            // switching the waveform mode requires switching the shader
320            self.pipeline_resources = None;
321        }
322        // todo: reuse existing buffer
323        self.waveform_resources = Some(WaveformResources::new(
324            waveform,
325            &self.device,
326            WaveformMode::YT,
327        ))
328    }
329
330    /// Load the samples to draw in the next call to [`Renderer::render_intermediate()`]
331    ///
332    /// Each individual sample is specified via its x and y coordinates, in a `[f32; 2]`.
333    /// If the samples are uniformly spaced along the x-axis, strongly consider using
334    /// [`Renderer::set_waveform_yt`] instead. It implicitly assigns x coordinates to the samples.
335    /// Not only does this save some memory, it may also significantly faster because it can efficiently
336    /// cull those samples with an x position outside the viewport.
337    ///
338    /// Loading a new waveform invalidates the GPU buffer that holds the waveform data.
339    pub fn set_waveform_xy(&mut self, waveform: &[[f32; 2]]) {
340        log::info!("Setting new XY waveform ({} samples)", waveform.len());
341        if self.waveform_resources.as_ref().map(|wf| wf.mode()) != Some(WaveformMode::XY) {
342            // switching the waveform mode requires switching the shader
343            self.pipeline_resources = None;
344        }
345        // todo: reuse existing buffer
346        self.waveform_resources = Some(WaveformResources::new(
347            bytemuck::cast_slice(waveform),
348            &self.device,
349            WaveformMode::XY,
350        ))
351    }
352
353    pub fn set_lut(&mut self, lut: impl IntoIterator<Item = (f32, RgbColor<f32>)>) {
354        self.lut_texture_resources.update_lut(Gradient::new(lut));
355    }
356}
357
358/// Natural decay behaviour of the simulated phosphor screen.
359///
360/// We provide two tuning knobs:
361/// - the decay between consecutive samples in the same trace
362/// - the decay between consecutive traces
363///
364/// The former models the finite time it takes for the scope to trace a single sweep across the screen.
365/// The latter models the time between two shots.
366///
367/// # `screen_decay`
368/// The decay factor applied to the entire phosphor screen, applied the next time when
369/// [`Renderer::render_intermediate()`] is called. Each pixel is faded according to a factor
370/// of `exp(-screen_decay)`. A value of either exactly 0.0 (no decay, waveforms are drawn on
371/// top of each other) or `+INFINITY` (new waveforms completely erase previous ones) bypass the
372/// blending logic and are slightly more efficient. A value below 0 is clamped.
373///
374/// # `sample_decay`
375/// The decay factor applied between consecutive samples in the same trace. The very last sample
376/// is painted with full nominal intensity, and the N-th sample before the last has its intensity
377/// scaled by a factor `(1 - sample_decay).pow(N)`. Setting `sample_decay` to 0.0 will render all
378/// samples at full intensity. A value outside the [0, 1] range is clamped.
379#[derive(Debug, Copy, Clone)]
380pub struct Decay {
381    screen_decay: f32,
382    sample_decay: f32,
383}
384
385impl Decay {
386    pub fn new(mut screen_decay: f32, mut sample_decay: f32) -> Decay {
387        screen_decay = screen_decay.max(0.);
388        sample_decay = sample_decay.clamp(0., 1.);
389        Decay {
390            screen_decay,
391            sample_decay,
392        }
393    }
394
395    pub fn with_sample_decay(mut self, sample_decay: f32) -> Decay {
396        self.sample_decay = sample_decay.clamp(0., 1.);
397        self
398    }
399
400    pub fn with_screen_decay(mut self, screen_decay: f32) -> Decay {
401        self.screen_decay = screen_decay.max(0.);
402        self
403    }
404}
405
406/// Ready-made configurations for common use cases.
407impl Decay {
408    /// No decay. All render commands draw on top of each other.
409    pub fn no_decay() -> Decay {
410        Decay {
411            sample_decay: 0.,
412            screen_decay: 0.,
413        }
414    }
415
416    /// No sample-to-sample decay, and no persistence between traces.
417    ///
418    /// Useful for 'single-shot digital storage oscilloscope' applications, where e.g. a full trace
419    /// is acquired once, and then frozen. The user can pan and zoom the viewport for inspecting
420    /// the waveform, without the trace fading.
421    ///
422    /// This is what the [`Default::default()`] implementation returns.
423    pub fn instant_redraw() -> Decay {
424        Decay {
425            screen_decay: f32::INFINITY,
426            sample_decay: 0.,
427        }
428    }
429
430    /// Decay simulating a continuously updated waveform with no dead-time between shots.
431    ///
432    /// Think of a free-running oscilloscope in XY mode.
433    ///
434    /// # Arguments
435    ///
436    /// * `time_since_last_frame` - Time (e.g. in seconds) since the last frame was drawn to the screen
437    /// * `persistence` - Time (same units) for the phosphor screen to decay to 37% (1/_e_) of its original
438    ///   brightness.
439    /// * `trace_length` - The number of samples in the waveform about to be drawn.
440    pub fn continuous_update(
441        time_since_last_trace: f32,
442        persistence: f32,
443        trace_length: usize,
444    ) -> Decay {
445        Decay::new(
446            time_since_last_trace / persistence,
447            time_since_last_trace / persistence / (trace_length as f32),
448        )
449    }
450}
451
452impl Default for Decay {
453    fn default() -> Self {
454        Decay::instant_redraw()
455    }
456}
457
458/// Methods for configuring decay behaviour.
459impl Renderer {
460    pub fn set_decay(&mut self, decay: Decay) {
461        self.render_config.decay = decay
462    }
463}
464
465/// Methods for rendering workflow.
466impl Renderer {
467    fn prepare(&mut self, queue: &Queue) -> Result<()> {
468        self.lut_texture_resources.prepare(queue);
469        if self.intermediate_texture_resources.is_none() {
470            self.intermediate_texture_resources =
471                Some(IntermediateTextureResources::new(self.size, &self.device));
472            self.pipeline_resources = None;
473        }
474        let intermediate_texture_resources = self.intermediate_texture_resources.as_ref().unwrap();
475
476        if self.auto_intensity_resources.is_none() {
477            let auto_intensity_data = AutoIntensityResources::new(&self.device);
478            self.auto_intensity_resources = Some(auto_intensity_data);
479            #[cfg(feature = "auto-intensity")]
480            self.reset_auto_intensity(queue)?;
481        }
482
483        let Some(waveform_resources) = self.waveform_resources.as_ref() else {
484            return Err(Error::NoWaveform);
485        };
486
487        let auto_intensity_data = self.auto_intensity_resources.as_ref().unwrap();
488        if self.pipeline_resources.is_none() {
489            self.pipeline_resources = Some(PipelineResources::new(
490                &self.render_shader,
491                &self.decay_shader,
492                &self.post_fx_shader,
493                #[cfg(feature = "auto-intensity")]
494                &self.auto_intensity_shader,
495                &self.device,
496                &self.texture_format,
497                intermediate_texture_resources,
498                &self.lut_texture_resources,
499                &self.uniform_resources,
500                waveform_resources,
501                auto_intensity_data,
502            ));
503        }
504
505        self.update_uniform_buffer(queue);
506
507        #[cfg(feature = "auto-intensity")]
508        if self.render_config.calc_auto_intensity {
509            // reset the auto-intensity value to its minimum. The shader performs atomic
510            // max operations, and thus can only increase it.
511            self.set_auto_intensity_value(queue, 1)?;
512        }
513
514        Ok(())
515    }
516
517    fn update_uniform_buffer(&mut self, queue: &Queue) {
518        let transform = AffineTransform::new()
519            .translate(
520                -self.render_config.viewport.origin.x,
521                -self.render_config.viewport.origin.y,
522            )
523            .scale(
524                1. / self.render_config.viewport.size.width,
525                1. / self.render_config.viewport.size.height,
526            )
527            .scale(self.size.width as f32, self.size.height as f32)
528            .wgpu_mat3x3();
529
530        let uniform_data = UniformData {
531            resolution: [self.size.width, self.size.height],
532            transform,
533            beam_radius: self.render_config.beam_radius,
534            intensity: self.render_config.intensity,
535            gamma: self.render_config.gamma,
536            decay_per_sample: self.render_config.decay.sample_decay,
537            num_samples: self
538                .waveform_resources
539                .as_ref()
540                .map(|wf| wf.num_samples())
541                .unwrap_or(0) as u32,
542            screen_decay: self.render_config.decay.screen_decay,
543        };
544        self.uniform_resources.update_data(queue, uniform_data);
545    }
546
547    /// Render the waveform into intermediate texture.
548    ///
549    /// Enqueue the resulting command buffer before `render()`, to update the intermediate
550    /// waveform texture. For large waveforms especially, this is the more expensive pass.
551    ///
552    /// This method automatically prepares resources as needed before rendering.
553    pub fn render_intermediate(&mut self, queue: &Queue) -> Result<CommandBuffer> {
554        self.prepare(queue)?;
555        let texture_data = self
556            .intermediate_texture_resources
557            .as_ref()
558            .expect("texture_data is missing");
559        let pipelines = self
560            .pipeline_resources
561            .as_ref()
562            .expect("pipeline_data is missing");
563
564        let Some(waveform_resources) = &self.waveform_resources else {
565            return Err(Error::NoWaveform);
566        };
567
568        let mut encoder = self
569            .device
570            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
571                label: Some("Encoder"),
572            });
573        {
574            // Determine load operation based on screen decay and initialization state
575            let load_op = if self.render_config.decay.screen_decay == f32::INFINITY
576                || !texture_data.is_texture_initialized()
577            {
578                texture_data.assume_texture_initialized();
579                wgpu::LoadOp::Clear(wgpu::Color::BLACK)
580            } else {
581                wgpu::LoadOp::Load
582            };
583
584            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
585                label: Some("Render Pass"),
586                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
587                    view: texture_data.texture_view(),
588                    resolve_target: None,
589                    ops: wgpu::Operations {
590                        load: load_op,
591                        store: wgpu::StoreOp::Store,
592                    },
593                })],
594                depth_stencil_attachment: None,
595                occlusion_query_set: None,
596                timestamp_writes: None,
597            });
598
599            // Apply screen decay if needed
600            if self.render_config.decay.screen_decay > 0.0
601                && self.render_config.decay.screen_decay != f32::INFINITY
602            {
603                render_pass.set_pipeline(pipelines.decay_pipeline());
604                render_pass.set_bind_group(0, self.uniform_resources.bind_group(), &[]);
605                render_pass.draw(0..3, 0..1); // Draw full-screen triangle
606            }
607
608            // Render waveform
609            render_pass.set_pipeline(pipelines.render_pipeline());
610            render_pass.set_bind_group(0, self.uniform_resources.bind_group(), &[]);
611            // We need at least 2 samples to draw a segment.
612            if waveform_resources.num_samples() >= 2 {
613                // In YT mode, we can cull a significant chunk of the waveform just based on the horizontal
614                // extent of the viewport. In XY mode, this is not possible.
615                let (start_index, end_index) = match waveform_resources.mode() {
616                    WaveformMode::YT => {
617                        // valid (!) indices into the waveform array that (maybe) are just outside the viewport
618                        let start_index: u32 =
619                            (self.render_config.viewport.origin.x.floor().max(0.) as i32)
620                                .min(waveform_resources.num_samples() as i32 - 1)
621                                .try_into()
622                                .unwrap();
623                        let end_index: u32 = ((self.render_config.viewport.origin.x
624                            + self.render_config.viewport.size.width)
625                            .ceil()
626                            .max(0.) as i32)
627                            .min(waveform_resources.num_samples() as i32 - 1)
628                            .try_into()
629                            .unwrap();
630                        (start_index, end_index)
631                    }
632                    WaveformMode::XY => (0, waveform_resources.num_samples() as u32 - 1),
633                };
634
635                // Draw even numbered line segments...
636                let span = end_index - start_index;
637                render_pass.set_vertex_buffer(0, waveform_resources.buffer().slice(..));
638                render_pass.draw(0..4, (start_index / 2)..(start_index + span).div_ceil(2));
639
640                // ... and the odd numbered ones.
641                let stride = waveform_resources.samples_size_bytes() as u64;
642                render_pass.set_vertex_buffer(0, waveform_resources.buffer().slice(stride..));
643                render_pass.draw(4..8, (start_index / 2)..(start_index + span) / 2);
644            }
645        }
646        #[cfg(feature = "auto-intensity")]
647        if self.render_config.calc_auto_intensity {
648            let auto_intensity_data = self
649                .auto_intensity_resources
650                .as_ref()
651                .expect("auto_intensity_data is missing");
652            let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
653                label: Some("Auto Intensity Compute Pass"),
654                timestamp_writes: None,
655            });
656            compute_pass.set_pipeline(pipelines.auto_intensity_pipeline());
657            compute_pass.set_bind_group(0, texture_data.bind_group(), &[]);
658            compute_pass.set_bind_group(1, auto_intensity_data.bind_group_compute(), &[]);
659            compute_pass.dispatch_workgroups(
660                self.size.width.div_ceil(16),
661                self.size.height.div_ceil(16),
662                1,
663            );
664        }
665
666        Ok(encoder.finish())
667    }
668
669    /// Render final output using caller-provided render pass.
670    ///
671    /// This just composites a texture into the output buffer, so it is a fairly inexpensive
672    /// operation.
673    pub fn render(&self, render_pass: &mut RenderPass) -> Result<()> {
674        let Some(ref intermediate_texture_data) = self.intermediate_texture_resources else {
675            return Err(Error::MissingResources("intermediate_texture_data"));
676        };
677        let Some(auto_intensity_data) = &self.auto_intensity_resources else {
678            return Err(Error::MissingResources("auto_intensity_data"));
679        };
680        let Some(ref pipelines) = self.pipeline_resources else {
681            return Err(Error::MissingResources("pipeline_data"));
682        };
683        render_pass.set_pipeline(pipelines.post_fx_pipeline());
684        render_pass.set_bind_group(0, intermediate_texture_data.bind_group(), &[]);
685        render_pass.set_bind_group(1, self.lut_texture_resources.bind_group(), &[]);
686        render_pass.set_bind_group(2, auto_intensity_data.bind_group_fragment(), &[]);
687        render_pass.set_bind_group(3, self.uniform_resources.bind_group(), &[]);
688        render_pass.draw(0..3, 0..1);
689        Ok(())
690    }
691}
692
693/// The following methods require feature flag `auto-intensity`.
694#[cfg(feature = "auto-intensity")]
695impl Renderer {
696    /// Enable automatic intensity calculation during subsequent calls to [`Renderer::render_intermediate()`].
697    ///
698    /// When drawing the trace, it will be normalized to the brightest pixel. A manually specified
699    /// intensity ([`Renderer::set_intensity()`]) will be applied to the normalized value.
700    /// The auto-intensity calculation will remain enabled for subsequent frames until disabled again.
701    /// Once disabled, the last normalization factor will stay in use until
702    /// [`Renderer::reset_auto_intensity()`] is called.
703    pub fn enable_auto_intensity(&mut self, value: bool) {
704        if value != self.render_config.calc_auto_intensity {
705            log::info!("Auto-intensity enabled: {value:?}");
706            self.render_config.calc_auto_intensity = value
707        }
708    }
709
710    /// Reset the previously computed auto-intensity to the default value.
711    ///
712    /// Note: this will not disable re-computation of the auto-intensity in the next frame.
713    /// Call [`Renderer::enable_auto_intensity`]`(false)` instead.
714    pub fn reset_auto_intensity(&self, queue: &Queue) -> Result<()> {
715        log::info!("Resetting auto-intensity value to default");
716        self.set_auto_intensity_value(queue, 1 << 16)
717    }
718
719    fn set_auto_intensity_value(&self, queue: &Queue, value: u32) -> Result<()> {
720        let Some(ref auto_intensity_data) = self.auto_intensity_resources else {
721            return Err(Error::MissingResources("auto_intensity_data"));
722        };
723        log::debug!("Setting auto-intensity value to {value}");
724        queue.write_buffer(
725            auto_intensity_data.max_value_buffer(),
726            0,
727            bytemuck::cast_slice(&[value]),
728        );
729
730        Ok(())
731    }
732}