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