1use std::{borrow::Cow, cmp::min, fmt::Debug, ops::Index};
2
3use beamterm_data::{FontAtlasData, FontStyle, GlyphEffect};
4use compact_str::{CompactString, CompactStringExt};
5use web_sys::{console, WebGl2RenderingContext};
6
7use crate::{
8 error::Error,
9 gl::{
10 buffer_upload_array, selection::SelectionTracker, ubo::UniformBufferObject, CellIterator,
11 Drawable, FontAtlas, RenderContext, ShaderProgram, GL,
12 },
13 mat4::Mat4,
14};
15
16#[derive(Debug)]
23pub struct TerminalGrid {
24 shader: ShaderProgram,
26 cells: Vec<CellDynamic>,
28 terminal_size: (u16, u16),
30 canvas_size_px: (i32, i32),
32 buffers: TerminalBuffers,
34 ubo_vertex: UniformBufferObject,
36 ubo_fragment: UniformBufferObject,
38 atlas: FontAtlas,
40 sampler_loc: web_sys::WebGlUniformLocation,
42 fallback_glyph: u16,
44 selection: SelectionTracker,
46 cells_pending_flush: bool,
48}
49
50#[derive(Debug)]
51struct TerminalBuffers {
52 vao: web_sys::WebGlVertexArrayObject,
53 vertices: web_sys::WebGlBuffer,
54 instance_pos: web_sys::WebGlBuffer,
55 instance_cell: web_sys::WebGlBuffer,
56 indices: web_sys::WebGlBuffer,
57}
58
59impl TerminalBuffers {
60 fn upload_instance_data<T>(&self, gl: &WebGl2RenderingContext, cell_data: &[T]) {
61 gl.bind_vertex_array(Some(&self.vao));
62 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&self.instance_cell));
63
64 buffer_upload_array(gl, GL::ARRAY_BUFFER, cell_data, GL::DYNAMIC_DRAW);
65
66 gl.bind_vertex_array(None);
67 }
68}
69
70impl TerminalGrid {
71 const FRAGMENT_GLSL: &'static str = include_str!("../shaders/cell.frag");
72 const VERTEX_GLSL: &'static str = include_str!("../shaders/cell.vert");
73
74 pub fn new(
75 gl: &WebGl2RenderingContext,
76 atlas: FontAtlas,
77 screen_size: (i32, i32),
78 ) -> Result<Self, Error> {
79 let vao = create_vao(gl)?;
81 gl.bind_vertex_array(Some(&vao));
82
83 let cell_size = atlas.cell_size();
85 let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
86
87 let cell_data = create_terminal_cell_data(cols, rows, &[' ' as u16]);
90 let cell_pos = CellStatic::create_grid(cols, rows);
91 let buffers = setup_buffers(gl, vao, &cell_pos, &cell_data, cell_size)?;
92
93 gl.bind_vertex_array(None);
95
96 let shader = ShaderProgram::create(gl, Self::VERTEX_GLSL, Self::FRAGMENT_GLSL)?;
98 shader.use_program(gl);
99
100 let ubo_vertex = UniformBufferObject::new(gl, CellVertexUbo::BINDING_POINT)?;
101 ubo_vertex.bind_to_shader(gl, &shader, "VertUbo")?;
102 let ubo_fragment = UniformBufferObject::new(gl, CellFragmentUbo::BINDING_POINT)?;
103 ubo_fragment.bind_to_shader(gl, &shader, "FragUbo")?;
104
105 let sampler_loc = gl
106 .get_uniform_location(&shader.program, "u_sampler")
107 .ok_or(Error::uniform_location_failed("u_sampler"))?;
108
109 console::log_2(&"terminal cells".into(), &cell_data.len().into());
110
111 let (cols, rows) = (screen_size.0 / cell_size.0, screen_size.1 / cell_size.1);
112 console::log_1(&format!("terminal size {cols}x{rows}").into());
113 let grid = Self {
114 shader,
115 terminal_size: (cols as u16, rows as u16),
116 canvas_size_px: screen_size,
117 cells: cell_data,
118 buffers,
119 ubo_vertex,
120 ubo_fragment,
121 atlas,
122 sampler_loc,
123 fallback_glyph: ' ' as u16,
124 selection: SelectionTracker::new(),
125 cells_pending_flush: false,
126 };
127
128 grid.upload_ubo_data(gl);
129
130 Ok(grid)
131 }
132
133 pub fn set_fallback_glyph(&mut self, fallback: &str) {
135 self.fallback_glyph = self.atlas.get_base_glyph_id(fallback).unwrap_or(' ' as u16);
136 }
137
138 pub fn atlas(&self) -> &FontAtlas {
140 &self.atlas
141 }
142
143 pub fn cell_size(&self) -> (i32, i32) {
145 self.atlas.cell_size()
146 }
147
148 pub fn terminal_size(&self) -> (u16, u16) {
150 self.terminal_size
151 }
152
153 pub(crate) fn selection_tracker(&self) -> SelectionTracker {
155 self.selection.clone()
156 }
157
158 pub(super) fn get_symbols(&self, selection: CellIterator) -> CompactString {
160 let (cols, rows) = self.terminal_size;
161 let mut text = CompactString::new("");
162
163 for (idx, require_newline_after) in selection {
164 text.push_str(&self.get_cell_symbol(idx));
165 if require_newline_after {
166 text.push('\n'); }
168 }
169
170 text
171 }
172
173 fn get_cell_symbol(&self, idx: usize) -> Cow<str> {
174 if idx < self.cells.len() {
175 let glyph_id = self.cells[idx].glyph_id();
176 self.atlas.get_symbol(glyph_id).unwrap_or_else(|| self.fallback_symbol())
177 } else {
178 self.fallback_symbol()
179 }
180 }
181
182 fn upload_ubo_data(&self, gl: &WebGl2RenderingContext) {
191 let vertex_ubo = CellVertexUbo::new(self.canvas_size_px, self.cell_size());
192 self.ubo_vertex.upload_data(gl, &vertex_ubo);
193
194 let fragment_ubo = CellFragmentUbo::new(&self.atlas);
195 self.ubo_fragment.upload_data(gl, &fragment_ubo);
196 }
197
198 pub fn cell_count(&self) -> usize {
200 self.cells.len()
201 }
202
203 pub fn update_cells<'a>(
217 &mut self,
218 gl: &WebGl2RenderingContext,
219 cells: impl Iterator<Item = CellData<'a>>,
220 ) -> Result<(), Error> {
221 let atlas = &self.atlas;
223
224 let fallback_glyph = self.fallback_glyph;
225 self.cells.iter_mut().zip(cells).for_each(|(cell, data)| {
226 let glyph_id = atlas.get_base_glyph_id(data.symbol).unwrap_or(fallback_glyph);
227
228 *cell = CellDynamic::new(glyph_id | data.style_bits, data.fg, data.bg);
229 });
230
231 self.cells_pending_flush = true;
232 Ok(())
233 }
234
235 pub(crate) fn update_cells_by_position<'a>(
236 &mut self,
237 gl: &WebGl2RenderingContext,
238 cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
239 ) -> Result<(), Error> {
240 let atlas = &self.atlas;
242
243 let cell_count = self.cells.len();
244 let fallback_glyph = self.fallback_glyph;
245 let w = self.terminal_size.0 as usize;
246 cells
247 .map(|(x, y, cell)| (w * y as usize + x as usize, cell))
248 .filter(|(idx, _)| *idx < cell_count)
249 .for_each(|(idx, cell)| {
250 let glyph_id = atlas.get_base_glyph_id(cell.symbol).unwrap_or(fallback_glyph);
251 self.cells[idx] = CellDynamic::new(glyph_id | cell.style_bits, cell.fg, cell.bg);
252 });
253
254 self.cells_pending_flush = true;
255
256 Ok(())
257 }
258
259 pub(crate) fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) {
260 let (cols, _) = self.terminal_size;
261 let idx = y as usize * cols as usize + x as usize;
262 self.update_cell_by_index(idx, cell_data);
263
264 self.cells_pending_flush = true;
265 }
266
267 pub(crate) fn update_cell_by_index(&mut self, idx: usize, cell_data: CellData) {
268 if idx >= self.cells.len() {
269 return;
270 }
271
272 let atlas = &self.atlas;
273 let fallback_glyph = self.fallback_glyph;
274 let glyph_id = atlas.get_base_glyph_id(cell_data.symbol).unwrap_or(fallback_glyph);
275
276 self.cells[idx] =
277 CellDynamic::new(glyph_id | cell_data.style_bits, cell_data.fg, cell_data.bg);
278
279 self.cells_pending_flush = true;
280 }
281
282 pub(crate) fn flush_cells(&mut self, gl: &WebGl2RenderingContext) -> Result<(), Error> {
284 if !self.cells_pending_flush {
285 return Ok(()); }
287
288 self.flip_selected_cell_colors();
292
293 self.buffers.upload_instance_data(gl, &self.cells);
294
295 self.flip_selected_cell_colors();
298
299 self.cells_pending_flush = false;
300 Ok(())
301 }
302
303 fn flip_selected_cell_colors(&mut self) {
304 if let Some(iter) = self.selected_cells_iter() {
305 iter.for_each(|(idx, _)| self.cells[idx].flip_colors());
306 }
307 }
308
309 fn selected_cells_iter(&self) -> Option<CellIterator> {
310 self.selection
311 .get_query()
312 .and_then(|query| query.range())
313 .map(|(start, end)| self.cell_iter(start, end, self.selection.mode()))
314 }
315
316 fn flip_cell_colors(&mut self, x: u16, y: u16) {
317 let (cols, _) = self.terminal_size;
318 let idx = y as usize * cols as usize + x as usize;
319 if idx < self.cells.len() {
320 self.cells[idx].flip_colors();
321 }
322 }
323
324 pub fn resize(
340 &mut self,
341 gl: &WebGl2RenderingContext,
342 canvas_size: (i32, i32),
343 ) -> Result<(), Error> {
344 self.canvas_size_px = canvas_size;
345
346 self.upload_ubo_data(gl);
348
349 let cell_size = self.atlas.cell_size();
350 let cols = canvas_size.0 / cell_size.0;
351 let rows = canvas_size.1 / cell_size.1;
352 if self.terminal_size == (cols as u16, rows as u16) {
353 return Ok(()); }
355
356 gl.bind_vertex_array(Some(&self.buffers.vao));
358
359 gl.delete_buffer(Some(&self.buffers.instance_cell));
361 gl.delete_buffer(Some(&self.buffers.instance_pos));
362
363 let current_size = (self.terminal_size.0 as i32, self.terminal_size.1 as i32);
365 let cell_data = resize_cell_grid(&self.cells, current_size, (cols, rows));
366 self.cells = cell_data;
367
368 let cell_pos = CellStatic::create_grid(cols, rows);
369
370 self.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
372 self.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
373
374 gl.bind_vertex_array(None);
376
377 self.terminal_size = (cols as u16, rows as u16);
378
379 Ok(())
380 }
381
382 pub fn base_glyph_id(&self, symbol: &str) -> Option<u16> {
384 self.atlas.get_base_glyph_id(symbol)
385 }
386
387 fn fallback_symbol(&self) -> Cow<str> {
388 self.atlas.get_symbol(self.fallback_glyph).unwrap_or(Cow::Borrowed(" "))
389 }
390
391 fn fill_glyphs(atlas: &FontAtlas) -> Vec<u16> {
392 [
393 ("🤫", FontStyle::Normal),
394 ("🙌", FontStyle::Normal),
395 ("n", FontStyle::Normal),
396 ("o", FontStyle::Normal),
397 ("r", FontStyle::Normal),
398 ("m", FontStyle::Normal),
399 ("a", FontStyle::Normal),
400 ("l", FontStyle::Normal),
401 ("b", FontStyle::Bold),
402 ("o", FontStyle::Bold),
403 ("l", FontStyle::Bold),
404 ("d", FontStyle::Bold),
405 ("i", FontStyle::Italic),
406 ("t", FontStyle::Italic),
407 ("a", FontStyle::Italic),
408 ("l", FontStyle::Italic),
409 ("i", FontStyle::Italic),
410 ("c", FontStyle::Italic),
411 ("b", FontStyle::BoldItalic),
412 ("-", FontStyle::BoldItalic),
413 ("i", FontStyle::BoldItalic),
414 ("t", FontStyle::BoldItalic),
415 ("a", FontStyle::BoldItalic),
416 ("l", FontStyle::BoldItalic),
417 ("i", FontStyle::BoldItalic),
418 ("c", FontStyle::BoldItalic),
419 ("🤪", FontStyle::Normal),
420 ("🤩", FontStyle::Normal),
421 ]
422 .into_iter()
423 .map(|(symbol, style)| atlas.get_base_glyph_id(symbol).map(|g| g | style as u16))
424 .map(|g| g.unwrap_or(' ' as u16))
425 .collect()
426 }
427}
428
429fn resize_cell_grid(
430 cells: &[CellDynamic],
431 old_size: (i32, i32),
432 new_size: (i32, i32),
433) -> Vec<CellDynamic> {
434 let new_len = new_size.0 * new_size.1;
435
436 let mut new_cells = Vec::with_capacity(new_len as usize);
437 for _ in 0..new_len {
438 new_cells.push(CellDynamic::new(' ' as u16, 0xFFFFFF, 0x000000));
439 }
440
441 for y in 0..min(old_size.1, new_size.1) {
442 for x in 0..min(old_size.0, new_size.0) {
443 let new_idx = (y * new_size.0 + x) as usize;
444 let old_idx = (y * old_size.0 + x) as usize;
445 new_cells[new_idx] = cells[old_idx];
446 }
447 }
448
449 new_cells
450}
451
452fn create_vao(gl: &WebGl2RenderingContext) -> Result<web_sys::WebGlVertexArrayObject, Error> {
453 gl.create_vertex_array().ok_or(Error::vertex_array_creation_failed())
454}
455
456fn setup_buffers(
457 gl: &WebGl2RenderingContext,
458 vao: web_sys::WebGlVertexArrayObject,
459 cell_pos: &[CellStatic],
460 cell_data: &[CellDynamic],
461 cell_size: (i32, i32),
462) -> Result<TerminalBuffers, Error> {
463 let (w, h) = (cell_size.0 as f32, cell_size.1 as f32);
464
465 let overlap = 0.0; #[rustfmt::skip]
468 let vertices = [
469 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 ];
475 let indices = [0, 1, 2, 0, 3, 1];
476
477 Ok(TerminalBuffers {
478 vao,
479 vertices: create_buffer_f32(gl, GL::ARRAY_BUFFER, &vertices, GL::STATIC_DRAW)?,
480 instance_pos: create_static_instance_buffer(gl, cell_pos)?,
481 instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
482 indices: create_buffer_u8(gl, GL::ELEMENT_ARRAY_BUFFER, &indices, GL::STATIC_DRAW)?,
483 })
484}
485
486fn create_buffer_u8(
487 gl: &WebGl2RenderingContext,
488 target: u32,
489 data: &[u8],
490 usage: u32,
491) -> Result<web_sys::WebGlBuffer, Error> {
492 let index_buf = gl.create_buffer().ok_or(Error::buffer_creation_failed("vbo-u8"))?;
493 gl.bind_buffer(target, Some(&index_buf));
494
495 gl.buffer_data_with_u8_array(target, data, usage);
496
497 Ok(index_buf)
498}
499
500fn create_buffer_f32(
501 gl: &WebGl2RenderingContext,
502 target: u32,
503 data: &[f32],
504 usage: u32,
505) -> Result<web_sys::WebGlBuffer, Error> {
506 let buffer = gl.create_buffer().ok_or(Error::buffer_creation_failed("vbo-f32"))?;
507
508 gl.bind_buffer(target, Some(&buffer));
509
510 unsafe {
511 let view = js_sys::Float32Array::view(data);
512 gl.buffer_data_with_array_buffer_view(target, &view, usage);
513 }
514
515 const STRIDE: i32 = (2 + 2) * 4; enable_vertex_attrib(gl, attrib::POS, 2, GL::FLOAT, 0, STRIDE);
518 enable_vertex_attrib(gl, attrib::UV, 2, GL::FLOAT, 8, STRIDE);
519
520 Ok(buffer)
521}
522
523fn create_static_instance_buffer(
524 gl: &WebGl2RenderingContext,
525 instance_data: &[CellStatic],
526) -> Result<web_sys::WebGlBuffer, Error> {
527 let instance_buf = gl
528 .create_buffer()
529 .ok_or(Error::buffer_creation_failed("static-instance-buffer"))?;
530
531 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
532 buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::STATIC_DRAW);
533
534 let stride = size_of::<CellStatic>() as i32;
535 enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, GL::UNSIGNED_SHORT, 0, stride);
536
537 Ok(instance_buf)
538}
539
540fn create_dynamic_instance_buffer(
541 gl: &WebGl2RenderingContext,
542 instance_data: &[CellDynamic],
543) -> Result<web_sys::WebGlBuffer, Error> {
544 let instance_buf = gl
545 .create_buffer()
546 .ok_or(Error::buffer_creation_failed("dynamic-instance-buffer"))?;
547
548 gl.bind_buffer(GL::ARRAY_BUFFER, Some(&instance_buf));
549 buffer_upload_array(gl, GL::ARRAY_BUFFER, instance_data, GL::DYNAMIC_DRAW);
550
551 let stride = size_of::<CellDynamic>() as i32;
552
553 enable_vertex_attrib_array(gl, attrib::PACKED_DEPTH_FG_BG, 2, GL::UNSIGNED_INT, 0, stride);
555
556 Ok(instance_buf)
557}
558
559fn enable_vertex_attrib_array(
560 gl: &WebGl2RenderingContext,
561 index: u32,
562 size: i32,
563 type_: u32,
564 offset: i32,
565 stride: i32,
566) {
567 enable_vertex_attrib(gl, index, size, type_, offset, stride);
568 gl.vertex_attrib_divisor(index, 1);
569}
570
571fn enable_vertex_attrib(
572 gl: &WebGl2RenderingContext,
573 index: u32,
574 size: i32,
575 type_: u32,
576 offset: i32,
577 stride: i32,
578) {
579 gl.enable_vertex_attrib_array(index);
580 if type_ == GL::FLOAT {
581 gl.vertex_attrib_pointer_with_i32(index, size, type_, false, stride, offset);
582 } else {
583 gl.vertex_attrib_i_pointer_with_i32(index, size, type_, stride, offset);
584 }
585}
586
587impl Drawable for TerminalGrid {
588 fn prepare(&self, context: &mut RenderContext) {
589 let gl = context.gl;
590
591 self.shader.use_program(gl);
592
593 gl.bind_vertex_array(Some(&self.buffers.vao));
594
595 self.atlas.bind(gl, 0);
596 self.ubo_vertex.bind(context.gl);
597 self.ubo_fragment.bind(context.gl);
598 gl.uniform1i(Some(&self.sampler_loc), 0);
599 }
600
601 fn draw(&self, context: &mut RenderContext) {
602 let gl = context.gl;
603 let cell_count = self.cells.len() as i32;
604 gl.draw_elements_instanced_with_i32(GL::TRIANGLES, 6, GL::UNSIGNED_BYTE, 0, cell_count);
605 }
606
607 fn cleanup(&self, context: &mut RenderContext) {
608 let gl = context.gl;
609 gl.bind_vertex_array(None);
610 gl.bind_texture(GL::TEXTURE_2D_ARRAY, None);
611
612 self.ubo_vertex.unbind(gl);
613 self.ubo_fragment.unbind(gl);
614 }
615}
616
617#[derive(Debug, Copy, Clone)]
629pub struct CellData<'a> {
630 symbol: &'a str,
632 style_bits: u16,
633 fg: u32,
634 bg: u32,
635}
636
637impl<'a> CellData<'a> {
638 pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
650 Self::new_with_style_bits(symbol, style.style_mask() | effect as u16, fg, bg)
651 }
652
653 pub fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
677 debug_assert!(0x81FF & style_bits == 0, "Invalid style bits: {style_bits:#04x}");
679 Self { symbol, style_bits, fg, bg }
680 }
681}
682
683#[repr(C, align(4))]
704struct CellStatic {
705 pub grid_xy: [u16; 2],
707}
708
709#[derive(Debug, Clone, Copy)]
737#[repr(C, align(4))]
738pub struct CellDynamic {
739 data: [u8; 8], }
752
753impl CellStatic {
754 fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
755 debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
756 debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
757
758 (0..rows)
759 .flat_map(|row| (0..cols).map(move |col| (col, row)))
760 .map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
761 .collect()
762 }
763}
764
765impl CellDynamic {
766 #[inline]
767 pub fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
768 let mut data = [0; 8];
769
770 let glyph_id = glyph_id.to_le_bytes();
772 data[0] = glyph_id[0];
773 data[1] = glyph_id[1];
774
775 let fg = fg.to_le_bytes();
776 data[2] = fg[2]; data[3] = fg[1]; data[4] = fg[0]; let bg = bg.to_le_bytes();
781 data[5] = bg[2]; data[6] = bg[1]; data[7] = bg[0]; Self { data }
786 }
787
788 fn flip_colors(&mut self) {
789 let fg = [self.data[2], self.data[3], self.data[4]];
791 self.data[2] = self.data[5]; self.data[3] = self.data[6]; self.data[4] = self.data[7]; self.data[5] = fg[0]; self.data[6] = fg[1]; self.data[7] = fg[2]; }
798
799 fn glyph_id(&self) -> u16 {
800 u16::from_le_bytes([self.data[0], self.data[1]])
801 }
802}
803
804#[repr(C, align(16))] struct CellVertexUbo {
806 pub projection: [f32; 16], pub cell_size: [f32; 2], pub _padding: [f32; 2],
809}
810
811#[repr(C, align(16))] struct CellFragmentUbo {
813 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],
819}
820
821impl CellVertexUbo {
822 pub const BINDING_POINT: u32 = 0;
823
824 fn new(canvas_size: (i32, i32), cell_size: (i32, i32)) -> Self {
825 let projection =
826 Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
827 Self {
828 projection,
829 cell_size: [cell_size.0 as f32, cell_size.1 as f32],
830 _padding: [0.0; 2], }
832 }
833}
834
835impl CellFragmentUbo {
836 pub const BINDING_POINT: u32 = 1;
837
838 fn new(atlas: &FontAtlas) -> Self {
839 let cell_size = atlas.cell_size();
840 let underline = atlas.underline();
841 let strikethrough = atlas.strikethrough();
842 Self {
843 padding_frac: [
844 FontAtlasData::PADDING as f32 / cell_size.0 as f32,
845 FontAtlasData::PADDING as f32 / cell_size.1 as f32,
846 ],
847 underline_pos: underline.position,
848 underline_thickness: underline.thickness,
849 strikethrough_pos: strikethrough.position,
850 strikethrough_thickness: strikethrough.thickness,
851 _padding: [0.0; 2], }
853 }
854}
855
856fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: &[u16]) -> Vec<CellDynamic> {
857 let glyph_len = fill_glyph.len();
858 (0..cols * rows)
859 .map(|i| CellDynamic::new(fill_glyph[i as usize % glyph_len], 0x00ff_ffff, 0x0000_0000))
860 .collect()
861}
862
863mod attrib {
864 pub const POS: u32 = 0;
865 pub const UV: u32 = 1;
866
867 pub const GRID_XY: u32 = 2;
868 pub const PACKED_DEPTH_FG_BG: u32 = 3;
869}