Skip to main content

beamterm_core/gl/
terminal_grid.rs

1use std::{cmp::min, fmt::Debug};
2
3use beamterm_data::{FontAtlasData, FontStyle, Glyph, GlyphEffect};
4use compact_str::CompactString;
5use glow::HasContext;
6
7use crate::{
8    CursorPosition,
9    error::Error,
10    gl::{
11        CellIterator, CellQuery, Drawable, GlState, RenderContext, ShaderProgram,
12        atlas::{FontAtlas, GlyphSlot},
13        buffer_upload_array,
14        selection::SelectionTracker,
15        ubo::UniformBufferObject,
16    },
17    mat4::Mat4,
18};
19
20/// A high-performance terminal grid renderer using instanced rendering.
21///
22/// `TerminalGrid` renders a grid of terminal cells using GL instanced drawing.
23/// Each cell can display a character from a font atlas with customizable foreground
24/// and background colors. The renderer uses a 2D texture array to efficiently
25/// store glyph data and supports real-time updates of cell content.
26#[derive(Debug)]
27#[must_use = "call `delete(gl)` before dropping to avoid GPU resource leaks"]
28pub struct TerminalGrid {
29    /// GPU resources (shader, buffers, UBOs) - recreated on context loss
30    gpu: GpuResources,
31    /// Terminal cell instance data
32    cells: Vec<CellDynamic>,
33    /// Terminal size in cells
34    terminal_size: (u16, u16),
35    /// Size of the canvas in pixels (physical)
36    canvas_size_px: (i32, i32),
37    /// Current device pixel ratio
38    pixel_ratio: f32,
39    /// Font atlas for rendering text.
40    atlas: FontAtlas,
41    /// Fallback glyph for missing symbols.
42    fallback_glyph: u16,
43    /// Selection tracker for managing cell selections.
44    selection: SelectionTracker,
45    /// Indicates whether there are cells pending flush to the GPU.
46    cells_pending_flush: bool,
47    /// Background cell opacity (0.0 = fully transparent, 1.0 = fully opaque).
48    bg_alpha: f32,
49}
50
51/// GPU resources that need to be recreated after a context loss.
52///
53/// This struct encapsulates all GL-dependent resources: shader program,
54/// vertex buffers, uniform buffer objects, and uniform locations. These
55/// resources become invalid after a context loss and must be recreated
56/// with a fresh GL context.
57#[derive(Debug)]
58struct GpuResources {
59    /// Shader program for rendering the terminal cells.
60    shader: ShaderProgram,
61    /// Buffers for the terminal grid (VAO, VBO, instance buffers)
62    buffers: TerminalBuffers,
63    /// Shared state for the vertex shader
64    ubo_vertex: UniformBufferObject,
65    /// Shared state for the fragment shader
66    ubo_fragment: UniformBufferObject,
67    /// Uniform location for the texture sampler.
68    sampler_loc: glow::UniformLocation,
69}
70
71impl GpuResources {
72    const FRAGMENT_GLSL: &'static str = include_str!("../shaders/cell.frag");
73    const VERTEX_GLSL: &'static str = include_str!("../shaders/cell.vert");
74
75    fn delete(&self, gl: &glow::Context) {
76        self.shader.delete(gl);
77        self.buffers.delete(gl);
78        self.ubo_vertex.delete(gl);
79        self.ubo_fragment.delete(gl);
80    }
81
82    /// Creates all GPU resources for the terminal grid.
83    ///
84    /// This method creates and initializes:
85    /// - Vertex Array Object (VAO)
86    /// - Vertex and index buffers
87    /// - Instance buffers for cell positions and data
88    /// - Shader program
89    /// - Uniform Buffer Objects (UBOs)
90    fn new(
91        gl: &glow::Context,
92        cell_pos: &[CellStatic],
93        cell_data: &[CellDynamic],
94        cell_size: (i32, i32),
95        glsl_version: &crate::GlslVersion,
96    ) -> Result<Self, Error> {
97        // Create and setup the Vertex Array Object
98        let vao =
99            unsafe { gl.create_vertex_array() }.map_err(Error::vertex_array_creation_failed)?;
100        unsafe { gl.bind_vertex_array(Some(vao)) };
101
102        // Create all buffers
103        let buffers = setup_buffers(gl, vao, cell_pos, cell_data, cell_size)?;
104
105        // Unbind VAO to prevent accidental modification
106        unsafe { gl.bind_vertex_array(None) };
107
108        // Setup shader and uniform data with version-injected sources
109        let vertex_source = format!("{}{}", glsl_version.vertex_preamble(), Self::VERTEX_GLSL);
110        let fragment_source = format!(
111            "{}{}",
112            glsl_version.fragment_preamble(),
113            Self::FRAGMENT_GLSL
114        );
115        let shader = ShaderProgram::create(gl, &vertex_source, &fragment_source)?;
116        shader.use_program(gl);
117
118        let ubo_vertex = UniformBufferObject::new(gl, CellVertexUbo::BINDING_POINT)?;
119        ubo_vertex.bind_to_shader(gl, &shader, "VertUbo")?;
120        let ubo_fragment = UniformBufferObject::new(gl, CellFragmentUbo::BINDING_POINT)?;
121        ubo_fragment.bind_to_shader(gl, &shader, "FragUbo")?;
122
123        let sampler_loc = unsafe { gl.get_uniform_location(shader.program, "u_sampler") }
124            .ok_or(Error::uniform_location_failed("u_sampler"))?;
125
126        Ok(Self {
127            shader,
128            buffers,
129            ubo_vertex,
130            ubo_fragment,
131            sampler_loc,
132        })
133    }
134}
135
136#[derive(Debug)]
137struct TerminalBuffers {
138    vao: glow::VertexArray,
139    vertices: glow::Buffer,
140    instance_pos: glow::Buffer,
141    instance_cell: glow::Buffer,
142    indices: glow::Buffer,
143}
144
145impl TerminalBuffers {
146    fn delete(&self, gl: &glow::Context) {
147        unsafe {
148            gl.delete_vertex_array(self.vao);
149            gl.delete_buffer(self.vertices);
150            gl.delete_buffer(self.instance_pos);
151            gl.delete_buffer(self.instance_cell);
152            gl.delete_buffer(self.indices);
153        }
154    }
155
156    fn upload_instance_data<T: Copy>(&self, gl: &glow::Context, cell_data: &[T]) {
157        unsafe {
158            gl.bind_vertex_array(Some(self.vao));
159            gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_cell));
160        }
161
162        unsafe { buffer_upload_array(gl, glow::ARRAY_BUFFER, cell_data, glow::DYNAMIC_DRAW) };
163
164        unsafe { gl.bind_vertex_array(None) };
165    }
166
167    /// Updates the vertex buffer with new cell dimensions.
168    fn update_vertex_buffer(&self, gl: &glow::Context, cell_size: (i32, i32)) {
169        let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
170
171        #[rustfmt::skip]
172        let vertices: [f32; 16] = [
173            //x    y    u    v
174              w, 0.0, 1.0, 0.0, // top-right
175            0.0,   h, 0.0, 1.0, // bottom-left
176              w,   h, 1.0, 1.0, // bottom-right
177            0.0, 0.0, 0.0, 0.0  // top-left
178        ];
179
180        unsafe {
181            gl.bind_vertex_array(Some(self.vao));
182            gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertices));
183            let bytes = std::slice::from_raw_parts(
184                vertices.as_ptr() as *const u8,
185                vertices.len() * size_of::<f32>(),
186            );
187            gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, bytes);
188            gl.bind_vertex_array(None);
189        }
190    }
191}
192
193impl TerminalGrid {
194    pub fn new(
195        gl: &glow::Context,
196        atlas: FontAtlas,
197        screen_size: (i32, i32),
198        pixel_ratio: f32,
199        glsl_version: &crate::GlslVersion,
200    ) -> Result<Self, Error> {
201        let cell_scale = atlas.cell_scale_for_dpr(pixel_ratio);
202        let base_cell_size = atlas.cell_size();
203        let cell_size = (
204            (base_cell_size.0 as f32 * cell_scale).round() as i32,
205            (base_cell_size.1 as f32 * cell_scale).round() as i32,
206        );
207        let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
208
209        let space_glyph = atlas.space_glyph_id();
210        let cell_data = create_terminal_cell_data(cols, rows, space_glyph);
211        let cell_pos = CellStatic::create_grid(cols, rows);
212
213        let grid = Self {
214            gpu: GpuResources::new(gl, &cell_pos, &cell_data, cell_size, glsl_version)?,
215            terminal_size: (cols as u16, rows as u16),
216            canvas_size_px: screen_size,
217            pixel_ratio,
218            cells: cell_data,
219            atlas,
220            fallback_glyph: space_glyph,
221            selection: SelectionTracker::new(),
222            cells_pending_flush: false,
223            bg_alpha: 1.0,
224        };
225
226        grid.upload_ubo_data(gl);
227
228        Ok(grid)
229    }
230
231    /// Deletes all GPU resources owned by this terminal grid.
232    ///
233    /// This must be called before dropping the `TerminalGrid` to avoid GPU
234    /// resource leaks on native OpenGL targets. On WASM, WebGL context teardown
235    /// handles cleanup automatically, but explicit deletion is still recommended.
236    pub fn delete(self, gl: &glow::Context) {
237        self.gpu.delete(gl);
238        self.atlas.delete(gl);
239    }
240
241    /// Returns the effective cell size for layout (base cell size * cell scale).
242    fn effective_cell_size(&self) -> (i32, i32) {
243        let cell_scale = self.atlas.cell_scale_for_dpr(self.pixel_ratio);
244        let base = self.atlas.cell_size();
245        (
246            (base.0 as f32 * cell_scale).round() as i32,
247            (base.1 as f32 * cell_scale).round() as i32,
248        )
249    }
250
251    /// Sets the fallback glyph for missing characters.
252    pub fn set_fallback_glyph(&mut self, fallback: &str) {
253        self.fallback_glyph = self
254            .atlas
255            .get_glyph_id(fallback, FontStyle::Normal as u16)
256            .unwrap_or(' ' as u16);
257    }
258
259    /// Replaces the current font atlas with a new one, translating all existing
260    /// glyph IDs to the new atlas.
261    ///
262    /// This method handles the transition between atlases by:
263    /// 1. Looking up the symbol for each existing glyph ID in the old atlas
264    /// 2. Resolving the corresponding glyph slot in the new atlas
265    /// 3. Updating double-width glyphs (emoji, wide chars) across both cells
266    /// 4. Resizing the grid if cell dimensions changed
267    pub fn replace_atlas(&mut self, gl: &glow::Context, atlas: FontAtlas) {
268        let glyph_mask = self.atlas.base_lookup_mask() as u16;
269        let style_mask = !glyph_mask;
270
271        // update fallback glyph to new atlas, before translating existing cells
272        self.fallback_glyph = self
273            .atlas
274            .get_symbol(self.fallback_glyph & glyph_mask)
275            .and_then(|symbol| {
276                let style_bits = self.fallback_glyph & style_mask;
277                atlas.resolve_glyph_slot(symbol.as_str(), style_bits)
278            })
279            .map(|slot| slot.slot_id())
280            .unwrap_or(atlas.space_glyph_id());
281
282        // translate existing glyph ids to new atlas
283        let mut skip_next = false;
284        for idx in 0..self.cells.len() {
285            if skip_next {
286                skip_next = false;
287                continue;
288            }
289
290            let old_glyph_id = self.cells[idx].glyph_id();
291            let style_bits = old_glyph_id & style_mask;
292
293            let slot = self
294                .atlas
295                .get_symbol(old_glyph_id & glyph_mask)
296                .and_then(|symbol| atlas.resolve_glyph_slot(symbol.as_str(), style_bits));
297
298            match slot {
299                Some(GlyphSlot::Normal(id)) => {
300                    self.cells[idx].set_glyph_id(id);
301                },
302                Some(GlyphSlot::Wide(id)) | Some(GlyphSlot::Emoji(id)) => {
303                    self.cells[idx].set_glyph_id(id);
304                    // update right-half in next cell if within bounds
305                    if let Some(next_cell) = self.cells.get_mut(idx + 1) {
306                        next_cell.set_glyph_id(id + 1);
307                        skip_next = true;
308                    }
309                },
310                None => {
311                    self.cells[idx].set_glyph_id(self.fallback_glyph);
312                },
313            }
314        }
315
316        // clear any active selection, just to keep it simple
317        self.selection.clear();
318
319        // replace atlas and resize grid accordingly
320        let old_atlas = std::mem::replace(&mut self.atlas, atlas);
321        old_atlas.delete(gl);
322        self.cells_pending_flush = true;
323
324        // update vertex buffer with new cell dimensions
325        self.gpu
326            .buffers
327            .update_vertex_buffer(gl, self.effective_cell_size());
328
329        let _ = self.resize(gl, self.canvas_size_px, self.pixel_ratio);
330    }
331
332    /// Returns the [`FontAtlas`] used by this terminal grid.
333    pub fn atlas(&self) -> &FontAtlas {
334        &self.atlas
335    }
336
337    /// Returns a mutable reference to the font atlas.
338    pub fn atlas_mut(&mut self) -> &mut FontAtlas {
339        &mut self.atlas
340    }
341
342    /// Sets the background opacity for terminal cells.
343    ///
344    /// When less than 1.0, cell backgrounds become semi-transparent, allowing
345    /// content rendered behind the terminal grid to show through. Text remains
346    /// fully opaque regardless of this setting.
347    ///
348    /// The change takes effect on the next call to [`resize`](Self::resize) or
349    /// when the UBO data is next uploaded.
350    pub fn set_bg_alpha(&mut self, gl: &glow::Context, alpha: f32) {
351        self.bg_alpha = alpha.clamp(0.0, 1.0);
352        self.upload_ubo_data(gl);
353    }
354
355    /// Returns the canvas size in pixels.
356    pub fn canvas_size(&self) -> (i32, i32) {
357        self.canvas_size_px
358    }
359
360    /// Returns the effective cell dimensions in pixels (base size * cell scale).
361    pub fn cell_size(&self) -> (i32, i32) {
362        self.effective_cell_size()
363    }
364
365    /// Returns the size of the terminal grid in cells.
366    pub fn terminal_size(&self) -> (u16, u16) {
367        self.terminal_size
368    }
369
370    /// Renders the terminal grid in a single call.
371    ///
372    /// This is a convenience method that constructs a [`RenderContext`] and
373    /// executes the full [`Drawable`] lifecycle (`prepare` → `draw` → `cleanup`).
374    /// For advanced use cases such as compositing with other GL content, use the
375    /// [`Drawable`] trait methods directly.
376    pub fn render(&self, gl: &glow::Context, state: &mut GlState) -> Result<(), crate::Error> {
377        let mut ctx = RenderContext { gl, state };
378        self.prepare(&mut ctx)?;
379        self.draw(&mut ctx);
380        self.cleanup(&mut ctx);
381        Ok(())
382    }
383
384    /// Returns a mutable reference to the cell data at the specified cell coordinates.
385    pub fn cell_data_mut(&mut self, x: u16, y: u16) -> Option<&mut CellDynamic> {
386        let (cols, _) = self.terminal_size;
387        let idx = y as usize * cols as usize + x as usize;
388        self.cells_pending_flush = true;
389        self.cells.get_mut(idx)
390    }
391
392    /// Returns the active selection state of the terminal grid.
393    pub fn selection_tracker(&self) -> SelectionTracker {
394        self.selection.clone()
395    }
396
397    /// Returns the symbols in the specified block range as a `CompactString`.
398    pub(super) fn get_symbols(&self, selection: CellIterator) -> CompactString {
399        let mut text = CompactString::new("");
400
401        for (idx, require_newline_after) in selection {
402            let cell_symbol = self.get_cell_symbol(idx);
403            if cell_symbol.is_some() {
404                text.push_str(&cell_symbol.unwrap_or_default());
405            }
406
407            if require_newline_after {
408                text.push('\n'); // add newline after each row
409            }
410        }
411
412        text
413    }
414
415    /// Returns the ASCII character at the given position, if it's an ASCII char.
416    ///
417    /// Returns `None` for non-ASCII characters or out-of-bounds positions.
418    /// This is an optimized path for URL detection that avoids string allocation.
419    pub(crate) fn get_ascii_char_at(&self, cursor: CursorPosition) -> Option<char> {
420        let idx = cursor.row as usize * self.terminal_size.0 as usize + cursor.col as usize;
421        if idx < self.cells.len() {
422            let glyph_id = self.cells[idx].glyph_id();
423            self.atlas.get_ascii_char(glyph_id)
424        } else {
425            None
426        }
427    }
428
429    pub fn hash_cells(&self, selection: CellQuery) -> u64 {
430        use std::hash::{Hash, Hasher};
431
432        use rustc_hash::FxHasher;
433
434        let mut hasher = FxHasher::default();
435        for (idx, _) in self.cell_iter(selection) {
436            self.cells[idx].hash(&mut hasher);
437        }
438
439        hasher.finish()
440    }
441
442    fn get_cell_symbol(&self, idx: usize) -> Option<CompactString> {
443        if idx < self.cells.len() {
444            let glyph_id = self.cells[idx].glyph_id();
445            let cell_symbol = self.atlas.get_symbol(glyph_id);
446            if cell_symbol.is_some() {
447                return cell_symbol;
448            }
449        }
450
451        self.fallback_symbol()
452    }
453
454    /// Uploads uniform buffer data for screen and cell dimensions.
455    fn upload_ubo_data(&self, gl: &glow::Context) {
456        let vertex_ubo = CellVertexUbo::new(self.canvas_size_px, self.effective_cell_size());
457        self.gpu.ubo_vertex.upload_data(gl, &vertex_ubo);
458
459        let fragment_ubo = CellFragmentUbo::new(&self.atlas, self.bg_alpha);
460        self.gpu
461            .ubo_fragment
462            .upload_data(gl, &fragment_ubo);
463    }
464
465    /// Returns the total number of cells in the terminal grid.
466    pub fn cell_count(&self) -> usize {
467        self.cells.len()
468    }
469
470    /// Updates the content of terminal cells with new data.
471    pub fn update_cells<'a>(
472        &mut self,
473        cells: impl Iterator<Item = CellData<'a>>,
474    ) -> Result<(), Error> {
475        // update instance buffer with new cell data
476        let atlas = &self.atlas;
477
478        let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
479
480        // handle double-width emoji that span two cells
481        let mut pending_cell: Option<CellDynamic> = None;
482        self.cells
483            .iter_mut()
484            .zip(cells)
485            .for_each(|(cell, data)| {
486                let glyph = atlas
487                    .resolve_glyph_slot(data.symbol, data.style_bits)
488                    .unwrap_or(fallback_glyph);
489
490                *cell = if let Some(second_cell) = pending_cell.take() {
491                    second_cell
492                } else {
493                    match glyph {
494                        GlyphSlot::Normal(id) => CellDynamic::new(id, data.fg, data.bg),
495
496                        GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
497                            // storing a double-width glyph, reserve next cell with right-half id
498                            pending_cell = Some(CellDynamic::new(id + 1, data.fg, data.bg));
499                            CellDynamic::new(id, data.fg, data.bg)
500                        },
501                    }
502                }
503            });
504
505        self.cells_pending_flush = true;
506        Ok(())
507    }
508
509    pub fn update_cells_by_position<'a>(
510        &mut self,
511        cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
512    ) -> Result<(), Error> {
513        let cols = self.terminal_size.0 as usize;
514        let cells_by_index = cells.map(|(x, y, data)| (y as usize * cols + x as usize, data));
515
516        self.update_cells_by_index(cells_by_index)
517    }
518
519    pub fn update_cells_by_index<'a>(
520        &mut self,
521        cells: impl Iterator<Item = (usize, CellData<'a>)>,
522    ) -> Result<(), Error> {
523        // update instance buffer with new cell data by position
524        let atlas = &self.atlas;
525
526        let cell_count = self.cells.len();
527        let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
528
529        // ratatui and beamterm can disagree on which emoji
530        // are double-width (beamterm assumes double-width for all emoji),
531        // so for ratatui and similar clients we need to skip the next cell
532        // if we just wrote a double-width emoji in the current cell.
533        let mut skip_idx = None;
534
535        cells
536            .filter(|(idx, _)| *idx < cell_count)
537            .for_each(|(idx, cell)| {
538                if skip_idx.take() == Some(idx) {
539                    // skip this cell, already handled as part of previous double-width emoji
540                    return;
541                }
542
543                let glyph = atlas
544                    .resolve_glyph_slot(cell.symbol, cell.style_bits)
545                    .unwrap_or(fallback_glyph);
546
547                match glyph {
548                    GlyphSlot::Normal(id) => {
549                        self.cells[idx] = CellDynamic::new(id, cell.fg, cell.bg);
550                    },
551
552                    GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
553                        // render left half in current cell
554                        self.cells[idx] = CellDynamic::new(id, cell.fg, cell.bg);
555
556                        // render right half in next cell, if within bounds
557                        if let Some(c) = self.cells.get_mut(idx + 1) {
558                            *c = CellDynamic::new(id + 1, cell.fg, cell.bg);
559                            skip_idx = Some(idx + 1);
560                        }
561                    },
562                }
563            });
564
565        self.cells_pending_flush = true;
566
567        Ok(())
568    }
569
570    pub fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) -> Result<(), Error> {
571        let (cols, _) = self.terminal_size;
572        let idx = y as usize * cols as usize + x as usize;
573        self.update_cell_by_index(idx, cell_data)
574    }
575
576    pub fn update_cell_by_index(&mut self, idx: usize, cell_data: CellData) -> Result<(), Error> {
577        self.update_cells_by_index(std::iter::once((idx, cell_data)))
578    }
579
580    /// Flushes pending cell updates to the GPU.
581    pub fn flush_cells(&mut self, gl: &glow::Context) -> Result<(), Error> {
582        if !self.cells_pending_flush {
583            return Ok(()); // no pending updates to flush
584        }
585
586        // if there is an active selected region with a content hash,
587        // check if the underlying content has changed; if so, clear the selection
588        self.clear_stale_selection();
589
590        // If there's an active selection, flip the colors of the selected cells.
591        // This ensures that the selected cells are rendered with inverted colors
592        // during the GPU upload process.
593        self.flip_selected_cell_colors();
594
595        self.gpu
596            .buffers
597            .upload_instance_data(gl, &self.cells);
598
599        // Restore the original colors of the selected cells after the upload.
600        // This ensures that the internal state of the cells remains consistent.
601        self.flip_selected_cell_colors();
602
603        self.cells_pending_flush = false;
604        Ok(())
605    }
606
607    fn flip_selected_cell_colors(&mut self) {
608        if let Some(iter) = self.selected_cells_iter() {
609            iter.for_each(|(idx, _)| self.cells[idx].flip_colors());
610        }
611    }
612
613    fn selected_cells_iter(&self) -> Option<CellIterator> {
614        self.selection
615            .get_query()
616            .map(|query| self.cell_iter(query))
617    }
618
619    /// Resizes the terminal grid to fit the new canvas dimensions.
620    pub fn resize(
621        &mut self,
622        gl: &glow::Context,
623        canvas_size: (i32, i32),
624        pixel_ratio: f32,
625    ) -> Result<(), Error> {
626        self.canvas_size_px = canvas_size;
627        self.pixel_ratio = pixel_ratio;
628
629        let cell_size = self.effective_cell_size();
630
631        // Update vertex buffer with new cell dimensions
632        self.gpu
633            .buffers
634            .update_vertex_buffer(gl, cell_size);
635
636        // Update the UBO with new screen size
637        self.upload_ubo_data(gl);
638
639        let cols = (canvas_size.0 / cell_size.0).max(1);
640        let rows = (canvas_size.1 / cell_size.1).max(1);
641        if self.terminal_size == (cols as u16, rows as u16) {
642            return Ok(()); // no change in terminal size
643        }
644
645        // update buffers; bind VAO to ensure correct state
646        unsafe {
647            gl.bind_vertex_array(Some(self.gpu.buffers.vao));
648
649            // delete old cell instance buffers
650            gl.delete_buffer(self.gpu.buffers.instance_cell);
651            gl.delete_buffer(self.gpu.buffers.instance_pos);
652        }
653
654        // resize cell data vector
655        let current_size = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
656        let cell_data = self.resize_cell_grid(current_size, (cols, rows));
657        self.cells = cell_data;
658
659        let cell_pos = CellStatic::create_grid(cols, rows);
660
661        // re-create buffers with new data
662        self.gpu.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
663        self.gpu.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
664
665        // unbind VAO
666        unsafe { gl.bind_vertex_array(None) };
667
668        self.terminal_size = (cols as u16, rows as u16);
669
670        Ok(())
671    }
672
673    /// Recreates all GPU resources after a context loss.
674    ///
675    /// Note: After a context loss, old GL resources are already invalid,
676    /// so we skip explicit deletion and just create fresh resources.
677    pub fn recreate_resources(
678        &mut self,
679        gl: &glow::Context,
680        glsl_version: &crate::GlslVersion,
681    ) -> Result<(), Error> {
682        let cell_size = self.effective_cell_size();
683        let (cols, rows) = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
684        let cell_pos = CellStatic::create_grid(cols, rows);
685
686        // Recreate all GPU resources (old ones are invalid after context loss)
687        self.gpu = GpuResources::new(gl, &cell_pos, &self.cells, cell_size, glsl_version)?;
688
689        // Upload UBO data
690        self.upload_ubo_data(gl);
691
692        // Mark cells as needing flush to upload to new buffers
693        self.cells_pending_flush = true;
694
695        Ok(())
696    }
697
698    /// Recreates the font atlas texture after a context loss.
699    pub fn recreate_atlas_texture(&mut self, gl: &glow::Context) -> Result<(), Error> {
700        self.atlas.recreate_texture(gl)
701    }
702
703    /// Returns the base glyph identifier for a given symbol.
704    pub fn base_glyph_id(&self, symbol: &str) -> Option<u16> {
705        self.atlas.get_base_glyph_id(symbol)
706    }
707
708    fn fallback_symbol(&self) -> Option<CompactString> {
709        self.atlas.get_symbol(self.fallback_glyph)
710    }
711
712    fn clear_stale_selection(&self) {
713        if let Some(query) = self.selection_tracker().get_query()
714            && let Some(hash) = query.content_hash
715            && hash != self.hash_cells(query)
716        {
717            self.selection.clear();
718        }
719    }
720
721    fn resize_cell_grid(&self, old_size: (i32, i32), new_size: (i32, i32)) -> Vec<CellDynamic> {
722        let empty_cell = CellDynamic::new(self.atlas.space_glyph_id(), 0xFFFFFF, 0x000000);
723
724        let new_len = new_size.0 * new_size.1;
725        let mut new_cells = Vec::with_capacity(new_len as usize);
726        for _ in 0..new_len {
727            new_cells.push(empty_cell);
728        }
729
730        let cells = &self.cells;
731        for y in 0..min(old_size.1, new_size.1) {
732            for x in 0..min(old_size.0, new_size.0) {
733                let new_idx = (y * new_size.0 + x) as usize;
734                let old_idx = (y * old_size.0 + x) as usize;
735                new_cells[new_idx] = cells[old_idx];
736            }
737        }
738
739        new_cells
740    }
741}
742
743fn setup_buffers(
744    gl: &glow::Context,
745    vao: glow::VertexArray,
746    cell_pos: &[CellStatic],
747    cell_data: &[CellDynamic],
748    cell_size: (i32, i32),
749) -> Result<TerminalBuffers, Error> {
750    let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
751
752    #[rustfmt::skip]
753    let vertices = [
754        //x    y    u    v
755          w, 0.0, 1.0, 0.0, // top-right
756        0.0,   h, 0.0, 1.0, // bottom-left
757          w,   h, 1.0, 1.0, // bottom-right
758        0.0, 0.0, 0.0, 0.0  // top-left
759    ];
760    let indices = [0, 1, 2, 0, 3, 1];
761
762    Ok(TerminalBuffers {
763        vao,
764        vertices: create_buffer_f32(gl, glow::ARRAY_BUFFER, &vertices, glow::STATIC_DRAW)?,
765        instance_pos: create_static_instance_buffer(gl, cell_pos)?,
766        instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
767        indices: create_buffer_u8(gl, glow::ELEMENT_ARRAY_BUFFER, &indices, glow::STATIC_DRAW)?,
768    })
769}
770
771fn create_buffer_u8(
772    gl: &glow::Context,
773    target: u32,
774    data: &[u8],
775    usage: u32,
776) -> Result<glow::Buffer, Error> {
777    let buffer =
778        unsafe { gl.create_buffer() }.map_err(|e| Error::buffer_creation_failed("vbo-u8", e))?;
779    unsafe {
780        gl.bind_buffer(target, Some(buffer));
781        gl.buffer_data_u8_slice(target, data, usage);
782    }
783    Ok(buffer)
784}
785
786fn create_buffer_f32(
787    gl: &glow::Context,
788    target: u32,
789    data: &[f32],
790    usage: u32,
791) -> Result<glow::Buffer, Error> {
792    let buffer =
793        unsafe { gl.create_buffer() }.map_err(|e| Error::buffer_creation_failed("vbo-f32", e))?;
794
795    unsafe {
796        gl.bind_buffer(target, Some(buffer));
797        let bytes =
798            std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data));
799        gl.buffer_data_u8_slice(target, bytes, usage);
800    }
801
802    // vertex attributes
803    const STRIDE: i32 = (2 + 2) * 4; // 4 floats per vertex
804    enable_vertex_attrib(gl, attrib::POS, 2, glow::FLOAT, 0, STRIDE);
805    enable_vertex_attrib(gl, attrib::UV, 2, glow::FLOAT, 8, STRIDE);
806
807    Ok(buffer)
808}
809
810fn create_static_instance_buffer(
811    gl: &glow::Context,
812    instance_data: &[CellStatic],
813) -> Result<glow::Buffer, Error> {
814    let buffer = unsafe { gl.create_buffer() }
815        .map_err(|e| Error::buffer_creation_failed("static-instance-buffer", e))?;
816
817    unsafe {
818        gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer));
819        buffer_upload_array(gl, glow::ARRAY_BUFFER, instance_data, glow::STATIC_DRAW);
820    }
821
822    let stride = size_of::<CellStatic>() as i32;
823    enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, glow::UNSIGNED_SHORT, 0, stride);
824
825    Ok(buffer)
826}
827
828fn create_dynamic_instance_buffer(
829    gl: &glow::Context,
830    instance_data: &[CellDynamic],
831) -> Result<glow::Buffer, Error> {
832    let buffer = unsafe { gl.create_buffer() }
833        .map_err(|e| Error::buffer_creation_failed("dynamic-instance-buffer", e))?;
834
835    unsafe {
836        gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer));
837        buffer_upload_array(gl, glow::ARRAY_BUFFER, instance_data, glow::DYNAMIC_DRAW);
838    }
839
840    let stride = size_of::<CellDynamic>() as i32;
841
842    // setup instance attributes (while VAO is bound)
843    enable_vertex_attrib_array(
844        gl,
845        attrib::PACKED_DEPTH_FG_BG,
846        2,
847        glow::UNSIGNED_INT,
848        0,
849        stride,
850    );
851
852    Ok(buffer)
853}
854
855fn enable_vertex_attrib_array(
856    gl: &glow::Context,
857    index: u32,
858    size: i32,
859    type_: u32,
860    offset: i32,
861    stride: i32,
862) {
863    enable_vertex_attrib(gl, index, size, type_, offset, stride);
864    unsafe { gl.vertex_attrib_divisor(index, 1) };
865}
866
867fn enable_vertex_attrib(
868    gl: &glow::Context,
869    index: u32,
870    size: i32,
871    type_: u32,
872    offset: i32,
873    stride: i32,
874) {
875    unsafe {
876        gl.enable_vertex_attrib_array(index);
877        if type_ == glow::FLOAT {
878            gl.vertex_attrib_pointer_f32(index, size, type_, false, stride, offset);
879        } else {
880            gl.vertex_attrib_pointer_i32(index, size, type_, stride, offset);
881        }
882    }
883}
884
885impl Drawable for TerminalGrid {
886    fn prepare(&self, context: &mut RenderContext) -> Result<(), crate::Error> {
887        let gl = context.gl;
888
889        self.gpu.shader.use_program(gl);
890
891        unsafe { gl.bind_vertex_array(Some(self.gpu.buffers.vao)) };
892
893        context.state.active_texture(gl, glow::TEXTURE0);
894        self.atlas.bind(gl);
895        self.atlas.flush(gl)?;
896        self.gpu.ubo_vertex.bind(context.gl);
897        self.gpu.ubo_fragment.bind(context.gl);
898        unsafe { gl.uniform_1_i32(Some(&self.gpu.sampler_loc), 0) };
899
900        Ok(())
901    }
902
903    fn draw(&self, context: &mut RenderContext) {
904        let gl = context.gl;
905        let cell_count = self.cells.len() as i32;
906
907        unsafe {
908            gl.draw_elements_instanced(glow::TRIANGLES, 6, glow::UNSIGNED_BYTE, 0, cell_count);
909        }
910    }
911
912    fn cleanup(&self, context: &mut RenderContext) {
913        let gl = context.gl;
914        unsafe {
915            gl.bind_vertex_array(None);
916            gl.bind_texture(glow::TEXTURE_2D_ARRAY, None);
917            gl.use_program(None);
918        }
919
920        self.gpu.ubo_vertex.unbind(gl);
921        self.gpu.ubo_fragment.unbind(gl);
922    }
923}
924
925/// Data for a single terminal cell including character and colors.
926///
927/// `CellData` represents the visual content of one terminal cell, including
928/// the character to display and its foreground and background colors.
929/// Colors are specified as RGB values packed into 32-bit integers.
930///
931/// # Color Format
932/// Colors use the format 0xRRGGBB where:
933/// - RR: Red component
934/// - GG: Green component
935/// - BB: Blue component
936#[derive(Debug, Copy, Clone)]
937pub struct CellData<'a> {
938    symbol: &'a str,
939    style_bits: u16,
940    fg: u32,
941    bg: u32,
942}
943
944impl<'a> CellData<'a> {
945    /// Creates new cell data with the specified character and colors.
946    pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
947        Self::new_with_style_bits(symbol, style.style_mask() | effect as u16, fg, bg)
948    }
949
950    /// Creates new cell data with pre-encoded style bits.
951    pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
952        // emoji and glyph base mask should not intersect with style bits
953        debug_assert!(
954            0x81FF & style_bits == 0,
955            "Invalid style bits: {style_bits:#04x}"
956        );
957        Self { symbol, style_bits, fg, bg }
958    }
959}
960
961/// Static instance data for terminal cell positioning.
962#[derive(Clone, Copy)]
963#[repr(C, align(4))]
964struct CellStatic {
965    /// Grid position as (x, y) coordinates in cell units.
966    pub grid_xy: [u16; 2],
967}
968
969/// Dynamic instance data for terminal cell appearance.
970///
971/// `CellDynamic` contains the frequently-changing visual data for each terminal
972/// cell, including the character glyph and colors.
973///
974/// # Memory Layout
975/// The 8-byte data array is packed as follows:
976/// - Bytes 0-1: Glyph depth/layer index (u16, little-endian)
977/// - Bytes 2-4: Foreground color RGB (3 bytes)
978/// - Bytes 5-7: Background color RGB (3 bytes)
979#[derive(Debug, Clone, Copy, Hash)]
980#[repr(C, align(4))]
981pub struct CellDynamic {
982    /// Packed cell data:
983    ///
984    /// # Byte Layout
985    /// - `data[0]`: Lower 8 bits of glyph depth/layer index
986    /// - `data[1]`: Upper 8 bits of glyph depth/layer index
987    /// - `data[2]`: Foreground red component (0-255)
988    /// - `data[3]`: Foreground green component (0-255)
989    /// - `data[4]`: Foreground blue component (0-255)
990    /// - `data[5]`: Background red component (0-255)
991    /// - `data[6]`: Background green component (0-255)
992    /// - `data[7]`: Background blue component (0-255)
993    data: [u8; 8], // 2b layer, fg:rgb, bg:rgb
994}
995
996impl CellStatic {
997    fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
998        debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
999        debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
1000
1001        (0..rows)
1002            .flat_map(|row| (0..cols).map(move |col| (col, row)))
1003            .map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
1004            .collect()
1005    }
1006}
1007
1008impl CellDynamic {
1009    const GLYPH_STYLE_MASK: u16 =
1010        Glyph::BOLD_FLAG | Glyph::ITALIC_FLAG | Glyph::UNDERLINE_FLAG | Glyph::STRIKETHROUGH_FLAG;
1011
1012    #[inline]
1013    pub fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
1014        let mut data = [0; 8];
1015
1016        // pack glyph ID into the first two bytes
1017        let glyph_id = glyph_id.to_le_bytes();
1018        data[0] = glyph_id[0];
1019        data[1] = glyph_id[1];
1020
1021        let fg = fg.to_le_bytes();
1022        data[2] = fg[2]; // R
1023        data[3] = fg[1]; // G
1024        data[4] = fg[0]; // B
1025
1026        let bg = bg.to_le_bytes();
1027        data[5] = bg[2]; // R
1028        data[6] = bg[1]; // G
1029        data[7] = bg[0]; // B
1030
1031        Self { data }
1032    }
1033
1034    /// Overwrites the current cell style bits with the provided style bits.
1035    pub fn style(&mut self, style_bits: u16) {
1036        let glyph_id = (self.glyph_id() & !Self::GLYPH_STYLE_MASK) | style_bits;
1037        self.data[..2].copy_from_slice(&glyph_id.to_le_bytes());
1038    }
1039
1040    /// Swaps foreground and background colors.
1041    pub fn flip_colors(&mut self) {
1042        // swap foreground and background colors
1043        let fg = [self.data[2], self.data[3], self.data[4]];
1044        self.data[2] = self.data[5]; // R
1045        self.data[3] = self.data[6]; // G
1046        self.data[4] = self.data[7]; // B
1047        self.data[5] = fg[0]; // R
1048        self.data[6] = fg[1]; // G
1049        self.data[7] = fg[2]; // B
1050    }
1051
1052    /// Sets the foreground color of the cell.
1053    pub fn fg_color(&mut self, fg: u32) {
1054        let fg = fg.to_le_bytes();
1055        self.data[2] = fg[2]; // R
1056        self.data[3] = fg[1]; // G
1057        self.data[4] = fg[0]; // B
1058    }
1059
1060    /// Sets the background color of the cell.
1061    pub fn bg_color(&mut self, bg: u32) {
1062        let bg = bg.to_le_bytes();
1063        self.data[5] = bg[2]; // R
1064        self.data[6] = bg[1]; // G
1065        self.data[7] = bg[0]; // B
1066    }
1067
1068    /// Returns foreground color as a packed RGB value.
1069    pub fn get_fg_color(&self) -> u32 {
1070        // unpack foreground color from data
1071        ((self.data[2] as u32) << 16) | ((self.data[3] as u32) << 8) | (self.data[4] as u32)
1072    }
1073
1074    /// Returns background color as a packed RGB value.
1075    pub fn get_bg_color(&self) -> u32 {
1076        // unpack background color from data
1077        ((self.data[5] as u32) << 16) | ((self.data[6] as u32) << 8) | (self.data[7] as u32)
1078    }
1079
1080    /// Returns the style bits for this cell, excluding id and emoji bits.
1081    pub fn get_style(&self) -> u16 {
1082        self.glyph_id() & Self::GLYPH_STYLE_MASK
1083    }
1084
1085    /// Returns true if the glyph is an emoji.
1086    pub fn is_emoji(&self) -> bool {
1087        self.glyph_id() & Glyph::EMOJI_FLAG != 0
1088    }
1089
1090    #[inline]
1091    fn glyph_id(&self) -> u16 {
1092        u16::from_le_bytes([self.data[0], self.data[1]])
1093    }
1094
1095    fn set_glyph_id(&mut self, glyph_id: u16) {
1096        let bytes = glyph_id.to_le_bytes();
1097        self.data[0] = bytes[0];
1098        self.data[1] = bytes[1];
1099    }
1100}
1101
1102#[derive(Clone, Copy)]
1103#[repr(C, align(16))] // std140 layout requires proper alignment
1104struct CellVertexUbo {
1105    pub projection: [f32; 16], // mat4
1106    pub cell_size: [f32; 2],   // vec2 - screen cell size
1107    pub _padding: [f32; 2],
1108}
1109
1110#[derive(Clone, Copy)]
1111#[repr(C, align(16))] // std140 layout requires proper alignment
1112struct CellFragmentUbo {
1113    pub padding_frac: [f32; 2],       // padding as a fraction of cell size
1114    pub underline_pos: f32,           // underline position (0.0 = top, 1.0 = bottom)
1115    pub underline_thickness: f32,     // underline thickness as fraction of cell height
1116    pub strikethrough_pos: f32,       // strikethrough position (0.0 = top, 1.0 = bottom)
1117    pub strikethrough_thickness: f32, // strikethrough thickness as fraction of cell height
1118    pub texture_lookup_mask: u32,     // static atlas: 0x1FFF, dynamic atlas: 0x0FFF
1119    pub bg_alpha: f32,                // background cell opacity (0.0 = transparent, 1.0 = opaque)
1120}
1121
1122impl CellVertexUbo {
1123    pub const BINDING_POINT: u32 = 0;
1124
1125    fn new(canvas_size: (i32, i32), cell_size: (i32, i32)) -> Self {
1126        let projection =
1127            Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
1128        Self {
1129            projection,
1130            cell_size: [cell_size.0 as f32, cell_size.1 as f32],
1131            _padding: [0.0; 2], // padding to ensure proper alignment
1132        }
1133    }
1134}
1135
1136impl CellFragmentUbo {
1137    pub const BINDING_POINT: u32 = 1;
1138
1139    fn new(atlas: &FontAtlas, bg_alpha: f32) -> Self {
1140        // Use texture cell size for padding calculation (physical pixels in texture)
1141        let texture_cell_size = atlas.texture_cell_size();
1142        let underline = atlas.underline();
1143        let strikethrough = atlas.strikethrough();
1144
1145        Self {
1146            padding_frac: [
1147                FontAtlasData::PADDING as f32 / texture_cell_size.0 as f32,
1148                FontAtlasData::PADDING as f32 / texture_cell_size.1 as f32,
1149            ],
1150            underline_pos: underline.position,
1151            underline_thickness: underline.thickness,
1152            strikethrough_pos: strikethrough.position,
1153            strikethrough_thickness: strikethrough.thickness,
1154            texture_lookup_mask: atlas.base_lookup_mask(),
1155            bg_alpha,
1156        }
1157    }
1158}
1159
1160fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: u16) -> Vec<CellDynamic> {
1161    (0..cols * rows)
1162        .map(|_i| CellDynamic::new(fill_glyph, 0x00ff_ffff, 0x0000_0000))
1163        .collect()
1164}
1165
1166mod attrib {
1167    pub const POS: u32 = 0;
1168    pub const UV: u32 = 1;
1169
1170    pub const GRID_XY: u32 = 2;
1171    pub const PACKED_DEPTH_FG_BG: u32 = 3;
1172}