onedrop_renderer/
render_chain.rs

1//! Per-preset rendering pipeline bundle.
2//!
3//! Groups the eight per-frame GPU passes that the renderer dispatches in
4//! order — warp, waveform, custom-wave, custom-shape, border, motion-vector,
5//! blur pyramid, comp — into a single owning struct. The renderer
6//! orchestrates these by calling [`RenderChain::record_passes`] inside its
7//! command encoder.
8//!
9//! A chain owns its own pipelines + waveform sample cache so the renderer
10//! can hold an optional secondary chain that fades the previous preset out
11//! while the new one fades in during preset transitions.
12
13use crate::blur_pipeline::BlurPipeline;
14use crate::border::BorderRenderer;
15use crate::chain_textures::ChainTextures;
16use crate::comp_pipeline::CompPipeline;
17use crate::custom_shape::{CustomShapeBatch, CustomShapeInstance, CustomShapeRenderer};
18use crate::custom_wave::{CustomWaveBatch, CustomWaveRenderer, CustomWaveVertex};
19use crate::error::Result;
20use crate::gpu_context::GpuContext;
21use crate::motion_vector::MotionVectorRenderer;
22use crate::sprite_pipeline::{
23    SpriteDrawCmd, SpriteFrame, SpritePipeline, SpritePool, build_sprite_uniform,
24};
25use crate::warp_pipeline::{WarpPipeline, WarpVertex};
26use crate::waveform::{
27    NUM_WAVE_SAMPLES, WaveUniforms, WaveformMode, WaveformRenderer, apply_smoothing,
28    build_uniforms as build_wave_uniforms,
29};
30
31use crate::config::{RenderState, WaveParams};
32use crate::warp_mesh::WarpMesh;
33use onedrop_codegen::ShaderUniforms;
34
35/// The eight per-frame pipelines plus the small per-chain caches and the
36/// per-chain feedback textures (render / prev / final / blur1-3 / scratch).
37pub struct RenderChain {
38    /// Resolution-dependent feedback textures owned by this chain.
39    /// Secondary chains during transitions get a fresh [`ChainTextures`]
40    /// so the two preset states don't collide on the warp / prev / blur
41    /// targets.
42    textures: ChainTextures,
43    warp: WarpPipeline,
44    blur: BlurPipeline,
45    comp: CompPipeline,
46    waveform: WaveformRenderer,
47    custom_wave: CustomWaveRenderer,
48    custom_shape: CustomShapeRenderer,
49    border: BorderRenderer,
50    motion_vector: MotionVectorRenderer,
51    /// Mono-mixed audio for the waveform pass, smoothed CPU-side per
52    /// `WaveParams::smoothing` before upload.
53    wave_samples: Vec<f32>,
54    /// Instantaneous frame-mean volume used by `b_mod_wave_alpha_by_volume`.
55    wave_volume: f32,
56    /// Latest `(cols, rows, WarpVertex[])` snapshot from the most
57    /// recent `update_warp_vertices` call. Read by the motion-vector
58    /// pass to build anisotropic segments aligned with the local
59    /// warp displacement.
60    last_warp_field: Option<(u32, u32, Vec<WarpVertex>)>,
61}
62
63impl RenderChain {
64    /// Build a chain against the supplied GPU context and warp mesh. The
65    /// chain's pipelines bind the context's render/prev/blur texture views
66    /// directly; callers must call [`Self::rebind_after_resize`] if the
67    /// context is later resized.
68    ///
69    /// The mesh stays owned by the renderer (the engine evaluates per-vertex
70    /// equations against it, so it has a single-owner constraint above the
71    /// renderer level); the chain only needs it at construction time to size
72    /// the vertex buffer.
73    pub fn new(gpu: &GpuContext, mesh: &WarpMesh) -> Result<Self> {
74        let textures = ChainTextures::new(&gpu.device, &gpu.config);
75        Self::with_textures(gpu, mesh, textures)
76    }
77
78    /// Same as [`Self::new`] but uses a caller-provided [`ChainTextures`].
79    /// Useful when reclaiming textures from a previous chain (e.g. swapping
80    /// the secondary into the primary slot at the end of a transition)
81    /// to avoid an unnecessary GPU allocation.
82    pub fn with_textures(
83        gpu: &GpuContext,
84        mesh: &WarpMesh,
85        textures: ChainTextures,
86    ) -> Result<Self> {
87        let warp = WarpPipeline::new(
88            &gpu.device,
89            gpu.config.texture_format.to_wgpu(),
90            &textures.prev_texture_view,
91            mesh,
92        )?;
93
94        let comp = CompPipeline::new(
95            &gpu.device,
96            &gpu.queue,
97            gpu.config.texture_format.to_wgpu(),
98            &textures.render_texture_view,
99            &gpu.comp_aux_views_for(&textures),
100        )?;
101
102        let blur = BlurPipeline::new(
103            &gpu.device,
104            gpu.config.texture_format.to_wgpu(),
105            gpu.config.width,
106            gpu.config.height,
107            &textures.render_texture_view,
108            &textures.blur1_texture_view,
109            &textures.blur2_texture_view,
110            &textures.blur3_texture_view,
111            &textures.blur1_scratch_texture_view,
112            &textures.blur2_scratch_texture_view,
113            &textures.blur3_scratch_texture_view,
114        );
115
116        let waveform = WaveformRenderer::new(&gpu.device, gpu.config.texture_format.to_wgpu());
117        let custom_wave = CustomWaveRenderer::new(&gpu.device, gpu.config.texture_format.to_wgpu());
118        let custom_shape = CustomShapeRenderer::new(
119            &gpu.device,
120            gpu.config.texture_format.to_wgpu(),
121            &textures.prev_texture_view,
122        );
123        let border = BorderRenderer::new(&gpu.device, gpu.config.texture_format.to_wgpu());
124        let motion_vector =
125            MotionVectorRenderer::new(&gpu.device, gpu.config.texture_format.to_wgpu());
126
127        Ok(Self {
128            textures,
129            warp,
130            blur,
131            comp,
132            waveform,
133            custom_wave,
134            custom_shape,
135            border,
136            motion_vector,
137            wave_samples: vec![0.0; NUM_WAVE_SAMPLES],
138            wave_volume: 0.0,
139            last_warp_field: None,
140        })
141    }
142
143    /// Borrow the chain's feedback textures — needed by the renderer to
144    /// read the final comp output (for blend / display) and to drive the
145    /// `copy_to_prev` step that closes the per-frame feedback loop.
146    pub fn textures(&self) -> &ChainTextures {
147        &self.textures
148    }
149
150    /// Push freshly computed warp UVs to the GPU. Also caches the
151    /// vertex array on the CPU so the motion-vector pass can sample
152    /// the per-cell warp displacement (anisotropic segments) when it
153    /// runs later in the same frame.
154    pub fn update_warp_vertices(
155        &mut self,
156        queue: &wgpu::Queue,
157        cols: u32,
158        rows: u32,
159        vertices: &[WarpVertex],
160    ) {
161        self.warp.update_vertices(queue, vertices);
162        // Cheap: ~80 KiB at the 96 × 72 default mesh, written once
163        // per frame. The motion-vector pass reads in the same frame
164        // so the lifetime is implicit.
165        match self.last_warp_field.as_mut() {
166            Some((c, r, v)) => {
167                *c = cols;
168                *r = rows;
169                v.clear();
170                v.extend_from_slice(vertices);
171            }
172            None => {
173                self.last_warp_field = Some((cols, rows, vertices.to_vec()));
174            }
175        }
176    }
177
178    /// Reallocate the warp pipeline's vertex/index buffers for a
179    /// different-sized mesh. Driven by [`MilkRenderer::set_mesh_size`].
180    pub fn rebuild_warp_mesh(&mut self, device: &wgpu::Device, mesh: &WarpMesh) {
181        self.warp.rebuild_mesh(device, mesh);
182    }
183
184    /// Push a fresh mono audio sample window for the waveform pass.
185    ///
186    /// Applies the IIR smoothing dictated by `wave_params.smoothing` on the
187    /// CPU side before upload; the buffer is clamped to [`NUM_WAVE_SAMPLES`]
188    /// and shorter inputs are zero-padded.
189    pub fn update_waveform_samples(
190        &mut self,
191        queue: &wgpu::Queue,
192        samples: &[f32],
193        instantaneous_volume: f32,
194        wave_params: WaveParams,
195    ) {
196        let n = samples.len().min(NUM_WAVE_SAMPLES);
197        self.wave_samples.clear();
198        self.wave_samples.extend_from_slice(&samples[..n]);
199        self.wave_samples.resize(NUM_WAVE_SAMPLES, 0.0);
200        apply_smoothing(&mut self.wave_samples, wave_params.smoothing);
201        self.wave_volume = instantaneous_volume.clamp(0.0, 1.0);
202        self.waveform.update_wave_samples(queue, &self.wave_samples);
203    }
204
205    /// Stereo variant of [`Self::update_waveform_samples`]. Uploads
206    /// both channels into the 2N-wide sample buffer so the waveform
207    /// pass can read L from offset `0` and R from offset `N` in two
208    /// dispatches.
209    ///
210    /// `instantaneous_volume` is computed from the perceptual downmix
211    /// (engine-side) so it represents the listener's loudness, not one
212    /// channel's. Smoothing is applied to each channel independently.
213    pub fn update_waveform_samples_lr(
214        &mut self,
215        queue: &wgpu::Queue,
216        left: &[f32],
217        right: &[f32],
218        instantaneous_volume: f32,
219        wave_params: WaveParams,
220    ) {
221        // Reuse `wave_samples` for the left channel so `wave_samples()`
222        // accessors (waveform-mode CPU consumers) still see something
223        // representative.
224        let nl = left.len().min(NUM_WAVE_SAMPLES);
225        self.wave_samples.clear();
226        self.wave_samples.extend_from_slice(&left[..nl]);
227        self.wave_samples.resize(NUM_WAVE_SAMPLES, 0.0);
228        apply_smoothing(&mut self.wave_samples, wave_params.smoothing);
229
230        let mut right_samples = vec![0.0f32; NUM_WAVE_SAMPLES];
231        let nr = right.len().min(NUM_WAVE_SAMPLES);
232        right_samples[..nr].copy_from_slice(&right[..nr]);
233        apply_smoothing(&mut right_samples, wave_params.smoothing);
234
235        self.wave_volume = instantaneous_volume.clamp(0.0, 1.0);
236        self.waveform
237            .update_wave_samples_lr(queue, &self.wave_samples, &right_samples);
238    }
239
240    pub fn wave_samples(&self) -> &[f32] {
241        &self.wave_samples
242    }
243
244    pub fn wave_volume(&self) -> f32 {
245        self.wave_volume
246    }
247
248    /// Upload the per-frame custom-wave vertex stream.
249    pub fn update_custom_waves(
250        &mut self,
251        queue: &wgpu::Queue,
252        vertices: &[CustomWaveVertex],
253        batches: &[CustomWaveBatch],
254    ) {
255        self.custom_wave.update(queue, vertices, batches);
256    }
257
258    pub fn custom_wave_vertex_count(&self) -> u32 {
259        self.custom_wave.vertex_count()
260    }
261
262    pub fn custom_wave_batch_count(&self) -> usize {
263        self.custom_wave.batch_count()
264    }
265
266    /// Upload the per-frame custom-shape instance stream.
267    pub fn update_custom_shapes(
268        &mut self,
269        queue: &wgpu::Queue,
270        instances: &[CustomShapeInstance],
271        batches: &[CustomShapeBatch],
272        aspect: f32,
273    ) {
274        self.custom_shape.update(queue, instances, batches, aspect);
275    }
276
277    pub fn custom_shape_instance_count(&self) -> u32 {
278        self.custom_shape.instance_count()
279    }
280
281    pub fn custom_shape_batch_count(&self) -> usize {
282        self.custom_shape.batch_count()
283    }
284
285    pub fn border_batch_count(&self) -> usize {
286        self.border.batch_count()
287    }
288
289    pub fn motion_vector_segment_count(&self) -> u32 {
290        self.motion_vector.segment_count()
291    }
292
293    /// Borrow the comp pipeline — needed by the renderer to swap in user
294    /// shaders (the user-shader pipeline owns the texture binding plan and
295    /// stays attached to MilkRenderer for now).
296    pub fn comp_pipeline(&self) -> &CompPipeline {
297        &self.comp
298    }
299
300    pub fn comp_pipeline_mut(&mut self) -> &mut CompPipeline {
301        &mut self.comp
302    }
303
304    /// Borrow the warp pipeline. Mirrors [`Self::comp_pipeline`]; needed
305    /// by the renderer's `set_user_warp_shader` path.
306    pub fn warp_pipeline(&self) -> &WarpPipeline {
307        &self.warp
308    }
309
310    pub fn warp_pipeline_mut(&mut self) -> &mut WarpPipeline {
311        &mut self.warp
312    }
313
314    /// Record the eight per-frame passes into the supplied encoder, in
315    /// MD2 order: warp → waveform (+ optional mirrored second pass for
316    /// double_line) → custom waves → custom shapes → borders → motion
317    /// vectors → blur pyramid → comp.
318    ///
319    /// Does **not** submit; the renderer is responsible for the submit and
320    /// for the `copy_to_prev` step that closes the feedback loop.
321    #[allow(clippy::too_many_arguments)]
322    pub fn record_passes(
323        &mut self,
324        gpu: &GpuContext,
325        encoder: &mut wgpu::CommandEncoder,
326        state: &RenderState,
327        comp_uniforms: &ShaderUniforms,
328        sprite_pipeline: &SpritePipeline,
329        sprite_pool: &SpritePool,
330        sprite_frames: &[SpriteFrame],
331    ) {
332        // Per-frame uniforms.
333        self.warp.update_uniforms(
334            &gpu.queue,
335            state.decay,
336            gpu.aspect_ratio(),
337            state.feedback.to_flags(),
338        );
339        // When a user warp shader is active it reads from the same
340        // `ShaderUniforms` layout the comp pass does — feed it the same
341        // per-frame values. No-op when the default warp path runs.
342        self.warp.update_user_uniforms(&gpu.queue, comp_uniforms);
343        self.comp.update_uniforms(&gpu.queue, comp_uniforms);
344
345        // Build + upload wave pass uniforms from the current state.
346        let wave_uniforms: WaveUniforms = build_wave_uniforms(
347            state.wave,
348            self.wave_volume,
349            gpu.config.width,
350            gpu.config.height,
351            state.time,
352        );
353        self.waveform.update_uniforms(&gpu.queue, &wave_uniforms);
354
355        // Borders + motion-vector grid. Both passes upload
356        // per-frame; they take their no-op fast path when alpha is zero.
357        self.border
358            .update(&gpu.queue, state.borders, gpu.aspect_ratio());
359        // Motion-vector grid: pull the per-cell direction from the
360        // cached warp-vertex field when available, so segments point
361        // along the local feedback flow instead of a flat horizontal
362        // stub. `None` falls back to the original horizontal default.
363        let warp_field =
364            self.last_warp_field
365                .as_ref()
366                .map(|(c, r, v)| crate::motion_vector::WarpField {
367                    cols: *c,
368                    rows: *r,
369                    vertices: v.as_slice(),
370                });
371        self.motion_vector
372            .update(&gpu.queue, state.motion_vectors, warp_field);
373
374        // Warp pass: render mesh sampling prev_texture into render_texture.
375        self.warp
376            .render(encoder, &self.textures.render_texture_view);
377
378        // Waveform overlay on top of the warp output.
379        let wp = state.wave;
380        let is_dots = wp.dots || WaveformMode::from_i32(wp.mode) == WaveformMode::ExplosiveHash;
381        if wp.split_lr {
382            // Top-vs-bottom L/R split. Two dispatches: left channel
383            // shifted to the upper half (`wave_y - 0.25`, reading
384            // offset `0`), right channel to the lower half
385            // (`wave_y + 0.25`, reading offset `NUM_WAVE_SAMPLES`).
386            // Skip the `is_double_pass` mode-6 mirror — the split
387            // already produces two traces, doubling again would
388            // stack four.
389            let mut left_uniforms = wave_uniforms;
390            left_uniforms.wave_y -= 0.25;
391            left_uniforms.sample_offset = 0;
392            self.waveform.update_uniforms(&gpu.queue, &left_uniforms);
393            self.waveform.render_with_blend(
394                encoder,
395                &self.textures.render_texture_view,
396                is_dots,
397                wp.additive,
398            );
399
400            let mut right_uniforms = wave_uniforms;
401            right_uniforms.wave_y += 0.25;
402            right_uniforms.sample_offset = NUM_WAVE_SAMPLES as u32;
403            self.waveform.update_uniforms(&gpu.queue, &right_uniforms);
404            self.waveform.render_with_blend(
405                encoder,
406                &self.textures.render_texture_view,
407                is_dots,
408                wp.additive,
409            );
410        } else {
411            self.waveform.render_with_blend(
412                encoder,
413                &self.textures.render_texture_view,
414                is_dots,
415                wp.additive,
416            );
417            if WaveformMode::from_i32(wp.mode).is_double_pass() {
418                let mut mirrored = wave_uniforms;
419                mirrored.wave_y = (2.0 * wp.y) - wave_uniforms.wave_y;
420                mirrored.wave_scale = -mirrored.wave_scale;
421                self.waveform.update_uniforms(&gpu.queue, &mirrored);
422                self.waveform.render_with_blend(
423                    encoder,
424                    &self.textures.render_texture_view,
425                    is_dots,
426                    wp.additive,
427                );
428            }
429        }
430
431        // Custom waves.
432        self.custom_wave
433            .render(encoder, &self.textures.render_texture_view);
434
435        // Custom shapes (read prev_texture for textured mode).
436        self.custom_shape
437            .render(encoder, &self.textures.render_texture_view);
438
439        // Borders + motion vectors.
440        self.border
441            .render(encoder, &self.textures.render_texture_view);
442        self.motion_vector
443            .render(encoder, &self.textures.render_texture_view);
444
445        // Sprite pass: textured quads driven by the engine's
446        // `MILK_IMG.INI` per-frame eval. Goes after borders/motion
447        // vectors so it paints on top of the per-frame overlays but
448        // before the blur pyramid + comp pass so its output still
449        // feeds back through `prev_texture` next frame (needed for
450        // `burn=1` persistence).
451        if !sprite_frames.is_empty() {
452            let cmds: Vec<SpriteDrawCmd> = sprite_frames
453                .iter()
454                .map(|f| {
455                    let tex = sprite_pool.get_or_fallback(f.texture_index);
456                    let uniform = build_sprite_uniform(
457                        f.x,
458                        f.y,
459                        f.sx,
460                        f.sy,
461                        f.rot,
462                        f.rgba,
463                        tex.width,
464                        tex.height,
465                        gpu.config.width,
466                        gpu.config.height,
467                    );
468                    SpriteDrawCmd {
469                        uniform,
470                        texture_view: &tex.view,
471                        blend: f.blend,
472                        burn: f.burn,
473                    }
474                })
475                .collect();
476            sprite_pipeline.record(
477                encoder,
478                &gpu.queue,
479                &gpu.device,
480                &self.textures.render_texture_view,
481                sprite_pool.sampler(),
482                &cmds,
483            );
484        }
485
486        // Blur pyramid: 3 cumulative Gaussian blurs of the warp output.
487        // Each level downsamples 2× from the previous (½ / ¼ / ⅛
488        // render resolution) — see `BlurPipeline` for the rationale.
489        self.blur.render(
490            encoder,
491            &self.textures.blur1_texture_view,
492            &self.textures.blur2_texture_view,
493            &self.textures.blur3_texture_view,
494            &self.textures.blur1_scratch_texture_view,
495            &self.textures.blur2_scratch_texture_view,
496            &self.textures.blur3_scratch_texture_view,
497        );
498
499        // Comp pass: read render_texture (current warp) + prev_texture (LAST
500        // frame's warp output, for echo), apply gamma_adj, write final_texture.
501        self.comp.render(encoder, &self.textures.final_texture_view);
502    }
503
504    /// Copy this chain's `render_texture` into its `prev_texture` so the
505    /// next frame's warp pass sees the current frame's output as feedback.
506    /// Caller drives this via the renderer's encoder after `record_passes`.
507    pub fn copy_to_prev(&self, encoder: &mut wgpu::CommandEncoder, gpu: &GpuContext) {
508        self.textures.copy_to_prev(encoder, &gpu.config);
509        // Snapshot this frame's blur pyramid so a user-translated warp
510        // shader on the *next* frame can sample `GetBlur1/2/3(uv)` from
511        // a real Gaussian-blurred copy (one frame delayed) instead of
512        // the un-blurred prev_texture fallback. Cheap — three GPU
513        // texture-to-texture copies on already-resident memory.
514        self.textures.copy_blur_to_prev(encoder, &gpu.config);
515    }
516
517    /// Re-allocate per-chain textures at the new resolution and rebind every
518    /// pass. Replaces both the renderer-level resize plumbing and the per-
519    /// chain rebind that V.0 left in place.
520    pub fn resize(&mut self, gpu: &GpuContext) {
521        self.textures.resize(&gpu.device, &gpu.config);
522        self.warp
523            .rebind_prev_texture(&gpu.device, &self.textures.prev_texture_view);
524        self.comp.rebind_input_texture(
525            &gpu.device,
526            &self.textures.render_texture_view,
527            &gpu.comp_aux_views_for(&self.textures),
528        );
529        self.blur.rebind(
530            &gpu.device,
531            &gpu.queue,
532            gpu.config.width,
533            gpu.config.height,
534            &self.textures.render_texture_view,
535            &self.textures.blur1_texture_view,
536            &self.textures.blur2_texture_view,
537            &self.textures.blur1_scratch_texture_view,
538            &self.textures.blur2_scratch_texture_view,
539            &self.textures.blur3_scratch_texture_view,
540        );
541        // The shape pass binds `prev_texture` for textured mode.
542        self.custom_shape
543            .rebind_prev_texture(&gpu.device, &self.textures.prev_texture_view);
544    }
545}