Skip to main content

par_term_render/cell_renderer/
pipeline.rs

1//! GPU pipeline creation for cell renderer.
2//!
3//! This module contains functions for creating wgpu render pipelines
4//! for backgrounds, text, background images, and visual bell.
5
6use wgpu::*;
7
8use super::types::{BackgroundInstance, TextInstance, Vertex};
9
10/// Custom blend state that blends RGB normally but replaces alpha.
11/// This prevents alpha accumulation across multiple layers, ensuring
12/// the final alpha equals window_opacity for proper window transparency.
13const ALPHA_BLEND_RGB_REPLACE_ALPHA: BlendState = BlendState {
14    color: BlendComponent {
15        src_factor: BlendFactor::SrcAlpha,
16        dst_factor: BlendFactor::OneMinusSrcAlpha,
17        operation: BlendOperation::Add,
18    },
19    alpha: BlendComponent {
20        // Replace alpha instead of accumulating: src * 1 + dst * 0 = src
21        src_factor: BlendFactor::One,
22        dst_factor: BlendFactor::Zero,
23        operation: BlendOperation::Add,
24    },
25};
26
27/// Create the background pipeline for cell backgrounds
28pub fn create_bg_pipeline(device: &Device, surface_format: TextureFormat) -> RenderPipeline {
29    let bg_shader = device.create_shader_module(include_wgsl!("../shaders/cell_bg.wgsl"));
30
31    let bg_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
32        label: Some("bg pipeline layout"),
33        bind_group_layouts: &[],
34        push_constant_ranges: &[],
35    });
36
37    device.create_render_pipeline(&RenderPipelineDescriptor {
38        label: Some("bg pipeline"),
39        layout: Some(&bg_pipeline_layout),
40        vertex: VertexState {
41            module: &bg_shader,
42            entry_point: Some("vs_main"),
43            compilation_options: Default::default(),
44            buffers: &[
45                VertexBufferLayout {
46                    array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
47                    step_mode: VertexStepMode::Vertex,
48                    attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2],
49                },
50                VertexBufferLayout {
51                    array_stride: std::mem::size_of::<BackgroundInstance>() as BufferAddress,
52                    step_mode: VertexStepMode::Instance,
53                    attributes: &vertex_attr_array![2 => Float32x2, 3 => Float32x2, 4 => Float32x4],
54                },
55            ],
56        },
57        fragment: Some(FragmentState {
58            module: &bg_shader,
59            entry_point: Some("fs_main"),
60            compilation_options: Default::default(),
61            targets: &[Some(ColorTargetState {
62                format: surface_format,
63                // Use custom blend that replaces alpha to prevent accumulation
64                // This ensures window_opacity is preserved across layers
65                blend: Some(ALPHA_BLEND_RGB_REPLACE_ALPHA),
66                write_mask: ColorWrites::ALL,
67            })],
68        }),
69        primitive: PrimitiveState {
70            topology: PrimitiveTopology::TriangleStrip,
71            ..Default::default()
72        },
73        depth_stencil: None,
74        multisample: MultisampleState::default(),
75        multiview: None,
76        cache: None,
77    })
78}
79
80/// Create the text bind group layout
81pub fn create_text_bind_group_layout(device: &Device) -> BindGroupLayout {
82    device.create_bind_group_layout(&BindGroupLayoutDescriptor {
83        label: Some("text bind group layout"),
84        entries: &[
85            BindGroupLayoutEntry {
86                binding: 0,
87                visibility: ShaderStages::FRAGMENT,
88                ty: BindingType::Texture {
89                    sample_type: TextureSampleType::Float { filterable: true },
90                    view_dimension: TextureViewDimension::D2,
91                    multisampled: false,
92                },
93                count: None,
94            },
95            BindGroupLayoutEntry {
96                binding: 1,
97                visibility: ShaderStages::FRAGMENT,
98                ty: BindingType::Sampler(SamplerBindingType::Filtering),
99                count: None,
100            },
101        ],
102    })
103}
104
105/// Create the text bind group
106pub fn create_text_bind_group(
107    device: &Device,
108    layout: &BindGroupLayout,
109    atlas_view: &TextureView,
110    atlas_sampler: &Sampler,
111) -> BindGroup {
112    device.create_bind_group(&BindGroupDescriptor {
113        label: Some("text bind group"),
114        layout,
115        entries: &[
116            BindGroupEntry {
117                binding: 0,
118                resource: BindingResource::TextureView(atlas_view),
119            },
120            BindGroupEntry {
121                binding: 1,
122                resource: BindingResource::Sampler(atlas_sampler),
123            },
124        ],
125    })
126}
127
128/// Create the text pipeline for glyph rendering
129pub fn create_text_pipeline(
130    device: &Device,
131    surface_format: TextureFormat,
132    text_bind_group_layout: &BindGroupLayout,
133) -> RenderPipeline {
134    let text_shader = device.create_shader_module(include_wgsl!("../shaders/cell_text.wgsl"));
135
136    let text_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
137        label: Some("text pipeline layout"),
138        bind_group_layouts: &[text_bind_group_layout],
139        push_constant_ranges: &[],
140    });
141
142    device.create_render_pipeline(&RenderPipelineDescriptor {
143        label: Some("text pipeline"),
144        layout: Some(&text_pipeline_layout),
145        vertex: VertexState {
146            module: &text_shader,
147            entry_point: Some("vs_main"),
148            compilation_options: Default::default(),
149            buffers: &[
150                VertexBufferLayout {
151                    array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
152                    step_mode: VertexStepMode::Vertex,
153                    attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2],
154                },
155                VertexBufferLayout {
156                    array_stride: std::mem::size_of::<TextInstance>() as BufferAddress,
157                    step_mode: VertexStepMode::Instance,
158                    attributes: &vertex_attr_array![
159                        2 => Float32x2,
160                        3 => Float32x2,
161                        4 => Float32x2,
162                        5 => Float32x2,
163                        6 => Float32x4,
164                        7 => Uint32
165                    ],
166                },
167            ],
168        },
169        fragment: Some(FragmentState {
170            module: &text_shader,
171            entry_point: Some("fs_main"),
172            compilation_options: Default::default(),
173            targets: &[Some(ColorTargetState {
174                format: surface_format,
175                // Use standard alpha blending for text - text renders last on specific
176                // glyph pixels only, so accumulation isn't an issue here
177                blend: Some(BlendState::ALPHA_BLENDING),
178                write_mask: ColorWrites::ALL,
179            })],
180        }),
181        primitive: PrimitiveState {
182            topology: PrimitiveTopology::TriangleStrip,
183            ..Default::default()
184        },
185        depth_stencil: None,
186        multisample: MultisampleState::default(),
187        multiview: None,
188        cache: None,
189    })
190}
191
192/// Create the background image bind group layout
193pub fn create_bg_image_bind_group_layout(device: &Device) -> BindGroupLayout {
194    device.create_bind_group_layout(&BindGroupLayoutDescriptor {
195        label: Some("bg image bind group layout"),
196        entries: &[
197            BindGroupLayoutEntry {
198                binding: 0,
199                visibility: ShaderStages::FRAGMENT,
200                ty: BindingType::Texture {
201                    sample_type: TextureSampleType::Float { filterable: true },
202                    view_dimension: TextureViewDimension::D2,
203                    multisampled: false,
204                },
205                count: None,
206            },
207            BindGroupLayoutEntry {
208                binding: 1,
209                visibility: ShaderStages::FRAGMENT,
210                ty: BindingType::Sampler(SamplerBindingType::Filtering),
211                count: None,
212            },
213            BindGroupLayoutEntry {
214                binding: 2,
215                visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT,
216                ty: BindingType::Buffer {
217                    ty: BufferBindingType::Uniform,
218                    has_dynamic_offset: false,
219                    min_binding_size: None,
220                },
221                count: None,
222            },
223        ],
224    })
225}
226
227/// Create the background image pipeline
228pub fn create_bg_image_pipeline(
229    device: &Device,
230    surface_format: TextureFormat,
231    bg_image_bind_group_layout: &BindGroupLayout,
232) -> RenderPipeline {
233    let bg_image_shader =
234        device.create_shader_module(include_wgsl!("../shaders/background_image.wgsl"));
235
236    let bg_image_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
237        label: Some("bg image pipeline layout"),
238        bind_group_layouts: &[bg_image_bind_group_layout],
239        push_constant_ranges: &[],
240    });
241
242    device.create_render_pipeline(&RenderPipelineDescriptor {
243        label: Some("bg image pipeline"),
244        layout: Some(&bg_image_pipeline_layout),
245        vertex: VertexState {
246            module: &bg_image_shader,
247            entry_point: Some("vs_main"),
248            compilation_options: Default::default(),
249            buffers: &[VertexBufferLayout {
250                array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
251                step_mode: VertexStepMode::Vertex,
252                attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2],
253            }],
254        },
255        fragment: Some(FragmentState {
256            module: &bg_image_shader,
257            entry_point: Some("fs_main"),
258            compilation_options: Default::default(),
259            targets: &[Some(ColorTargetState {
260                format: surface_format,
261                // Use premultiplied alpha blending since shader outputs premultiplied colors
262                blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
263                write_mask: ColorWrites::ALL,
264            })],
265        }),
266        primitive: PrimitiveState {
267            topology: PrimitiveTopology::TriangleStrip,
268            ..Default::default()
269        },
270        depth_stencil: None,
271        multisample: MultisampleState::default(),
272        multiview: None,
273        cache: None,
274    })
275}
276
277/// Create the visual bell pipeline (reuses background shader)
278pub fn create_visual_bell_pipeline(
279    device: &Device,
280    surface_format: TextureFormat,
281) -> (RenderPipeline, BindGroup, BindGroupLayout, Buffer) {
282    let visual_bell_shader = device.create_shader_module(include_wgsl!("../shaders/cell_bg.wgsl"));
283
284    let visual_bell_bind_group_layout =
285        device.create_bind_group_layout(&BindGroupLayoutDescriptor {
286            label: Some("visual bell bind group layout"),
287            entries: &[BindGroupLayoutEntry {
288                binding: 0,
289                visibility: ShaderStages::FRAGMENT,
290                ty: BindingType::Buffer {
291                    ty: BufferBindingType::Uniform,
292                    has_dynamic_offset: false,
293                    min_binding_size: None,
294                },
295                count: None,
296            }],
297        });
298
299    let visual_bell_pipeline_layout = device.create_pipeline_layout(&PipelineLayoutDescriptor {
300        label: Some("visual bell pipeline layout"),
301        bind_group_layouts: &[&visual_bell_bind_group_layout],
302        push_constant_ranges: &[],
303    });
304
305    let visual_bell_pipeline = device.create_render_pipeline(&RenderPipelineDescriptor {
306        label: Some("visual bell pipeline"),
307        layout: Some(&visual_bell_pipeline_layout),
308        vertex: VertexState {
309            module: &visual_bell_shader,
310            entry_point: Some("vs_main"),
311            compilation_options: Default::default(),
312            buffers: &[
313                VertexBufferLayout {
314                    array_stride: std::mem::size_of::<Vertex>() as BufferAddress,
315                    step_mode: VertexStepMode::Vertex,
316                    attributes: &vertex_attr_array![0 => Float32x2, 1 => Float32x2],
317                },
318                VertexBufferLayout {
319                    array_stride: std::mem::size_of::<BackgroundInstance>() as BufferAddress,
320                    step_mode: VertexStepMode::Instance,
321                    attributes: &vertex_attr_array![2 => Float32x2, 3 => Float32x2, 4 => Float32x4],
322                },
323            ],
324        },
325        fragment: Some(FragmentState {
326            module: &visual_bell_shader,
327            entry_point: Some("fs_main"),
328            compilation_options: Default::default(),
329            targets: &[Some(ColorTargetState {
330                format: surface_format,
331                // Use premultiplied alpha blending for visual bell overlay
332                blend: Some(BlendState::PREMULTIPLIED_ALPHA_BLENDING),
333                write_mask: ColorWrites::ALL,
334            })],
335        }),
336        primitive: PrimitiveState {
337            topology: PrimitiveTopology::TriangleStrip,
338            ..Default::default()
339        },
340        depth_stencil: None,
341        multisample: MultisampleState::default(),
342        multiview: None,
343        cache: None,
344    });
345
346    let visual_bell_uniform_buffer = device.create_buffer(&BufferDescriptor {
347        label: Some("visual bell uniform buffer"),
348        size: 64,
349        usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
350        mapped_at_creation: false,
351    });
352
353    let visual_bell_bind_group = device.create_bind_group(&BindGroupDescriptor {
354        label: Some("visual bell bind group"),
355        layout: &visual_bell_bind_group_layout,
356        entries: &[BindGroupEntry {
357            binding: 0,
358            resource: visual_bell_uniform_buffer.as_entire_binding(),
359        }],
360    });
361
362    (
363        visual_bell_pipeline,
364        visual_bell_bind_group,
365        visual_bell_bind_group_layout,
366        visual_bell_uniform_buffer,
367    )
368}
369
370/// Create the glyph atlas texture and sampler
371pub fn create_atlas(device: &Device) -> (Texture, TextureView, Sampler) {
372    let atlas_size = 2048;
373    let atlas_texture = device.create_texture(&TextureDescriptor {
374        label: Some("atlas texture"),
375        size: Extent3d {
376            width: atlas_size,
377            height: atlas_size,
378            depth_or_array_layers: 1,
379        },
380        mip_level_count: 1,
381        sample_count: 1,
382        dimension: TextureDimension::D2,
383        format: TextureFormat::Rgba8Unorm,
384        usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
385        view_formats: &[],
386    });
387    let atlas_view = atlas_texture.create_view(&TextureViewDescriptor::default());
388    let atlas_sampler = device.create_sampler(&SamplerDescriptor {
389        address_mode_u: AddressMode::ClampToEdge,
390        address_mode_v: AddressMode::ClampToEdge,
391        mag_filter: FilterMode::Linear,
392        min_filter: FilterMode::Linear,
393        ..Default::default()
394    });
395
396    (atlas_texture, atlas_view, atlas_sampler)
397}
398
399/// Create the vertex buffer with unit quad vertices
400pub fn create_vertex_buffer(device: &Device) -> Buffer {
401    use wgpu::util::DeviceExt;
402
403    let vertices = [
404        Vertex {
405            position: [0.0, 0.0],
406            tex_coords: [0.0, 0.0],
407        },
408        Vertex {
409            position: [1.0, 0.0],
410            tex_coords: [1.0, 0.0],
411        },
412        Vertex {
413            position: [0.0, 1.0],
414            tex_coords: [0.0, 1.0],
415        },
416        Vertex {
417            position: [1.0, 1.0],
418            tex_coords: [1.0, 1.0],
419        },
420    ];
421
422    device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
423        label: Some("vertex buffer"),
424        contents: bytemuck::cast_slice(&vertices),
425        usage: BufferUsages::VERTEX,
426    })
427}
428
429/// Create instance buffers for backgrounds and text
430pub fn create_instance_buffers(
431    device: &Device,
432    max_bg_instances: usize,
433    max_text_instances: usize,
434) -> (Buffer, Buffer) {
435    let bg_instance_buffer = device.create_buffer(&BufferDescriptor {
436        label: Some("bg instance buffer"),
437        size: (max_bg_instances * std::mem::size_of::<BackgroundInstance>()) as u64,
438        usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
439        mapped_at_creation: false,
440    });
441    let text_instance_buffer = device.create_buffer(&BufferDescriptor {
442        label: Some("text instance buffer"),
443        size: (max_text_instances * std::mem::size_of::<TextInstance>()) as u64,
444        usage: BufferUsages::VERTEX | BufferUsages::COPY_DST,
445        mapped_at_creation: false,
446    });
447
448    (bg_instance_buffer, text_instance_buffer)
449}
450
451/// Create the background image uniform buffer
452pub fn create_bg_image_uniform_buffer(device: &Device) -> Buffer {
453    device.create_buffer(&BufferDescriptor {
454        label: Some("bg image uniform buffer"),
455        size: 64,
456        usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
457        mapped_at_creation: false,
458    })
459}