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#[derive(Debug)]
22pub struct TerminalGrid {
23 shader: ShaderProgram,
25 cells: Vec<CellDynamic>,
27 terminal_size: (u16, u16),
29 canvas_size_px: (i32, i32),
31 buffers: TerminalBuffers,
33 ubo_vertex: UniformBufferObject,
35 ubo_fragment: UniformBufferObject,
37 atlas: FontAtlas,
39 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 let vao = create_vao(gl)?;
74 gl.bind_vertex_array(Some(&vao));
75
76 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 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 gl.bind_vertex_array(None);
88
89 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 pub fn cell_size(&self) -> (i32, i32) {
125 self.atlas.cell_size()
126 }
127
128 pub fn terminal_size(&self) -> (u16, u16) {
130 self.terminal_size
131 }
132
133 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 pub fn cell_count(&self) -> usize {
151 self.cells.len()
152 }
153
154 pub fn update_cells<'a>(
168 &mut self,
169 gl: &WebGl2RenderingContext,
170 cells: impl Iterator<Item = CellData<'a>>,
171 ) -> Result<(), Error> {
172 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 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 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 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 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(()); }
267
268 gl.bind_vertex_array(Some(&self.buffers.vao));
270
271 gl.delete_buffer(Some(&self.buffers.instance_cell));
273 gl.delete_buffer(Some(&self.buffers.instance_pos));
274
275 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 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 gl.bind_vertex_array(None);
288
289 self.terminal_size = (cols as u16, rows as u16);
290
291 Ok(())
292 }
293
294 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.0; #[rustfmt::skip]
376 let vertices = [
377 w + overlap, -overlap, 1.0, 0.0, -overlap, h + overlap, 0.0, 1.0, w + overlap, h + overlap, 1.0, 1.0, -overlap, -overlap, 0.0, 0.0 ];
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 const STRIDE: i32 = (2 + 2) * 4; 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 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#[derive(Debug, Copy, Clone)]
537pub struct CellData<'a> {
538 symbol: &'a str,
540 style_bits: u16,
541 fg: u32,
542 bg: u32,
543}
544
545impl<'a> CellData<'a> {
546 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 pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
585 debug_assert!(0x81FF & style_bits == 0, "Invalid style bits: {style_bits:#04x}");
587 Self { symbol, style_bits, fg, bg }
588 }
589}
590
591#[repr(C, align(4))]
612struct CellStatic {
613 pub grid_xy: [u16; 2],
615}
616
617#[derive(Debug, Clone, Copy)]
645#[repr(C, align(4))]
646struct CellDynamic {
647 pub data: [u8; 8], }
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 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]; data[3] = fg[1]; data[4] = fg[0]; let bg = bg.to_le_bytes();
689 data[5] = bg[2]; data[6] = bg[1]; data[7] = bg[0]; Self { data }
694 }
695}
696
697#[repr(C, align(16))] struct CellVertexUbo {
699 pub projection: [f32; 16], pub cell_size: [f32; 2], pub _padding: [f32; 2],
702}
703
704#[repr(C, align(16))] struct CellFragmentUbo {
706 pub padding_frac: [f32; 2], pub underline_pos: f32, pub underline_thickness: f32, pub strikethrough_pos: f32, pub strikethrough_thickness: f32, 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], }
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], }
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}