onedrop_renderer/
user_warp_pipeline.rs

1//! User-shader path for the warp pass.
2//!
3//! Sibling of [`crate::CompPipeline`]'s user-shader path. When a preset
4//! ships a `warp_shader=…` block, [`onedrop-codegen::ShaderCompiler`]
5//! translates it through the same HLSL→WGSL chain the comp pass uses
6//! and emits a complete WGSL module against the `ShaderUniforms` bind-
7//! group layout. The default [`crate::WarpPipeline`] runs the engine's
8//! hand-written `warp.wgsl` with a minimal 3-binding layout; this
9//! parallel pipeline runs the user's translated module against the
10//! full 25-binding layout the codegen wrapper assumes.
11//!
12//! ## Wiring
13//!
14//! - **Vertex format**: same `WarpVertex` the default warp pipeline
15//!   consumes (`pos_clip: vec2<f32>` + `uv_warp: vec2<f32>`), so the
16//!   warp mesh vertex buffer is shared verbatim.
17//! - **Binding 1** (`sampler_main_texture`): the renderer binds
18//!   `prev_texture` here. `GetPixel(uv)` in the user shader therefore
19//!   samples the previous frame at the per-pixel warped UV — exactly
20//!   the MD2 warp convention.
21//! - **Bindings 3..5** (`sampler_blur1/2/3_texture`): the warp pass
22//!   runs *before* the blur pipeline, so this frame's blurred copies
23//!   don't exist yet. We bind the *previous* frame's blur pyramid
24//!   (`prev_blur1/2/3_texture` in [`crate::ChainTextures`]), mirroring
25//!   how `prev_texture` carries the previous warp output. Authored
26//!   `GetBlur1(uv)` calls inside a translated warp shader therefore
27//!   sample a true Gaussian-blurred copy — one frame stale — instead
28//!   of un-blurred `prev_texture`.
29//! - **Bindings 6..10** (`sampler_noise_*_texture`): real noise pack
30//!   views from [`crate::GpuContext`]'s `NoisePack` — identical to
31//!   what the comp pipeline sees.
32//! - **Bindings 15..22** (`sampler_user_<n>_texture`): per-preset
33//!   disk-loaded user textures, threaded through from the same
34//!   `TextureBindingPlan` the comp side uses.
35//! - **Binding 23** (`sampler_prev_main_texture`): aliased to
36//!   `prev_texture`.
37
38use onedrop_codegen::{ShaderUniforms, USER_TEXTURE_FIRST_BINDING, USER_TEXTURE_SLOTS};
39use wgpu::util::DeviceExt;
40
41use crate::comp_pipeline::{
42    PREV_MAIN_BINDING, build_user_shader_bind_group_layout, make_fallback_user_texture_view,
43    make_md2_sampler,
44};
45use crate::error::Result;
46use crate::warp_pipeline::WarpVertex;
47
48/// View pack for the user warp pass.
49///
50/// Separate from `CompAuxViews` because the warp pass binds the
51/// *previous* frame's blur pyramid (`prev_blur1/2/3`) — the current
52/// frame's blur doesn't exist yet at warp time. The noise pack is
53/// the same on both passes (constant for the program's lifetime).
54pub struct WarpAuxViews<'a> {
55    pub prev_blur1: &'a wgpu::TextureView,
56    pub prev_blur2: &'a wgpu::TextureView,
57    pub prev_blur3: &'a wgpu::TextureView,
58    pub noise_lq: &'a wgpu::TextureView,
59    pub noise_mq: &'a wgpu::TextureView,
60    pub noise_hq: &'a wgpu::TextureView,
61    pub noisevol_lq: &'a wgpu::TextureView,
62    pub noisevol_hq: &'a wgpu::TextureView,
63}
64
65/// Pipeline + bind group + uniforms buffer for the user-authored warp
66/// fragment shader. Instantiated lazily by [`crate::WarpPipeline`]'s
67/// `set_user_shader_with_plan`; lives alongside the default warp path
68/// and replaces it at draw time when present.
69pub struct UserWarpPipeline {
70    pipeline: wgpu::RenderPipeline,
71    bind_group: wgpu::BindGroup,
72    bind_group_layout: wgpu::BindGroupLayout,
73    /// Kept alive only so the pipeline's bind-group layout reference
74    /// stays valid for the lifetime of `pipeline`; wgpu owns the
75    /// underlying handle indirectly.
76    #[allow(dead_code)]
77    pipeline_layout: wgpu::PipelineLayout,
78    uniforms_buffer: wgpu::Buffer,
79    /// `sampler_main` — linear / clamp-to-edge sampler bound at @binding(2).
80    sampler: wgpu::Sampler,
81    /// The four MD2 sampler variants the codegen wrapper expects at
82    /// @bindings 11..14. Created fresh here rather than borrowed from
83    /// the comp pipeline so the warp pass is self-contained.
84    sampler_fw: wgpu::Sampler,
85    sampler_fc: wgpu::Sampler,
86    sampler_pw: wgpu::Sampler,
87    sampler_pc: wgpu::Sampler,
88    /// 1×1 white texture for unfilled user-texture slots. Sampled `*X`
89    /// degrades to `X`, matching the comp pipeline's fallback policy.
90    /// Holds the texture alive (`user_texture_views` clones reference
91    /// it for unfilled slots).
92    #[allow(dead_code)]
93    fallback_user_view: wgpu::TextureView,
94    /// One view per user-texture slot. Updated by
95    /// `install_user_textures` on every shader swap.
96    user_texture_views: [wgpu::TextureView; USER_TEXTURE_SLOTS],
97}
98
99impl UserWarpPipeline {
100    /// Compile + wrap is the caller's responsibility. `wrapped_wgsl`
101    /// must be the fully-formed module produced by
102    /// `onedrop-codegen::wrap_user_warp_shader_with_plan`.
103    ///
104    /// `prev_view` is bound everywhere the wrapper expects a "current
105    /// frame" texture (binding 1, 3, 4, 5, 23). Real `noise_*` views
106    /// from the GPU context's [`crate::noise::NoisePack`] are bound at
107    /// 6..10. The `user_views` array supplies any preset-loaded disk
108    /// textures (slots set to `None` get the 1×1 white fallback).
109    pub fn new(
110        device: &wgpu::Device,
111        queue: &wgpu::Queue,
112        target_format: wgpu::TextureFormat,
113        wrapped_wgsl: &str,
114        prev_view: &wgpu::TextureView,
115        aux: &WarpAuxViews,
116        user_views: [Option<wgpu::TextureView>; USER_TEXTURE_SLOTS],
117    ) -> Result<Self> {
118        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
119            label: Some("User Warp Sampler Main"),
120            address_mode_u: wgpu::AddressMode::ClampToEdge,
121            address_mode_v: wgpu::AddressMode::ClampToEdge,
122            address_mode_w: wgpu::AddressMode::ClampToEdge,
123            mag_filter: wgpu::FilterMode::Linear,
124            min_filter: wgpu::FilterMode::Linear,
125            mipmap_filter: wgpu::MipmapFilterMode::Linear,
126            ..Default::default()
127        });
128        let sampler_fw = make_md2_sampler(device, "User Warp Sampler FW", true, true);
129        let sampler_fc = make_md2_sampler(device, "User Warp Sampler FC", true, false);
130        let sampler_pw = make_md2_sampler(device, "User Warp Sampler PW", false, true);
131        let sampler_pc = make_md2_sampler(device, "User Warp Sampler PC", false, false);
132
133        let fallback_user_view = make_fallback_user_texture_view(device, queue);
134        let user_texture_views: [wgpu::TextureView; USER_TEXTURE_SLOTS] = {
135            let mut arr = std::array::from_fn(|_| fallback_user_view.clone());
136            for (slot, opt) in user_views.into_iter().enumerate() {
137                if let Some(v) = opt {
138                    arr[slot] = v;
139                }
140            }
141            arr
142        };
143
144        let initial = ShaderUniforms::default();
145        let uniforms_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
146            label: Some("User Warp ShaderUniforms"),
147            contents: bytemuck::bytes_of(&initial),
148            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
149        });
150
151        let bind_group_layout =
152            build_user_shader_bind_group_layout(device, "User Warp Bind Group Layout");
153        let user_view_refs: [&wgpu::TextureView; USER_TEXTURE_SLOTS] =
154            std::array::from_fn(|i| &user_texture_views[i]);
155        let bind_group = create_user_warp_bind_group(
156            device,
157            &bind_group_layout,
158            &uniforms_buffer,
159            prev_view,
160            &sampler,
161            aux,
162            &sampler_fw,
163            &sampler_fc,
164            &sampler_pw,
165            &sampler_pc,
166            &user_view_refs,
167        );
168
169        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
170            label: Some("User Warp Pipeline Layout"),
171            bind_group_layouts: &[Some(&bind_group_layout)],
172            immediate_size: 0,
173        });
174
175        let pipeline =
176            build_warp_pipeline_from_wgsl(device, &pipeline_layout, target_format, wrapped_wgsl)?;
177
178        Ok(Self {
179            pipeline,
180            bind_group,
181            bind_group_layout,
182            pipeline_layout,
183            uniforms_buffer,
184            sampler,
185            sampler_fw,
186            sampler_fc,
187            sampler_pw,
188            sampler_pc,
189            fallback_user_view,
190            user_texture_views,
191        })
192    }
193
194    /// Upload a fully-populated [`ShaderUniforms`] for this frame.
195    pub fn update_uniforms(&self, queue: &wgpu::Queue, uniforms: &ShaderUniforms) {
196        queue.write_buffer(&self.uniforms_buffer, 0, bytemuck::bytes_of(uniforms));
197    }
198
199    /// Rebuild the bind group with fresh texture views (resize, prev
200    /// texture invalidation, etc.). `aux` follows the same convention
201    /// as the comp pipeline's `rebind_input_texture` path.
202    pub fn rebind_textures(
203        &mut self,
204        device: &wgpu::Device,
205        prev_view: &wgpu::TextureView,
206        aux: &WarpAuxViews,
207    ) {
208        let user_view_refs: [&wgpu::TextureView; USER_TEXTURE_SLOTS] =
209            std::array::from_fn(|i| &self.user_texture_views[i]);
210        self.bind_group = create_user_warp_bind_group(
211            device,
212            &self.bind_group_layout,
213            &self.uniforms_buffer,
214            prev_view,
215            &self.sampler,
216            aux,
217            &self.sampler_fw,
218            &self.sampler_fc,
219            &self.sampler_pw,
220            &self.sampler_pc,
221            &user_view_refs,
222        );
223    }
224
225    /// Issue the warp pass with the user shader. Caller owns the
226    /// encoder and the vertex/index buffers (shared with the default
227    /// warp pipeline so the per-frame mesh upload is amortised).
228    #[allow(clippy::too_many_arguments)]
229    pub fn render(
230        &self,
231        encoder: &mut wgpu::CommandEncoder,
232        output_view: &wgpu::TextureView,
233        vertex_buffer: &wgpu::Buffer,
234        index_buffer: &wgpu::Buffer,
235        index_count: u32,
236    ) {
237        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
238            label: Some("User Warp Render Pass"),
239            color_attachments: &[Some(wgpu::RenderPassColorAttachment {
240                view: output_view,
241                depth_slice: None,
242                resolve_target: None,
243                ops: wgpu::Operations {
244                    load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
245                    store: wgpu::StoreOp::Store,
246                },
247            })],
248            depth_stencil_attachment: None,
249            timestamp_writes: None,
250            occlusion_query_set: None,
251            multiview_mask: None,
252        });
253        pass.set_pipeline(&self.pipeline);
254        pass.set_bind_group(0, &self.bind_group, &[]);
255        pass.set_vertex_buffer(0, vertex_buffer.slice(..));
256        pass.set_index_buffer(index_buffer.slice(..), wgpu::IndexFormat::Uint32);
257        pass.draw_indexed(0..index_count, 0, 0..1);
258    }
259}
260
261/// Build the 25-entry bind group for the user warp pass.
262///
263/// `prev_view` is bound to everywhere the wrapper expects a "main"
264/// texture (1, 3, 4, 5, 23). Blur slots fall back to `prev_view` because
265/// the warp pass runs before this frame's blur pipeline — a one-frame
266/// delayed blur pyramid would be more accurate but is deferred work.
267#[allow(clippy::too_many_arguments)]
268fn create_user_warp_bind_group(
269    device: &wgpu::Device,
270    layout: &wgpu::BindGroupLayout,
271    uniforms_buffer: &wgpu::Buffer,
272    prev_view: &wgpu::TextureView,
273    sampler: &wgpu::Sampler,
274    aux: &WarpAuxViews,
275    sampler_fw: &wgpu::Sampler,
276    sampler_fc: &wgpu::Sampler,
277    sampler_pw: &wgpu::Sampler,
278    sampler_pc: &wgpu::Sampler,
279    user_textures: &[&wgpu::TextureView; USER_TEXTURE_SLOTS],
280) -> wgpu::BindGroup {
281    let mut entries: Vec<wgpu::BindGroupEntry> = vec![
282        wgpu::BindGroupEntry {
283            binding: 0,
284            resource: uniforms_buffer.as_entire_binding(),
285        },
286        wgpu::BindGroupEntry {
287            binding: 1,
288            resource: wgpu::BindingResource::TextureView(prev_view),
289        },
290        wgpu::BindGroupEntry {
291            binding: 2,
292            resource: wgpu::BindingResource::Sampler(sampler),
293        },
294        // Blur slots — one-frame-delayed pyramid (see module doc).
295        wgpu::BindGroupEntry {
296            binding: 3,
297            resource: wgpu::BindingResource::TextureView(aux.prev_blur1),
298        },
299        wgpu::BindGroupEntry {
300            binding: 4,
301            resource: wgpu::BindingResource::TextureView(aux.prev_blur2),
302        },
303        wgpu::BindGroupEntry {
304            binding: 5,
305            resource: wgpu::BindingResource::TextureView(aux.prev_blur3),
306        },
307        // Noise pack — real views from the GpuContext NoisePack.
308        wgpu::BindGroupEntry {
309            binding: 6,
310            resource: wgpu::BindingResource::TextureView(aux.noise_lq),
311        },
312        wgpu::BindGroupEntry {
313            binding: 7,
314            resource: wgpu::BindingResource::TextureView(aux.noise_mq),
315        },
316        wgpu::BindGroupEntry {
317            binding: 8,
318            resource: wgpu::BindingResource::TextureView(aux.noise_hq),
319        },
320        wgpu::BindGroupEntry {
321            binding: 9,
322            resource: wgpu::BindingResource::TextureView(aux.noisevol_lq),
323        },
324        wgpu::BindGroupEntry {
325            binding: 10,
326            resource: wgpu::BindingResource::TextureView(aux.noisevol_hq),
327        },
328        // MD2 sampler variants.
329        wgpu::BindGroupEntry {
330            binding: 11,
331            resource: wgpu::BindingResource::Sampler(sampler_fw),
332        },
333        wgpu::BindGroupEntry {
334            binding: 12,
335            resource: wgpu::BindingResource::Sampler(sampler_fc),
336        },
337        wgpu::BindGroupEntry {
338            binding: 13,
339            resource: wgpu::BindingResource::Sampler(sampler_pw),
340        },
341        wgpu::BindGroupEntry {
342            binding: 14,
343            resource: wgpu::BindingResource::Sampler(sampler_pc),
344        },
345    ];
346    for (n, view) in user_textures.iter().enumerate() {
347        entries.push(wgpu::BindGroupEntry {
348            binding: USER_TEXTURE_FIRST_BINDING + n as u32,
349            resource: wgpu::BindingResource::TextureView(view),
350        });
351    }
352    entries.push(wgpu::BindGroupEntry {
353        binding: PREV_MAIN_BINDING,
354        resource: wgpu::BindingResource::TextureView(prev_view),
355    });
356    device.create_bind_group(&wgpu::BindGroupDescriptor {
357        label: Some("User Warp Bind Group"),
358        layout,
359        entries: &entries,
360    })
361}
362
363/// Build the wgpu render pipeline. Vertex layout matches `WarpVertex`
364/// (`pos_clip` at @location(0), `uv_warp` at @location(1)); fragment
365/// targets `target_format` with `BlendState::REPLACE` (the warp pass
366/// owns the entire render target).
367fn build_warp_pipeline_from_wgsl(
368    device: &wgpu::Device,
369    pipeline_layout: &wgpu::PipelineLayout,
370    target_format: wgpu::TextureFormat,
371    source: &str,
372) -> Result<wgpu::RenderPipeline> {
373    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
374        label: Some("User Warp Shader"),
375        source: wgpu::ShaderSource::Wgsl(source.into()),
376    });
377    Ok(
378        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
379            label: Some("User Warp Pipeline"),
380            layout: Some(pipeline_layout),
381            vertex: wgpu::VertexState {
382                module: &shader,
383                entry_point: Some("vs_main"),
384                buffers: &[wgpu::VertexBufferLayout {
385                    array_stride: std::mem::size_of::<WarpVertex>() as wgpu::BufferAddress,
386                    step_mode: wgpu::VertexStepMode::Vertex,
387                    attributes: &[
388                        wgpu::VertexAttribute {
389                            offset: 0,
390                            shader_location: 0,
391                            format: wgpu::VertexFormat::Float32x2,
392                        },
393                        wgpu::VertexAttribute {
394                            offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
395                            shader_location: 1,
396                            format: wgpu::VertexFormat::Float32x2,
397                        },
398                    ],
399                }],
400                compilation_options: Default::default(),
401            },
402            fragment: Some(wgpu::FragmentState {
403                module: &shader,
404                entry_point: Some("fs_main"),
405                targets: &[Some(wgpu::ColorTargetState {
406                    format: target_format,
407                    blend: Some(wgpu::BlendState::REPLACE),
408                    write_mask: wgpu::ColorWrites::ALL,
409                })],
410                compilation_options: Default::default(),
411            }),
412            primitive: wgpu::PrimitiveState {
413                topology: wgpu::PrimitiveTopology::TriangleList,
414                ..Default::default()
415            },
416            depth_stencil: None,
417            multisample: wgpu::MultisampleState::default(),
418            multiview_mask: None,
419            cache: None,
420        }),
421    )
422}