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