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}