beamterm_renderer/gl/
terminal_grid.rs

1use std::{borrow::Cow, cmp::min, fmt::Debug, ops::Index};
2
3use beamterm_data::{FontAtlasData, FontStyle, GlyphEffect};
4use compact_str::{CompactString, CompactStringExt};
5use web_sys::{console, WebGl2RenderingContext};
6
7use crate::{
8    error::Error,
9    gl::{
10        buffer_upload_array, selection::SelectionTracker, ubo::UniformBufferObject, CellIterator,
11        Drawable, FontAtlas, RenderContext, ShaderProgram, GL,
12    },
13    mat4::Mat4,
14};
15
16/// A high-performance terminal grid renderer using instanced rendering.
17///
18/// `TerminalGrid` renders a grid of terminal cells using WebGL2 instanced drawing.
19/// Each cell can display a character from a font atlas with customizable foreground
20/// and background colors. The renderer uses a 2D texture array to efficiently
21/// store glyph data and supports real-time updates of cell content.
22#[derive(Debug)]
23pub struct TerminalGrid {
24    /// Shader program for rendering the terminal cells.
25    shader: ShaderProgram,
26    /// Terminal cell instance data
27    cells: Vec<CellDynamic>,
28    /// Terminal size in cells
29    terminal_size: (u16, u16),
30    /// Size of the canvas in pixels
31    canvas_size_px: (i32, i32),
32    /// Buffers for the terminal grid
33    buffers: TerminalBuffers,
34    /// shared state for the vertex shader
35    ubo_vertex: UniformBufferObject,
36    /// shared state for the fragment shader
37    ubo_fragment: UniformBufferObject,
38    /// Font atlas for rendering text.
39    atlas: FontAtlas,
40    /// Uniform location for the texture sampler.
41    sampler_loc: web_sys::WebGlUniformLocation,
42    /// Fallback glyph for missing symbols.
43    fallback_glyph: u16,
44    /// Selection tracker for managing cell selections.
45    selection: SelectionTracker,
46}
47
48#[derive(Debug)]
49struct TerminalBuffers {
50    vao: web_sys::WebGlVertexArrayObject,
51    vertices: web_sys::WebGlBuffer,
52    instance_pos: web_sys::WebGlBuffer,
53    instance_cell: web_sys::WebGlBuffer,
54    indices: web_sys::WebGlBuffer,
55}
56
57impl TerminalBuffers {
58    fn upload_instance_data<T>(&self, gl: &WebGl2RenderingContext, cell_data: &[T]) {
59        gl.bind_vertex_array(Some(&self.vao));
60        gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.instance_cell));
61
62        buffer_upload_array(gl, GL::ARRAY_BUFFER, cell_data, GL::DYNAMIC_DRAW);
63
64        gl.bind_vertex_array(None);
65    }
66}
67
68impl TerminalGrid {
69    const FRAGMENT_GLSL: &'static str = include_str!("../shaders/cell.frag");
70    const VERTEX_GLSL: &'static str = include_str!("../shaders/cell.vert");
71
72    pub fn new(
73        gl: &WebGl2RenderingContext,
74        atlas: FontAtlas,
75        screen_size: (i32, i32),
76    ) -> Result<Self, Error> {
77        // create and setup the Vertex Array Object
78        let vao = create_vao(gl)?;
79        gl.bind_vertex_array(Some(&vao));
80
81        // prepare vertex, index and instance buffers
82        let cell_size = atlas.cell_size();
83        let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
84
85        // let fill_glyphs = Self::fill_glyphs(&atlas);
86        // let cell_data = create_terminal_cell_data(cols, rows, &fill_glyphs);
87        let cell_data = create_terminal_cell_data(cols, rows, &[' ' as u16]);
88        let cell_pos = CellStatic::create_grid(cols, rows);
89        let buffers = setup_buffers(gl, vao, &cell_pos, &cell_data, cell_size)?;
90
91        // unbind VAO to prevent accidental modification
92        gl.bind_vertex_array(None);
93
94        // setup shader and uniform data
95        let shader = ShaderProgram::create(gl, Self::VERTEX_GLSL, Self::FRAGMENT_GLSL)?;
96        shader.use_program(gl);
97
98        let ubo_vertex = UniformBufferObject::new(gl, CellVertexUbo::BINDING_POINT)?;
99        ubo_vertex.bind_to_shader(gl, &shader, "VertUbo")?;
100        let ubo_fragment = UniformBufferObject::new(gl, CellFragmentUbo::BINDING_POINT)?;
101        ubo_fragment.bind_to_shader(gl, &shader, "FragUbo")?;
102
103        let sampler_loc = gl
104            .get_uniform_location(&shader.program, "u_sampler")
105            .ok_or(Error::uniform_location_failed("u_sampler"))?;
106
107        console::log_2(&"terminal cells".into(), &cell_data.len().into());
108
109        let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
110        console::log_1(&format!("terminal size {cols}x{rows}").into());
111        let grid = Self {
112            shader,
113            terminal_size: (cols as u16, rows as u16),
114            canvas_size_px: screen_size,
115            cells: cell_data,
116            buffers,
117            ubo_vertex,
118            ubo_fragment,
119            atlas,
120            sampler_loc,
121            fallback_glyph: ' ' as u16,
122            selection: SelectionTracker::new(),
123        };
124
125        grid.upload_ubo_data(gl);
126
127        Ok(grid)
128    }
129
130    /// Sets the fallback glyph for missing characters.
131    pub fn set_fallback_glyph(&mut self, fallback: &str) {
132        self.fallback_glyph = self.atlas.get_base_glyph_id(fallback).unwrap_or(' ' as u16);
133    }
134
135    /// Returns the [`FontAtlas`] used by this terminal grid.
136    pub fn atlas(&self) -> &FontAtlas {
137        &self.atlas
138    }
139
140    /// Returns the unpadded cell dimensions in pixels.
141    pub fn cell_size(&self) -> (i32, i32) {
142        self.atlas.cell_size()
143    }
144
145    /// Returns the size of the terminal grid in cells.
146    pub fn terminal_size(&self) -> (u16, u16) {
147        self.terminal_size
148    }
149
150    /// Returns the active selection state of the terminal grid.
151    pub(crate) fn selection_tracker(&self) -> SelectionTracker {
152        self.selection.clone()
153    }
154
155    /// Returns the symbols in the specified block range as a `CompactString`.
156    pub(super) fn get_symbols(&self, selection: CellIterator) -> CompactString {
157        let (cols, rows) = self.terminal_size;
158        let mut text = CompactString::new("");
159
160        for (idx, require_newline_after) in selection {
161            text.push_str(&self.get_cell_symbol(idx));
162            if require_newline_after {
163                text.push('\n'); // add newline after each row
164            }
165        }
166
167        text
168    }
169
170    fn get_cell_symbol(&self, idx: usize) -> Cow<str> {
171        if idx < self.cells.len() {
172            let glyph_id = self.cells[idx].glyph_id();
173            self.atlas.get_symbol(glyph_id).unwrap_or_else(|| self.fallback_symbol())
174        } else {
175            self.fallback_symbol()
176        }
177    }
178
179    /// Uploads uniform buffer data for screen and cell dimensions.
180    ///
181    /// This method updates the shader uniform buffers with the current screen
182    /// size and cell dimensions. Must be called when the screen size changes
183    /// or when initializing the grid.
184    ///
185    /// # Parameters
186    /// * `gl` - WebGL2 rendering context
187    fn upload_ubo_data(&self, gl: &WebGl2RenderingContext) {
188        let vertex_ubo = CellVertexUbo::new(self.canvas_size_px, self.cell_size());
189        self.ubo_vertex.upload_data(gl, &vertex_ubo);
190
191        let fragment_ubo = CellFragmentUbo::new(&self.atlas);
192        self.ubo_fragment.upload_data(gl, &fragment_ubo);
193    }
194
195    /// Returns the total number of cells in the terminal grid.
196    pub fn cell_count(&self) -> usize {
197        self.cells.len()
198    }
199
200    /// Updates the content of terminal cells with new data.
201    ///
202    /// This method efficiently updates the dynamic instance buffer with new
203    /// cell data. The iterator must provide exactly the same number of cells
204    /// as the grid contains, in row-major order.
205    ///
206    /// # Parameters
207    /// * `gl` - WebGL2 rendering context
208    /// * `cells` - Iterator providing `CellData` for each cell in the grid
209    ///
210    /// # Returns
211    /// * `Ok(())` - Successfully updated cell data
212    /// * `Err(Error)` - Failed to update buffer or other WebGL error
213    pub fn update_cells<'a>(
214        &mut self,
215        gl: &WebGl2RenderingContext,
216        cells: impl Iterator<Item = CellData<'a>>,
217    ) -> Result<(), Error> {
218        // update instance buffer with new cell data
219        let atlas = &self.atlas;
220
221        let fallback_glyph = self.fallback_glyph;
222        self.cells.iter_mut().zip(cells).for_each(|(cell, data)| {
223            let glyph_id = atlas.get_base_glyph_id(data.symbol).unwrap_or(fallback_glyph);
224
225            *cell = CellDynamic::new(glyph_id | data.style_bits, data.fg, data.bg);
226        });
227
228        self.flush_cells(gl)?;
229
230        Ok(())
231    }
232
233    pub(crate) fn update_cells_by_position<'a>(
234        &mut self,
235        gl: &WebGl2RenderingContext,
236        cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
237    ) -> Result<(), Error> {
238        // update instance buffer with new cell data by position
239        let atlas = &self.atlas;
240
241        let cell_count = self.cells.len();
242        let fallback_glyph = self.fallback_glyph;
243        let w = self.terminal_size.0 as usize;
244        cells
245            .map(|(x, y, cell)| (w * y as usize + x as usize, cell))
246            .filter(|(idx, _)| *idx < cell_count)
247            .for_each(|(idx, cell)| {
248                let glyph_id = atlas.get_base_glyph_id(cell.symbol).unwrap_or(fallback_glyph);
249                self.cells[idx] = CellDynamic::new(glyph_id | cell.style_bits, cell.fg, cell.bg);
250            });
251
252        self.flush_cells(gl)?;
253
254        Ok(())
255    }
256
257    pub(crate) fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) {
258        let (cols, _) = self.terminal_size;
259        let idx = y as usize * cols as usize + x as usize;
260        self.update_cell_by_index(idx, cell_data);
261    }
262
263    pub(crate) fn update_cell_by_index(&mut self, idx: usize, cell_data: CellData) {
264        if idx >= self.cells.len() {
265            return;
266        }
267
268        let atlas = &self.atlas;
269        let fallback_glyph = self.fallback_glyph;
270        let glyph_id = atlas.get_base_glyph_id(cell_data.symbol).unwrap_or(fallback_glyph);
271
272        self.cells[idx] =
273            CellDynamic::new(glyph_id | cell_data.style_bits, cell_data.fg, cell_data.bg);
274    }
275
276    /// Flushes pending cell updates to the GPU.
277    pub(crate) fn flush_cells(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
278        // If there's an active selection, flip the colors of the selected cells.
279        // This ensures that the selected cells are rendered with inverted colors
280        // during the GPU upload process.
281        self.flip_selected_cell_colors();
282
283        self.buffers.upload_instance_data(gl, &self.cells);
284
285        // Restore the original colors of the selected cells after the upload.
286        // This ensures that the internal state of the cells remains consistent.
287        self.flip_selected_cell_colors();
288
289        Ok(())
290    }
291
292    fn flip_selected_cell_colors(&mut self) {
293        if let Some(iter) = self.selected_cells_iter() {
294            iter.for_each(|(idx, _)| self.cells[idx].flip_colors());
295        }
296    }
297
298    fn selected_cells_iter(&self) -> Option<CellIterator> {
299        self.selection
300            .get_query()
301            .and_then(|query| query.range())
302            .map(|(start, end)| self.cell_iter(start, end, self.selection.mode()))
303    }
304
305    fn flip_cell_colors(&mut self, x: u16, y: u16) {
306        let (cols, _) = self.terminal_size;
307        let idx = y as usize * cols as usize + x as usize;
308        if idx < self.cells.len() {
309            self.cells[idx].flip_colors();
310        }
311    }
312
313    /// Resizes the terminal grid to fit the new canvas dimensions.
314    ///
315    /// This method recalculates the terminal dimensions based on the canvas size and cell
316    /// dimensions, then recreates the necessary GPU buffers if the grid size changed.
317    /// Existing cell content is preserved where possible during resizing.
318    ///
319    /// # Parameters
320    ///
321    /// * `gl` - WebGL2 rendering context
322    /// * `canvas_size` - New canvas dimensions in pixels as `(width, height)`
323    ///
324    /// # Returns
325    ///
326    /// * `Ok(())` - Successfully resized the terminal
327    /// * `Err(Error)` - Failed to recreate buffers or other WebGL error
328    pub fn resize(
329        &mut self,
330        gl: &WebGl2RenderingContext,
331        canvas_size: (i32, i32),
332    ) -> Result<(), Error> {
333        self.canvas_size_px = canvas_size;
334
335        // update the UBO with new screen size
336        self.upload_ubo_data(gl);
337
338        let cell_size = self.atlas.cell_size();
339        let cols = canvas_size.0 / cell_size.0;
340        let rows = canvas_size.1 / cell_size.1;
341        if self.terminal_size == (cols as u16, rows as u16) {
342            return Ok(()); // no change in terminal size
343        }
344
345        // update buffers; bind VAO to ensure correct state
346        gl.bind_vertex_array(Some(&self.buffers.vao));
347
348        // delete old cell instance buffers
349        gl.delete_buffer(Some(&self.buffers.instance_cell));
350        gl.delete_buffer(Some(&self.buffers.instance_pos));
351
352        // resize cell data vector
353        let current_size = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
354        let cell_data = resize_cell_grid(&self.cells, current_size, (cols, rows));
355        self.cells = cell_data;
356
357        let cell_pos = CellStatic::create_grid(cols, rows);
358
359        // re-create buffers with new data
360        self.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
361        self.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
362
363        // unbind VAO
364        gl.bind_vertex_array(None);
365
366        self.terminal_size = (cols as u16, rows as u16);
367
368        Ok(())
369    }
370
371    /// Returns the base glyph identifier for a given symbol.
372    pub fn base_glyph_id(&self, symbol: &str) -> Option<u16> {
373        self.atlas.get_base_glyph_id(symbol)
374    }
375
376    fn fallback_symbol(&self) -> Cow<str> {
377        self.atlas.get_symbol(self.fallback_glyph).unwrap_or(Cow::Borrowed(" "))
378    }
379
380    fn fill_glyphs(atlas: &FontAtlas) -> Vec<u16> {
381        [
382            ("🤫", FontStyle::Normal),
383            ("🙌", FontStyle::Normal),
384            ("n", FontStyle::Normal),
385            ("o", FontStyle::Normal),
386            ("r", FontStyle::Normal),
387            ("m", FontStyle::Normal),
388            ("a", FontStyle::Normal),
389            ("l", FontStyle::Normal),
390            ("b", FontStyle::Bold),
391            ("o", FontStyle::Bold),
392            ("l", FontStyle::Bold),
393            ("d", FontStyle::Bold),
394            ("i", FontStyle::Italic),
395            ("t", FontStyle::Italic),
396            ("a", FontStyle::Italic),
397            ("l", FontStyle::Italic),
398            ("i", FontStyle::Italic),
399            ("c", FontStyle::Italic),
400            ("b", FontStyle::BoldItalic),
401            ("-", FontStyle::BoldItalic),
402            ("i", FontStyle::BoldItalic),
403            ("t", FontStyle::BoldItalic),
404            ("a", FontStyle::BoldItalic),
405            ("l", FontStyle::BoldItalic),
406            ("i", FontStyle::BoldItalic),
407            ("c", FontStyle::BoldItalic),
408            ("🤪", FontStyle::Normal),
409            ("🤩", FontStyle::Normal),
410        ]
411        .into_iter()
412        .map(|(symbol, style)| atlas.get_base_glyph_id(symbol).map(|g| g | style as u16))
413        .map(|g| g.unwrap_or(' ' as u16))
414        .collect()
415    }
416}
417
418fn resize_cell_grid(
419    cells: &[CellDynamic],
420    old_size: (i32, i32),
421    new_size: (i32, i32),
422) -> Vec<CellDynamic> {
423    let new_len = new_size.0 * new_size.1;
424
425    let mut new_cells = Vec::with_capacity(new_len as usize);
426    for _ in 0..new_len {
427        new_cells.push(CellDynamic::new(' ' as u16, 0xFFFFFF, 0x000000));
428    }
429
430    for y in 0..min(old_size.1, new_size.1) {
431        for x in 0..min(old_size.0, new_size.0) {
432            let new_idx = (y * new_size.0 + x) as usize;
433            let old_idx = (y * old_size.0 + x) as usize;
434            new_cells[new_idx] = cells[old_idx];
435        }
436    }
437
438    new_cells
439}
440
441fn create_vao(gl: &WebGl2RenderingContext) -> Result<web_sys::WebGlVertexArrayObject, Error> {
442    gl.create_vertex_array().ok_or(Error::vertex_array_creation_failed())
443}
444
445fn setup_buffers(
446    gl: &WebGl2RenderingContext,
447    vao: web_sys::WebGlVertexArrayObject,
448    cell_pos: &[CellStatic],
449    cell_data: &[CellDynamic],
450    cell_size: (i32, i32),
451) -> Result<TerminalBuffers, Error> {
452    let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
453
454    // let overlap = 0.5;
455    let overlap = 0.0; // no overlap for now, can be adjusted later
456    #[rustfmt::skip]
457    let vertices = [
458        //    x            y       u    v
459        w + overlap,    -overlap, 1.0, 0.0, // top-right
460           -overlap, h + overlap, 0.0, 1.0, // bottom-left
461        w + overlap, h + overlap, 1.0, 1.0, // bottom-right
462           -overlap,    -overlap, 0.0, 0.0  // top-left
463    ];
464    let indices = [0, 1, 2, 0, 3, 1];
465
466    Ok(TerminalBuffers {
467        vao,
468        vertices: create_buffer_f32(gl, GL::ARRAY_BUFFER, &vertices, GL::STATIC_DRAW)?,
469        instance_pos: create_static_instance_buffer(gl, cell_pos)?,
470        instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
471        indices: create_buffer_u8(gl, GL::ELEMENT_ARRAY_BUFFER, &indices, GL::STATIC_DRAW)?,
472    })
473}
474
475fn create_buffer_u8(
476    gl: &WebGl2RenderingContext,
477    target: u32,
478    data: &[u8],
479    usage: u32,
480) -> Result<web_sys::WebGlBuffer, Error> {
481    let index_buf = gl.create_buffer().ok_or(Error::buffer_creation_failed("vbo-u8"))?;
482    gl.bind_buffer(target, Some(&index_buf));
483
484    gl.buffer_data_with_u8_array(target, data, usage);
485
486    Ok(index_buf)
487}
488
489fn create_buffer_f32(
490    gl: &WebGl2RenderingContext,
491    target: u32,
492    data: &[f32],
493    usage: u32,
494) -> Result<web_sys::WebGlBuffer, Error> {
495    let buffer = gl.create_buffer().ok_or(Error::buffer_creation_failed("vbo-f32"))?;
496
497    gl.bind_buffer(target, Some(&buffer));
498
499    unsafe {
500        let view = js_sys::Float32Array::view(data);
501        gl.buffer_data_with_array_buffer_view(target, &view, usage);
502    }
503
504    // vertex attributes \\
505    const STRIDE: i32 = (2 + 2) * 4; // 4 floats per vertex
506    enable_vertex_attrib(gl, attrib::POS, 2, GL::FLOAT, 0, STRIDE);
507    enable_vertex_attrib(gl, attrib::UV, 2, GL::FLOAT, 8, STRIDE);
508
509    Ok(buffer)
510}
511
512fn create_static_instance_buffer(
513    gl: &WebGl2RenderingContext,
514    instance_data: &[CellStatic],
515) -> Result<web_sys::WebGlBuffer, Error> {
516    let instance_buf = gl
517        .create_buffer()
518        .ok_or(Error::buffer_creation_failed("static-instance-buffer"))?;
519
520    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
521    buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::STATIC_DRAW);
522
523    let stride = size_of::<CellStatic>() as i32;
524    enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, GL::UNSIGNED_SHORT, 0, stride);
525
526    Ok(instance_buf)
527}
528
529fn create_dynamic_instance_buffer(
530    gl: &WebGl2RenderingContext,
531    instance_data: &[CellDynamic],
532) -> Result<web_sys::WebGlBuffer, Error> {
533    let instance_buf = gl
534        .create_buffer()
535        .ok_or(Error::buffer_creation_failed("dynamic-instance-buffer"))?;
536
537    gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
538    buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::DYNAMIC_DRAW);
539
540    let stride = size_of::<CellDynamic>() as i32;
541
542    // setup instance attributes (while VAO is bound)
543    enable_vertex_attrib_array(gl, attrib::PACKED_DEPTH_FG_BG, 2, GL::UNSIGNED_INT, 0, stride);
544
545    Ok(instance_buf)
546}
547
548fn enable_vertex_attrib_array(
549    gl: &WebGl2RenderingContext,
550    index: u32,
551    size: i32,
552    type_: u32,
553    offset: i32,
554    stride: i32,
555) {
556    enable_vertex_attrib(gl, index, size, type_, offset, stride);
557    gl.vertex_attrib_divisor(index, 1);
558}
559
560fn enable_vertex_attrib(
561    gl: &WebGl2RenderingContext,
562    index: u32,
563    size: i32,
564    type_: u32,
565    offset: i32,
566    stride: i32,
567) {
568    gl.enable_vertex_attrib_array(index);
569    if type_ == GL::FLOAT {
570        gl.vertex_attrib_pointer_with_i32(index, size, type_, false, stride, offset);
571    } else {
572        gl.vertex_attrib_i_pointer_with_i32(index, size, type_, stride, offset);
573    }
574}
575
576impl Drawable for TerminalGrid {
577    fn prepare(&self, context: &mut RenderContext) {
578        let gl = context.gl;
579
580        self.shader.use_program(gl);
581
582        gl.bind_vertex_array(Some(&self.buffers.vao));
583
584        self.atlas.bind(gl, 0);
585        self.ubo_vertex.bind(context.gl);
586        self.ubo_fragment.bind(context.gl);
587        gl.uniform1i(Some(&self.sampler_loc), 0);
588    }
589
590    fn draw(&self, context: &mut RenderContext) {
591        let gl = context.gl;
592        let cell_count = self.cells.len() as i32;
593        gl.draw_elements_instanced_with_i32(GL::TRIANGLES, 6, GL::UNSIGNED_BYTE, 0, cell_count);
594    }
595
596    fn cleanup(&self, context: &mut RenderContext) {
597        let gl = context.gl;
598        gl.bind_vertex_array(None);
599        gl.bind_texture(GL::TEXTURE_2D_ARRAY, None);
600
601        self.ubo_vertex.unbind(gl);
602        self.ubo_fragment.unbind(gl);
603    }
604}
605
606/// Data for a single terminal cell including character and colors.
607///
608/// `CellData` represents the visual content of one terminal cell, including
609/// the character to display and its foreground and background colors.
610/// Colors are specified as RGB values packed into 32-bit integers.
611///
612/// # Color Format
613/// Colors use the format 0xRRGGBB where:
614/// - RR: Red component
615/// - GG: Green component  
616/// - BB: Blue component
617#[derive(Debug, Copy, Clone)]
618pub struct CellData<'a> {
619    // todo: try to pre-pack the available glyph id bits
620    symbol: &'a str,
621    style_bits: u16,
622    fg: u32,
623    bg: u32,
624}
625
626impl<'a> CellData<'a> {
627    /// Creates new cell data with the specified character and colors.
628    ///
629    /// # Parameters
630    /// * `symbol` - Character to display (should be a single character)
631    /// * `style` - Font style for the character (e.g. bold, italic)
632    /// * `effect` - Optional glyph effect (e.g. underline, strikethrough)
633    /// * `fg` - Foreground color as RGB value (0xRRGGBB)
634    /// * `bg` - Background color as RGB value (0xRRGGBB)
635    ///
636    /// # Returns
637    /// New `CellData` instance
638    pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
639        Self::new_with_style_bits(symbol, style.style_mask() | effect as u16, fg, bg)
640    }
641
642    /// Creates new cell data with pre-encoded style bits.
643    ///
644    /// This is a lower-level constructor that accepts pre-encoded style bits rather than
645    /// separate `FontStyle` and `GlyphEffect` parameters. Use this when you have already
646    /// combined the style flags or when working directly with the bit representation.
647    ///
648    /// # Parameters
649    /// * `symbol` - Character to display
650    /// * `style_bits` - Pre-encoded style flags. Must not overlap with base glyph ID bits (0x01FF).
651    ///   Valid bits include:
652    ///   - `0x0200` - Bold
653    ///   - `0x0400` - Italic
654    ///   - `0x0800` - Emoji (set automatically by the renderer for emoji glyphs)
655    ///   - `0x1000` - Underline
656    ///   - `0x2000` - Strikethrough
657    /// * `fg` - Foreground color as RGB value (0xRRGGBB)
658    /// * `bg` - Background color as RGB value (0xRRGGBB)
659    ///
660    /// # Returns
661    /// New `CellData` instance
662    ///
663    /// # Panics
664    /// Debug builds will panic if `style_bits` contains any invalid bits.
665    pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
666        // emoji and glyph base mask should not intersect with style bits
667        debug_assert!(0x81FF & style_bits == 0, "Invalid style bits: {style_bits:#04x}");
668        Self { symbol, style_bits, fg, bg }
669    }
670}
671
672/// Static instance data for terminal cell positioning.
673///
674/// `CellStatic` represents the unchanging positional data for each terminal cell
675/// in the grid. This data is uploaded once during initialization and remains
676/// constant throughout the lifetime of the terminal grid. Each instance
677/// corresponds to one cell position in the terminal grid.
678///
679/// # Memory Layout
680/// This struct uses `#[repr(C, align(4))]` to ensure:
681/// - C-compatible memory layout for GPU buffer uploads
682/// - 4-byte alignment for efficient GPU access
683/// - Predictable field ordering (grid_xy at offset 0)
684///
685/// # GPU Usage
686/// This data is used as per-instance vertex attributes in the vertex shader,
687/// allowing the same cell geometry to be rendered at different grid positions
688/// using instanced drawing.
689///
690/// # Buffer Upload
691/// Uploaded to GPU using `GL::STATIC_DRAW` since positions don't change.
692#[repr(C, align(4))]
693struct CellStatic {
694    /// Grid position as (x, y) coordinates in cell units.
695    pub grid_xy: [u16; 2],
696}
697
698/// Dynamic instance data for terminal cell appearance.
699///
700/// `CellDynamic` contains the frequently-changing visual data for each terminal
701/// cell, including the character glyph and colors. This data is updated whenever
702/// cell content changes and is efficiently uploaded to the GPU using dynamic
703/// buffer updates.
704///
705/// # Memory Layout
706/// The 8-byte data array is packed as follows:
707/// - Bytes 0-1: Glyph depth/layer index (u16, little-endian)
708/// - Bytes 2-4: Foreground color RGB (3 bytes)
709/// - Bytes 5-7: Background color RGB (3 bytes)
710///
711/// This compact layout minimizes GPU memory usage and allows efficient
712/// instanced rendering of the entire terminal grid.
713///
714/// # Color Format
715/// Colors are stored as RGB bytes (no alpha channel in the instance data).
716/// The alpha channel is handled separately in the shader based on glyph
717/// transparency from the texture atlas.
718///
719/// # GPU Usage
720/// Uploaded as instance attributes and accessed in both vertex and fragment
721/// shaders for character selection and color application.
722///
723/// # Buffer Upload
724/// Uploaded to GPU using `GL::DYNAMIC_DRAW` for efficient updates.
725#[derive(Debug, Clone, Copy)]
726#[repr(C, align(4))]
727struct CellDynamic {
728    /// Packed cell data:
729    ///
730    /// # Byte Layout
731    /// - `data[0]`: Lower 8 bits of glyph depth/layer index
732    /// - `data[1]`: Upper 8 bits of glyph depth/layer index  
733    /// - `data[2]`: Foreground red component (0-255)
734    /// - `data[3]`: Foreground green component (0-255)
735    /// - `data[4]`: Foreground blue component (0-255)
736    /// - `data[5]`: Background red component (0-255)
737    /// - `data[6]`: Background green component (0-255)
738    /// - `data[7]`: Background blue component (0-255)
739    pub data: [u8; 8], // 2b layer, fg:rgb, bg:rgb
740}
741
742impl CellStatic {
743    fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
744        debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
745        debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
746
747        (0..rows)
748            .flat_map(|row| (0..cols).map(move |col| (col, row)))
749            .map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
750            .collect()
751    }
752}
753
754impl CellDynamic {
755    fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
756        let mut data = [0; 8];
757
758        // pack glyph ID into the first two bytes
759        let glyph_id = glyph_id.to_le_bytes();
760        data[0] = glyph_id[0];
761        data[1] = glyph_id[1];
762
763        let fg = fg.to_le_bytes();
764        data[2] = fg[2]; // R
765        data[3] = fg[1]; // G
766        data[4] = fg[0]; // B
767
768        let bg = bg.to_le_bytes();
769        data[5] = bg[2]; // R
770        data[6] = bg[1]; // G
771        data[7] = bg[0]; // B
772
773        Self { data }
774    }
775
776    fn flip_colors(&mut self) {
777        // swap foreground and background colors
778        let fg = [self.data[2], self.data[3], self.data[4]];
779        self.data[2] = self.data[5]; // R
780        self.data[3] = self.data[6]; // G
781        self.data[4] = self.data[7]; // B
782        self.data[5] = fg[0]; // R
783        self.data[6] = fg[1]; // G
784        self.data[7] = fg[2]; // B
785    }
786
787    fn glyph_id(&self) -> u16 {
788        u16::from_le_bytes([self.data[0], self.data[1]])
789    }
790}
791
792#[repr(C, align(16))] // std140 layout requires proper alignment
793struct CellVertexUbo {
794    pub projection: [f32; 16], // mat4
795    pub cell_size: [f32; 2],   // vec2 - screen cell size
796    pub _padding: [f32; 2],
797}
798
799#[repr(C, align(16))] // std140 layout requires proper alignment
800struct CellFragmentUbo {
801    pub padding_frac: [f32; 2],       // padding as a fraction of cell size
802    pub underline_pos: f32,           // underline position (0.0 = top, 1.0 = bottom)
803    pub underline_thickness: f32,     // underline thickness as fraction of cell height
804    pub strikethrough_pos: f32,       // strikethrough position (0.0 = top, 1.0 = bottom)
805    pub strikethrough_thickness: f32, // strikethrough thickness as fraction of cell height
806    pub _padding: [f32; 2],
807}
808
809impl CellVertexUbo {
810    pub const BINDING_POINT: u32 = 0;
811
812    fn new(canvas_size: (i32, i32), cell_size: (i32, i32)) -> Self {
813        let projection =
814            Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
815        Self {
816            projection,
817            cell_size: [cell_size.0 as f32, cell_size.1 as f32],
818            _padding: [0.0; 2], // padding to ensure proper alignment
819        }
820    }
821}
822
823impl CellFragmentUbo {
824    pub const BINDING_POINT: u32 = 1;
825
826    fn new(atlas: &FontAtlas) -> Self {
827        let cell_size = atlas.cell_size();
828        let underline = atlas.underline();
829        let strikethrough = atlas.strikethrough();
830        Self {
831            padding_frac: [
832                FontAtlasData::PADDING as f32 / cell_size.0 as f32,
833                FontAtlasData::PADDING as f32 / cell_size.1 as f32,
834            ],
835            underline_pos: underline.position,
836            underline_thickness: underline.thickness,
837            strikethrough_pos: strikethrough.position,
838            strikethrough_thickness: strikethrough.thickness,
839            _padding: [0.0; 2], // padding to ensure proper alignment
840        }
841    }
842}
843
844fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: &[u16]) -> Vec<CellDynamic> {
845    let glyph_len = fill_glyph.len();
846    (0..cols * rows)
847        .map(|i| CellDynamic::new(fill_glyph[i as usize % glyph_len], 0x00ff_ffff, 0x0000_0000))
848        .collect()
849}
850
851mod attrib {
852    pub const POS: u32 = 0;
853    pub const UV: u32 = 1;
854
855    pub const GRID_XY: u32 = 2;
856    pub const PACKED_DEPTH_FG_BG: u32 = 3;
857}