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