beamterm_renderer/gl/
terminal_grid.rs

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