onedrop_renderer/
chain_textures.rs

1//! Per-chain feedback textures.
2//!
3//! Each rendering chain owns its own warp output, previous-frame copy,
4//! final comp output, and three-level blur pyramid (plus a scratch
5//! horizontal-pass target). Sharing these between chains would cross the
6//! feedback streams during preset transitions — the old preset's accumulated
7//! warp output must keep evolving on the secondary chain while the new
8//! preset fills the primary chain. Procedural noise stays shared in
9//! `GpuContext` because it is constant for the program lifetime.
10//!
11//! Sizes track `RenderConfig::{width, height}`; [`Self::resize`] rebuilds
12//! every texture when the host window resizes.
13
14use crate::config::RenderConfig;
15
16/// All resolution-dependent textures consumed and produced by one
17/// rendering chain.
18pub struct ChainTextures {
19    /// Warp pass output. Fed back into next frame's warp pass via
20    /// [`Self::prev_texture`], and read by the comp pass for the current
21    /// frame's display output. Display-only filters never write here so they
22    /// don't leak into the feedback loop.
23    pub render_texture: wgpu::Texture,
24    pub render_texture_view: wgpu::TextureView,
25
26    /// Last frame's warp output. Sampled by the warp pass on the next
27    /// frame's feedback step. `copy_to_prev` swaps the current frame's
28    /// `render_texture` into this slot at the end of each render.
29    pub prev_texture: wgpu::Texture,
30    pub prev_texture_view: wgpu::TextureView,
31
32    /// Comp pass output for this chain. The user-visible image (after
33    /// gamma + echo + anaglyph + user comp shader) — but **not** the
34    /// display: secondary chains during transitions render here too, and
35    /// the final-blend pipeline composites the chain's `final_texture`
36    /// into the renderer's display target.
37    pub final_texture: wgpu::Texture,
38    pub final_texture_view: wgpu::TextureView,
39
40    // Blur pyramid: three cumulative Gaussian-blur levels of
41    // `render_texture` exposed to user comp shaders via
42    // `GetBlur1/2/3`. Each level downsamples by 2× from the previous,
43    // so Blur1 is ½ render, Blur2 is ¼, Blur3 is ⅛ — drops GPU cost
44    // ~8× total vs. running all three at full resolution, with
45    // negligible visual difference because the Gaussian kernel
46    // already spreads each level far beyond a single texel. Sampling
47    // is normalised UV-space, so user shaders' `GetBlur1/2/3(uv)`
48    // doesn't care that the underlying texture shrank — the
49    // hardware bilinear sampler handles the upscale at sample time.
50    //
51    // Each level needs its own horizontal-pass scratch because the
52    // shared scratch idea would have forced the scratch to the
53    // largest level's resolution, which defeats the savings.
54    pub blur1_texture: wgpu::Texture,
55    pub blur1_texture_view: wgpu::TextureView,
56    pub blur1_scratch_texture: wgpu::Texture,
57    pub blur1_scratch_texture_view: wgpu::TextureView,
58    pub blur2_texture: wgpu::Texture,
59    pub blur2_texture_view: wgpu::TextureView,
60    pub blur2_scratch_texture: wgpu::Texture,
61    pub blur2_scratch_texture_view: wgpu::TextureView,
62    pub blur3_texture: wgpu::Texture,
63    pub blur3_texture_view: wgpu::TextureView,
64    pub blur3_scratch_texture: wgpu::Texture,
65    pub blur3_scratch_texture_view: wgpu::TextureView,
66
67    /// One-frame-delayed copy of the blur pyramid. Populated at the
68    /// end of each frame (after the blur pipeline writes the current
69    /// frame's pyramid); read by the next frame's warp pass — where
70    /// a user warp shader's `GetBlur1/2/3(uv)` sample the *previous*
71    /// frame's blur, mirroring how `prev_texture` mirrors the
72    /// previous warp output. Required because the warp pass runs
73    /// before this frame's blur pipeline; sampling this-frame's
74    /// blur would be a self-dependency.
75    pub prev_blur1_texture: wgpu::Texture,
76    pub prev_blur1_texture_view: wgpu::TextureView,
77    pub prev_blur2_texture: wgpu::Texture,
78    pub prev_blur2_texture_view: wgpu::TextureView,
79    pub prev_blur3_texture: wgpu::Texture,
80    pub prev_blur3_texture_view: wgpu::TextureView,
81}
82
83impl ChainTextures {
84    /// Allocate a fresh set of per-chain textures at the resolution + format
85    /// declared by `config`. Initial contents are undefined; the first warp
86    /// pass clears `render_texture` and the first `copy_to_prev` populates
87    /// `prev_texture`.
88    pub fn new(device: &wgpu::Device, config: &RenderConfig) -> Self {
89        let render_texture = make_color_texture(device, config, "Render Texture");
90        let render_texture_view =
91            render_texture.create_view(&wgpu::TextureViewDescriptor::default());
92
93        let prev_texture = make_color_texture(device, config, "Previous Frame Texture");
94        let prev_texture_view = prev_texture.create_view(&wgpu::TextureViewDescriptor::default());
95
96        let final_texture = make_color_texture(device, config, "Final Texture");
97        let final_texture_view = final_texture.create_view(&wgpu::TextureViewDescriptor::default());
98
99        // Downsampled blur dimensions. `max(1, …)` defends against
100        // pathologically small render targets (e.g. headless tests at
101        // 1×1) that would otherwise produce zero-area textures and
102        // trip wgpu validation.
103        let (b1_w, b1_h) = (config.width.max(2) / 2, config.height.max(2) / 2);
104        let (b2_w, b2_h) = (config.width.max(4) / 4, config.height.max(4) / 4);
105        let (b3_w, b3_h) = (config.width.max(8) / 8, config.height.max(8) / 8);
106
107        let (blur1_texture, blur1_texture_view) =
108            make_blur_texture_at(device, config, b1_w, b1_h, "Blur1 Texture");
109        let (blur1_scratch_texture, blur1_scratch_texture_view) =
110            make_blur_texture_at(device, config, b1_w, b1_h, "Blur1 Scratch Texture");
111        let (blur2_texture, blur2_texture_view) =
112            make_blur_texture_at(device, config, b2_w, b2_h, "Blur2 Texture");
113        let (blur2_scratch_texture, blur2_scratch_texture_view) =
114            make_blur_texture_at(device, config, b2_w, b2_h, "Blur2 Scratch Texture");
115        let (blur3_texture, blur3_texture_view) =
116            make_blur_texture_at(device, config, b3_w, b3_h, "Blur3 Texture");
117        let (blur3_scratch_texture, blur3_scratch_texture_view) =
118            make_blur_texture_at(device, config, b3_w, b3_h, "Blur3 Scratch Texture");
119        let (prev_blur1_texture, prev_blur1_texture_view) =
120            make_blur_texture_at(device, config, b1_w, b1_h, "Prev Blur1 Texture");
121        let (prev_blur2_texture, prev_blur2_texture_view) =
122            make_blur_texture_at(device, config, b2_w, b2_h, "Prev Blur2 Texture");
123        let (prev_blur3_texture, prev_blur3_texture_view) =
124            make_blur_texture_at(device, config, b3_w, b3_h, "Prev Blur3 Texture");
125
126        Self {
127            render_texture,
128            render_texture_view,
129            prev_texture,
130            prev_texture_view,
131            final_texture,
132            final_texture_view,
133            blur1_texture,
134            blur1_texture_view,
135            blur1_scratch_texture,
136            blur1_scratch_texture_view,
137            blur2_texture,
138            blur2_texture_view,
139            blur2_scratch_texture,
140            blur2_scratch_texture_view,
141            blur3_texture,
142            blur3_texture_view,
143            blur3_scratch_texture,
144            blur3_scratch_texture_view,
145            prev_blur1_texture,
146            prev_blur1_texture_view,
147            prev_blur2_texture,
148            prev_blur2_texture_view,
149            prev_blur3_texture,
150            prev_blur3_texture_view,
151        }
152    }
153
154    /// Re-allocate every texture at the new resolution. All `TextureView`s
155    /// become invalid after this call; downstream pipelines (warp, comp,
156    /// blur, custom_shape) must be rebound — see
157    /// [`crate::render_chain::RenderChain::rebind_after_resize`].
158    pub fn resize(&mut self, device: &wgpu::Device, config: &RenderConfig) {
159        *self = Self::new(device, config);
160    }
161
162    /// Copy `blur{1,2,3}_texture` → `prev_blur{1,2,3}_texture` so the
163    /// next frame's warp pass (which runs before that frame's blur
164    /// pipeline) can sample a one-frame-delayed pyramid through a
165    /// user-translated warp shader's `GetBlur1/2/3(uv)`. Cheap — three
166    /// GPU `copy_texture_to_texture` calls on already-resident
167    /// memory. Must run AFTER the blur pipeline has written this
168    /// frame's `blur1/2/3` (otherwise we'd snapshot stale data).
169    pub fn copy_blur_to_prev(&self, encoder: &mut wgpu::CommandEncoder, _config: &RenderConfig) {
170        let zero = wgpu::Origin3d::ZERO;
171        let aspect = wgpu::TextureAspect::All;
172        // Each pyramid level lives at its own resolution post-
173        // downsampling (½ / ¼ / ⅛), so we can't share one `Extent3d`
174        // across the three copies — read it back from each source.
175        for (src, dst) in [
176            (&self.blur1_texture, &self.prev_blur1_texture),
177            (&self.blur2_texture, &self.prev_blur2_texture),
178            (&self.blur3_texture, &self.prev_blur3_texture),
179        ] {
180            let extent = wgpu::Extent3d {
181                width: src.width(),
182                height: src.height(),
183                depth_or_array_layers: 1,
184            };
185            encoder.copy_texture_to_texture(
186                wgpu::TexelCopyTextureInfo {
187                    texture: src,
188                    mip_level: 0,
189                    origin: zero,
190                    aspect,
191                },
192                wgpu::TexelCopyTextureInfo {
193                    texture: dst,
194                    mip_level: 0,
195                    origin: zero,
196                    aspect,
197                },
198                extent,
199            );
200        }
201    }
202
203    /// Copy `render_texture` → `prev_texture` to close the feedback loop
204    /// for next frame. Must run AFTER the comp pass for the current frame
205    /// has consumed `prev_texture` (the echo blend reads frame N-1's
206    /// warp output via `prev_texture`).
207    pub fn copy_to_prev(&self, encoder: &mut wgpu::CommandEncoder, config: &RenderConfig) {
208        encoder.copy_texture_to_texture(
209            wgpu::TexelCopyTextureInfo {
210                texture: &self.render_texture,
211                mip_level: 0,
212                origin: wgpu::Origin3d::ZERO,
213                aspect: wgpu::TextureAspect::All,
214            },
215            wgpu::TexelCopyTextureInfo {
216                texture: &self.prev_texture,
217                mip_level: 0,
218                origin: wgpu::Origin3d::ZERO,
219                aspect: wgpu::TextureAspect::All,
220            },
221            wgpu::Extent3d {
222                width: config.width,
223                height: config.height,
224                depth_or_array_layers: 1,
225            },
226        );
227    }
228}
229
230fn make_color_texture(device: &wgpu::Device, config: &RenderConfig, label: &str) -> wgpu::Texture {
231    device.create_texture(&wgpu::TextureDescriptor {
232        label: Some(label),
233        size: wgpu::Extent3d {
234            width: config.width,
235            height: config.height,
236            depth_or_array_layers: 1,
237        },
238        mip_level_count: 1,
239        sample_count: 1,
240        dimension: wgpu::TextureDimension::D2,
241        format: config.texture_format.to_wgpu(),
242        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
243            | wgpu::TextureUsages::TEXTURE_BINDING
244            | wgpu::TextureUsages::COPY_SRC
245            | wgpu::TextureUsages::COPY_DST,
246        view_formats: &[],
247    })
248}
249
250/// Allocate a blur-stage texture at an explicit `(width, height)` —
251/// the downsampled pyramid wants each level smaller than the last,
252/// so the helper takes the dimensions directly rather than deriving
253/// them from `config`. Texture format follows `config.texture_format`
254/// so the blur output composes cleanly with `render_texture` /
255/// `final_texture` in the comp pass.
256fn make_blur_texture_at(
257    device: &wgpu::Device,
258    config: &RenderConfig,
259    width: u32,
260    height: u32,
261    label: &str,
262) -> (wgpu::Texture, wgpu::TextureView) {
263    let tex = device.create_texture(&wgpu::TextureDescriptor {
264        label: Some(label),
265        size: wgpu::Extent3d {
266            width,
267            height,
268            depth_or_array_layers: 1,
269        },
270        mip_level_count: 1,
271        sample_count: 1,
272        dimension: wgpu::TextureDimension::D2,
273        format: config.texture_format.to_wgpu(),
274        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
275            | wgpu::TextureUsages::TEXTURE_BINDING
276            | wgpu::TextureUsages::COPY_SRC
277            | wgpu::TextureUsages::COPY_DST,
278        view_formats: &[],
279    });
280    let view = tex.create_view(&wgpu::TextureViewDescriptor::default());
281    (tex, view)
282}