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 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 gl.bind_vertex_array(None);
87
88 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 pub fn cell_size(&self) -> (i32, i32) {
124 self.atlas.cell_size()
125 }
126
127 pub fn terminal_size(&self) -> (u16, u16) {
129 self.terminal_size
130 }
131
132 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 pub fn cell_count(&self) -> usize {
152 self.cells.len()
153 }
154
155 pub fn update_cells<'a>(
169 &mut self,
170 gl: &WebGl2RenderingContext,
171 cells: impl Iterator<Item = CellData<'a>>,
172 ) -> Result<(), Error> {
173 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 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 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(()); }
219
220 gl.bind_vertex_array(Some(&self.buffers.vao));
222
223 gl.delete_buffer(Some(&self.buffers.instance_cell));
225 gl.delete_buffer(Some(&self.buffers.instance_pos));
226
227 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 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 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.0; #[rustfmt::skip]
323 let vertices = [
324 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 ];
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 const STRIDE: i32 = (2 + 2) * 4; 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 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#[derive(Debug)]
484pub struct CellData<'a> {
485 symbol: &'a str,
487 style_bits: u16,
488 fg: u32,
489 bg: u32,
490}
491
492impl<'a> CellData<'a> {
493 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 pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
532 debug_assert!(0x81FF & style_bits == 0, "Invalid style bits: {style_bits:#04x}");
534 Self { symbol, style_bits, fg, bg }
535 }
536}
537
538#[repr(C, align(4))]
559struct CellStatic {
560 pub grid_xy: [u16; 2],
562}
563
564#[derive(Debug, Clone, Copy)]
592#[repr(C, align(4))]
593struct CellDynamic {
594 pub data: [u8; 8], }
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; data[3] = ((fg >> 8) & 0xFF) as u8; data[4] = ((fg) & 0xFF) as u8; data[5] = ((bg >> 16) & 0xFF) as u8; data[6] = ((bg >> 8) & 0xFF) as u8; data[7] = ((bg) & 0xFF) as u8; Self { data }
637 }
638}
639
640#[repr(C, align(16))] struct CellVertexUbo {
642 pub projection: [f32; 16], pub cell_size: [f32; 2], pub _padding: [f32; 2],
645}
646
647#[repr(C, align(16))] struct CellFragmentUbo {
649 pub padding_frac: [f32; 2], 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], }
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], }
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}