Skip to main content

unlit_wgpu/
line_shader.rs

1//! The caller's own shader code, spliced into the built-in `line.wesl`.
2//!
3//! The built-in entry shader keeps three marks, each a comment naming the
4//! place it sits: one at module scope, one at the end of the vertex stage and
5//! one at the end of the fragment stage. A shader is spliced in at those marks
6//! while the module is still WESL, before anything is compiled, so each
7//! snippet is ordinary WESL: it sees every binding, flag and helper the place
8//! it lands in sees, it takes part in the conditional compilation, and its
9//! diagnostics point at the line it was written on rather than at a fragment
10//! of generated WGSL.
11//!
12//! The marks sit where the stages still have something left to do, which is
13//! what keeps a snippet short:
14//!
15//! * The module mark is after every built-in binding and helper, so a snippet
16//!   can declare the functions, types and constants the stages call.
17//! * The vertex mark is after the quad corner is built, before it is returned.
18//!   The output is a `var out` there, so a snippet can rewrite any varying —
19//!   the coverage color among them — in place.
20//! * The fragment mark is after the coverage is applied to the colour, before
21//!   the hard-edge cut and the return. The colour is a `var color` there, so
22//!   a snippet can rewrite it in place, and what it leaves is what the target
23//!   receives — a snippet of its own may return instead, which is how a
24//!   caller replaces the shading the stroke ends with.
25//!
26//! The layouts, the instance record and the entry-point names stay the
27//! variant's, exactly as they are for the built-in shader: a snippet changes
28//! what a stage computes, not what the pipeline reads. A snippet that declares
29//! a binding of its own therefore fails pipeline creation rather than drawing
30//! something else; one that declares functions, types or constants over the
31//! bindings every variant already declares works.
32//!
33//! The label names the shader: it is the shader module's label, and a
34//! composition error is reported under it.
35
36use std::sync::Arc;
37
38use crate::specialize::{PipelineDescriptor, SurfaceTarget};
39
40/// A caller's own shader code, spliced into the built-in `line.wesl`.
41#[derive(Clone, PartialEq, Eq, Hash)]
42pub struct LineShader {
43    label: Arc<str>,
44    module: Arc<str>,
45    vertex: Arc<str>,
46    fragment: Arc<str>,
47}
48
49impl LineShader {
50    /// A shader named `label` whose `module`, `vertex` and `fragment`
51    /// snippets are spliced into the built-in entry shader.
52    ///
53    /// Any snippet may be empty, which leaves its place exactly as the
54    /// built-in shader has it.
55    pub fn new(
56        label: impl Into<Arc<str>>,
57        module: impl Into<Arc<str>>,
58        vertex: impl Into<Arc<str>>,
59        fragment: impl Into<Arc<str>>,
60    ) -> Self {
61        Self {
62            label: label.into(),
63            module: module.into(),
64            vertex: vertex.into(),
65            fragment: fragment.into(),
66        }
67    }
68
69    /// The name this shader is reported under.
70    pub fn label(&self) -> &str {
71        &self.label
72    }
73
74    /// The WESL declarations spliced in at module scope.
75    pub fn module(&self) -> &str {
76        &self.module
77    }
78
79    /// The WESL statements spliced into the vertex stage.
80    pub fn vertex(&self) -> &str {
81        &self.vertex
82    }
83
84    /// The WESL statements spliced into the fragment stage.
85    pub fn fragment(&self) -> &str {
86        &self.fragment
87    }
88}
89
90impl core::fmt::Debug for LineShader {
91    /// Prints the label and each snippet's length rather than the snippets
92    /// themselves, so a [`LineOptions`] dump stays readable.
93    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
94        f.debug_struct("LineShader")
95            .field("label", &self.label)
96            .field("module", &format_args!("{} bytes", self.module.len()))
97            .field("vertex", &format_args!("{} bytes", self.vertex.len()))
98            .field("fragment", &format_args!("{} bytes", self.fragment.len()))
99            .finish()
100    }
101}
102
103/// The byte width of one instance the line pipeline reads: the sum of the
104/// attribute sizes below.
105///
106/// A constant rather than a computed sum because a constant is what a
107/// caller's record type can assert its own size against at compile time.
108pub const SEGMENT_STRIDE: u64 = 44;
109
110/// The instance-step layout a line pipeline reads its segment records through.
111///
112/// The stride is [SEGMENT_STRIDE], the sum of the declared attribute sizes,
113/// so a caller's segment record is tightly packed by the same tokens: a type
114/// feeding the stream asserts its own size against the constant.
115pub fn segment_layout() -> crate::specialize::VertexBufferLayoutDesc {
116    const ATTRIBUTES: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
117        0 => Float32x3,
118        1 => Float32x3,
119        2 => Unorm8x4,
120        3 => Unorm8x4,
121        4 => Float32x2,
122        5 => Uint32,
123    ];
124    crate::specialize::VertexBufferLayoutDesc {
125        array_stride: SEGMENT_STRIDE,
126        step_mode: wgpu::VertexStepMode::Instance,
127        attributes: crate::specialize::VertexAttributes::from_slice(&ATTRIBUTES),
128    }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    /// The constant the record types assert against stays the sum of the
136    /// attribute sizes, so a layout edit cannot quietly desync the two.
137    #[test]
138    fn the_stride_constant_is_the_attribute_sizes() {
139        const ATTRIBUTES: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
140            0 => Float32x3,
141            1 => Float32x3,
142            2 => Unorm8x4,
143            3 => Unorm8x4,
144            4 => Float32x2,
145            5 => Uint32,
146        ];
147        let summed: u64 = ATTRIBUTES
148            .iter()
149            .map(|attribute| attribute.format.size())
150            .sum();
151        assert_eq!(SEGMENT_STRIDE, summed);
152        assert_eq!(segment_layout().array_stride, summed);
153    }
154
155    /// The one flag a line variant carries is the options' hard-edge word.
156    #[test]
157    fn the_features_carry_the_hard_edge() {
158        let surface = crate::specialize::SurfaceKey {
159            color_format: wgpu::TextureFormat::Bgra8UnormSrgb,
160            depth_stencil_format: None,
161            sample_count: 1,
162        };
163        let variant = LineVariant {
164            surface,
165            options: LineOptions::opaque(),
166        };
167        assert_eq!(variant.features(), [("HARD_EDGE", true)]);
168
169        let variant = LineVariant {
170            surface,
171            options: LineOptions::blended(),
172        };
173        assert_eq!(variant.features(), [("HARD_EDGE", false)]);
174
175        // A multisampled target antialiases through alpha-to-coverage, so the
176        // options' hard-edge word yields there.
177        let multisampled = crate::specialize::SurfaceKey {
178            sample_count: 4,
179            ..surface
180        };
181        let variant = LineVariant {
182            surface: multisampled,
183            options: LineOptions::opaque(),
184        };
185        assert_eq!(variant.features(), [("HARD_EDGE", false)]);
186    }
187}
188
189/// The policy a caller decides about a built-in line pipeline.
190///
191/// Everything a draw resolves is absent: the instance record, the render
192/// target and the device's storage-buffer answer are facts a draw folds into
193/// an [`LineVariant`] when it composes a pipeline, not choices a caller can
194/// make. The target owns the color format, the sample count and whether
195/// there is a depth attachment at all; what is left is what a caller actually
196/// picks.
197///
198/// The zero value is the usual stroke: an antialiased edge blended over the
199/// frame, testing against the depth the pass holds without writing its own.
200#[derive(Clone, Debug, PartialEq, Eq, Hash)]
201pub struct LineOptions {
202    /// The entry shader this policy draws with, or `None` for the built-in
203    /// `line.wesl`.
204    ///
205    /// A caller's snippets extend the built-in source and nothing else: the
206    /// layouts, the instance record and the entry-point names stay the
207    /// variant's. See [`LineShader`].
208    ///
209    /// The shader takes part in equality and hashing whole, label included:
210    /// two options whose shaders differ at all — a name among the
211    /// differences — build two pipelines, which is the observable
212    /// difference.
213    pub shader: Option<LineShader>,
214    /// Whether the fragment stage cuts a pixel whose coverage falls below
215    /// one half instead of blending its partial colour.
216    ///
217    /// An antialiased edge is a veil of partial pixels; a stroke that covers
218    /// the frame, as an opaque one does, has no colour to veil the background
219    /// with, so it cuts cleanly at the pixel it covers instead. The cut is a
220    /// whole-pixel decision, so it works at any sample count — but on a
221    /// multisampled target the pipeline drops the cut and leaves the edge to
222    /// the samples: state [a2c_hint](Self::a2c_hint) to have the alpha turned
223    /// into the sample mask, which smooths the edge the cut would jag. See
224    /// [LineVariant].
225    pub hard_edge: bool,
226    /// How the pipeline tests and writes the depth attachment, for a target
227    /// that has one.
228    ///
229    /// A target without a depth attachment runs the pipeline without a
230    /// depth-stencil state at all, whatever this field says. The format is
231    /// the attachment's own, which the options cannot know: it is the
232    /// target's word, and the variant carries the target.
233    ///
234    /// The default states the stroke convention: the comparison is the
235    /// renderer's reverse-z one, and a stroke writes its own depth, which is
236    /// what lets strokes cover one another. A stroke that blends over the
237    /// frame instead states the default with the write cleared, so it hides
238    /// nothing behind it.
239    pub depth_stencil: crate::pipeline::DepthStencilPolicy,
240    /// The blend state the fragment output composites with, or `None` to
241    /// write it unblended.
242    ///
243    /// The common stroke composites with the target's alpha: the fragment's
244    /// coverage rides its alpha, so `ALPHA_BLENDING` is what makes an edge
245    /// antialiased rather than jagged. A stroke under a caller's own policy —
246    /// an additive energy line, say — states its own.
247    pub blend: Option<wgpu::BlendState>,
248    /// Which color channels the pipeline writes to the target.
249    pub write_mask: wgpu::ColorWrites,
250    /// Whether the fragment's alpha states coverage, on a multisampled
251    /// target.
252    ///
253    /// Alpha-to-coverage turns the fragment's alpha into the sample mask, so
254    /// an antialiased edge lands as partially covered samples instead of a
255    /// blend. That is only honest when the alpha *is* coverage: a wash whose
256    /// alpha is transparency, blended over the frame, would have its
257    /// contribution squared — masked to a fraction of the samples and then
258    /// blended by the same fraction again — so the hint defaults to off and a
259    /// stroke states it when its alpha means coverage. Nothing happens on a
260    /// single-sampled target, where there are no samples to spread over.
261    pub a2c_hint: bool,
262}
263
264impl Default for LineOptions {
265    /// A stroke blended over the frame, testing depth without writing it.
266    fn default() -> Self {
267        Self {
268            shader: None,
269            hard_edge: false,
270            depth_stencil: crate::pipeline::DepthStencilPolicy {
271                depth_write_enabled: false,
272                depth_compare: wgpu::CompareFunction::Greater,
273            },
274            blend: Some(wgpu::BlendState::ALPHA_BLENDING),
275            write_mask: wgpu::ColorWrites::ALL,
276            a2c_hint: false,
277        }
278    }
279}
280
281impl LineOptions {
282    /// A stroke that covers the frame: no blend, the depth written.
283    ///
284    /// The cut at half coverage is what keeps its edge from veiling the
285    /// background with a colour it does not own; see
286    /// [hard_edge](Self::hard_edge). On a multisampled target the cut yields
287    /// to alpha-to-coverage, which keeps the edge smooth there.
288    pub const fn opaque() -> Self {
289        Self {
290            shader: None,
291            hard_edge: true,
292            depth_stencil: crate::pipeline::DepthStencilPolicy {
293                depth_write_enabled: true,
294                depth_compare: wgpu::CompareFunction::Greater,
295            },
296            blend: None,
297            write_mask: wgpu::ColorWrites::ALL,
298            a2c_hint: true,
299        }
300    }
301
302    /// A stroke blended over the frame, testing depth without writing it.
303    pub const fn blended() -> Self {
304        Self {
305            shader: None,
306            hard_edge: false,
307            depth_stencil: crate::pipeline::DepthStencilPolicy {
308                depth_write_enabled: false,
309                depth_compare: wgpu::CompareFunction::Greater,
310            },
311            blend: Some(wgpu::BlendState::ALPHA_BLENDING),
312            write_mask: wgpu::ColorWrites::ALL,
313            a2c_hint: false,
314        }
315    }
316}
317
318/// The pipeline variant a line draw resolves to.
319///
320/// The target decides the surface, and the options decide the rest. Two
321/// options that differ only where the pipeline cannot see — the snippets'
322/// names among them — are two variants, because a pipeline built for one
323/// draws differently than one built for the other.
324///
325/// A difference that would change nothing is folded instead: on a
326/// single-sampled target the antialiasing hint says nothing, so two
327/// options that differ only in it share the one variant.
328///
329/// One difference the target does reach is the antialiasing: on a
330/// multisampled target the variant drops the hard-edge cut, and it turns the
331/// fragment's alpha into the alpha-to-coverage sample mask when the options
332/// hint that the alpha states coverage, so an options' hard-edge word holds
333/// only on a single-sampled one.
334#[derive(Clone, Debug, PartialEq, Eq, Hash)]
335pub struct LineVariant {
336    /// The target the stroke is drawn into.
337    pub surface: crate::specialize::SurfaceKey,
338    /// The caller's policy for the stroke.
339    pub options: LineOptions,
340}
341
342impl LineVariant {
343    /// The conditional-compilation flags the variant's shader module composes
344    /// with. The hard-edge cut is the one flag a line variant carries, and it
345    /// holds only where no samples antialias in its place: a multisampled
346    /// target leaves the edge to its samples — smoothed by
347    /// alpha-to-coverage when the options hint at it — so the cut is left to
348    /// a single-sampled one.
349    fn features(&self) -> [(&'static str, bool); 1] {
350        [(
351            "HARD_EDGE",
352            self.options.hard_edge && self.surface.sample_count == 1,
353        )]
354    }
355}
356
357impl crate::specialize::PipelineVariant<wgpu::RenderPipeline> for LineVariant {
358    type Descriptor = Self;
359
360    fn descriptor(&self, _device: &wgpu::Device) -> Self {
361        self.clone()
362    }
363}
364
365impl crate::specialize::PipelineDescriptor<wgpu::RenderPipeline> for LineVariant {
366    fn create(&self, device: &wgpu::Device) -> wgpu::RenderPipeline {
367        pipeline(device, self)
368    }
369}
370
371/// The pipeline a line variant draws with.
372fn pipeline(device: &wgpu::Device, variant: &LineVariant) -> wgpu::RenderPipeline {
373    let label = variant
374        .options
375        .shader
376        .as_ref()
377        .map_or("unlit_wgpu::line", |shader| shader.label());
378    let features = variant.features();
379    let replacement = variant.options.shader.as_ref().map(|shader| {
380        crate::compose::splice(
381            crate::shader::line::MODULE.source,
382            shader.module(),
383            shader.vertex(),
384            shader.fragment(),
385        )
386    });
387    let wgsl = crate::compose::compose("line", &features, replacement.as_deref())
388        .unwrap_or_else(|error| panic!("{label}: {error}"));
389    let module = crate::util::shader_module(device, &wgsl, label);
390    let layout = crate::pipeline::global_bind_group_layout(
391        device,
392        crate::pipeline::GlobalBindings::default(),
393        !crate::pipeline::supports_storage_buffers(device),
394    );
395    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
396        label: Some("unlit_wgpu::line::pipeline_layout"),
397        bind_group_layouts: &[Some(&layout)],
398        immediate_size: 0,
399    });
400    let mut descriptor = crate::specialize::RenderPipelineDesc {
401        label: Some("unlit_wgpu::line".to_owned()),
402        layout: Some(pipeline_layout),
403        vertex: crate::specialize::VertexStateDesc {
404            module: module.clone(),
405            entry_point: Some("vs_main".to_owned()),
406            compilation_options: Default::default(),
407            buffers: vec![Some(segment_layout())],
408        },
409        // A line is a flat quad seen from whichever side the camera is on,
410        // and its winding depends on which way the segment happens to run, so
411        // neither face is culled.
412        primitive: wgpu::PrimitiveState {
413            topology: wgpu::PrimitiveTopology::TriangleList,
414            front_face: wgpu::FrontFace::Ccw,
415            cull_mode: None,
416            ..Default::default()
417        },
418        // The target decides whether there is a depth state at all; the
419        // options decide what it does.
420        depth_stencil: variant
421            .surface
422            .depth_stencil_format
423            .map(|format| wgpu::DepthStencilState {
424                format,
425                depth_write_enabled: Some(variant.options.depth_stencil.depth_write_enabled),
426                depth_compare: Some(variant.options.depth_stencil.depth_compare),
427                stencil: wgpu::StencilState::default(),
428                bias: wgpu::DepthBiasState::default(),
429            }),
430        // Alpha-to-coverage needs no feature gate of its own; it means
431        // anything only on a multisampled target, and only where the options
432        // state that the alpha is coverage rather than transparency.
433        multisample: wgpu::MultisampleState {
434            count: variant.surface.sample_count,
435            alpha_to_coverage_enabled: variant.options.a2c_hint && variant.surface.sample_count > 1,
436            ..Default::default()
437        },
438        fragment: Some(crate::specialize::FragmentStateDesc {
439            module,
440            entry_point: Some("fs_main".to_owned()),
441            compilation_options: Default::default(),
442            targets: vec![Some(wgpu::ColorTargetState {
443                format: variant.surface.color_format,
444                blend: variant.options.blend,
445                write_mask: variant.options.write_mask,
446            })],
447        }),
448        multiview_mask: None,
449        cache: None,
450    };
451    descriptor.set_surface(variant.surface);
452    descriptor.create(device)
453}