Skip to main content

par_term_render/renderer/
rendering.rs

1// ARC-009: `take_screenshot` and its shader chain moved to `renderer/screenshot.rs`.
2// If this file needs cutting again, the next seams are:
3//
4//   split_layout.rs   — Split-pane geometry (render_split_panes_with_data)
5//   separator_draw.rs — Separator-mark draw calls; QA-001 and QA-008 affect this area
6//
7// Tracking: Issue ARC-009 in AUDIT.md.
8
9// ARC-004: `render_split_panes` used to end every pane in its own `queue.submit`.
10// Panes are now built in one pass and drawn from a single command encoder — one
11// render pass per pane, one submit per pane batch — so pane submits per frame went
12// from N to 1. The exception is auto-dim without full-content mode, the one
13// configuration that renders the panes twice: once into the shader's intermediate
14// texture as a content mask, then again onto the content view. That is 2.
15//
16// That was unsafe until three single-instance GPU resources were made per-pane,
17// each of which had been correct only because a submit separated one pane's write
18// from the next's:
19//
20//   1. bg_instance_buffer / text_instance_buffer — every pane rebuilt them at
21//      offset 0. Panes now suballocate: `begin_pane_batch` resets the cursors,
22//      each build appends at them, and `emit_three_phase_draw_calls` takes
23//      absolute ranges. The buffers stay bound at offset 0, so no vertex-buffer
24//      offset alignment is involved and there is still one draw ordering.
25//   2. bg_state.pane_bg_uniform_cache — was keyed by image path while the uniform
26//      carries pane position and size, so two panes sharing an image aliased even
27//      before batching. Now keyed by pane index, with the path stored in the entry
28//      so the bind group is rebuilt only when a pane's image changes.
29//   3. The scrollbar's thumb, track and mark uniforms (scrollbar.rs) — one set for
30//      the whole renderer. Now one slot per pane. Hit testing stays
31//      single-instance and follows the *focused* pane rather than whichever pane
32//      was updated last.
33//
34// Capacity: `max_bg_instances` is sized for one full-window grid, which is not
35// enough once panes are simultaneously resident — each pane also needs a viewport
36// fill quad, its own separator rows and its own cursor overlay slots.
37// `begin_pane_batch` sums `pane_instance_capacity` over the frame's panes, keeps
38// the single-grid requirement as a floor, and grows the buffers when needed. The
39// emitters' bounds guards drop instances silently, so a build that reaches the cap
40// logs an error (once per allocation) instead of quietly losing content.
41//
42// Not measured: verifying the frame-rate effect needs a full workspace build, which
43// this change was not able to run. No speedup is claimed.
44
45use crate::cell_renderer::{PaneViewport, pane_instance_capacity};
46use anyhow::Result;
47
48use super::{
49    DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo, Renderer, SeparatorMark,
50    fill_visible_separator_marks,
51};
52
53// This file contains the multi-pane frame-level helper `render_split_panes`.
54
55fn should_populate_terminal_intermediate_texture(
56    full_content_mode: bool,
57    auto_dim_under_text: bool,
58    auto_dim_strength: f32,
59) -> bool {
60    full_content_mode || (auto_dim_under_text && auto_dim_strength > 0.0)
61}
62
63/// Per-batch settings for [`Renderer::prepare_and_draw_panes`].
64///
65/// The capacities are the summed `pane_instance_capacity` of the panes in the
66/// batch; the two flags depend on which target the batch is being drawn to.
67struct PaneBatchSettings {
68    bg_capacity: usize,
69    text_capacity: usize,
70    skip_background_image: bool,
71    fill_default_bg_cells: bool,
72}
73
74/// The pane content of one composited frame.
75///
76/// QA-011: this is the input [`Renderer::composite_panes`] needs, and it is all
77/// an offscreen capture can supply. [`SplitPanesRenderParams`] adds the two
78/// surface-only concerns — the egui overlay and its opacity override — which
79/// have no meaning off-surface: `egui::FullOutput` is produced once per frame by
80/// the live egui pass and consumed by value there, so a capture taken between
81/// frames has none to hand over.
82pub struct PaneCaptureParams<'a> {
83    pub panes: &'a [PaneRenderInfo<'a>],
84    pub dividers: &'a [DividerRenderInfo],
85    pub pane_titles: &'a [PaneTitleInfo],
86    pub focused_viewport: Option<&'a PaneViewport>,
87    pub divider_settings: &'a PaneDividerSettings,
88}
89
90/// Parameters for [`Renderer::render_split_panes`].
91pub struct SplitPanesRenderParams<'a> {
92    pub panes: &'a [PaneRenderInfo<'a>],
93    pub dividers: &'a [DividerRenderInfo],
94    pub pane_titles: &'a [PaneTitleInfo],
95    pub focused_viewport: Option<&'a PaneViewport>,
96    pub divider_settings: &'a PaneDividerSettings,
97    pub egui_data: Option<(egui::FullOutput, &'a egui::Context)>,
98    pub force_egui_opaque: bool,
99}
100
101impl Renderer {
102    /// Update one pane's scrollbar slot.
103    ///
104    /// Hit-test geometry is single-instance and follows the focused pane, so a
105    /// focused pane without a scrollbar has to drop it rather than leave another
106    /// pane's bounds in place.
107    fn update_pane_scrollbar(&mut self, index: usize, pane: &PaneRenderInfo<'_>) {
108        if pane.show_scrollbar {
109            let total_lines = pane.scrollback_len + pane.grid_size.1;
110            self.cell_renderer.update_scrollbar_for_pane(
111                index,
112                pane.scroll_offset,
113                pane.grid_size.1,
114                total_lines,
115                &pane.marks,
116                &pane.viewport,
117            );
118        } else if pane.viewport.focused {
119            self.cell_renderer.scrollbar.clear_hit_state();
120        }
121    }
122
123    /// Build every pane's instances and scrollbar slot, then draw the whole batch
124    /// to `target_view` from one command encoder (ARC-004).
125    fn prepare_and_draw_panes(
126        &mut self,
127        target_view: &wgpu::TextureView,
128        panes: &[PaneRenderInfo<'_>],
129        settings: PaneBatchSettings,
130    ) -> Result<()> {
131        self.cell_renderer
132            .begin_pane_batch(settings.bg_capacity, settings.text_capacity);
133
134        // `scratch` is declared outside the loop so its capacity is preserved
135        // across iterations, avoiding a per-pane heap allocation.
136        let mut scratch: Vec<SeparatorMark> = Vec::new();
137        let mut prepared = Vec::with_capacity(panes.len());
138        for (index, pane) in panes.iter().enumerate() {
139            self.update_pane_scrollbar(index, pane);
140            fill_visible_separator_marks(
141                &mut scratch,
142                &pane.marks,
143                pane.scrollback_len,
144                pane.scroll_offset,
145                pane.grid_size.1,
146            );
147            prepared.push(self.cell_renderer.prepare_pane(
148                index,
149                crate::cell_renderer::PaneRenderViewParams {
150                    viewport: &pane.viewport,
151                    cells: pane.cells,
152                    cols: pane.grid_size.0,
153                    rows: pane.grid_size.1,
154                    cursor_pos: pane.cursor_pos,
155                    cursor_opacity: pane.cursor_opacity,
156                    show_scrollbar: pane.show_scrollbar,
157                    clear_first: false, // Don't clear - the target was already cleared
158                    skip_background_image: settings.skip_background_image,
159                    fill_default_bg_cells: settings.fill_default_bg_cells,
160                    separator_marks: &scratch,
161                    pane_background: pane.background.as_ref(),
162                },
163            )?);
164        }
165
166        self.cell_renderer
167            .draw_prepared_panes(target_view, &prepared);
168        Ok(())
169    }
170
171    /// Load any per-pane background textures that aren't cached yet.
172    ///
173    /// Kept out of [`Renderer::composite_panes`] so the live path still runs it
174    /// *before* acquiring the swapchain drawable — the first frame using a pane
175    /// background reads it from disk, and holding the drawable across that read
176    /// would stall presentation.
177    fn preload_pane_backgrounds(&mut self, panes: &[PaneRenderInfo<'_>]) {
178        for pane in panes.iter() {
179            if let Some(ref bg) = pane.background
180                && let Some(ref path) = bg.image_path
181                && let Err(e) = self.cell_renderer.load_pane_background(path)
182            {
183                log::error!("Failed to load pane background '{}': {}", path, e);
184            }
185        }
186    }
187
188    /// Composite one frame of pane content onto `final_view`.
189    ///
190    /// Covers the shader chain, every pane's cells and inline graphics, dividers,
191    /// pane titles, the visual bell and the focus indicator, ending with the
192    /// cursor-shader composite onto `final_view`.
193    ///
194    /// QA-011: deliberately excludes the surface-only tail (egui overlay,
195    /// opaque-alpha stamp, `present`) and the `dirty` bookkeeping, so
196    /// [`Renderer::take_screenshot`] can reuse it — a capture must render even
197    /// when the renderer is clean, and must not consume the live path's dirty
198    /// flag. Callers must run [`Renderer::preload_pane_backgrounds`] first.
199    fn composite_panes(
200        &mut self,
201        p: PaneCaptureParams<'_>,
202        final_view: &wgpu::TextureView,
203    ) -> Result<()> {
204        let PaneCaptureParams {
205            panes,
206            dividers,
207            pane_titles,
208            focused_viewport,
209            divider_settings,
210        } = p;
211
212        let has_custom_shader = self.custom_shader_renderer.is_some();
213        // Only use cursor shader if it's enabled and not disabled for alt screen
214        let use_cursor_shader =
215            self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
216
217        // Instance-buffer capacity this frame's panes need, all resident at once.
218        let (batch_bg_capacity, batch_text_capacity) =
219            panes.iter().fold((0usize, 0usize), |(bg, text), pane| {
220                let (pane_bg, pane_text) =
221                    pane_instance_capacity(pane.grid_size.0, pane.grid_size.1);
222                (bg + pane_bg, text + pane_text)
223            });
224
225        // When cursor shader is active, render all content to its intermediate texture.
226        // The cursor shader will then composite the result onto `final_view`.
227        let cursor_intermediate: Option<wgpu::TextureView> = if use_cursor_shader {
228            Some(
229                self.cursor_shader_renderer
230                    .as_ref()
231                    .ok_or_else(|| {
232                        crate::error::RenderError::ShaderUnavailable(
233                            "cursor_shader_renderer unavailable (GPU device loss?)".into(),
234                        )
235                    })?
236                    .intermediate_texture_view()
237                    .clone(),
238            )
239        } else {
240            None
241        };
242        // Content render target: cursor shader intermediate (if active) or the
243        // final target directly
244        let content_view = cursor_intermediate.as_ref().unwrap_or(final_view);
245
246        // Clear color for content rendering. When cursor shader will apply opacity,
247        // use non-premultiplied color so opacity isn't applied twice.
248        let opacity = self.cell_renderer.window_opacity as f64;
249        let clear_color = if self.cell_renderer.pipelines.bg_image_bind_group.is_some() {
250            wgpu::Color::TRANSPARENT
251        } else if use_cursor_shader {
252            // Cursor shader applies opacity — use full-opacity background
253            wgpu::Color {
254                r: self.cell_renderer.background_color[0] as f64,
255                g: self.cell_renderer.background_color[1] as f64,
256                b: self.cell_renderer.background_color[2] as f64,
257                a: 1.0,
258            }
259        } else {
260            wgpu::Color {
261                r: self.cell_renderer.background_color[0] as f64 * opacity,
262                g: self.cell_renderer.background_color[1] as f64 * opacity,
263                b: self.cell_renderer.background_color[2] as f64 * opacity,
264                a: opacity,
265            }
266        };
267
268        // Determine if the shader needs terminal pixels in iChannel4.
269        // Full-content mode processes terminal content directly; auto-dim uses the same
270        // texture as a content mask so it can dim only beneath text/content pixels.
271        let (full_content_mode, populate_terminal_intermediate_texture) = self
272            .custom_shader_renderer
273            .as_ref()
274            .map(|s| {
275                let full_content_mode = s.full_content_mode();
276                (
277                    full_content_mode,
278                    should_populate_terminal_intermediate_texture(
279                        full_content_mode,
280                        s.auto_dim_under_text,
281                        s.auto_dim_strength,
282                    ),
283                )
284            })
285            .unwrap_or((false, false));
286
287        // Render pane content to the shader's intermediate texture BEFORE running the
288        // shader when it needs terminal pixels via iChannel4.
289        // This must happen outside the `custom_shader_renderer` mutable borrow scope
290        // because rendering panes requires `&mut self`.
291        if populate_terminal_intermediate_texture {
292            let custom_shader = self.custom_shader_renderer.as_mut().ok_or_else(|| {
293                crate::error::RenderError::ShaderUnavailable(
294                    "custom_shader_renderer unavailable for iChannel4 content (GPU device loss?)"
295                        .into(),
296                )
297            })?;
298            custom_shader.clear_intermediate_texture(
299                self.cell_renderer.device(),
300                self.cell_renderer.queue(),
301            );
302            let intermediate_view = custom_shader.intermediate_texture_view().clone();
303
304            // Render each pane's content to the intermediate texture.
305            // Scrollbar geometry is updated per-pane so unfocused panes can also
306            // show their own scrollbar.
307            self.prepare_and_draw_panes(
308                &intermediate_view,
309                panes,
310                PaneBatchSettings {
311                    bg_capacity: batch_bg_capacity,
312                    text_capacity: batch_text_capacity,
313                    skip_background_image: true, // Shader handles background
314                    fill_default_bg_cells: false, // Shader shows through default-bg cells
315                },
316            )?;
317
318            // Render inline graphics to intermediate so shader can process them
319            for pane in panes.iter() {
320                if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
321                    self.render_pane_sixel_graphics(
322                        &intermediate_view,
323                        &pane.viewport,
324                        &pane.graphics,
325                        pane.scroll_offset,
326                        pane.scrollback_len,
327                        pane.grid_size.1,
328                        pane.cells,
329                        pane.grid_size.0,
330                        &pane.virtual_placements,
331                    )?;
332                }
333            }
334        }
335
336        // If custom shader is enabled, render it to the content target
337        // (the shader's render pass will handle clearing the target)
338        if let Some(ref mut custom_shader) = self.custom_shader_renderer {
339            if !populate_terminal_intermediate_texture {
340                // Background-only mode without auto-dim: clear intermediate texture
341                // (shader doesn't need terminal content, panes will be rendered on top)
342                custom_shader.clear_intermediate_texture(
343                    self.cell_renderer.device(),
344                    self.cell_renderer.queue(),
345                );
346            }
347
348            // Render shader effect. When cursor shader is chained, render to cursor
349            // shader's intermediate without applying opacity (cursor shader will do it).
350            // When no cursor shader, render directly to surface with opacity applied.
351            custom_shader.render_with_clear_color(
352                self.cell_renderer.device(),
353                self.cell_renderer.queue(),
354                content_view,
355                !use_cursor_shader, // Apply opacity only when not chaining to cursor shader
356                clear_color,
357            )?;
358        } else {
359            // No custom shader - just clear the content target with background color
360            let mut encoder = self.cell_renderer.device().create_command_encoder(
361                &wgpu::CommandEncoderDescriptor {
362                    label: Some("split pane clear encoder"),
363                },
364            );
365
366            {
367                let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
368                    label: Some("surface clear pass"),
369                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
370                        view: content_view,
371                        resolve_target: None,
372                        ops: wgpu::Operations {
373                            load: wgpu::LoadOp::Clear(clear_color),
374                            store: wgpu::StoreOp::Store,
375                        },
376                        depth_slice: None,
377                    })],
378                    depth_stencil_attachment: None,
379                    timestamp_writes: None,
380                    occlusion_query_set: None,
381                    multiview_mask: None,
382                });
383            }
384
385            self.cell_renderer
386                .queue()
387                .submit(std::iter::once(encoder.finish()));
388        }
389
390        // Render background image (full-screen, after shader but before panes)
391        // Skip if custom shader is handling the background.
392        // Also skip if any pane has a per-pane background configured -
393        // per-pane backgrounds are rendered individually in render_pane_to_view.
394        let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
395        let has_background_image = if !has_custom_shader && !any_pane_has_background {
396            self.cell_renderer
397                .render_background_only(content_view, false)?
398        } else {
399            false
400        };
401
402        // In full content mode, panes were already rendered to the shader's intermediate
403        // texture and the shader output includes the processed terminal content.
404        // Skip re-rendering panes to the content view.
405        if !full_content_mode {
406            // Render each pane's content (skip background image since we rendered it full-screen).
407            // Scrollbar geometry is updated per-pane so unfocused panes can also
408            // show their own scrollbar.
409            self.prepare_and_draw_panes(
410                content_view,
411                panes,
412                PaneBatchSettings {
413                    bg_capacity: batch_bg_capacity,
414                    text_capacity: batch_text_capacity,
415                    skip_background_image: has_background_image || has_custom_shader,
416                    // Only fill gaps in bg-image mode; shader shows through
417                    fill_default_bg_cells: has_background_image,
418                },
419            )?;
420
421            // Render inline graphics (Sixel/iTerm2/Kitty) for each pane, clipped to its bounds
422            for pane in panes {
423                if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
424                    self.render_pane_sixel_graphics(
425                        content_view,
426                        &pane.viewport,
427                        &pane.graphics,
428                        pane.scroll_offset,
429                        pane.scrollback_len,
430                        pane.grid_size.1,
431                        pane.cells,
432                        pane.grid_size.0,
433                        &pane.virtual_placements,
434                    )?;
435                }
436            }
437        }
438
439        // Render dividers between panes
440        if !dividers.is_empty() {
441            self.render_dividers(content_view, dividers, divider_settings)?;
442        }
443
444        // Render pane title bars (background + text)
445        if !pane_titles.is_empty() {
446            self.render_pane_titles(content_view, pane_titles)?;
447        }
448
449        // Render visual bell overlay (fullscreen flash)
450        if self.cell_renderer.visual_bell_intensity > 0.0 {
451            let uniforms: [f32; 8] = [
452                -1.0,                                     // position.x (NDC left)
453                -1.0,                                     // position.y (NDC bottom)
454                2.0,                                      // size.x (full width in NDC)
455                2.0,                                      // size.y (full height in NDC)
456                self.cell_renderer.visual_bell_color[0],  // color.r
457                self.cell_renderer.visual_bell_color[1],  // color.g
458                self.cell_renderer.visual_bell_color[2],  // color.b
459                self.cell_renderer.visual_bell_intensity, // color.a (intensity)
460            ];
461            self.cell_renderer.queue().write_buffer(
462                &self.cell_renderer.buffers.visual_bell_uniform_buffer,
463                0,
464                bytemuck::cast_slice(&uniforms),
465            );
466
467            let mut encoder = self.cell_renderer.device().create_command_encoder(
468                &wgpu::CommandEncoderDescriptor {
469                    label: Some("visual bell encoder"),
470                },
471            );
472            {
473                let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
474                    label: Some("visual bell pass"),
475                    color_attachments: &[Some(wgpu::RenderPassColorAttachment {
476                        view: content_view,
477                        resolve_target: None,
478                        ops: wgpu::Operations {
479                            load: wgpu::LoadOp::Load,
480                            store: wgpu::StoreOp::Store,
481                        },
482                        depth_slice: None,
483                    })],
484                    depth_stencil_attachment: None,
485                    timestamp_writes: None,
486                    occlusion_query_set: None,
487                    multiview_mask: None,
488                });
489                render_pass.set_pipeline(&self.cell_renderer.pipelines.visual_bell_pipeline);
490                render_pass.set_bind_group(
491                    0,
492                    &self.cell_renderer.pipelines.visual_bell_bind_group,
493                    &[],
494                );
495                render_pass.draw(0..4, 0..1); // 4 vertices = triangle strip quad
496            }
497            self.cell_renderer
498                .queue()
499                .submit(std::iter::once(encoder.finish()));
500        }
501
502        // Render focus indicator around focused pane (only if multiple panes)
503        if panes.len() > 1
504            && let Some(viewport) = focused_viewport
505        {
506            self.render_focus_indicator(content_view, viewport, divider_settings)?;
507        }
508
509        // Apply cursor shader if active: composite content to the final target
510        if use_cursor_shader {
511            self.cursor_shader_renderer
512                .as_mut()
513                .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
514                    "cursor_shader_renderer unavailable during final composite (GPU device loss?)".into(),
515                ))?
516                .render(
517                    self.cell_renderer.device(),
518                    self.cell_renderer.queue(),
519                    final_view,
520                    true, // Apply opacity - final render to the target
521                )?;
522        }
523
524        Ok(())
525    }
526
527    /// Render split panes with dividers and focus indicator
528    ///
529    /// This is the main entry point for rendering a split pane layout.
530    /// It handles:
531    /// 1. Clearing the surface
532    /// 2. Rendering each pane's content
533    /// 3. Rendering dividers between panes
534    /// 4. Rendering focus indicator around the focused pane
535    /// 5. Rendering egui overlay if provided
536    /// 6. Presenting the surface
537    ///
538    /// Steps 1–4 are shared with [`Renderer::take_screenshot`] via
539    /// [`Renderer::composite_panes`]; this method adds the surface acquisition,
540    /// the egui overlay, presentation and the `dirty` bookkeeping.
541    ///
542    /// # Arguments
543    /// * `panes` - List of panes to render with their viewport info
544    /// * `dividers` - List of dividers between panes with hover state
545    /// * `focused_viewport` - Viewport of the focused pane (for focus indicator)
546    /// * `divider_settings` - Settings for divider and focus indicator appearance
547    /// * `egui_data` - Optional egui overlay data
548    /// * `force_egui_opaque` - Force egui to render at full opacity
549    ///
550    /// # Returns
551    /// `true` if rendering was performed, `false` if skipped
552    pub fn render_split_panes(&mut self, params: SplitPanesRenderParams<'_>) -> Result<bool> {
553        let SplitPanesRenderParams {
554            panes,
555            dividers,
556            pane_titles,
557            focused_viewport,
558            divider_settings,
559            egui_data,
560            force_egui_opaque,
561        } = params;
562        // Check if we need to render
563        let force_render = self.needs_continuous_render();
564        if !self.dirty && !force_render && egui_data.is_none() {
565            return Ok(false);
566        }
567
568        self.preload_pane_backgrounds(panes);
569
570        // Get the surface texture
571        let surface_texture = match self.cell_renderer.surface.get_current_texture() {
572            wgpu::CurrentSurfaceTexture::Success(t)
573            | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
574            other => return Err(crate::error::RenderError::Surface(format!("{other:?}")).into()),
575        };
576        let surface_view = surface_texture
577            .texture
578            .create_view(&wgpu::TextureViewDescriptor::default());
579
580        self.composite_panes(
581            PaneCaptureParams {
582                panes,
583                dividers,
584                pane_titles,
585                focused_viewport,
586                divider_settings,
587            },
588            &surface_view,
589        )?;
590
591        // Render egui overlay if provided
592        if let Some((egui_output, egui_ctx)) = egui_data {
593            self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
594        }
595
596        // Ensure opaque surface when window_opacity == 1.0 (skipped for transparent windows)
597        self.cell_renderer.render_opaque_alpha(&surface_texture)?;
598
599        // Present the surface
600        surface_texture.present();
601
602        self.dirty = false;
603        Ok(true)
604    }
605
606    /// Composite a frame of pane content into an offscreen target (QA-011).
607    ///
608    /// Used by [`Renderer::take_screenshot`]; kept next to the live path so the
609    /// two stay in step.
610    pub(super) fn composite_panes_offscreen(
611        &mut self,
612        params: PaneCaptureParams<'_>,
613        target_view: &wgpu::TextureView,
614    ) -> Result<()> {
615        self.preload_pane_backgrounds(params.panes);
616        self.composite_panes(params, target_view)
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623
624    #[test]
625    fn background_shader_auto_dim_requires_terminal_intermediate_texture() {
626        assert!(should_populate_terminal_intermediate_texture(
627            false, true, 0.35
628        ));
629    }
630
631    #[test]
632    fn full_content_mode_requires_terminal_intermediate_texture() {
633        assert!(should_populate_terminal_intermediate_texture(
634            true, false, 0.0
635        ));
636    }
637
638    #[test]
639    fn background_only_shader_without_auto_dim_skips_terminal_intermediate_texture() {
640        assert!(!should_populate_terminal_intermediate_texture(
641            false, false, 0.35
642        ));
643        assert!(!should_populate_terminal_intermediate_texture(
644            false, true, 0.0
645        ));
646    }
647}