beamterm_renderer/gl/
terminal_grid.rs

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