Skip to main content

agg_gui_wgpu/
pipelines.rs

1//! All `wgpu::RenderPipeline` objects and associated `wgpu::BindGroupLayout`
2//! resources created once at startup.
3//!
4//! `WgpuPipelines` is created in `WgpuGfxCtx::new()` and stored for the
5//! lifetime of the context.  Every pipeline, bind-group layout, and shared
6//! sampler lives here.
7//!
8//! Uniform data is NOT stored here — each draw command in `end_frame` creates
9//! a small `wgpu::Buffer` via `device.create_buffer_init()` so that multiple
10//! draw commands of the same pipeline type in one frame do not overwrite each
11//! other's uniforms.
12
13use std::mem::size_of;
14
15use bytemuck::{Pod, Zeroable};
16
17use crate::shaders::{
18    AA_SOLID_WGSL, AA_TEXTURE_WGSL, GRADIENT_WGSL, LAYER_WGSL, LCB_FLATTEN_WGSL, LCB_WGSL,
19    LCD_WGSL, SOLID_WGSL, TEXT_GRAY_WGSL, TEX_DOWNSAMPLE_3X_WGSL, TEX_DOWNSAMPLE_4X_WGSL, TEX_WGSL,
20};
21
22// ---------------------------------------------------------------------------
23// Uniform structs (byte layouts must exactly match WGSL structs in shaders.rs)
24// ---------------------------------------------------------------------------
25
26/// 32-byte uniform block for the solid and AA-solid pipelines.
27#[repr(C)]
28#[derive(Clone, Copy, Debug, Pod, Zeroable)]
29pub(crate) struct SolidUniforms {
30    pub resolution: [f32; 2],
31    pub _pad: [f32; 2],
32    pub color: [f32; 4],
33}
34const _: () = assert!(size_of::<SolidUniforms>() == 32);
35
36/// 32-byte uniform block for the AA-texture pipeline (agg-sharp port).
37/// Same layout as `SolidUniforms` but kept as a separate type so the
38/// pipeline plumbing can read intent at a glance.
39#[repr(C)]
40#[derive(Clone, Copy, Debug, Pod, Zeroable)]
41pub(crate) struct AaTexUniforms {
42    pub resolution: [f32; 2],
43    pub _pad: [f32; 2],
44    pub color: [f32; 4],
45}
46const _: () = assert!(size_of::<AaTexUniforms>() == 32);
47
48/// 32-byte uniform block for the textured-quad pipeline. `tint` is
49/// a per-draw RGBA multiplier so callers can fade image blits — set
50/// `[1.0, 1.0, 1.0, alpha]` for a straight alpha fade, or zero out
51/// channels for a quick recolor. The 4×4-box downsample pipeline
52/// reuses the same layout and simply doesn't read `tint`.
53#[repr(C)]
54#[derive(Clone, Copy, Debug, Pod, Zeroable)]
55pub(crate) struct TexUniforms {
56    pub resolution: [f32; 2],
57    pub _pad: [f32; 2],
58    pub tint: [f32; 4],
59}
60const _: () = assert!(size_of::<TexUniforms>() == 32);
61
62/// 48-byte uniform block for the layer-composite pipeline.
63#[repr(C)]
64#[derive(Clone, Copy, Debug, Pod, Zeroable)]
65pub(crate) struct LayerUniforms {
66    pub resolution: [f32; 2],
67    pub alpha: f32,
68    pub mask_enabled: u32,
69    pub layer_size: [f32; 2],
70    pub mask_radius: f32,
71    pub _pad0: f32,
72    pub mask_rect: [f32; 4],
73}
74const _: () = assert!(size_of::<LayerUniforms>() == 48);
75
76/// 32-byte uniform block for the LCD subpixel-text pipeline.
77#[repr(C)]
78#[derive(Clone, Copy, Debug, Pod, Zeroable)]
79pub(crate) struct LcdUniforms {
80    pub resolution: [f32; 2],
81    pub channel: u32,
82    pub _pad: u32,
83    pub color: [f32; 4],
84}
85const _: () = assert!(size_of::<LcdUniforms>() == 32);
86
87/// 16-byte uniform block for the LCD backbuffer composite pipeline.
88///
89/// `global_alpha` folds the context's active alpha into the premultiplied
90/// blit so a direct `set_global_alpha` fades cached LCD text — matching the
91/// Textured and LcdMask paths, which already carry it.  Occupies the slot
92/// that used to be dead padding, so the struct is still 16 bytes.
93#[repr(C)]
94#[derive(Clone, Copy, Debug, Pod, Zeroable)]
95pub(crate) struct LcbUniforms {
96    pub resolution: [f32; 2],
97    pub channel: u32,
98    pub global_alpha: f32,
99}
100const _: () = assert!(size_of::<LcbUniforms>() == 16);
101
102// ---------------------------------------------------------------------------
103// Blend states
104// ---------------------------------------------------------------------------
105
106/// Standard 2-D composite: src-alpha / one-minus-src-alpha for colour, with
107/// premultiplied-style alpha accumulation so the same pipelines work both
108/// against the surface (alpha stays pinned at 1) and against transparent layer
109/// textures (alpha builds up correctly so the layer composite has something
110/// to alpha-blend against).
111///
112/// Earlier wired this with `alpha = { Zero, One }` to "preserve framebuffer
113/// alpha", which is fine on the surface but leaves layer textures stuck at
114/// alpha=0 — the composite then degenerates into pure additive blending and
115/// every windowed widget renders as washed-out white.  `One / OMSA` is a
116/// fixed-point at 1 on the surface AND accumulates correctly on a layer.
117const BLEND_STANDARD: wgpu::BlendState = wgpu::BlendState {
118    color: wgpu::BlendComponent {
119        src_factor: wgpu::BlendFactor::SrcAlpha,
120        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
121        operation: wgpu::BlendOperation::Add,
122    },
123    alpha: wgpu::BlendComponent {
124        src_factor: wgpu::BlendFactor::One,
125        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
126        operation: wgpu::BlendOperation::Add,
127    },
128};
129
130/// Premultiplied src-over for layer compositing and LCB backbuffer blits.
131const BLEND_PREMUL: wgpu::BlendState = wgpu::BlendState {
132    color: wgpu::BlendComponent {
133        src_factor: wgpu::BlendFactor::One,
134        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
135        operation: wgpu::BlendOperation::Add,
136    },
137    alpha: wgpu::BlendComponent {
138        src_factor: wgpu::BlendFactor::One,
139        dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
140        operation: wgpu::BlendOperation::Add,
141    },
142};
143
144// ---------------------------------------------------------------------------
145// WgpuPipelines
146// ---------------------------------------------------------------------------
147
148/// All render pipelines, bind-group layouts, and shared samplers.
149///
150/// Created once per [`crate::WgpuGfxCtx`] and reused for the lifetime of the
151/// context.  The struct is publicly visible so library primitives like
152/// [`crate::ssaa::SsaaFramebuffer::blit_to`] can take a reference and reuse
153/// the shared 2-D textured-quad pipeline; the fields themselves stay
154/// `pub(crate)` so external code can hold a `&WgpuPipelines` but cannot
155/// reach in and rebuild individual pipelines.
156pub struct WgpuPipelines {
157    // ── Solid colour ─────────────────────────────────────────────────────────
158    pub solid_pipeline: wgpu::RenderPipeline,
159    pub solid_bgl: wgpu::BindGroupLayout,
160
161    // ── AA solid colour (per-vertex alpha from tess2 halo strip) ─────────────
162    pub aa_solid_pipeline: wgpu::RenderPipeline,
163    pub aa_solid_bgl: wgpu::BindGroupLayout,
164
165    // ── AA texture (agg-sharp `Graphics2DGpu` port; 1024-wide α-step texture).
166    pub aa_texture_pipeline: wgpu::RenderPipeline,
167    pub aa_texture_bgl0: wgpu::BindGroupLayout,
168    pub aa_texture_bgl1: wgpu::BindGroupLayout,
169
170    // ── Gradient (linear + radial, SVG spread modes) ──────────────────────────
171    pub gradient_pipeline: wgpu::RenderPipeline,
172    pub gradient_bgl0: wgpu::BindGroupLayout,
173    pub gradient_bgl1: wgpu::BindGroupLayout,
174
175    // ── Textured quad (image blit, Label backbuffer) ──────────────────────────
176    pub tex_pipeline: wgpu::RenderPipeline,
177    pub tex_bgl0: wgpu::BindGroupLayout,
178    pub tex_bgl1: wgpu::BindGroupLayout,
179
180    // ── 3×3-box downsample (SSAA at 3× linear) ─────────────────────────────
181    /// Same vertex / bind groups as `tex_pipeline`; fragment shader runs
182    /// 9 point-aligned taps at {-1, 0, +1}² texel offsets for an exact 3×3
183    /// box average.  Used by the cube widget at the 9× SSAA setting where
184    /// a single bilinear minification would drop 5 of 9 source texels per
185    /// output pixel (visually indistinguishable from no AA).
186    pub tex_downsample_3x_pipeline: wgpu::RenderPipeline,
187
188    // ── 4×4-box downsample (SSAA at 4× linear) ─────────────────────────────
189    /// Same vertex / bind groups as `tex_pipeline`; fragment shader runs
190    /// 4 bilinear taps in a 2×2 quadrant grid for an exact 4×4 box average.
191    /// Used by the cube widget at the highest SSAA setting where a single
192    /// bilinear minification would drop 12 of 16 source texels per output.
193    pub tex_downsample_4x_pipeline: wgpu::RenderPipeline,
194
195    // ── Layer composite (SDF rounded-corner mask in fragment shader) ──────────
196    pub layer_pipeline: wgpu::RenderPipeline,
197    /// Masked variant of `layer_pipeline`: composites a clip layer through the
198    /// tessellated clip path (`pos2 + uv2 + coverage`).  Built in
199    /// `crate::layer_mask`, which also owns its shader.
200    pub layer_mesh_pipeline: wgpu::RenderPipeline,
201    pub layer_bgl0: wgpu::BindGroupLayout,
202    pub layer_bgl1: wgpu::BindGroupLayout,
203
204    // ── LCD subpixel text — three pipelines, one per colour channel ───────────
205    pub lcd_r: wgpu::RenderPipeline,
206    pub lcd_g: wgpu::RenderPipeline,
207    pub lcd_b: wgpu::RenderPipeline,
208    pub lcd_bgl0: wgpu::BindGroupLayout,
209    pub lcd_bgl1: wgpu::BindGroupLayout,
210
211    // ── LCD backbuffer composite — three pipelines, premultiplied blend ───────
212    pub lcb_r: wgpu::RenderPipeline,
213    pub lcb_g: wgpu::RenderPipeline,
214    pub lcb_b: wgpu::RenderPipeline,
215    pub lcb_bgl0: wgpu::BindGroupLayout,
216    pub lcb_bgl1: wgpu::BindGroupLayout,
217
218    // ── Inside-layer text (single-pass, alpha-writing) ────────────────────────
219    /// Grayscale coverage text quad — reuses the LCD bind-group layouts but
220    /// writes ALL colour channels + alpha in one pass.  Routed to when text is
221    /// drawn inside a compositing layer so the layer texture gets correct alpha
222    /// (the 3-pass LCD pipelines never write alpha → washed-out glyphs on pop).
223    pub text_gray: wgpu::RenderPipeline,
224    /// Two-plane backbuffer flatten quad — reuses the LCB bind-group layouts,
225    /// collapses the per-channel planes into a single premultiplied sample in
226    /// one alpha-writing pass.  Inside-layer companion to `lcb_r/g/b`.
227    pub lcb_flatten: wgpu::RenderPipeline,
228
229    // ── Shared samplers ───────────────────────────────────────────────────────
230    /// Nearest-neighbour — used for LCD mask textures and Label backbuffers.
231    pub nearest_sampler: wgpu::Sampler,
232    /// Bilinear — used for images, layers, and gradient ramp textures.
233    pub linear_sampler: wgpu::Sampler,
234}
235
236impl WgpuPipelines {
237    /// Construct all pipelines.  Panics if any WGSL shader fails to compile —
238    /// a startup crash is preferable to a silent runtime black screen.
239    ///
240    /// `sample_count` bakes into every pipeline's `MultisampleState`.  Must
241    /// match the sample count of every render-pass color attachment those
242    /// pipelines target — both the surface MSAA buffer and any layer MSAA
243    /// buffers.  `1` disables MSAA.
244    pub(crate) fn new(
245        device: &wgpu::Device,
246        surface_format: wgpu::TextureFormat,
247        sample_count: u32,
248    ) -> Self {
249        // ── Samplers ─────────────────────────────────────────────────────────
250        let nearest_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
251            label: Some("nearest"),
252            address_mode_u: wgpu::AddressMode::ClampToEdge,
253            address_mode_v: wgpu::AddressMode::ClampToEdge,
254            address_mode_w: wgpu::AddressMode::ClampToEdge,
255            mag_filter: wgpu::FilterMode::Nearest,
256            min_filter: wgpu::FilterMode::Nearest,
257            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
258            ..Default::default()
259        });
260        let linear_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
261            label: Some("linear"),
262            address_mode_u: wgpu::AddressMode::ClampToEdge,
263            address_mode_v: wgpu::AddressMode::ClampToEdge,
264            address_mode_w: wgpu::AddressMode::ClampToEdge,
265            mag_filter: wgpu::FilterMode::Linear,
266            min_filter: wgpu::FilterMode::Linear,
267            mipmap_filter: wgpu::MipmapFilterMode::Linear,
268            ..Default::default()
269        });
270
271        // ── Shader modules ───────────────────────────────────────────────────
272        let solid_sm = mk_shader(device, "solid", SOLID_WGSL);
273        let aa_solid_sm = mk_shader(device, "aa_solid", AA_SOLID_WGSL);
274        let gradient_sm = mk_shader(device, "gradient", GRADIENT_WGSL);
275        let aa_texture_sm = mk_shader(device, "aa_texture", AA_TEXTURE_WGSL);
276        let tex_sm = mk_shader(device, "tex", TEX_WGSL);
277        let layer_sm = mk_shader(device, "layer", LAYER_WGSL);
278        let lcd_sm = mk_shader(device, "lcd", LCD_WGSL);
279        let lcb_sm = mk_shader(device, "lcb", LCB_WGSL);
280
281        // ── Bind group layouts ───────────────────────────────────────────────
282        let solid_bgl = mk_uniform_bgl(device, "solid", size_of::<SolidUniforms>() as u64);
283        let aa_solid_bgl = mk_uniform_bgl(device, "aa_solid", size_of::<SolidUniforms>() as u64);
284        let gradient_bgl0 = mk_uniform_bgl(
285            device,
286            "gradient0",
287            size_of::<crate::gradient::GradientUniforms>() as u64,
288        );
289        let gradient_bgl1 = mk_tex1_bgl(device, "gradient1");
290        let aa_texture_bgl0 =
291            mk_uniform_bgl(device, "aa_texture0", size_of::<AaTexUniforms>() as u64);
292        let aa_texture_bgl1 = mk_tex1_bgl(device, "aa_texture1");
293        let tex_bgl0 = mk_uniform_bgl(device, "tex0", size_of::<TexUniforms>() as u64);
294        let tex_bgl1 = mk_tex1_bgl(device, "tex1");
295        let layer_bgl0 = mk_uniform_bgl(device, "layer0", size_of::<LayerUniforms>() as u64);
296        let layer_bgl1 = mk_tex1_bgl(device, "layer1");
297        let lcd_bgl0 = mk_uniform_bgl(device, "lcd0", size_of::<LcdUniforms>() as u64);
298        let lcd_bgl1 = mk_tex1_bgl(device, "lcd1");
299        let lcb_bgl0 = mk_uniform_bgl(device, "lcb0", size_of::<LcbUniforms>() as u64);
300        let lcb_bgl1 = mk_tex2_bgl(device, "lcb1");
301
302        // ── Pipeline layouts ─────────────────────────────────────────────────
303        let solid_pl = mk_layout(device, "solid", &[&solid_bgl]);
304        let aa_solid_pl = mk_layout(device, "aa_solid", &[&aa_solid_bgl]);
305        let gradient_pl = mk_layout(device, "gradient", &[&gradient_bgl0, &gradient_bgl1]);
306        let aa_texture_pl = mk_layout(device, "aa_texture", &[&aa_texture_bgl0, &aa_texture_bgl1]);
307        let tex_pl = mk_layout(device, "tex", &[&tex_bgl0, &tex_bgl1]);
308        let layer_pl = mk_layout(device, "layer", &[&layer_bgl0, &layer_bgl1]);
309        let lcd_pl = mk_layout(device, "lcd", &[&lcd_bgl0, &lcd_bgl1]);
310        let lcb_pl = mk_layout(device, "lcb", &[&lcb_bgl0, &lcb_bgl1]);
311
312        // ── Shared vertex attribute slices ───────────────────────────────────
313        // Defined once here so that every VertexBufferLayout below can borrow them
314        // without moving them (they outlive all the pipeline create calls below).
315        let pos2_attrs = [wgpu::VertexAttribute {
316            format: wgpu::VertexFormat::Float32x2,
317            offset: 0,
318            shader_location: 0,
319        }];
320        let pos2_alpha_attrs = [
321            wgpu::VertexAttribute {
322                format: wgpu::VertexFormat::Float32x2,
323                offset: 0,
324                shader_location: 0,
325            },
326            wgpu::VertexAttribute {
327                format: wgpu::VertexFormat::Float32,
328                offset: 8,
329                shader_location: 1,
330            },
331        ];
332        let pos2_uv2_attrs = [
333            wgpu::VertexAttribute {
334                format: wgpu::VertexFormat::Float32x2,
335                offset: 0,
336                shader_location: 0,
337            },
338            wgpu::VertexAttribute {
339                format: wgpu::VertexFormat::Float32x2,
340                offset: 8,
341                shader_location: 1,
342            },
343        ];
344
345        // Convenience closures — return a fresh VertexBufferLayout borrowing the
346        // attribute arrays above.  Closures rather than values avoid requiring
347        // VertexBufferLayout to be Copy (it only derives Clone).
348        let vbl_pos2 = || wgpu::VertexBufferLayout {
349            array_stride: 8,
350            step_mode: wgpu::VertexStepMode::Vertex,
351            attributes: &pos2_attrs,
352        };
353        let vbl_pos2_alpha = || wgpu::VertexBufferLayout {
354            array_stride: 12,
355            step_mode: wgpu::VertexStepMode::Vertex,
356            attributes: &pos2_alpha_attrs,
357        };
358        let vbl_pos2_uv2 = || wgpu::VertexBufferLayout {
359            array_stride: 16,
360            step_mode: wgpu::VertexStepMode::Vertex,
361            attributes: &pos2_uv2_attrs,
362        };
363
364        // ── Render pipelines ─────────────────────────────────────────────────
365        let solid_pipeline = build_pipeline(
366            device,
367            "solid",
368            &solid_pl,
369            &solid_sm,
370            &solid_sm,
371            &[vbl_pos2()],
372            surface_format,
373            Some(BLEND_STANDARD),
374            wgpu::ColorWrites::ALL,
375            sample_count,
376        );
377        let aa_solid_pipeline = build_pipeline(
378            device,
379            "aa_solid",
380            &aa_solid_pl,
381            &aa_solid_sm,
382            &aa_solid_sm,
383            &[vbl_pos2_alpha()],
384            surface_format,
385            Some(BLEND_STANDARD),
386            wgpu::ColorWrites::ALL,
387            sample_count,
388        );
389        // Premultiplied blend matches agg-sharp's
390        // `gl.BlendFunc(One, OneMinusSrcAlpha)` in `Graphics2DGpu.PreRender`
391        // — the shader emits `(color * α, α)` and the blend composites it
392        // correctly into the surface.
393        let aa_texture_pipeline = build_pipeline(
394            device,
395            "aa_texture",
396            &aa_texture_pl,
397            &aa_texture_sm,
398            &aa_texture_sm,
399            &[vbl_pos2_uv2()],
400            surface_format,
401            Some(BLEND_PREMUL),
402            wgpu::ColorWrites::ALL,
403            sample_count,
404        );
405        let gradient_pipeline = build_pipeline(
406            device,
407            "gradient",
408            &gradient_pl,
409            &gradient_sm,
410            &gradient_sm,
411            &[vbl_pos2_alpha()],
412            surface_format,
413            Some(BLEND_STANDARD),
414            wgpu::ColorWrites::ALL,
415            sample_count,
416        );
417        let tex_pipeline = build_pipeline(
418            device,
419            "tex",
420            &tex_pl,
421            &tex_sm,
422            &tex_sm,
423            &[vbl_pos2_uv2()],
424            surface_format,
425            Some(BLEND_STANDARD),
426            wgpu::ColorWrites::ALL,
427            sample_count,
428        );
429        let tex_ds3_sm = mk_shader(device, "tex_downsample_3x", TEX_DOWNSAMPLE_3X_WGSL);
430        let tex_downsample_3x_pipeline = build_pipeline(
431            device,
432            "tex_downsample_3x",
433            &tex_pl,
434            &tex_ds3_sm,
435            &tex_ds3_sm,
436            &[vbl_pos2_uv2()],
437            surface_format,
438            Some(BLEND_STANDARD),
439            wgpu::ColorWrites::ALL,
440            sample_count,
441        );
442        let tex_ds4_sm = mk_shader(device, "tex_downsample_4x", TEX_DOWNSAMPLE_4X_WGSL);
443        let tex_downsample_4x_pipeline = build_pipeline(
444            device,
445            "tex_downsample_4x",
446            &tex_pl,
447            &tex_ds4_sm,
448            &tex_ds4_sm,
449            &[vbl_pos2_uv2()],
450            surface_format,
451            Some(BLEND_STANDARD),
452            wgpu::ColorWrites::ALL,
453            sample_count,
454        );
455        let layer_pipeline = build_pipeline(
456            device,
457            "layer",
458            &layer_pl,
459            &layer_sm,
460            &layer_sm,
461            &[vbl_pos2_uv2()],
462            surface_format,
463            Some(BLEND_PREMUL),
464            wgpu::ColorWrites::ALL,
465            sample_count,
466        );
467        let layer_mesh_pipeline = crate::layer_mask::build_layer_mesh_pipeline(
468            device,
469            &layer_bgl0,
470            &layer_bgl1,
471            surface_format,
472            BLEND_PREMUL,
473            sample_count,
474        );
475        let lcd_r = build_pipeline(
476            device,
477            "lcd_r",
478            &lcd_pl,
479            &lcd_sm,
480            &lcd_sm,
481            &[vbl_pos2_uv2()],
482            surface_format,
483            Some(BLEND_STANDARD),
484            wgpu::ColorWrites::RED,
485            sample_count,
486        );
487        let lcd_g = build_pipeline(
488            device,
489            "lcd_g",
490            &lcd_pl,
491            &lcd_sm,
492            &lcd_sm,
493            &[vbl_pos2_uv2()],
494            surface_format,
495            Some(BLEND_STANDARD),
496            wgpu::ColorWrites::GREEN,
497            sample_count,
498        );
499        let lcd_b = build_pipeline(
500            device,
501            "lcd_b",
502            &lcd_pl,
503            &lcd_sm,
504            &lcd_sm,
505            &[vbl_pos2_uv2()],
506            surface_format,
507            Some(BLEND_STANDARD),
508            wgpu::ColorWrites::BLUE,
509            sample_count,
510        );
511        let lcb_r = build_pipeline(
512            device,
513            "lcb_r",
514            &lcb_pl,
515            &lcb_sm,
516            &lcb_sm,
517            &[vbl_pos2_uv2()],
518            surface_format,
519            Some(BLEND_PREMUL),
520            wgpu::ColorWrites::RED,
521            sample_count,
522        );
523        let lcb_g = build_pipeline(
524            device,
525            "lcb_g",
526            &lcb_pl,
527            &lcb_sm,
528            &lcb_sm,
529            &[vbl_pos2_uv2()],
530            surface_format,
531            Some(BLEND_PREMUL),
532            wgpu::ColorWrites::GREEN,
533            sample_count,
534        );
535        let lcb_b = build_pipeline(
536            device,
537            "lcb_b",
538            &lcb_pl,
539            &lcb_sm,
540            &lcb_sm,
541            &[vbl_pos2_uv2()],
542            surface_format,
543            Some(BLEND_PREMUL),
544            wgpu::ColorWrites::BLUE,
545            sample_count,
546        );
547
548        // Inside-layer text: single-pass, alpha-writing.  `text_gray` reuses the
549        // LCD layout (`lcd_pl`) + a straight-alpha blend; `lcb_flatten` reuses
550        // the LCB layout (`lcb_pl`) + a premultiplied blend.  Both write ALL
551        // channels so a transparent layer accumulates correct alpha.
552        let text_gray_sm = mk_shader(device, "text_gray", TEXT_GRAY_WGSL);
553        let text_gray = build_pipeline(
554            device,
555            "text_gray",
556            &lcd_pl,
557            &text_gray_sm,
558            &text_gray_sm,
559            &[vbl_pos2_uv2()],
560            surface_format,
561            Some(BLEND_STANDARD),
562            wgpu::ColorWrites::ALL,
563            sample_count,
564        );
565        let lcb_flatten_sm = mk_shader(device, "lcb_flatten", LCB_FLATTEN_WGSL);
566        let lcb_flatten = build_pipeline(
567            device,
568            "lcb_flatten",
569            &lcb_pl,
570            &lcb_flatten_sm,
571            &lcb_flatten_sm,
572            &[vbl_pos2_uv2()],
573            surface_format,
574            Some(BLEND_PREMUL),
575            wgpu::ColorWrites::ALL,
576            sample_count,
577        );
578
579        Self {
580            solid_pipeline,
581            solid_bgl,
582            aa_solid_pipeline,
583            aa_solid_bgl,
584            aa_texture_pipeline,
585            aa_texture_bgl0,
586            aa_texture_bgl1,
587            gradient_pipeline,
588            gradient_bgl0,
589            gradient_bgl1,
590            tex_pipeline,
591            tex_bgl0,
592            tex_bgl1,
593            tex_downsample_3x_pipeline,
594            tex_downsample_4x_pipeline,
595            layer_pipeline,
596            layer_mesh_pipeline,
597            layer_bgl0,
598            layer_bgl1,
599            lcd_r,
600            lcd_g,
601            lcd_b,
602            lcd_bgl0,
603            lcd_bgl1,
604            lcb_r,
605            lcb_g,
606            lcb_b,
607            lcb_bgl0,
608            lcb_bgl1,
609            text_gray,
610            lcb_flatten,
611            nearest_sampler,
612            linear_sampler,
613        }
614    }
615}
616
617// ---------------------------------------------------------------------------
618// Private helpers
619// ---------------------------------------------------------------------------
620
621fn mk_shader(device: &wgpu::Device, label: &str, src: &str) -> wgpu::ShaderModule {
622    device.create_shader_module(wgpu::ShaderModuleDescriptor {
623        label: Some(label),
624        source: wgpu::ShaderSource::Wgsl(src.into()),
625    })
626}
627
628/// Bind-group layout with a single uniform buffer at binding 0, visible to
629/// both vertex and fragment stages.
630fn mk_uniform_bgl(device: &wgpu::Device, label: &str, size: u64) -> wgpu::BindGroupLayout {
631    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
632        label: Some(label),
633        entries: &[wgpu::BindGroupLayoutEntry {
634            binding: 0,
635            visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
636            ty: wgpu::BindingType::Buffer {
637                ty: wgpu::BufferBindingType::Uniform,
638                has_dynamic_offset: false,
639                min_binding_size: wgpu::BufferSize::new(size),
640            },
641            count: None,
642        }],
643    })
644}
645
646/// Bind-group layout for group 1 with one `texture_2d<f32>` (binding 0) and
647/// one filtering sampler (binding 1).
648fn mk_tex1_bgl(device: &wgpu::Device, label: &str) -> wgpu::BindGroupLayout {
649    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
650        label: Some(label),
651        entries: &[
652            wgpu::BindGroupLayoutEntry {
653                binding: 0,
654                visibility: wgpu::ShaderStages::FRAGMENT,
655                ty: wgpu::BindingType::Texture {
656                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
657                    view_dimension: wgpu::TextureViewDimension::D2,
658                    multisampled: false,
659                },
660                count: None,
661            },
662            wgpu::BindGroupLayoutEntry {
663                binding: 1,
664                visibility: wgpu::ShaderStages::FRAGMENT,
665                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
666                count: None,
667            },
668        ],
669    })
670}
671
672/// Bind-group layout for the LCB pipeline's group 1: two `texture_2d<f32>`
673/// (bindings 0 and 1, colour plane + alpha plane) and one sampler (binding 2).
674fn mk_tex2_bgl(device: &wgpu::Device, label: &str) -> wgpu::BindGroupLayout {
675    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
676        label: Some(label),
677        entries: &[
678            wgpu::BindGroupLayoutEntry {
679                binding: 0,
680                visibility: wgpu::ShaderStages::FRAGMENT,
681                ty: wgpu::BindingType::Texture {
682                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
683                    view_dimension: wgpu::TextureViewDimension::D2,
684                    multisampled: false,
685                },
686                count: None,
687            },
688            wgpu::BindGroupLayoutEntry {
689                binding: 1,
690                visibility: wgpu::ShaderStages::FRAGMENT,
691                ty: wgpu::BindingType::Texture {
692                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
693                    view_dimension: wgpu::TextureViewDimension::D2,
694                    multisampled: false,
695                },
696                count: None,
697            },
698            wgpu::BindGroupLayoutEntry {
699                binding: 2,
700                visibility: wgpu::ShaderStages::FRAGMENT,
701                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
702                count: None,
703            },
704        ],
705    })
706}
707
708fn mk_layout(
709    device: &wgpu::Device,
710    label: &str,
711    bgls: &[&wgpu::BindGroupLayout],
712) -> wgpu::PipelineLayout {
713    let opt: Vec<Option<&wgpu::BindGroupLayout>> = bgls.iter().copied().map(Some).collect();
714    device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
715        label: Some(label),
716        bind_group_layouts: &opt,
717        immediate_size: 0,
718    })
719}
720
721/// Create a `RenderPipeline` from a single shader module (vs and fs are the
722/// same module), a single vertex buffer slot, a blend state, and a colour
723/// write mask.  All 2D pipelines use `TriangleList` topology with no depth
724/// test, no culling, and no MSAA.
725fn build_pipeline(
726    device: &wgpu::Device,
727    label: &str,
728    layout: &wgpu::PipelineLayout,
729    vs: &wgpu::ShaderModule,
730    fs: &wgpu::ShaderModule,
731    vertex_buffers: &[wgpu::VertexBufferLayout<'_>],
732    surface_format: wgpu::TextureFormat,
733    blend: Option<wgpu::BlendState>,
734    write_mask: wgpu::ColorWrites,
735    sample_count: u32,
736) -> wgpu::RenderPipeline {
737    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
738        label: Some(label),
739        layout: Some(layout),
740        vertex: wgpu::VertexState {
741            module: vs,
742            entry_point: Some("vs_main"),
743            compilation_options: Default::default(),
744            buffers: vertex_buffers,
745        },
746        fragment: Some(wgpu::FragmentState {
747            module: fs,
748            entry_point: Some("fs_main"),
749            compilation_options: Default::default(),
750            targets: &[Some(wgpu::ColorTargetState {
751                format: surface_format,
752                blend,
753                write_mask,
754            })],
755        }),
756        primitive: wgpu::PrimitiveState {
757            topology: wgpu::PrimitiveTopology::TriangleList,
758            cull_mode: None,
759            ..Default::default()
760        },
761        depth_stencil: None,
762        multisample: wgpu::MultisampleState {
763            count: sample_count,
764            mask: !0,
765            alpha_to_coverage_enabled: false,
766        },
767        multiview_mask: None,
768        cache: None,
769    })
770}