Skip to main content

par_term_render/cell_renderer/
render.rs

1use super::CellRenderer;
2use anyhow::Result;
3use std::ops::Range;
4
5/// Background instance slots reserved for cursor overlays.
6///
7/// Re-exported here because `instance_buffers` is a private module and the slot
8/// count is part of the buffer layout this module defines. Anything checking the
9/// layout must read it from here rather than restating `10`.
10pub use super::instance_buffers::CURSOR_OVERLAY_SLOTS;
11
12/// Which GPU pipeline a phase draws with.
13#[derive(Clone, Copy, PartialEq, Eq, Debug)]
14pub enum CellPipeline {
15    /// The background-quad pipeline, fed from `bg_instance_buffer`.
16    Background,
17    /// The glyph pipeline, fed from `text_instance_buffer`.
18    Text,
19}
20
21/// One phase of the three-phase cell draw, in the order it must be emitted.
22///
23/// The ordering constraint is the reason this enum exists: cursor overlays are
24/// drawn with the background pipeline but must be emitted *after* the text, or a
25/// beam or underline cursor ends up hidden underneath the glyphs.
26#[derive(Clone, Copy, PartialEq, Eq, Debug)]
27pub enum CellDrawPhase {
28    /// Phase 1 — cell background quads.
29    Background,
30    /// Phase 1b — separator / gutter quads that sit after the cursor overlays.
31    ExtraBackground,
32    /// Phase 2 — glyph and underline quads.
33    Text,
34    /// Phase 3 — cursor overlays.
35    CursorOverlays,
36}
37
38impl CellDrawPhase {
39    /// The pipeline this phase draws with.
40    pub fn pipeline(self) -> CellPipeline {
41        match self {
42            Self::Background | Self::ExtraBackground | Self::CursorOverlays => {
43                CellPipeline::Background
44            }
45            Self::Text => CellPipeline::Text,
46        }
47    }
48}
49
50/// Instance-buffer layout of the single-grid (offscreen) path.
51///
52/// One background buffer holds four back-to-back regions. Four call sites used to
53/// restate this arithmetic — the builder's write offsets, the draw ranges, the
54/// capacity check and the initial allocation — so a change to one could silently
55/// disagree with the others. They all read it from here now.
56#[derive(Clone, PartialEq, Eq, Debug)]
57pub struct SingleGridLayout {
58    /// One quad per cell, in row-major order.
59    pub cells: Range<usize>,
60    /// [`CURSOR_OVERLAY_SLOTS`] slots, written whether or not a cursor is shown.
61    pub cursor_overlays: Range<usize>,
62    /// One command-separator line slot per row.
63    pub separators: Range<usize>,
64    /// One gutter-indicator slot per row.
65    pub gutters: Range<usize>,
66}
67
68impl SingleGridLayout {
69    /// The layout for a `cols` × `rows` grid.
70    pub fn new(cols: usize, rows: usize) -> Self {
71        let cells = 0..cols * rows;
72        let cursor_overlays = cells.end..cells.end + CURSOR_OVERLAY_SLOTS;
73        let separators = cursor_overlays.end..cursor_overlays.end + rows;
74        let gutters = separators.end..separators.end + rows;
75        Self {
76            cells,
77            cursor_overlays,
78            separators,
79            gutters,
80        }
81    }
82
83    /// Total background instances the layout occupies.
84    pub fn bg_instances(&self) -> usize {
85        self.gutters.end
86    }
87}
88
89/// Absolute instance ranges for one three-phase cell draw.
90///
91/// ARC-004: the pane path suballocates the shared instance buffers, so a pane's
92/// instances no longer start at index 0. Every range here is absolute into
93/// `bg_instance_buffer` / `text_instance_buffer`, which lets several panes stay
94/// resident at once and be drawn from a single command encoder.
95#[derive(Clone, PartialEq, Eq, Debug)]
96pub struct ThreePhaseRanges {
97    /// Phase 1 — cell background quads (includes the pane's viewport fill quad).
98    pub bg: Range<u32>,
99    /// Phase 1b — separator / gutter quads that live *after* the cursor overlays.
100    /// Empty on the pane path, which packs separators before the overlays.
101    pub extra_bg: Range<u32>,
102    /// Phase 2 — glyph and underline quads.
103    pub text: Range<u32>,
104    /// Phase 3 — cursor overlays, drawn last so they sit on top of the glyphs.
105    pub cursor_overlays: Range<u32>,
106}
107
108impl ThreePhaseRanges {
109    /// The phases in the order they must be drawn, with the range each covers.
110    ///
111    /// This is the single source of truth for the ordering.
112    /// [`CellRenderer::emit_three_phase_draw_calls`] walks exactly this sequence,
113    /// so the order cannot be changed there without changing it here — which is
114    /// what makes the invariant testable without a GPU.
115    pub fn draw_sequence(&self) -> [(CellDrawPhase, Range<u32>); 4] {
116        [
117            (CellDrawPhase::Background, self.bg.clone()),
118            (CellDrawPhase::ExtraBackground, self.extra_bg.clone()),
119            (CellDrawPhase::Text, self.text.clone()),
120            (CellDrawPhase::CursorOverlays, self.cursor_overlays.clone()),
121        ]
122    }
123
124    /// Draw ranges for the single-grid layout `build_instance_buffers` writes.
125    ///
126    /// `actual_bg_instances` is clamped up to the end of the overlay slots so a
127    /// grid that has not been built yet cannot produce an inverted range.
128    pub fn for_single_grid(
129        layout: &SingleGridLayout,
130        actual_bg_instances: usize,
131        actual_text_instances: usize,
132    ) -> Self {
133        let overlay_end = layout.cursor_overlays.end;
134        let bg_end = actual_bg_instances.max(overlay_end);
135        Self {
136            bg: layout.cells.start as u32..layout.cells.end as u32,
137            extra_bg: overlay_end as u32..bg_end as u32,
138            text: 0..actual_text_instances as u32,
139            cursor_overlays: layout.cursor_overlays.start as u32..overlay_end as u32,
140        }
141    }
142
143    /// Draw ranges for one pane's slice of the shared instance buffers.
144    ///
145    /// The pane builder packs separators *before* the cursor overlays and emits
146    /// the overlays immediately after its background run, so phase 1b is empty
147    /// and the overlays are contiguous with `bg`.
148    pub fn for_pane(bg: Range<usize>, cursor_overlays: Range<usize>, text: Range<usize>) -> Self {
149        debug_assert_eq!(
150            bg.end, cursor_overlays.start,
151            "a pane's cursor overlays must directly follow its background run"
152        );
153        Self {
154            bg: bg.start as u32..bg.end as u32,
155            extra_bg: 0..0,
156            text: text.start as u32..text.end as u32,
157            cursor_overlays: cursor_overlays.start as u32..cursor_overlays.end as u32,
158        }
159    }
160}
161
162impl CellRenderer {
163    /// Emit the standard 3-phase draw calls into an existing render pass.
164    ///
165    /// This is the single source of truth for the cell rendering draw call sequence.
166    /// Background images / pane backgrounds must be drawn by the caller before this.
167    ///
168    /// **Phase 1**: Cell backgrounds
169    /// **Phase 1b**: Separators / gutter — skipped when the range is empty
170    ///   (the pane path packs these before the cursor overlays)
171    /// **Phase 2**: Text glyphs
172    /// **Phase 3**: Cursor overlays — must stay last, or beam and underline
173    ///   cursors are hidden under the glyphs drawn in phase 2.
174    ///
175    /// The order is not written out here: this walks
176    /// [`ThreePhaseRanges::draw_sequence`], which is what a test can check
177    /// without a GPU. Empty ranges are skipped, and pipeline state is set only
178    /// when the phase changes which pipeline it needs.
179    pub(crate) fn emit_three_phase_draw_calls(
180        &self,
181        render_pass: &mut wgpu::RenderPass<'_>,
182        ranges: &ThreePhaseRanges,
183    ) {
184        let mut bound: Option<CellPipeline> = None;
185
186        for (phase, range) in ranges.draw_sequence() {
187            if range.is_empty() {
188                continue;
189            }
190
191            let pipeline = phase.pipeline();
192            if bound != Some(pipeline) {
193                match pipeline {
194                    CellPipeline::Background => {
195                        render_pass.set_pipeline(&self.pipelines.bg_pipeline);
196                        render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
197                        render_pass.set_vertex_buffer(1, self.buffers.bg_instance_buffer.slice(..));
198                    }
199                    CellPipeline::Text => {
200                        render_pass.set_pipeline(&self.pipelines.text_pipeline);
201                        render_pass.set_bind_group(0, &self.pipelines.text_bind_group, &[]);
202                        render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
203                        render_pass
204                            .set_vertex_buffer(1, self.buffers.text_instance_buffer.slice(..));
205                    }
206                }
207                bound = Some(pipeline);
208            }
209
210            render_pass.draw(0..4, range);
211        }
212    }
213
214    /// Ranges for the single-grid layout that `build_instance_buffers` writes.
215    fn single_grid_ranges(&self) -> ThreePhaseRanges {
216        ThreePhaseRanges::for_single_grid(
217            &SingleGridLayout::new(self.grid.cols, self.grid.rows),
218            self.buffers.actual_bg_instances,
219            self.buffers.actual_text_instances,
220        )
221    }
222
223    /// Render terminal content to an intermediate texture for shader processing.
224    ///
225    /// # Arguments
226    /// * `target_view` - The texture view to render to
227    /// * `skip_background_image` - If true, skip rendering the background image. Use this when
228    ///   a custom shader will handle the background image via iChannel0 instead.
229    ///
230    /// Note: Solid color backgrounds are NOT rendered here. For cursor shaders, the solid color
231    /// is passed to the shader's render function as the clear color instead.
232    ///
233    /// QA-011: this used to acquire a `SurfaceTexture` it never drew to and never
234    /// presented. The caller dropped it, which returns the drawable to the swapchain
235    /// unpresented and can stall the next real frame's acquire. Nothing here targets
236    /// the surface — `target_view` is the only render target — so the acquire is gone.
237    pub fn render_to_texture(
238        &mut self,
239        target_view: &wgpu::TextureView,
240        skip_background_image: bool,
241    ) -> Result<()> {
242        self.build_instance_buffers()?;
243
244        // Render background to intermediate texture via bg_image_pipeline when available.
245        // This covers all modes (Image, Color, Default) with a full-screen opaque quad.
246        let render_background_image =
247            !skip_background_image && self.pipelines.bg_image_bind_group.is_some();
248
249        if render_background_image {
250            // Pass Some(1.0) to render the background image at full opacity for this
251            // intermediate texture; the shader wrapper will apply window_opacity at the end.
252            // This avoids temporarily mutating self.window_opacity (which could be skipped
253            // on restoration if an early return via `?` fires after this point).
254            self.update_bg_image_uniforms(Some(1.0));
255        }
256
257        let mut encoder = self
258            .device
259            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
260                label: Some("render to texture encoder"),
261            });
262
263        // Always clear with TRANSPARENT for intermediate textures
264        let clear_color = wgpu::Color::TRANSPARENT;
265
266        {
267            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
268                label: Some("render pass"),
269                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
270                    view: target_view,
271                    resolve_target: None,
272                    ops: wgpu::Operations {
273                        load: wgpu::LoadOp::Clear(clear_color),
274                        store: wgpu::StoreOp::Store,
275                    },
276                    depth_slice: None,
277                })],
278                depth_stencil_attachment: None,
279                timestamp_writes: None,
280                occlusion_query_set: None,
281                multiview_mask: None,
282            });
283
284            // Render background IMAGE (not solid color) via bg_image_pipeline at full opacity
285            if render_background_image
286                && let Some(ref bg_bind_group) = self.pipelines.bg_image_bind_group
287            {
288                render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
289                render_pass.set_bind_group(0, bg_bind_group, &[]);
290                render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
291                render_pass.draw(0..4, 0..1);
292            }
293
294            self.emit_three_phase_draw_calls(&mut render_pass, &self.single_grid_ranges());
295        }
296
297        self.queue.submit(std::iter::once(encoder.finish()));
298
299        // Restore the uniforms to use the actual window_opacity now that the intermediate
300        // texture has been submitted.  No state mutation occurred above — self.window_opacity
301        // was never changed — so we simply write the real value back into the buffer.
302        if render_background_image {
303            self.update_bg_image_uniforms(None);
304        }
305
306        Ok(())
307    }
308
309    /// Render only the background (image or solid color) to a view.
310    ///
311    /// This is useful for split pane rendering where the background should be
312    /// rendered once full-screen before rendering each pane's cells on top.
313    ///
314    /// # Arguments
315    /// * `target_view` - The texture view to render to
316    /// * `clear_first` - If true, clear the surface before rendering
317    ///
318    /// # Returns
319    /// `true` if a background image was rendered, `false` if only clear color was used
320    pub fn render_background_only(
321        &self,
322        target_view: &wgpu::TextureView,
323        clear_first: bool,
324    ) -> Result<bool> {
325        let mut encoder = self
326            .device
327            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
328                label: Some("background only encoder"),
329            });
330
331        // Use bg_image_pipeline when a bind group exists (Image, Color, or Default modes).
332        // This renders a full-screen opaque quad, preventing macOS alpha artifacts.
333        let use_bg_image_pipeline = self.pipelines.bg_image_bind_group.is_some();
334        let clear_color = if use_bg_image_pipeline {
335            wgpu::Color::TRANSPARENT
336        } else {
337            wgpu::Color {
338                r: self.background_color[0] as f64 * self.window_opacity as f64,
339                g: self.background_color[1] as f64 * self.window_opacity as f64,
340                b: self.background_color[2] as f64 * self.window_opacity as f64,
341                a: self.window_opacity as f64,
342            }
343        };
344
345        let load_op = if clear_first {
346            wgpu::LoadOp::Clear(clear_color)
347        } else {
348            wgpu::LoadOp::Load
349        };
350
351        {
352            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
353                label: Some("background only render pass"),
354                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
355                    view: target_view,
356                    resolve_target: None,
357                    ops: wgpu::Operations {
358                        load: load_op,
359                        store: wgpu::StoreOp::Store,
360                    },
361                    depth_slice: None,
362                })],
363                depth_stencil_attachment: None,
364                timestamp_writes: None,
365                occlusion_query_set: None,
366                multiview_mask: None,
367            });
368
369            // Render background via bg_image_pipeline (full-screen opaque quad)
370            if use_bg_image_pipeline
371                && let Some(ref bg_bind_group) = self.pipelines.bg_image_bind_group
372            {
373                render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
374                render_pass.set_bind_group(0, bg_bind_group, &[]);
375                render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
376                render_pass.draw(0..4, 0..1);
377            }
378        }
379
380        self.queue.submit(std::iter::once(encoder.finish()));
381        Ok(use_bg_image_pipeline)
382    }
383
384    /// Render terminal content to a view for screenshots.
385    /// This renders without requiring the surface texture.
386    ///
387    /// QA-011: this used to skip the rebuild on the theory that a normal render had
388    /// just left the buffers up to date. It has not been true since the pane path
389    /// became the only live path: the pane builder writes pane-relative geometry at
390    /// pane offsets, while the draw ranges below assume the single-grid layout, so
391    /// the capture drew stale bytes from an older frame. Rebuilding makes the buffer
392    /// contents and the ranges agree — the same thing `render_to_texture` does for
393    /// the shader-active branch.
394    pub fn render_to_view(&mut self, target_view: &wgpu::TextureView) -> Result<()> {
395        self.build_instance_buffers()?;
396
397        let mut encoder = self
398            .device
399            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
400                label: Some("screenshot render encoder"),
401            });
402
403        // Use bg_image_pipeline when a bind group exists (Image, Color, or Default modes).
404        let use_bg_image_pipeline = self.pipelines.bg_image_bind_group.is_some();
405        let clear_color = if use_bg_image_pipeline {
406            wgpu::Color::TRANSPARENT
407        } else {
408            wgpu::Color {
409                r: self.background_color[0] as f64 * self.window_opacity as f64,
410                g: self.background_color[1] as f64 * self.window_opacity as f64,
411                b: self.background_color[2] as f64 * self.window_opacity as f64,
412                a: self.window_opacity as f64,
413            }
414        };
415
416        {
417            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
418                label: Some("screenshot render pass"),
419                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
420                    view: target_view,
421                    resolve_target: None,
422                    ops: wgpu::Operations {
423                        load: wgpu::LoadOp::Clear(clear_color),
424                        store: wgpu::StoreOp::Store,
425                    },
426                    depth_slice: None,
427                })],
428                depth_stencil_attachment: None,
429                timestamp_writes: None,
430                occlusion_query_set: None,
431                multiview_mask: None,
432            });
433
434            // Render background via bg_image_pipeline (full-screen opaque quad)
435            if use_bg_image_pipeline
436                && let Some(ref bg_bind_group) = self.pipelines.bg_image_bind_group
437            {
438                render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
439                render_pass.set_bind_group(0, bg_bind_group, &[]);
440                render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
441                render_pass.draw(0..4, 0..1);
442            }
443
444            self.emit_three_phase_draw_calls(&mut render_pass, &self.single_grid_ranges());
445
446            // Render scrollbar (slot 0 — the single-grid path's slot)
447            self.scrollbar.render(&mut render_pass, 0);
448        }
449
450        self.queue.submit(std::iter::once(encoder.finish()));
451        Ok(())
452    }
453
454    pub fn render_overlays(
455        &mut self,
456        surface_texture: &wgpu::SurfaceTexture,
457        show_scrollbar: bool,
458    ) -> Result<()> {
459        // Early return if no overlays to render - avoid creating empty command buffers
460        if !show_scrollbar && self.visual_bell_intensity <= 0.0 {
461            return Ok(());
462        }
463
464        let view = surface_texture
465            .texture
466            .create_view(&wgpu::TextureViewDescriptor::default());
467        let mut encoder = self
468            .device
469            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
470                label: Some("overlay encoder"),
471            });
472
473        {
474            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
475                label: Some("overlay pass"),
476                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
477                    view: &view,
478                    resolve_target: None,
479                    ops: wgpu::Operations {
480                        load: wgpu::LoadOp::Load,
481                        store: wgpu::StoreOp::Store,
482                    },
483                    depth_slice: None,
484                })],
485                depth_stencil_attachment: None,
486                timestamp_writes: None,
487                occlusion_query_set: None,
488                multiview_mask: None,
489            });
490
491            if show_scrollbar {
492                // Slot 0 — the single-grid path's slot.
493                self.scrollbar.render(&mut render_pass, 0);
494            }
495
496            if self.visual_bell_intensity > 0.0 {
497                // Update visual bell uniform buffer with fullscreen quad params
498                // Layout: position (vec2) + size (vec2) + color (vec4) = 32 bytes
499                let uniforms: [f32; 8] = [
500                    -1.0,                       // position.x (NDC left)
501                    -1.0,                       // position.y (NDC bottom)
502                    2.0,                        // size.x (full width in NDC)
503                    2.0,                        // size.y (full height in NDC)
504                    self.visual_bell_color[0],  // color.r
505                    self.visual_bell_color[1],  // color.g
506                    self.visual_bell_color[2],  // color.b
507                    self.visual_bell_intensity, // color.a (intensity)
508                ];
509                self.queue.write_buffer(
510                    &self.buffers.visual_bell_uniform_buffer,
511                    0,
512                    bytemuck::cast_slice(&uniforms),
513                );
514
515                render_pass.set_pipeline(&self.pipelines.visual_bell_pipeline);
516                render_pass.set_bind_group(0, &self.pipelines.visual_bell_bind_group, &[]);
517                render_pass.draw(0..4, 0..1); // 4 vertices = triangle strip quad
518            }
519        }
520
521        self.queue.submit(std::iter::once(encoder.finish()));
522        Ok(())
523    }
524
525    /// Stamp alpha=1.0 over the entire surface without modifying RGB values.
526    ///
527    /// On macOS with `CompositeAlphaMode::PreMultiplied`, any framebuffer pixel with
528    /// alpha < 1.0 becomes translucent through to the desktop. Multiple rendering
529    /// passes (anti-aliased text, overlay compositing) can inadvertently reduce alpha.
530    /// This single full-screen triangle guarantees an opaque surface.
531    ///
532    /// Skipped when `window_opacity < 1.0` so that user-configured transparency works.
533    pub fn render_opaque_alpha(&self, surface_texture: &wgpu::SurfaceTexture) -> Result<()> {
534        // Checked before creating the view, not only inside the delegate: this runs
535        // once per frame, and a transparent window would otherwise allocate and drop
536        // a `TextureView` on the hot path for a call that does nothing.
537        if self.window_opacity < 1.0 {
538            return Ok(());
539        }
540        let view = surface_texture
541            .texture
542            .create_view(&wgpu::TextureViewDescriptor::default());
543        self.render_opaque_alpha_to_view(&view)
544    }
545
546    /// [`CellRenderer::render_opaque_alpha`] against an arbitrary target view.
547    ///
548    /// QA-011: the offscreen screenshot target needs the same alpha stamp as the
549    /// surface, or a capture of an opaque window reads back semi-transparent
550    /// wherever anti-aliased text reduced alpha.
551    pub fn render_opaque_alpha_to_view(&self, view: &wgpu::TextureView) -> Result<()> {
552        if self.window_opacity < 1.0 {
553            return Ok(());
554        }
555
556        let mut encoder = self
557            .device
558            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
559                label: Some("opaque alpha encoder"),
560            });
561
562        {
563            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
564                label: Some("opaque alpha pass"),
565                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
566                    view,
567                    resolve_target: None,
568                    ops: wgpu::Operations {
569                        load: wgpu::LoadOp::Load,
570                        store: wgpu::StoreOp::Store,
571                    },
572                    depth_slice: None,
573                })],
574                depth_stencil_attachment: None,
575                timestamp_writes: None,
576                occlusion_query_set: None,
577                multiview_mask: None,
578            });
579
580            render_pass.set_pipeline(&self.pipelines.opaque_alpha_pipeline);
581            render_pass.draw(0..3, 0..1);
582        }
583
584        self.queue.submit(std::iter::once(encoder.finish()));
585        Ok(())
586    }
587}
588
589#[cfg(test)]
590mod tests {
591    use super::*;
592
593    /// Position of a phase within the draw sequence.
594    fn phase_index(ranges: &ThreePhaseRanges, phase: CellDrawPhase) -> usize {
595        ranges
596            .draw_sequence()
597            .iter()
598            .position(|(candidate, _)| *candidate == phase)
599            .unwrap_or_else(|| panic!("{phase:?} is missing from the draw sequence"))
600    }
601
602    /// A pane's ranges as `build_pane_instance_buffers` returns them: the viewport
603    /// fill, its cell backgrounds and separators, then the overlays, then the
604    /// pane's text region in the other buffer.
605    fn pane_ranges(bg_base: usize, bg_len: usize, overlays: usize) -> ThreePhaseRanges {
606        let bg = bg_base..bg_base + bg_len;
607        ThreePhaseRanges::for_pane(bg.clone(), bg.end..bg.end + overlays, 40..90)
608    }
609
610    /// The invariant: cursor overlays are emitted after the text, or a beam or
611    /// underline cursor is hidden under the glyphs.
612    ///
613    /// `emit_three_phase_draw_calls` walks `draw_sequence()`, so this holds it to
614    /// the order the GPU actually receives without needing an adapter.
615    #[test]
616    fn cursor_overlays_are_drawn_after_text() {
617        let single_grid = ThreePhaseRanges::for_single_grid(
618            &SingleGridLayout::new(80, 24),
619            80 * 24 + CURSOR_OVERLAY_SLOTS + 24 + 24,
620            80 * 24 * 2,
621        );
622        let pane = pane_ranges(0, 1 + 80 * 24, CURSOR_OVERLAY_SLOTS);
623
624        for ranges in [single_grid, pane] {
625            assert!(
626                phase_index(&ranges, CellDrawPhase::CursorOverlays)
627                    > phase_index(&ranges, CellDrawPhase::Text),
628                "cursor overlays must be emitted after the text: {ranges:?}"
629            );
630        }
631    }
632
633    /// Backgrounds come first, and phase 1b stays on the background side of the
634    /// text so separators and gutters cannot cover a glyph.
635    #[test]
636    fn backgrounds_are_drawn_before_text() {
637        let ranges = ThreePhaseRanges::for_single_grid(
638            &SingleGridLayout::new(10, 4),
639            SingleGridLayout::new(10, 4).bg_instances(),
640            80,
641        );
642        let text = phase_index(&ranges, CellDrawPhase::Text);
643        assert!(phase_index(&ranges, CellDrawPhase::Background) < text);
644        assert!(phase_index(&ranges, CellDrawPhase::ExtraBackground) < text);
645    }
646
647    /// Overlays are drawn with the background pipeline but after the text, which
648    /// is exactly why the phase cannot be folded back into phase 1.
649    #[test]
650    fn overlays_use_the_background_pipeline_in_a_later_phase() {
651        assert_eq!(
652            CellDrawPhase::CursorOverlays.pipeline(),
653            CellDrawPhase::Background.pipeline()
654        );
655        assert_ne!(
656            CellDrawPhase::CursorOverlays.pipeline(),
657            CellDrawPhase::Text.pipeline()
658        );
659    }
660
661    /// Every phase appears exactly once, so no draw is silently dropped or doubled.
662    #[test]
663    fn the_sequence_covers_each_phase_once() {
664        let ranges = pane_ranges(7, 30, CURSOR_OVERLAY_SLOTS);
665        let sequence = ranges.draw_sequence();
666        for phase in [
667            CellDrawPhase::Background,
668            CellDrawPhase::ExtraBackground,
669            CellDrawPhase::Text,
670            CellDrawPhase::CursorOverlays,
671        ] {
672            assert_eq!(
673                sequence
674                    .iter()
675                    .filter(|(candidate, _)| *candidate == phase)
676                    .count(),
677                1,
678                "{phase:?} appears more than once"
679            );
680        }
681    }
682
683    /// The four single-grid regions tile the background buffer without gaps or
684    /// overlaps, in the order the builder writes them.
685    #[test]
686    fn the_single_grid_regions_tile_the_background_buffer() {
687        let (cols, rows) = (80usize, 24usize);
688        let layout = SingleGridLayout::new(cols, rows);
689
690        assert_eq!(layout.cells, 0..cols * rows);
691        assert_eq!(layout.cursor_overlays.start, layout.cells.end);
692        assert_eq!(layout.cursor_overlays.len(), CURSOR_OVERLAY_SLOTS);
693        assert_eq!(layout.separators.start, layout.cursor_overlays.end);
694        assert_eq!(layout.separators.len(), rows);
695        assert_eq!(layout.gutters.start, layout.separators.end);
696        assert_eq!(layout.gutters.len(), rows);
697        assert_eq!(
698            layout.bg_instances(),
699            cols * rows + CURSOR_OVERLAY_SLOTS + rows + rows
700        );
701    }
702
703    /// The cell quads and the overlays must not address the same instances, or
704    /// the overlays would redraw cell backgrounds over the text.
705    #[test]
706    fn single_grid_overlays_do_not_overlap_the_cells() {
707        let layout = SingleGridLayout::new(80, 24);
708        let ranges = ThreePhaseRanges::for_single_grid(&layout, layout.bg_instances(), 3840);
709
710        assert!(ranges.bg.end <= ranges.cursor_overlays.start);
711        assert!(ranges.extra_bg.start >= ranges.cursor_overlays.end);
712        assert_eq!(ranges.cursor_overlays.len(), CURSOR_OVERLAY_SLOTS);
713    }
714
715    /// A grid whose instances have never been built reports zero used instances;
716    /// phase 1b must come out empty rather than inverted.
717    #[test]
718    fn an_unbuilt_single_grid_yields_an_empty_extra_phase() {
719        let layout = SingleGridLayout::new(80, 24);
720        let ranges = ThreePhaseRanges::for_single_grid(&layout, 0, 0);
721        assert!(ranges.extra_bg.is_empty());
722        assert!(ranges.extra_bg.start <= ranges.extra_bg.end);
723    }
724
725    /// Panes suballocate, so their ranges are absolute and phase 1b is unused:
726    /// the pane builder packs separators before the overlays.
727    #[test]
728    fn a_pane_packs_its_overlays_directly_after_its_backgrounds() {
729        let ranges = pane_ranges(500, 120, CURSOR_OVERLAY_SLOTS);
730
731        assert_eq!(ranges.bg, 500..620);
732        assert_eq!(ranges.cursor_overlays.start, ranges.bg.end);
733        assert!(
734            ranges.extra_bg.is_empty(),
735            "the pane path has nothing to draw after its overlays"
736        );
737    }
738
739    /// A pane with no cursor emits no overlay instances; the empty range must be
740    /// skipped rather than issuing a zero-instance draw.
741    #[test]
742    fn a_pane_without_a_cursor_has_an_empty_overlay_phase() {
743        let ranges = pane_ranges(0, 60, 0);
744        let sequence = ranges.draw_sequence();
745        let drawn: Vec<CellDrawPhase> = sequence
746            .iter()
747            .filter(|(_, range)| !range.is_empty())
748            .map(|(phase, _)| *phase)
749            .collect();
750        assert_eq!(drawn, vec![CellDrawPhase::Background, CellDrawPhase::Text]);
751    }
752
753    /// With a background shader active the pane skips its viewport fill, and a
754    /// screenful of default-background cells emits no background quads either —
755    /// so phase 1 is empty while the cursor overlays are not.
756    ///
757    /// The emitter binds pipeline state lazily on the first non-empty phase, so
758    /// this is the configuration where an overlay would be the first thing drawn.
759    /// It must still come after the text.
760    #[test]
761    fn a_shaded_pane_with_no_background_quads_still_draws_overlays_last() {
762        let ranges = ThreePhaseRanges::for_pane(300..300, 300..300 + CURSOR_OVERLAY_SLOTS, 90..140);
763        assert!(ranges.bg.is_empty(), "the shader path emits no bg quads");
764
765        let sequence = ranges.draw_sequence();
766        let drawn: Vec<CellDrawPhase> = sequence
767            .iter()
768            .filter(|(_, range)| !range.is_empty())
769            .map(|(phase, _)| *phase)
770            .collect();
771        assert_eq!(
772            drawn,
773            vec![CellDrawPhase::Text, CellDrawPhase::CursorOverlays],
774            "the overlays must follow the text even when nothing precedes them"
775        );
776
777        // The two phases need different pipelines, so the emitter rebinds
778        // between them rather than reusing whatever the caller left bound.
779        assert_ne!(
780            CellDrawPhase::Text.pipeline(),
781            CellDrawPhase::CursorOverlays.pipeline()
782        );
783    }
784}