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