Skip to main content

par_term_render/cell_renderer/
render.rs

1use super::CellRenderer;
2use anyhow::Result;
3
4impl CellRenderer {
5    /// Emit the standard 3-phase draw calls into an existing render pass.
6    ///
7    /// This is the single source of truth for the cell rendering draw call sequence.
8    /// Background images / pane backgrounds must be drawn by the caller before this.
9    ///
10    /// **Phase 1**: Cell backgrounds (`0..cursor_overlay_start`)
11    /// **Phase 1b**: Separators / gutter (`cursor_overlay_end..actual_bg_instances`) — skipped
12    ///   when the range is empty (pane path packs these before cursor overlays)
13    /// **Phase 2**: Text glyphs (`0..actual_text_instances`)
14    /// **Phase 3**: Cursor overlays (`cursor_overlay_start..cursor_overlay_end`)
15    pub(crate) fn emit_three_phase_draw_calls(
16        &self,
17        render_pass: &mut wgpu::RenderPass<'_>,
18        cursor_overlay_start: u32,
19        cursor_overlay_end: u32,
20    ) {
21        // Phase 1: cell backgrounds (before text)
22        render_pass.set_pipeline(&self.pipelines.bg_pipeline);
23        render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
24        render_pass.set_vertex_buffer(1, self.buffers.bg_instance_buffer.slice(..));
25        render_pass.draw(0..4, 0..cursor_overlay_start);
26
27        // Phase 1b: separator + gutter overlays (before text, background elements)
28        // Skipped when cursor_overlay_end == actual_bg_instances (pane path).
29        if cursor_overlay_end < self.buffers.actual_bg_instances as u32 {
30            render_pass.draw(
31                0..4,
32                cursor_overlay_end..self.buffers.actual_bg_instances as u32,
33            );
34        }
35
36        // Phase 2: text (on top of cell backgrounds)
37        render_pass.set_pipeline(&self.pipelines.text_pipeline);
38        render_pass.set_bind_group(0, &self.pipelines.text_bind_group, &[]);
39        render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
40        render_pass.set_vertex_buffer(1, self.buffers.text_instance_buffer.slice(..));
41        render_pass.draw(0..4, 0..self.buffers.actual_text_instances as u32);
42
43        // Phase 3: cursor overlays (beam/underline bar + hollow outline) ON TOP of text
44        if cursor_overlay_start < cursor_overlay_end {
45            render_pass.set_pipeline(&self.pipelines.bg_pipeline);
46            render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
47            render_pass.set_vertex_buffer(1, self.buffers.bg_instance_buffer.slice(..));
48            render_pass.draw(0..4, cursor_overlay_start..cursor_overlay_end);
49        }
50    }
51
52    /// Render terminal content to an intermediate texture for shader processing.
53    ///
54    /// # Arguments
55    /// * `target_view` - The texture view to render to
56    /// * `skip_background_image` - If true, skip rendering the background image. Use this when
57    ///   a custom shader will handle the background image via iChannel0 instead.
58    ///
59    /// Note: Solid color backgrounds are NOT rendered here. For cursor shaders, the solid color
60    /// is passed to the shader's render function as the clear color instead.
61    pub fn render_to_texture(
62        &mut self,
63        target_view: &wgpu::TextureView,
64        skip_background_image: bool,
65    ) -> Result<wgpu::SurfaceTexture> {
66        let output = match self.surface.get_current_texture() {
67            wgpu::CurrentSurfaceTexture::Success(t)
68            | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
69            other => return Err(crate::error::RenderError::Surface(format!("{other:?}")).into()),
70        };
71        self.build_instance_buffers()?;
72
73        // Render background to intermediate texture via bg_image_pipeline when available.
74        // This covers all modes (Image, Color, Default) with a full-screen opaque quad.
75        let render_background_image =
76            !skip_background_image && self.pipelines.bg_image_bind_group.is_some();
77
78        if render_background_image {
79            // Pass Some(1.0) to render the background image at full opacity for this
80            // intermediate texture; the shader wrapper will apply window_opacity at the end.
81            // This avoids temporarily mutating self.window_opacity (which could be skipped
82            // on restoration if an early return via `?` fires after this point).
83            self.update_bg_image_uniforms(Some(1.0));
84        }
85
86        let mut encoder = self
87            .device
88            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
89                label: Some("render to texture encoder"),
90            });
91
92        // Always clear with TRANSPARENT for intermediate textures
93        let clear_color = wgpu::Color::TRANSPARENT;
94
95        {
96            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
97                label: Some("render pass"),
98                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
99                    view: target_view,
100                    resolve_target: None,
101                    ops: wgpu::Operations {
102                        load: wgpu::LoadOp::Clear(clear_color),
103                        store: wgpu::StoreOp::Store,
104                    },
105                    depth_slice: None,
106                })],
107                depth_stencil_attachment: None,
108                timestamp_writes: None,
109                occlusion_query_set: None,
110                multiview_mask: None,
111            });
112
113            // Render background IMAGE (not solid color) via bg_image_pipeline at full opacity
114            if render_background_image
115                && let Some(ref bg_bind_group) = self.pipelines.bg_image_bind_group
116            {
117                render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
118                render_pass.set_bind_group(0, bg_bind_group, &[]);
119                render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
120                render_pass.draw(0..4, 0..1);
121            }
122
123            let cursor_overlay_start = (self.grid.cols * self.grid.rows) as u32;
124            let cursor_overlay_end =
125                cursor_overlay_start + super::instance_buffers::CURSOR_OVERLAY_SLOTS as u32;
126            self.emit_three_phase_draw_calls(
127                &mut render_pass,
128                cursor_overlay_start,
129                cursor_overlay_end,
130            );
131        }
132
133        self.queue.submit(std::iter::once(encoder.finish()));
134
135        // Restore the uniforms to use the actual window_opacity now that the intermediate
136        // texture has been submitted.  No state mutation occurred above — self.window_opacity
137        // was never changed — so we simply write the real value back into the buffer.
138        if render_background_image {
139            self.update_bg_image_uniforms(None);
140        }
141
142        Ok(output)
143    }
144
145    /// Render only the background (image or solid color) to a view.
146    ///
147    /// This is useful for split pane rendering where the background should be
148    /// rendered once full-screen before rendering each pane's cells on top.
149    ///
150    /// # Arguments
151    /// * `target_view` - The texture view to render to
152    /// * `clear_first` - If true, clear the surface before rendering
153    ///
154    /// # Returns
155    /// `true` if a background image was rendered, `false` if only clear color was used
156    pub fn render_background_only(
157        &self,
158        target_view: &wgpu::TextureView,
159        clear_first: bool,
160    ) -> Result<bool> {
161        let mut encoder = self
162            .device
163            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
164                label: Some("background only encoder"),
165            });
166
167        // Use bg_image_pipeline when a bind group exists (Image, Color, or Default modes).
168        // This renders a full-screen opaque quad, preventing macOS alpha artifacts.
169        let use_bg_image_pipeline = self.pipelines.bg_image_bind_group.is_some();
170        let clear_color = if use_bg_image_pipeline {
171            wgpu::Color::TRANSPARENT
172        } else {
173            wgpu::Color {
174                r: self.background_color[0] as f64 * self.window_opacity as f64,
175                g: self.background_color[1] as f64 * self.window_opacity as f64,
176                b: self.background_color[2] as f64 * self.window_opacity as f64,
177                a: self.window_opacity as f64,
178            }
179        };
180
181        let load_op = if clear_first {
182            wgpu::LoadOp::Clear(clear_color)
183        } else {
184            wgpu::LoadOp::Load
185        };
186
187        {
188            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
189                label: Some("background only render pass"),
190                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
191                    view: target_view,
192                    resolve_target: None,
193                    ops: wgpu::Operations {
194                        load: load_op,
195                        store: wgpu::StoreOp::Store,
196                    },
197                    depth_slice: None,
198                })],
199                depth_stencil_attachment: None,
200                timestamp_writes: None,
201                occlusion_query_set: None,
202                multiview_mask: None,
203            });
204
205            // Render background via bg_image_pipeline (full-screen opaque quad)
206            if use_bg_image_pipeline
207                && let Some(ref bg_bind_group) = self.pipelines.bg_image_bind_group
208            {
209                render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
210                render_pass.set_bind_group(0, bg_bind_group, &[]);
211                render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
212                render_pass.draw(0..4, 0..1);
213            }
214        }
215
216        self.queue.submit(std::iter::once(encoder.finish()));
217        Ok(use_bg_image_pipeline)
218    }
219
220    /// Render terminal content to a view for screenshots.
221    /// This renders without requiring the surface texture.
222    pub fn render_to_view(&self, target_view: &wgpu::TextureView) -> Result<()> {
223        // Note: We don't rebuild instance buffers here since this is typically called
224        // right after a normal render, and the buffers should already be up to date.
225
226        let mut encoder = self
227            .device
228            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
229                label: Some("screenshot render encoder"),
230            });
231
232        // Use bg_image_pipeline when a bind group exists (Image, Color, or Default modes).
233        let use_bg_image_pipeline = self.pipelines.bg_image_bind_group.is_some();
234        let clear_color = if use_bg_image_pipeline {
235            wgpu::Color::TRANSPARENT
236        } else {
237            wgpu::Color {
238                r: self.background_color[0] as f64 * self.window_opacity as f64,
239                g: self.background_color[1] as f64 * self.window_opacity as f64,
240                b: self.background_color[2] as f64 * self.window_opacity as f64,
241                a: self.window_opacity as f64,
242            }
243        };
244
245        {
246            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
247                label: Some("screenshot render pass"),
248                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
249                    view: target_view,
250                    resolve_target: None,
251                    ops: wgpu::Operations {
252                        load: wgpu::LoadOp::Clear(clear_color),
253                        store: wgpu::StoreOp::Store,
254                    },
255                    depth_slice: None,
256                })],
257                depth_stencil_attachment: None,
258                timestamp_writes: None,
259                occlusion_query_set: None,
260                multiview_mask: None,
261            });
262
263            // Render background via bg_image_pipeline (full-screen opaque quad)
264            if use_bg_image_pipeline
265                && let Some(ref bg_bind_group) = self.pipelines.bg_image_bind_group
266            {
267                render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
268                render_pass.set_bind_group(0, bg_bind_group, &[]);
269                render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
270                render_pass.draw(0..4, 0..1);
271            }
272
273            let cursor_overlay_start = (self.grid.cols * self.grid.rows) as u32;
274            let cursor_overlay_end =
275                cursor_overlay_start + super::instance_buffers::CURSOR_OVERLAY_SLOTS as u32;
276            self.emit_three_phase_draw_calls(
277                &mut render_pass,
278                cursor_overlay_start,
279                cursor_overlay_end,
280            );
281
282            // Render scrollbar
283            self.scrollbar.render(&mut render_pass);
284        }
285
286        self.queue.submit(std::iter::once(encoder.finish()));
287        Ok(())
288    }
289
290    pub fn render_overlays(
291        &mut self,
292        surface_texture: &wgpu::SurfaceTexture,
293        show_scrollbar: bool,
294    ) -> Result<()> {
295        // Early return if no overlays to render - avoid creating empty command buffers
296        if !show_scrollbar && self.visual_bell_intensity <= 0.0 {
297            return Ok(());
298        }
299
300        let view = surface_texture
301            .texture
302            .create_view(&wgpu::TextureViewDescriptor::default());
303        let mut encoder = self
304            .device
305            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
306                label: Some("overlay encoder"),
307            });
308
309        {
310            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
311                label: Some("overlay pass"),
312                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
313                    view: &view,
314                    resolve_target: None,
315                    ops: wgpu::Operations {
316                        load: wgpu::LoadOp::Load,
317                        store: wgpu::StoreOp::Store,
318                    },
319                    depth_slice: None,
320                })],
321                depth_stencil_attachment: None,
322                timestamp_writes: None,
323                occlusion_query_set: None,
324                multiview_mask: None,
325            });
326
327            if show_scrollbar {
328                self.scrollbar.render(&mut render_pass);
329            }
330
331            if self.visual_bell_intensity > 0.0 {
332                // Update visual bell uniform buffer with fullscreen quad params
333                // Layout: position (vec2) + size (vec2) + color (vec4) = 32 bytes
334                let uniforms: [f32; 8] = [
335                    -1.0,                       // position.x (NDC left)
336                    -1.0,                       // position.y (NDC bottom)
337                    2.0,                        // size.x (full width in NDC)
338                    2.0,                        // size.y (full height in NDC)
339                    self.visual_bell_color[0],  // color.r
340                    self.visual_bell_color[1],  // color.g
341                    self.visual_bell_color[2],  // color.b
342                    self.visual_bell_intensity, // color.a (intensity)
343                ];
344                self.queue.write_buffer(
345                    &self.buffers.visual_bell_uniform_buffer,
346                    0,
347                    bytemuck::cast_slice(&uniforms),
348                );
349
350                render_pass.set_pipeline(&self.pipelines.visual_bell_pipeline);
351                render_pass.set_bind_group(0, &self.pipelines.visual_bell_bind_group, &[]);
352                render_pass.draw(0..4, 0..1); // 4 vertices = triangle strip quad
353            }
354        }
355
356        self.queue.submit(std::iter::once(encoder.finish()));
357        Ok(())
358    }
359
360    /// Stamp alpha=1.0 over the entire surface without modifying RGB values.
361    ///
362    /// On macOS with `CompositeAlphaMode::PreMultiplied`, any framebuffer pixel with
363    /// alpha < 1.0 becomes translucent through to the desktop. Multiple rendering
364    /// passes (anti-aliased text, overlay compositing) can inadvertently reduce alpha.
365    /// This single full-screen triangle guarantees an opaque surface.
366    ///
367    /// Skipped when `window_opacity < 1.0` so that user-configured transparency works.
368    pub fn render_opaque_alpha(&self, surface_texture: &wgpu::SurfaceTexture) -> Result<()> {
369        if self.window_opacity < 1.0 {
370            return Ok(());
371        }
372
373        let view = surface_texture
374            .texture
375            .create_view(&wgpu::TextureViewDescriptor::default());
376        let mut encoder = self
377            .device
378            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
379                label: Some("opaque alpha encoder"),
380            });
381
382        {
383            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
384                label: Some("opaque alpha pass"),
385                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
386                    view: &view,
387                    resolve_target: None,
388                    ops: wgpu::Operations {
389                        load: wgpu::LoadOp::Load,
390                        store: wgpu::StoreOp::Store,
391                    },
392                    depth_slice: None,
393                })],
394                depth_stencil_attachment: None,
395                timestamp_writes: None,
396                occlusion_query_set: None,
397                multiview_mask: None,
398            });
399
400            render_pass.set_pipeline(&self.pipelines.opaque_alpha_pipeline);
401            render_pass.draw(0..3, 0..1);
402        }
403
404        self.queue.submit(std::iter::once(encoder.finish()));
405        Ok(())
406    }
407}