Skip to main content

oxiui_render_wgpu/gpu/
pipeline.rs

1//! Render-pipeline construction for the solid-fill / SDF shader and the
2//! gradient pipeline.
3//!
4//! [`SolidPipeline`] owns the compiled `solid.wgsl` module, the uniform bind
5//! group layout (viewport [`Globals`]), and the [`wgpu::RenderPipeline`].
6//!
7//! [`GradientPipeline`] owns the compiled `gradient.wgsl` module, a bind group
8//! layout for the viewport uniform plus a per-draw gradient uniform buffer, and
9//! the gradient render pipeline.
10//!
11//! Vertex attribute layouts are derived by hand to match the field offsets of
12//! [`Vertex`] / [`GradientVertex`] exactly — the compile-time size assertions
13//! in [`crate::gpu::buffer`] guard against drift.
14//!
15//! Solid pipeline vertex layout (56 bytes = 14 × f32):
16//!   pos      vec2  @0   offset  0
17//!   color    vec4  @1   offset  8
18//!   local    vec2  @2   offset 24
19//!   shape_xy vec2  @3   offset 32
20//!   shape_r  f32   @4   offset 40
21//!   kind     f32   @5   offset 44
22//!   extra    vec2  @6   offset 48
23//!
24//! [`Globals`]: crate::gpu::buffer::Globals
25//! [`Vertex`]: crate::gpu::buffer::Vertex
26//! [`GradientVertex`]: crate::gpu::buffer::GradientVertex
27
28use crate::gpu::buffer::{GradientUniforms, GradientVertex, TexVertex, Vertex};
29use crate::gpu::device::TARGET_FORMAT;
30
31// ── SolidPipeline ────────────────────────────────────────────────────────────
32
33/// The compiled solid-fill / SDF pipeline plus the bind-group layout its draws
34/// need.
35pub struct SolidPipeline {
36    /// The render pipeline (vertex + fragment stages, alpha blending).
37    pub pipeline: wgpu::RenderPipeline,
38    /// Layout of bind group 0 (the viewport uniform).
39    pub globals_layout: wgpu::BindGroupLayout,
40}
41
42impl SolidPipeline {
43    /// Build the solid pipeline for a colour target in [`TARGET_FORMAT`].
44    ///
45    /// `sample_count` sets the MSAA multisample count (1 = no MSAA, 4 or 8 =
46    /// MSAA).  Passing `1` reproduces the exact original code path.
47    pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
48        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
49            label: Some("oxiui-render-wgpu solid.wgsl"),
50            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/solid.wgsl").into()),
51        });
52
53        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
54            label: Some("oxiui-render-wgpu globals layout"),
55            entries: &[wgpu::BindGroupLayoutEntry {
56                binding: 0,
57                visibility: wgpu::ShaderStages::VERTEX,
58                ty: wgpu::BindingType::Buffer {
59                    ty: wgpu::BufferBindingType::Uniform,
60                    has_dynamic_offset: false,
61                    min_binding_size: None,
62                },
63                count: None,
64            }],
65        });
66
67        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
68            label: Some("oxiui-render-wgpu solid pipeline layout"),
69            bind_group_layouts: &[Some(&globals_layout)],
70            immediate_size: 0,
71        });
72
73        // Vertex attributes mirror `Vertex` byte offsets exactly (56 bytes):
74        //   pos      vec2  @0   offset  0
75        //   color    vec4  @1   offset  8
76        //   local    vec2  @2   offset 24
77        //   shape_xy vec2  @3   offset 32
78        //   shape_r  f32   @4   offset 40
79        //   kind     f32   @5   offset 44
80        //   extra    vec2  @6   offset 48
81        let attrs = [
82            wgpu::VertexAttribute {
83                format: wgpu::VertexFormat::Float32x2,
84                offset: 0,
85                shader_location: 0,
86            },
87            wgpu::VertexAttribute {
88                format: wgpu::VertexFormat::Float32x4,
89                offset: 8,
90                shader_location: 1,
91            },
92            wgpu::VertexAttribute {
93                format: wgpu::VertexFormat::Float32x2,
94                offset: 24,
95                shader_location: 2,
96            },
97            wgpu::VertexAttribute {
98                format: wgpu::VertexFormat::Float32x2,
99                offset: 32,
100                shader_location: 3,
101            },
102            wgpu::VertexAttribute {
103                format: wgpu::VertexFormat::Float32,
104                offset: 40,
105                shader_location: 4,
106            },
107            wgpu::VertexAttribute {
108                format: wgpu::VertexFormat::Float32,
109                offset: 44,
110                shader_location: 5,
111            },
112            wgpu::VertexAttribute {
113                format: wgpu::VertexFormat::Float32x2,
114                offset: 48,
115                shader_location: 6,
116            },
117        ];
118
119        let vertex_layout = wgpu::VertexBufferLayout {
120            array_stride: core::mem::size_of::<Vertex>() as wgpu::BufferAddress,
121            step_mode: wgpu::VertexStepMode::Vertex,
122            attributes: &attrs,
123        };
124
125        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
126            label: Some("oxiui-render-wgpu solid pipeline"),
127            layout: Some(&pipeline_layout),
128            vertex: wgpu::VertexState {
129                module: &shader,
130                entry_point: Some("vs_main"),
131                buffers: &[vertex_layout],
132                compilation_options: wgpu::PipelineCompilationOptions::default(),
133            },
134            fragment: Some(wgpu::FragmentState {
135                module: &shader,
136                entry_point: Some("fs_main"),
137                targets: &[Some(wgpu::ColorTargetState {
138                    format: TARGET_FORMAT,
139                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
140                    write_mask: wgpu::ColorWrites::ALL,
141                })],
142                compilation_options: wgpu::PipelineCompilationOptions::default(),
143            }),
144            primitive: wgpu::PrimitiveState {
145                topology: wgpu::PrimitiveTopology::TriangleList,
146                strip_index_format: None,
147                front_face: wgpu::FrontFace::Ccw,
148                cull_mode: None,
149                unclipped_depth: false,
150                polygon_mode: wgpu::PolygonMode::Fill,
151                conservative: false,
152            },
153            depth_stencil: None,
154            multisample: wgpu::MultisampleState {
155                count: sample_count,
156                mask: !0,
157                alpha_to_coverage_enabled: false,
158            },
159            multiview_mask: None,
160            cache: None,
161        });
162
163        Self {
164            pipeline,
165            globals_layout,
166        }
167    }
168}
169
170// ── GradientPipeline ─────────────────────────────────────────────────────────
171
172/// The compiled gradient pipeline: gradient quads rendered via a per-draw
173/// uniform buffer carrying gradient type, stops, and geometry.
174pub struct GradientPipeline {
175    /// The render pipeline (vertex + fragment stages, alpha blending).
176    pub pipeline: wgpu::RenderPipeline,
177    /// Layout of bind group 0: viewport uniform (binding 0) and gradient
178    /// uniform buffer (binding 1).
179    pub bind_group_layout: wgpu::BindGroupLayout,
180}
181
182impl GradientPipeline {
183    /// Build the gradient pipeline for a colour target in [`TARGET_FORMAT`].
184    ///
185    /// `sample_count` sets the MSAA multisample count (1 = no MSAA, 4 or 8 =
186    /// MSAA).  Passing `1` reproduces the exact original code path.
187    pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
188        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
189            label: Some("oxiui-render-wgpu gradient.wgsl"),
190            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/gradient.wgsl").into()),
191        });
192
193        // Bind group 0:
194        //   binding 0 — Globals (viewport, vertex stage only, static offset)
195        //   binding 1 — GradientUniforms (fragment stage only, dynamic offset for batching)
196        //
197        // Binding 1 uses has_dynamic_offset: true so that a single combined
198        // uniform buffer can be shared across all gradient draws in one render
199        // pass — each draw issues set_bind_group with a different byte offset
200        // into the same buffer (stride = align_up(288, min_uniform_buffer_offset_alignment)).
201        let grad_uniform_size = core::mem::size_of::<GradientUniforms>() as u64;
202        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
203            label: Some("oxiui-render-wgpu gradient bind group layout"),
204            entries: &[
205                wgpu::BindGroupLayoutEntry {
206                    binding: 0,
207                    visibility: wgpu::ShaderStages::VERTEX,
208                    ty: wgpu::BindingType::Buffer {
209                        ty: wgpu::BufferBindingType::Uniform,
210                        has_dynamic_offset: false,
211                        min_binding_size: None,
212                    },
213                    count: None,
214                },
215                wgpu::BindGroupLayoutEntry {
216                    binding: 1,
217                    visibility: wgpu::ShaderStages::FRAGMENT,
218                    ty: wgpu::BindingType::Buffer {
219                        ty: wgpu::BufferBindingType::Uniform,
220                        has_dynamic_offset: true,
221                        min_binding_size: core::num::NonZeroU64::new(grad_uniform_size),
222                    },
223                    count: None,
224                },
225            ],
226        });
227
228        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
229            label: Some("oxiui-render-wgpu gradient pipeline layout"),
230            bind_group_layouts: &[Some(&bind_group_layout)],
231            immediate_size: 0,
232        });
233
234        // Gradient vertex layout (16 bytes):
235        //   position  vec2  @0   offset 0
236        //   local     vec2  @1   offset 8
237        let attrs = [
238            wgpu::VertexAttribute {
239                format: wgpu::VertexFormat::Float32x2,
240                offset: 0,
241                shader_location: 0,
242            },
243            wgpu::VertexAttribute {
244                format: wgpu::VertexFormat::Float32x2,
245                offset: 8,
246                shader_location: 1,
247            },
248        ];
249
250        let vertex_layout = wgpu::VertexBufferLayout {
251            array_stride: core::mem::size_of::<GradientVertex>() as wgpu::BufferAddress,
252            step_mode: wgpu::VertexStepMode::Vertex,
253            attributes: &attrs,
254        };
255
256        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
257            label: Some("oxiui-render-wgpu gradient pipeline"),
258            layout: Some(&pipeline_layout),
259            vertex: wgpu::VertexState {
260                module: &shader,
261                entry_point: Some("vs_main"),
262                buffers: &[vertex_layout],
263                compilation_options: wgpu::PipelineCompilationOptions::default(),
264            },
265            fragment: Some(wgpu::FragmentState {
266                module: &shader,
267                entry_point: Some("fs_main"),
268                targets: &[Some(wgpu::ColorTargetState {
269                    format: TARGET_FORMAT,
270                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
271                    write_mask: wgpu::ColorWrites::ALL,
272                })],
273                compilation_options: wgpu::PipelineCompilationOptions::default(),
274            }),
275            primitive: wgpu::PrimitiveState {
276                topology: wgpu::PrimitiveTopology::TriangleList,
277                strip_index_format: None,
278                front_face: wgpu::FrontFace::Ccw,
279                cull_mode: None,
280                unclipped_depth: false,
281                polygon_mode: wgpu::PolygonMode::Fill,
282                conservative: false,
283            },
284            depth_stencil: None,
285            multisample: wgpu::MultisampleState {
286                count: sample_count,
287                mask: !0,
288                alpha_to_coverage_enabled: false,
289            },
290            multiview_mask: None,
291            cache: None,
292        });
293
294        Self {
295            pipeline,
296            bind_group_layout,
297        }
298    }
299}
300
301// ── TexturedPipeline ──────────────────────────────────────────────────────────
302
303/// The compiled textured pipeline for rendering image quads and nine-slice
304/// patches with optional tint.
305///
306/// Two bind group layouts:
307/// - `globals_layout` (group 0): the viewport `Globals` uniform (vertex stage).
308/// - `texture_layout` (group 1): a `texture_2d<f32>` (binding 0, fragment) and
309///   a `sampler` (binding 1, fragment).
310pub struct TexturedPipeline {
311    /// The render pipeline (vertex + fragment stages, alpha blending).
312    pub pipeline: wgpu::RenderPipeline,
313    /// Layout of bind group 0 (the viewport `Globals` uniform).
314    pub globals_layout: wgpu::BindGroupLayout,
315    /// Layout of bind group 1 (texture + sampler).
316    pub texture_layout: wgpu::BindGroupLayout,
317}
318
319impl TexturedPipeline {
320    /// Build the textured pipeline for a colour target in [`TARGET_FORMAT`].
321    ///
322    /// `sample_count` sets the MSAA multisample count (1 = no MSAA, 4 or 8 =
323    /// MSAA).  Passing `1` reproduces the exact original code path.
324    pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
325        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
326            label: Some("oxiui-render-wgpu textured.wgsl"),
327            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/textured.wgsl").into()),
328        });
329
330        // Bind group 0: Globals uniform (vertex stage only)
331        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
332            label: Some("oxiui-render-wgpu textured globals layout"),
333            entries: &[wgpu::BindGroupLayoutEntry {
334                binding: 0,
335                visibility: wgpu::ShaderStages::VERTEX,
336                ty: wgpu::BindingType::Buffer {
337                    ty: wgpu::BufferBindingType::Uniform,
338                    has_dynamic_offset: false,
339                    min_binding_size: None,
340                },
341                count: None,
342            }],
343        });
344
345        // Bind group 1: texture (binding 0) + sampler (binding 1), fragment stage
346        let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
347            label: Some("oxiui-render-wgpu textured texture layout"),
348            entries: &[
349                wgpu::BindGroupLayoutEntry {
350                    binding: 0,
351                    visibility: wgpu::ShaderStages::FRAGMENT,
352                    ty: wgpu::BindingType::Texture {
353                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
354                        view_dimension: wgpu::TextureViewDimension::D2,
355                        multisampled: false,
356                    },
357                    count: None,
358                },
359                wgpu::BindGroupLayoutEntry {
360                    binding: 1,
361                    visibility: wgpu::ShaderStages::FRAGMENT,
362                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
363                    count: None,
364                },
365            ],
366        });
367
368        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
369            label: Some("oxiui-render-wgpu textured pipeline layout"),
370            bind_group_layouts: &[Some(&globals_layout), Some(&texture_layout)],
371            immediate_size: 0,
372        });
373
374        // Vertex attributes mirror `TexVertex` byte offsets exactly (32 bytes):
375        //   position  vec2  @0   offset  0
376        //   uv        vec2  @1   offset  8
377        //   tint      vec4  @2   offset 16
378        let attrs = [
379            wgpu::VertexAttribute {
380                format: wgpu::VertexFormat::Float32x2,
381                offset: 0,
382                shader_location: 0,
383            },
384            wgpu::VertexAttribute {
385                format: wgpu::VertexFormat::Float32x2,
386                offset: 8,
387                shader_location: 1,
388            },
389            wgpu::VertexAttribute {
390                format: wgpu::VertexFormat::Float32x4,
391                offset: 16,
392                shader_location: 2,
393            },
394        ];
395
396        let vertex_layout = wgpu::VertexBufferLayout {
397            array_stride: core::mem::size_of::<TexVertex>() as wgpu::BufferAddress,
398            step_mode: wgpu::VertexStepMode::Vertex,
399            attributes: &attrs,
400        };
401
402        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
403            label: Some("oxiui-render-wgpu textured pipeline"),
404            layout: Some(&pipeline_layout),
405            vertex: wgpu::VertexState {
406                module: &shader,
407                entry_point: Some("vs_main"),
408                buffers: &[vertex_layout],
409                compilation_options: wgpu::PipelineCompilationOptions::default(),
410            },
411            fragment: Some(wgpu::FragmentState {
412                module: &shader,
413                entry_point: Some("fs_main"),
414                targets: &[Some(wgpu::ColorTargetState {
415                    format: TARGET_FORMAT,
416                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
417                    write_mask: wgpu::ColorWrites::ALL,
418                })],
419                compilation_options: wgpu::PipelineCompilationOptions::default(),
420            }),
421            primitive: wgpu::PrimitiveState {
422                topology: wgpu::PrimitiveTopology::TriangleList,
423                strip_index_format: None,
424                front_face: wgpu::FrontFace::Ccw,
425                cull_mode: None,
426                unclipped_depth: false,
427                polygon_mode: wgpu::PolygonMode::Fill,
428                conservative: false,
429            },
430            depth_stencil: None,
431            multisample: wgpu::MultisampleState {
432                count: sample_count,
433                mask: !0,
434                alpha_to_coverage_enabled: false,
435            },
436            multiview_mask: None,
437            cache: None,
438        });
439
440        Self {
441            pipeline,
442            globals_layout,
443            texture_layout,
444        }
445    }
446}
447
448// ── BlurPipeline ──────────────────────────────────────────────────────────────
449
450/// The compiled separable Gaussian blur pipeline.
451///
452/// Bind group layout:
453/// - `globals_layout` (group 0): viewport `Globals` uniform (vertex stage).
454/// - `source_layout` (group 1): source texture (binding 0), sampler (binding 1),
455///   [`crate::gpu::buffer::BlurUniforms`] (binding 2) — all fragment stage.
456pub struct BlurPipeline {
457    /// The render pipeline (vertex + fragment stages, alpha blending).
458    pub pipeline: wgpu::RenderPipeline,
459    /// Layout of bind group 0 (the viewport `Globals` uniform).
460    pub globals_layout: wgpu::BindGroupLayout,
461    /// Layout of bind group 1 (source texture + sampler + `BlurUniforms`).
462    pub source_layout: wgpu::BindGroupLayout,
463}
464
465impl BlurPipeline {
466    /// Build the blur pipeline for a colour target in [`TARGET_FORMAT`].
467    ///
468    /// `sample_count` sets the MSAA multisample count.  Shadow ping-pong
469    /// textures are always count=1, so this should always be called with `1`.
470    /// The parameter is present for consistency with the other pipelines.
471    pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
472        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
473            label: Some("oxiui-render-wgpu blur.wgsl"),
474            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blur.wgsl").into()),
475        });
476
477        // Bind group 0: Globals uniform (vertex stage only).
478        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
479            label: Some("oxiui-render-wgpu blur globals layout"),
480            entries: &[wgpu::BindGroupLayoutEntry {
481                binding: 0,
482                visibility: wgpu::ShaderStages::VERTEX,
483                ty: wgpu::BindingType::Buffer {
484                    ty: wgpu::BufferBindingType::Uniform,
485                    has_dynamic_offset: false,
486                    min_binding_size: None,
487                },
488                count: None,
489            }],
490        });
491
492        // Bind group 1: source texture (0) + sampler (1) + BlurUniforms (2), fragment stage.
493        let source_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
494            label: Some("oxiui-render-wgpu blur source layout"),
495            entries: &[
496                wgpu::BindGroupLayoutEntry {
497                    binding: 0,
498                    visibility: wgpu::ShaderStages::FRAGMENT,
499                    ty: wgpu::BindingType::Texture {
500                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
501                        view_dimension: wgpu::TextureViewDimension::D2,
502                        multisampled: false,
503                    },
504                    count: None,
505                },
506                wgpu::BindGroupLayoutEntry {
507                    binding: 1,
508                    visibility: wgpu::ShaderStages::FRAGMENT,
509                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
510                    count: None,
511                },
512                wgpu::BindGroupLayoutEntry {
513                    binding: 2,
514                    visibility: wgpu::ShaderStages::FRAGMENT,
515                    ty: wgpu::BindingType::Buffer {
516                        ty: wgpu::BufferBindingType::Uniform,
517                        has_dynamic_offset: false,
518                        min_binding_size: None,
519                    },
520                    count: None,
521                },
522            ],
523        });
524
525        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
526            label: Some("oxiui-render-wgpu blur pipeline layout"),
527            bind_group_layouts: &[Some(&globals_layout), Some(&source_layout)],
528            immediate_size: 0,
529        });
530
531        // Reuse GradientVertex layout (16 bytes): position @0, local @1.
532        let attrs = [
533            wgpu::VertexAttribute {
534                format: wgpu::VertexFormat::Float32x2,
535                offset: 0,
536                shader_location: 0,
537            },
538            wgpu::VertexAttribute {
539                format: wgpu::VertexFormat::Float32x2,
540                offset: 8,
541                shader_location: 1,
542            },
543        ];
544
545        let vertex_layout = wgpu::VertexBufferLayout {
546            array_stride: core::mem::size_of::<GradientVertex>() as wgpu::BufferAddress,
547            step_mode: wgpu::VertexStepMode::Vertex,
548            attributes: &attrs,
549        };
550
551        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
552            label: Some("oxiui-render-wgpu blur pipeline"),
553            layout: Some(&pipeline_layout),
554            vertex: wgpu::VertexState {
555                module: &shader,
556                entry_point: Some("vs_main"),
557                buffers: &[vertex_layout],
558                compilation_options: wgpu::PipelineCompilationOptions::default(),
559            },
560            fragment: Some(wgpu::FragmentState {
561                module: &shader,
562                entry_point: Some("fs_main"),
563                targets: &[Some(wgpu::ColorTargetState {
564                    format: TARGET_FORMAT,
565                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
566                    write_mask: wgpu::ColorWrites::ALL,
567                })],
568                compilation_options: wgpu::PipelineCompilationOptions::default(),
569            }),
570            primitive: wgpu::PrimitiveState {
571                topology: wgpu::PrimitiveTopology::TriangleList,
572                strip_index_format: None,
573                front_face: wgpu::FrontFace::Ccw,
574                cull_mode: None,
575                unclipped_depth: false,
576                polygon_mode: wgpu::PolygonMode::Fill,
577                conservative: false,
578            },
579            depth_stencil: None,
580            multisample: wgpu::MultisampleState {
581                count: sample_count,
582                mask: !0,
583                alpha_to_coverage_enabled: false,
584            },
585            multiview_mask: None,
586            cache: None,
587        });
588
589        Self {
590            pipeline,
591            globals_layout,
592            source_layout,
593        }
594    }
595}
596
597// ── CompositePipeline ─────────────────────────────────────────────────────────
598
599/// The compiled shadow composite pipeline.
600///
601/// Bind group layout mirrors [`BlurPipeline`]: group 0 = `Globals`, group 1 =
602/// mask texture + sampler + [`crate::gpu::buffer::CompUniforms`].
603pub struct CompositePipeline {
604    /// The render pipeline (vertex + fragment stages, alpha blending).
605    pub pipeline: wgpu::RenderPipeline,
606    /// Layout of bind group 0 (the viewport `Globals` uniform).
607    pub globals_layout: wgpu::BindGroupLayout,
608    /// Layout of bind group 1 (mask texture + sampler + `CompUniforms`).
609    pub source_layout: wgpu::BindGroupLayout,
610}
611
612impl CompositePipeline {
613    /// Build the composite pipeline for a colour target in [`TARGET_FORMAT`].
614    ///
615    /// `sample_count` sets the MSAA multisample count for the output target.
616    /// The composite pass writes to the main screen target, which may have MSAA
617    /// active, so this should match `GpuContext::sample_count`.
618    pub fn new(device: &wgpu::Device, sample_count: u32) -> Self {
619        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
620            label: Some("oxiui-render-wgpu composite.wgsl"),
621            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/composite.wgsl").into()),
622        });
623
624        // Bind group 0: Globals uniform (vertex stage only).
625        let globals_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
626            label: Some("oxiui-render-wgpu composite globals layout"),
627            entries: &[wgpu::BindGroupLayoutEntry {
628                binding: 0,
629                visibility: wgpu::ShaderStages::VERTEX,
630                ty: wgpu::BindingType::Buffer {
631                    ty: wgpu::BufferBindingType::Uniform,
632                    has_dynamic_offset: false,
633                    min_binding_size: None,
634                },
635                count: None,
636            }],
637        });
638
639        // Bind group 1: mask texture (0) + sampler (1) + CompUniforms (2), fragment stage.
640        let source_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
641            label: Some("oxiui-render-wgpu composite source layout"),
642            entries: &[
643                wgpu::BindGroupLayoutEntry {
644                    binding: 0,
645                    visibility: wgpu::ShaderStages::FRAGMENT,
646                    ty: wgpu::BindingType::Texture {
647                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
648                        view_dimension: wgpu::TextureViewDimension::D2,
649                        multisampled: false,
650                    },
651                    count: None,
652                },
653                wgpu::BindGroupLayoutEntry {
654                    binding: 1,
655                    visibility: wgpu::ShaderStages::FRAGMENT,
656                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
657                    count: None,
658                },
659                wgpu::BindGroupLayoutEntry {
660                    binding: 2,
661                    visibility: wgpu::ShaderStages::FRAGMENT,
662                    ty: wgpu::BindingType::Buffer {
663                        ty: wgpu::BufferBindingType::Uniform,
664                        has_dynamic_offset: false,
665                        min_binding_size: None,
666                    },
667                    count: None,
668                },
669            ],
670        });
671
672        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
673            label: Some("oxiui-render-wgpu composite pipeline layout"),
674            bind_group_layouts: &[Some(&globals_layout), Some(&source_layout)],
675            immediate_size: 0,
676        });
677
678        // Reuse GradientVertex layout (16 bytes): position @0, local @1.
679        let attrs = [
680            wgpu::VertexAttribute {
681                format: wgpu::VertexFormat::Float32x2,
682                offset: 0,
683                shader_location: 0,
684            },
685            wgpu::VertexAttribute {
686                format: wgpu::VertexFormat::Float32x2,
687                offset: 8,
688                shader_location: 1,
689            },
690        ];
691
692        let vertex_layout = wgpu::VertexBufferLayout {
693            array_stride: core::mem::size_of::<GradientVertex>() as wgpu::BufferAddress,
694            step_mode: wgpu::VertexStepMode::Vertex,
695            attributes: &attrs,
696        };
697
698        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
699            label: Some("oxiui-render-wgpu composite pipeline"),
700            layout: Some(&pipeline_layout),
701            vertex: wgpu::VertexState {
702                module: &shader,
703                entry_point: Some("vs_main"),
704                buffers: &[vertex_layout],
705                compilation_options: wgpu::PipelineCompilationOptions::default(),
706            },
707            fragment: Some(wgpu::FragmentState {
708                module: &shader,
709                entry_point: Some("fs_main"),
710                targets: &[Some(wgpu::ColorTargetState {
711                    format: TARGET_FORMAT,
712                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
713                    write_mask: wgpu::ColorWrites::ALL,
714                })],
715                compilation_options: wgpu::PipelineCompilationOptions::default(),
716            }),
717            primitive: wgpu::PrimitiveState {
718                topology: wgpu::PrimitiveTopology::TriangleList,
719                strip_index_format: None,
720                front_face: wgpu::FrontFace::Ccw,
721                cull_mode: None,
722                unclipped_depth: false,
723                polygon_mode: wgpu::PolygonMode::Fill,
724                conservative: false,
725            },
726            depth_stencil: None,
727            multisample: wgpu::MultisampleState {
728                count: sample_count,
729                mask: !0,
730                alpha_to_coverage_enabled: false,
731            },
732            multiview_mask: None,
733            cache: None,
734        });
735
736        Self {
737            pipeline,
738            globals_layout,
739            source_layout,
740        }
741    }
742}