Skip to main content

beamterm_core/gl/
terminal_grid.rs

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