Skip to main content

viewport_lib/resources/
plugin_builders.rs

1//! Plugin-facing accessors and pipeline builders on [`DeviceResources`].
2//!
3//! See [`crate::plugin_api`] for the published types these methods return.
4
5use crate::plugin_api::{
6    MaskTargetDesc, OitTargetDesc, OpaqueTargetDesc, PickTargetDesc, ShadowTargetDesc,
7    SharedBindings,
8    target_desc::{OIT_ACCUM_BLEND, OIT_REVEAL_BLEND},
9};
10use crate::resources::DeviceResources;
11
12/// HDR colour format used by the scene buffer. Plugins targeting the HDR
13/// path build pipelines against this format.
14pub const HDR_COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
15
16/// Depth-stencil format shared by every scene render pass.
17pub const SCENE_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
18
19/// Shadow atlas depth format.
20pub const SHADOW_DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
21
22/// Outline-mask colour format.
23pub const MASK_COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R8Unorm;
24
25/// Pick-id colour format.
26pub const PICK_COLOR_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::R32Uint;
27
28impl DeviceResources {
29    // ------------------------------------------------------------------
30    // Target descriptors and SharedBindings accessor
31    // ------------------------------------------------------------------
32
33    /// Group-0 bind layout shared by every scene pipeline. Use as group 0
34    /// when building a plugin pipeline layout.
35    pub fn shared_bindings(&self) -> SharedBindings<'_> {
36        SharedBindings {
37            group0_layout: &self.camera_bind_group_layout,
38        }
39    }
40
41    /// Render-target descriptor for the HDR opaque scene pass.
42    pub fn opaque_target_desc(&self) -> OpaqueTargetDesc {
43        OpaqueTargetDesc {
44            color_format: HDR_COLOR_FORMAT,
45            depth_format: SCENE_DEPTH_FORMAT,
46            sample_count: self.sample_count,
47        }
48    }
49
50    /// Render-target descriptor for the OIT pass (MRT: accum + reveal).
51    pub fn oit_target_desc(&self) -> OitTargetDesc {
52        OitTargetDesc {
53            accum_format: HDR_COLOR_FORMAT,
54            reveal_format: MASK_COLOR_FORMAT,
55            depth_format: SCENE_DEPTH_FORMAT,
56            accum_blend: OIT_ACCUM_BLEND,
57            reveal_blend: OIT_REVEAL_BLEND,
58            sample_count: self.sample_count,
59        }
60    }
61
62    /// Render-target descriptor for the outline-mask pass.
63    pub fn mask_target_desc(&self) -> MaskTargetDesc {
64        MaskTargetDesc {
65            color_format: MASK_COLOR_FORMAT,
66            depth_format: SCENE_DEPTH_FORMAT,
67            sample_count: 1,
68        }
69    }
70
71    /// Render-target descriptor for the pick-id pass.
72    pub fn pick_target_desc(&self) -> PickTargetDesc {
73        PickTargetDesc {
74            color_format: PICK_COLOR_FORMAT,
75            depth_format: SCENE_DEPTH_FORMAT,
76            sample_count: 1,
77        }
78    }
79
80    /// Render-target descriptor for the shadow-atlas pass.
81    pub fn shadow_target_desc(&self) -> ShadowTargetDesc {
82        ShadowTargetDesc {
83            depth_format: SHADOW_DEPTH_FORMAT,
84            sample_count: 1,
85        }
86    }
87
88    // ------------------------------------------------------------------
89    // Texture-id namespace accessors
90    // ------------------------------------------------------------------
91
92    /// Borrow the `TextureView` for a texture previously uploaded via
93    /// [`upload_texture`](Self::upload_texture) or
94    /// [`upload_normal_map`](Self::upload_normal_map).
95    ///
96    /// Returns `None` if `id` does not refer to a live texture (a stale handle
97    /// whose texture was freed, or one out of range).
98    ///
99    /// Lifetime contract: the returned view is valid until the texture is freed
100    /// with [`free_texture`](Self::free_texture). Plugins that build a bind
101    /// group from this view must rebuild it after any operation that could
102    /// invalidate the texture (a free, device recreation). A safer pattern is to
103    /// fetch the view each frame just before building / rebuilding the bind
104    /// group.
105    pub fn texture_view(&self, id: crate::resources::TextureId) -> Option<&wgpu::TextureView> {
106        self.content.textures.get(id).map(|t| &t.view)
107    }
108
109    /// Borrow the sampler the texture was uploaded with.
110    ///
111    /// Most user textures are uploaded with a shared linear-repeat sampler;
112    /// prefer [`material_sampler`](Self::material_sampler) when you need
113    /// the shared lib sampler rather than the per-texture instance.
114    pub fn texture_sampler(&self, id: crate::resources::TextureId) -> Option<&wgpu::Sampler> {
115        self.content.textures.get(id).map(|t| &t.sampler)
116    }
117
118    /// Shared linear-repeat sampler used by the lib's material pipelines.
119    ///
120    /// Use this when building a plugin bind group that samples user
121    /// textures the same way `Material` does (linear filter, repeat wrap).
122    pub fn material_sampler(&self) -> &wgpu::Sampler {
123        &self.material_sampler
124    }
125
126    /// Shared linear-clamp sampler used by the lib for colormap LUTs.
127    ///
128    /// Use this when sampling 1D LUT-style data (colourmaps, transfer
129    /// functions) where the texture should not wrap.
130    pub fn lut_sampler(&self) -> &wgpu::Sampler {
131        &self.lut_sampler
132    }
133
134    /// Comparison sampler used for PCF shadow filtering.
135    ///
136    /// Plugins that sample the shadow atlas directly (rather than through
137    /// `viewport_sample_csm`) use this sampler when binding the atlas.
138    pub fn shadow_filter_sampler(&self) -> &wgpu::Sampler {
139        &self.shadow_sampler
140    }
141
142    /// Bind group layout for the per-vertex deformation sidecar.
143    ///
144    /// Plugins building pipelines that draw meshes with registered deformers
145    /// add this layout at group 2 so their vertex stage can read from the
146    /// shared `deform_data` / `deform_instance_data` storage buffers.
147    pub fn deform_bind_group_layout(&self) -> &wgpu::BindGroupLayout {
148        &self.deform.bind_group_layout
149    }
150
151    /// Number of live user-uploaded textures.
152    ///
153    /// `id` values in `0..texture_count()` are addressable via
154    /// [`texture_view`](Self::texture_view), with the caveat that promoted
155    /// IDs from async uploads may sit at the high end.
156    pub fn texture_count(&self) -> usize {
157        self.content.textures.len()
158    }
159
160    // ------------------------------------------------------------------
161    // Pipeline builders
162    // ------------------------------------------------------------------
163
164    /// Build an opaque scene pipeline that draws into the HDR scene pass.
165    ///
166    /// Standard depth state: `LessEqual` test, depth write on. The pipeline
167    /// layout lists [`shared_bindings`](Self::shared_bindings) as group 0,
168    /// then `extra_bind_group_layouts` as groups 1.., in order. The plugin
169    /// owns all groups past 0.
170    pub fn build_opaque_pipeline(
171        &self,
172        device: &wgpu::Device,
173        opts: &PluginPipelineOpts<'_>,
174    ) -> wgpu::RenderPipeline {
175        let layout = build_layout(device, opts.label, self, opts.extra_bind_group_layouts);
176        let desc = self.opaque_target_desc();
177        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
178            label: opts.label,
179            layout: Some(&layout),
180            vertex: wgpu::VertexState {
181                module: opts.shader,
182                entry_point: Some(opts.vs_entry),
183                buffers: opts.vertex_layouts,
184                compilation_options: Default::default(),
185            },
186            fragment: Some(wgpu::FragmentState {
187                module: opts.shader,
188                entry_point: Some(opts.fs_entry),
189                targets: &[Some(wgpu::ColorTargetState {
190                    format: desc.color_format,
191                    blend: opts.color_blend,
192                    write_mask: wgpu::ColorWrites::ALL,
193                })],
194                compilation_options: Default::default(),
195            }),
196            primitive: opts.primitive,
197            depth_stencil: Some(wgpu::DepthStencilState {
198                format: desc.depth_format,
199                depth_write_enabled: opts.depth_write,
200                depth_compare: opts.depth_compare,
201                stencil: wgpu::StencilState::default(),
202                bias: wgpu::DepthBiasState::default(),
203            }),
204            multisample: wgpu::MultisampleState {
205                count: desc.sample_count,
206                ..Default::default()
207            },
208            multiview: None,
209            cache: None,
210        })
211    }
212
213    /// Build a transparent pipeline that draws into the OIT pass.
214    ///
215    /// The fragment shader must return [`OitOutput`](crate::plugin_api::shared_wgsl::SHARED_OIT_WGSL),
216    /// writing both `@location(0)` (accum) and `@location(1)` (reveal).
217    /// Depth state: `LessEqual` test, depth write off.
218    pub fn build_oit_pipeline(
219        &self,
220        device: &wgpu::Device,
221        opts: &PluginPipelineOpts<'_>,
222    ) -> wgpu::RenderPipeline {
223        let layout = build_layout(device, opts.label, self, opts.extra_bind_group_layouts);
224        let desc = self.oit_target_desc();
225        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
226            label: opts.label,
227            layout: Some(&layout),
228            vertex: wgpu::VertexState {
229                module: opts.shader,
230                entry_point: Some(opts.vs_entry),
231                buffers: opts.vertex_layouts,
232                compilation_options: Default::default(),
233            },
234            fragment: Some(wgpu::FragmentState {
235                module: opts.shader,
236                entry_point: Some(opts.fs_entry),
237                targets: &[
238                    Some(wgpu::ColorTargetState {
239                        format: desc.accum_format,
240                        blend: Some(desc.accum_blend),
241                        write_mask: wgpu::ColorWrites::ALL,
242                    }),
243                    Some(wgpu::ColorTargetState {
244                        format: desc.reveal_format,
245                        blend: Some(desc.reveal_blend),
246                        write_mask: wgpu::ColorWrites::RED,
247                    }),
248                ],
249                compilation_options: Default::default(),
250            }),
251            primitive: opts.primitive,
252            depth_stencil: Some(wgpu::DepthStencilState {
253                format: desc.depth_format,
254                depth_write_enabled: false,
255                depth_compare: wgpu::CompareFunction::LessEqual,
256                stencil: wgpu::StencilState::default(),
257                bias: wgpu::DepthBiasState::default(),
258            }),
259            multisample: wgpu::MultisampleState {
260                count: desc.sample_count,
261                ..Default::default()
262            },
263            multiview: None,
264            cache: None,
265        })
266    }
267
268    /// Build a pipeline for the outline-mask pass (R8 target).
269    ///
270    /// Fragment shader must write `1.0` at `@location(0)` for any covered
271    /// pixel; use [`SHARED_MASK_WGSL`](crate::plugin_api::shared_wgsl::SHARED_MASK_WGSL).
272    /// Depth state: `LessEqual` test, no depth write.
273    pub fn build_mask_pipeline(
274        &self,
275        device: &wgpu::Device,
276        opts: &PluginPipelineOpts<'_>,
277    ) -> wgpu::RenderPipeline {
278        let layout = build_layout(device, opts.label, self, opts.extra_bind_group_layouts);
279        let desc = self.mask_target_desc();
280        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
281            label: opts.label,
282            layout: Some(&layout),
283            vertex: wgpu::VertexState {
284                module: opts.shader,
285                entry_point: Some(opts.vs_entry),
286                buffers: opts.vertex_layouts,
287                compilation_options: Default::default(),
288            },
289            fragment: Some(wgpu::FragmentState {
290                module: opts.shader,
291                entry_point: Some(opts.fs_entry),
292                targets: &[Some(wgpu::ColorTargetState {
293                    format: desc.color_format,
294                    blend: None,
295                    write_mask: wgpu::ColorWrites::RED,
296                })],
297                compilation_options: Default::default(),
298            }),
299            primitive: opts.primitive,
300            depth_stencil: Some(wgpu::DepthStencilState {
301                format: desc.depth_format,
302                depth_write_enabled: false,
303                depth_compare: wgpu::CompareFunction::LessEqual,
304                stencil: wgpu::StencilState::default(),
305                bias: wgpu::DepthBiasState::default(),
306            }),
307            multisample: wgpu::MultisampleState {
308                count: desc.sample_count,
309                ..Default::default()
310            },
311            multiview: None,
312            cache: None,
313        })
314    }
315
316    /// Build a pipeline for the pick-id pass (R32Uint target).
317    ///
318    /// Fragment shader must write the item's `PickId` value at
319    /// `@location(0)`; use [`SHARED_PICK_WGSL`](crate::plugin_api::shared_wgsl::SHARED_PICK_WGSL).
320    pub fn build_pick_pipeline(
321        &self,
322        device: &wgpu::Device,
323        opts: &PluginPipelineOpts<'_>,
324    ) -> wgpu::RenderPipeline {
325        let layout = build_layout(device, opts.label, self, opts.extra_bind_group_layouts);
326        let desc = self.pick_target_desc();
327        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
328            label: opts.label,
329            layout: Some(&layout),
330            vertex: wgpu::VertexState {
331                module: opts.shader,
332                entry_point: Some(opts.vs_entry),
333                buffers: opts.vertex_layouts,
334                compilation_options: Default::default(),
335            },
336            fragment: Some(wgpu::FragmentState {
337                module: opts.shader,
338                entry_point: Some(opts.fs_entry),
339                targets: &[Some(wgpu::ColorTargetState {
340                    format: desc.color_format,
341                    blend: None,
342                    write_mask: wgpu::ColorWrites::RED,
343                })],
344                compilation_options: Default::default(),
345            }),
346            primitive: opts.primitive,
347            depth_stencil: Some(wgpu::DepthStencilState {
348                format: desc.depth_format,
349                depth_write_enabled: true,
350                depth_compare: wgpu::CompareFunction::LessEqual,
351                stencil: wgpu::StencilState::default(),
352                bias: wgpu::DepthBiasState::default(),
353            }),
354            multisample: wgpu::MultisampleState {
355                count: desc.sample_count,
356                ..Default::default()
357            },
358            multiview: None,
359            cache: None,
360        })
361    }
362
363    /// Build a depth-only pipeline for the shadow-atlas pass.
364    ///
365    /// No fragment output. The fragment entry is optional; pass an empty
366    /// string to use a depth-only configuration with no fragment stage.
367    /// Standard depth state: `LessEqual` test, depth write on, with the
368    /// lib's standard depth bias.
369    pub fn build_shadow_pipeline(
370        &self,
371        device: &wgpu::Device,
372        opts: &PluginPipelineOpts<'_>,
373    ) -> wgpu::RenderPipeline {
374        let layout = build_layout(device, opts.label, self, opts.extra_bind_group_layouts);
375        let desc = self.shadow_target_desc();
376        let fragment = if opts.fs_entry.is_empty() {
377            None
378        } else {
379            Some(wgpu::FragmentState {
380                module: opts.shader,
381                entry_point: Some(opts.fs_entry),
382                targets: &[],
383                compilation_options: Default::default(),
384            })
385        };
386        device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
387            label: opts.label,
388            layout: Some(&layout),
389            vertex: wgpu::VertexState {
390                module: opts.shader,
391                entry_point: Some(opts.vs_entry),
392                buffers: opts.vertex_layouts,
393                compilation_options: Default::default(),
394            },
395            fragment,
396            primitive: opts.primitive,
397            depth_stencil: Some(wgpu::DepthStencilState {
398                format: desc.depth_format,
399                depth_write_enabled: true,
400                depth_compare: wgpu::CompareFunction::LessEqual,
401                stencil: wgpu::StencilState::default(),
402                bias: wgpu::DepthBiasState {
403                    constant: 2,
404                    slope_scale: 2.0,
405                    clamp: 0.0,
406                },
407            }),
408            multisample: wgpu::MultisampleState {
409                count: desc.sample_count,
410                ..Default::default()
411            },
412            multiview: None,
413            cache: None,
414        })
415    }
416}
417
418/// Inputs to a plugin pipeline builder. All builders take this struct; the
419/// builder picks the target descriptor and blend state.
420pub struct PluginPipelineOpts<'a> {
421    /// Pipeline debug label. Forwarded to wgpu.
422    pub label: Option<&'a str>,
423    /// Shader module containing both the vertex and fragment entry points.
424    pub shader: &'a wgpu::ShaderModule,
425    /// Vertex-stage entry-point name (e.g. `"vs_main"`).
426    pub vs_entry: &'a str,
427    /// Fragment-stage entry-point name (e.g. `"fs_main"`). For
428    /// `build_shadow_pipeline`, pass `""` to skip the fragment stage.
429    pub fs_entry: &'a str,
430    /// Vertex buffer layouts.
431    pub vertex_layouts: &'a [wgpu::VertexBufferLayout<'a>],
432    /// Bind group layouts for groups 1.. (the plugin's per-object data).
433    /// Group 0 is supplied automatically from
434    /// [`DeviceResources::shared_bindings`].
435    pub extra_bind_group_layouts: &'a [&'a wgpu::BindGroupLayout],
436    /// Primitive topology, cull mode, polygon mode.
437    pub primitive: wgpu::PrimitiveState,
438    /// Optional blend state for the opaque builder. `None` = no blending
439    /// (the default for opaque pipelines). Ignored by OIT / mask / pick /
440    /// shadow builders, which use their pass-specific blend state.
441    pub color_blend: Option<wgpu::BlendState>,
442    /// Whether the opaque builder writes depth. Ignored by the other
443    /// builders. Default `true`.
444    pub depth_write: bool,
445    /// Depth-compare function for the opaque builder. Ignored by the other
446    /// builders.
447    pub depth_compare: wgpu::CompareFunction,
448}
449
450impl<'a> PluginPipelineOpts<'a> {
451    /// Construct an opts struct with sensible defaults for the variable
452    /// fields (`TriangleList`, back-face cull, depth write on,
453    /// `LessEqual`, no blend). Callers must supply `shader`, vertex / fragment
454    /// entry points, and the vertex layout.
455    pub fn new(
456        label: Option<&'a str>,
457        shader: &'a wgpu::ShaderModule,
458        vs_entry: &'a str,
459        fs_entry: &'a str,
460        vertex_layouts: &'a [wgpu::VertexBufferLayout<'a>],
461    ) -> Self {
462        Self {
463            label,
464            shader,
465            vs_entry,
466            fs_entry,
467            vertex_layouts,
468            extra_bind_group_layouts: &[],
469            primitive: wgpu::PrimitiveState {
470                topology: wgpu::PrimitiveTopology::TriangleList,
471                cull_mode: Some(wgpu::Face::Back),
472                ..Default::default()
473            },
474            color_blend: None,
475            depth_write: true,
476            depth_compare: wgpu::CompareFunction::LessEqual,
477        }
478    }
479}
480
481fn build_layout(
482    device: &wgpu::Device,
483    label: Option<&str>,
484    res: &DeviceResources,
485    extras: &[&wgpu::BindGroupLayout],
486) -> wgpu::PipelineLayout {
487    let mut bgls: Vec<&wgpu::BindGroupLayout> = Vec::with_capacity(1 + extras.len());
488    bgls.push(&res.camera_bind_group_layout);
489    bgls.extend(extras.iter().copied());
490    device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
491        label,
492        bind_group_layouts: &bgls,
493        push_constant_ranges: &[],
494    })
495}