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}