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