use std::{cmp::min, fmt::Debug};
use beamterm_data::{CellSize, FontAtlasData, FontStyle, Glyph, GlyphEffect, TerminalSize};
use compact_str::CompactString;
use glow::HasContext;
use crate::{
CursorPosition,
error::Error,
gl::{
CellIterator, CellQuery, Drawable, GlState, RenderContext, ShaderProgram,
atlas::{self, FontAtlas, GlyphSlot},
buffer_upload_array,
dirty_regions::DirtyRegions,
selection::SelectionTracker,
ubo::UniformBufferObject,
},
mat4::Mat4,
};
#[derive(Debug)]
#[must_use = "call `delete(gl)` before dropping to avoid GPU resource leaks"]
pub struct TerminalGrid {
gpu: GpuResources,
cells: Vec<CellDynamic>,
terminal_size: TerminalSize,
canvas_size_px: (i32, i32),
pixel_ratio: f32,
atlas: FontAtlas,
fallback_glyph: u16,
selection: SelectionTracker,
dirty_regions: DirtyRegions,
bg_alpha: f32,
}
#[derive(Debug)]
struct GpuResources {
shader: ShaderProgram,
buffers: TerminalBuffers,
ubo_vertex: UniformBufferObject,
ubo_fragment: UniformBufferObject,
sampler_loc: glow::UniformLocation,
}
impl GpuResources {
const FRAGMENT_GLSL: &'static str = include_str!("../shaders/cell.frag");
const VERTEX_GLSL: &'static str = include_str!("../shaders/cell.vert");
fn delete(&self, gl: &glow::Context) {
self.shader.delete(gl);
self.buffers.delete(gl);
self.ubo_vertex.delete(gl);
self.ubo_fragment.delete(gl);
}
fn new(
gl: &glow::Context,
cell_pos: &[CellStatic],
cell_data: &[CellDynamic],
cell_size: CellSize,
glsl_version: &crate::GlslVersion,
) -> Result<Self, Error> {
let vao =
unsafe { gl.create_vertex_array() }.map_err(Error::vertex_array_creation_failed)?;
unsafe { gl.bind_vertex_array(Some(vao)) };
let buffers = setup_buffers(gl, vao, cell_pos, cell_data, cell_size)?;
unsafe { gl.bind_vertex_array(None) };
let vertex_source = format!("{}{}", glsl_version.vertex_preamble(), Self::VERTEX_GLSL);
let fragment_source = format!(
"{}{}",
glsl_version.fragment_preamble(),
Self::FRAGMENT_GLSL
);
let shader = ShaderProgram::create(gl, &vertex_source, &fragment_source)?;
shader.use_program(gl);
let ubo_vertex = UniformBufferObject::new(gl, CellVertexUbo::BINDING_POINT)?;
ubo_vertex.bind_to_shader(gl, &shader, "VertUbo")?;
let ubo_fragment = UniformBufferObject::new(gl, CellFragmentUbo::BINDING_POINT)?;
ubo_fragment.bind_to_shader(gl, &shader, "FragUbo")?;
let sampler_loc = unsafe { gl.get_uniform_location(shader.program, "u_sampler") }
.ok_or(Error::uniform_location_failed("u_sampler"))?;
Ok(Self {
shader,
buffers,
ubo_vertex,
ubo_fragment,
sampler_loc,
})
}
}
#[derive(Debug)]
struct TerminalBuffers {
vao: glow::VertexArray,
vertices: glow::Buffer,
instance_pos: glow::Buffer,
instance_cell: glow::Buffer,
indices: glow::Buffer,
}
impl TerminalBuffers {
fn delete(&self, gl: &glow::Context) {
unsafe {
gl.delete_vertex_array(self.vao);
gl.delete_buffer(self.vertices);
gl.delete_buffer(self.instance_pos);
gl.delete_buffer(self.instance_cell);
gl.delete_buffer(self.indices);
}
}
fn bind_instance_buffer(&self, gl: &glow::Context) {
unsafe {
gl.bind_vertex_array(Some(self.vao));
gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.instance_cell));
}
}
fn unbind_instance_buffer(&self, gl: &glow::Context) {
unsafe { gl.bind_vertex_array(None) };
}
fn upload_instance_data<T: Copy>(&self, gl: &glow::Context, cell_data: &[T]) {
unsafe { buffer_upload_array(gl, glow::ARRAY_BUFFER, cell_data, glow::DYNAMIC_DRAW) };
}
fn upload_instance_data_range<T: Copy>(
&self,
gl: &glow::Context,
cell_data: &[T],
byte_offset: usize,
) {
unsafe {
let bytes =
std::slice::from_raw_parts(cell_data.as_ptr() as *const u8, size_of_val(cell_data));
gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, byte_offset as i32, bytes);
}
}
fn update_vertex_buffer(&self, gl: &glow::Context, cell_size: CellSize) {
let (w, h) = (cell_size.width as f32, cell_size.height as f32);
#[rustfmt::skip]
let vertices: [f32; 16] = [
w, 0.0, 1.0, 0.0, 0.0, h, 0.0, 1.0, w, h, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0 ];
unsafe {
gl.bind_vertex_array(Some(self.vao));
gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertices));
let bytes = std::slice::from_raw_parts(
vertices.as_ptr() as *const u8,
vertices.len() * size_of::<f32>(),
);
gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, bytes);
gl.bind_vertex_array(None);
}
}
}
impl TerminalGrid {
pub fn new(
gl: &glow::Context,
mut atlas: FontAtlas,
screen_size: (i32, i32),
pixel_ratio: f32,
glsl_version: &crate::GlslVersion,
) -> Result<Self, Error> {
let cell_scale = atlas.cell_scale_for_dpr(pixel_ratio);
let cell_size = atlas.cell_size().scale(cell_scale);
let cols = screen_size.0 / cell_size.width;
let rows = screen_size.1 / cell_size.height;
let space_glyph = atlas.space_glyph_id();
let cell_data = create_terminal_cell_data(cols, rows, space_glyph);
let cell_pos = CellStatic::create_grid(cols, rows);
let grid = Self {
gpu: GpuResources::new(gl, &cell_pos, &cell_data, cell_size, glsl_version)?,
terminal_size: TerminalSize::new(cols as u16, rows as u16),
canvas_size_px: screen_size,
pixel_ratio,
cells: cell_data,
atlas,
fallback_glyph: space_glyph,
selection: SelectionTracker::new(),
dirty_regions: DirtyRegions::new((cols * rows) as usize),
bg_alpha: 1.0,
};
grid.upload_ubo_data(gl);
Ok(grid)
}
pub fn delete(self, gl: &glow::Context) {
self.gpu.delete(gl);
self.atlas.delete(gl);
}
fn effective_cell_size(&self) -> CellSize {
let cell_scale = self.atlas.cell_scale_for_dpr(self.pixel_ratio);
self.atlas.cell_size().scale(cell_scale)
}
pub fn set_fallback_glyph(&mut self, fallback: &str) {
self.fallback_glyph = self
.atlas
.resolve_glyph_slot(fallback, FontStyle::Normal as u16)
.map(|slot| slot.slot_id())
.unwrap_or(' ' as u16);
}
pub fn replace_atlas(&mut self, gl: &glow::Context, mut atlas: FontAtlas) {
let glyph_mask = atlas::GLYPH_SLOT_MASK as u16;
let style_mask = !glyph_mask;
let space_glyph = atlas.space_glyph_id();
self.fallback_glyph = self
.atlas
.get_symbol(self.fallback_glyph & glyph_mask)
.and_then(|symbol| {
let style_bits = self.fallback_glyph & style_mask;
atlas.resolve_glyph_slot(symbol.as_str(), style_bits)
})
.map(|slot| slot.slot_id())
.unwrap_or(space_glyph);
let mut skip_next = false;
for idx in 0..self.cells.len() {
if skip_next {
skip_next = false;
continue;
}
let old_glyph_id = self.cells[idx].glyph_id();
let style_bits = old_glyph_id & style_mask;
let slot = self
.atlas
.get_symbol(old_glyph_id & glyph_mask)
.and_then(|symbol| atlas.resolve_glyph_slot(symbol.as_str(), style_bits));
match slot {
Some(GlyphSlot::Normal(id)) => {
self.cells[idx].set_glyph_id(id);
},
Some(GlyphSlot::Wide(id)) | Some(GlyphSlot::Emoji(id)) => {
self.cells[idx].set_glyph_id(id);
if let Some(next_cell) = self.cells.get_mut(idx + 1) {
next_cell.set_glyph_id(id + 1);
skip_next = true;
}
},
None => {
self.cells[idx].set_glyph_id(self.fallback_glyph);
},
}
}
self.selection.clear();
let old_atlas = std::mem::replace(&mut self.atlas, atlas);
old_atlas.delete(gl);
self.dirty_regions.mark_all();
self.gpu
.buffers
.update_vertex_buffer(gl, self.effective_cell_size());
let _ = self.resize(gl, self.canvas_size_px, self.pixel_ratio);
}
pub fn atlas(&self) -> &FontAtlas {
&self.atlas
}
pub fn atlas_mut(&mut self) -> &mut FontAtlas {
&mut self.atlas
}
pub fn set_bg_alpha(&mut self, gl: &glow::Context, alpha: f32) {
self.bg_alpha = alpha.clamp(0.0, 1.0);
self.upload_ubo_data(gl);
}
pub fn canvas_size(&self) -> (i32, i32) {
self.canvas_size_px
}
pub fn cell_size(&self) -> CellSize {
self.effective_cell_size()
}
pub fn css_cell_size(&self) -> (f32, f32) {
let cs = self.effective_cell_size();
if self.pixel_ratio <= 0.0 {
return (cs.width as f32, cs.height as f32);
}
(
cs.width as f32 / self.pixel_ratio,
cs.height as f32 / self.pixel_ratio,
)
}
pub fn terminal_size(&self) -> TerminalSize {
self.terminal_size
}
pub fn render(&self, gl: &glow::Context, state: &mut GlState) -> Result<(), crate::Error> {
let mut ctx = RenderContext { gl, state };
self.prepare(&mut ctx)?;
self.draw(&mut ctx);
self.cleanup(&mut ctx);
Ok(())
}
pub fn cell_data_mut(&mut self, x: u16, y: u16) -> Option<&mut CellDynamic> {
let cols = self.terminal_size.cols;
let idx = y as usize * cols as usize + x as usize;
self.dirty_regions.mark(idx);
self.cells.get_mut(idx)
}
pub fn selection_tracker(&self) -> SelectionTracker {
self.selection.clone()
}
pub(super) fn get_symbols(&self, selection: CellIterator) -> CompactString {
let mut text = CompactString::new("");
for (idx, require_newline_after) in selection {
let cell_symbol = self.get_cell_symbol(idx);
if cell_symbol.is_some() {
text.push_str(&cell_symbol.unwrap_or_default());
}
if require_newline_after {
text.push('\n'); }
}
text
}
pub(crate) fn get_ascii_char_at(&self, cursor: CursorPosition) -> Option<char> {
let idx = cursor.row as usize * self.terminal_size.cols as usize + cursor.col as usize;
if idx < self.cells.len() {
let glyph_id = self.cells[idx].glyph_id();
self.atlas.get_ascii_char(glyph_id)
} else {
None
}
}
#[doc(hidden)]
pub fn hash_cells(&self, selection: CellQuery) -> u64 {
use std::hash::{Hash, Hasher};
use rustc_hash::FxHasher;
let mut hasher = FxHasher::default();
for (idx, _) in self.cell_iter(selection) {
self.cells[idx].hash(&mut hasher);
}
hasher.finish()
}
fn get_cell_symbol(&self, idx: usize) -> Option<CompactString> {
if idx < self.cells.len() {
let glyph_id = self.cells[idx].glyph_id();
let cell_symbol = self.atlas.get_symbol(glyph_id);
if cell_symbol.is_some() {
return cell_symbol;
}
}
self.fallback_symbol()
}
fn upload_ubo_data(&self, gl: &glow::Context) {
let vertex_ubo = CellVertexUbo::new(self.canvas_size_px, self.effective_cell_size());
self.gpu.ubo_vertex.upload_data(gl, &vertex_ubo);
let fragment_ubo = CellFragmentUbo::new(&self.atlas, self.bg_alpha);
self.gpu
.ubo_fragment
.upload_data(gl, &fragment_ubo);
}
pub fn cell_count(&self) -> usize {
self.cells.len()
}
pub fn update_cells<'a>(
&mut self,
cells: impl Iterator<Item = CellData<'a>>,
) -> Result<(), Error> {
let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
let atlas = &mut self.atlas;
let cell_buf = &mut self.cells;
let mut pending_cell: Option<CellDynamic> = None;
cell_buf
.iter_mut()
.zip(cells)
.for_each(|(cell, data)| {
let glyph = atlas
.resolve_glyph_slot(data.symbol, data.style_bits)
.unwrap_or(fallback_glyph);
*cell = if let Some(second_cell) = pending_cell.take() {
second_cell
} else {
match glyph {
GlyphSlot::Normal(id) => CellDynamic::new(id, data.fg, data.bg),
GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
pending_cell = Some(CellDynamic::new(id + 1, data.fg, data.bg));
CellDynamic::new(id, data.fg, data.bg)
},
}
}
});
self.dirty_regions.mark_all();
Ok(())
}
pub fn update_cells_by_position<'a>(
&mut self,
cells: impl Iterator<Item = (u16, u16, CellData<'a>)>,
) -> Result<(), Error> {
let cols = self.terminal_size.cols as usize;
let cells_by_index = cells.map(|(x, y, data)| (y as usize * cols + x as usize, data));
self.update_cells_by_index(cells_by_index)
}
pub fn update_cells_by_index<'a>(
&mut self,
cells: impl Iterator<Item = (usize, CellData<'a>)>,
) -> Result<(), Error> {
let fallback_glyph = GlyphSlot::Normal(self.fallback_glyph);
let atlas = &mut self.atlas;
let cell_buf = &mut self.cells;
let dirty_regions = &mut self.dirty_regions;
let cell_count = cell_buf.len();
let mut skip_idx = None;
cells
.filter(|(idx, _)| *idx < cell_count)
.for_each(|(idx, cell)| {
if skip_idx.take() == Some(idx) {
return;
}
let glyph = atlas
.resolve_glyph_slot(cell.symbol, cell.style_bits)
.unwrap_or(fallback_glyph);
match glyph {
GlyphSlot::Normal(id) => {
cell_buf[idx] = CellDynamic::new(id, cell.fg, cell.bg);
dirty_regions.mark(idx);
},
GlyphSlot::Wide(id) | GlyphSlot::Emoji(id) => {
cell_buf[idx] = CellDynamic::new(id, cell.fg, cell.bg);
dirty_regions.mark(idx);
if let Some(c) = cell_buf.get_mut(idx + 1) {
*c = CellDynamic::new(id + 1, cell.fg, cell.bg);
dirty_regions.mark(idx + 1);
skip_idx = Some(idx + 1);
}
},
}
});
Ok(())
}
pub fn update_cell(&mut self, x: u16, y: u16, cell_data: CellData) -> Result<(), Error> {
let cols = self.terminal_size.cols;
let idx = y as usize * cols as usize + x as usize;
self.update_cell_by_index(idx, cell_data)
}
pub fn update_cell_by_index(&mut self, idx: usize, cell_data: CellData) -> Result<(), Error> {
self.update_cells_by_index(std::iter::once((idx, cell_data)))
}
pub fn flush_cells(&mut self, gl: &glow::Context) -> Result<(), Error> {
self.atlas.bind(gl);
self.atlas.flush(gl)?;
if self.dirty_regions.is_clean() {
return Ok(()); }
self.clear_stale_selection();
self.flip_selected_cell_colors();
self.gpu.buffers.bind_instance_buffer(gl);
if self.dirty_regions.is_all_active_dirty() {
self.gpu
.buffers
.upload_instance_data(gl, &self.cells);
self.dirty_regions.clear();
} else {
for (start, end) in self.dirty_regions.drain() {
self.gpu.buffers.upload_instance_data_range(
gl,
&self.cells[start..end],
start * CellDynamic::SIZE,
);
}
}
self.gpu.buffers.unbind_instance_buffer(gl);
self.flip_selected_cell_colors();
Ok(())
}
fn flip_selected_cell_colors(&mut self) {
if let Some(iter) = self.selected_cells_iter() {
iter.for_each(|(idx, _)| {
self.cells[idx].flip_colors();
self.dirty_regions.mark(idx);
});
}
}
fn selected_cells_iter(&self) -> Option<CellIterator> {
self.selection
.get_query()
.map(|query| self.cell_iter(query))
}
pub fn resize(
&mut self,
gl: &glow::Context,
canvas_size: (i32, i32),
pixel_ratio: f32,
) -> Result<(), Error> {
self.canvas_size_px = canvas_size;
self.pixel_ratio = pixel_ratio;
let cell_size = self.effective_cell_size();
self.gpu
.buffers
.update_vertex_buffer(gl, cell_size);
self.upload_ubo_data(gl);
let cols = (canvas_size.0 / cell_size.width).max(1);
let rows = (canvas_size.1 / cell_size.height).max(1);
if self.terminal_size == TerminalSize::new(cols as u16, rows as u16) {
return Ok(()); }
unsafe {
gl.bind_vertex_array(Some(self.gpu.buffers.vao));
gl.delete_buffer(self.gpu.buffers.instance_cell);
gl.delete_buffer(self.gpu.buffers.instance_pos);
}
let current_size = (
self.terminal_size.cols as i32,
self.terminal_size.rows as i32,
);
let cell_data = self.resize_cell_grid(current_size, (cols, rows));
self.cells = cell_data;
let cell_pos = CellStatic::create_grid(cols, rows);
self.gpu.buffers.instance_cell = create_dynamic_instance_buffer(gl, &self.cells)?;
self.gpu.buffers.instance_pos = create_static_instance_buffer(gl, &cell_pos)?;
unsafe { gl.bind_vertex_array(None) };
self.terminal_size = TerminalSize::new(cols as u16, rows as u16);
self.dirty_regions = DirtyRegions::new(self.cells.len());
Ok(())
}
pub fn recreate_resources(
&mut self,
gl: &glow::Context,
glsl_version: &crate::GlslVersion,
) -> Result<(), Error> {
let cell_size = self.effective_cell_size();
let (cols, rows) = (
self.terminal_size.cols as i32,
self.terminal_size.rows as i32,
);
let cell_pos = CellStatic::create_grid(cols, rows);
self.gpu = GpuResources::new(gl, &cell_pos, &self.cells, cell_size, glsl_version)?;
self.upload_ubo_data(gl);
self.dirty_regions.mark_all();
Ok(())
}
pub fn recreate_atlas_texture(&mut self, gl: &glow::Context) -> Result<(), Error> {
self.atlas.recreate_texture(gl)
}
pub fn base_glyph_id(&mut self, symbol: &str) -> Option<u16> {
self.atlas.get_base_glyph_id(symbol)
}
fn fallback_symbol(&self) -> Option<CompactString> {
self.atlas.get_symbol(self.fallback_glyph)
}
fn clear_stale_selection(&self) {
if let Some(query) = self.selection_tracker().get_query()
&& let Some(hash) = query.content_hash
&& hash != self.hash_cells(query)
{
self.selection.clear();
}
}
fn resize_cell_grid(&mut self, old_size: (i32, i32), new_size: (i32, i32)) -> Vec<CellDynamic> {
let empty_cell = CellDynamic::new(self.atlas.space_glyph_id(), 0xFFFFFF, 0x000000);
let new_len = new_size.0 * new_size.1;
let mut new_cells = Vec::with_capacity(new_len as usize);
for _ in 0..new_len {
new_cells.push(empty_cell);
}
let cells = &self.cells;
for y in 0..min(old_size.1, new_size.1) {
for x in 0..min(old_size.0, new_size.0) {
let new_idx = (y * new_size.0 + x) as usize;
let old_idx = (y * old_size.0 + x) as usize;
new_cells[new_idx] = cells[old_idx];
}
}
new_cells
}
}
fn setup_buffers(
gl: &glow::Context,
vao: glow::VertexArray,
cell_pos: &[CellStatic],
cell_data: &[CellDynamic],
cell_size: CellSize,
) -> Result<TerminalBuffers, Error> {
let (w, h) = (cell_size.width as f32, cell_size.height as f32);
#[rustfmt::skip]
let vertices = [
w, 0.0, 1.0, 0.0, 0.0, h, 0.0, 1.0, w, h, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0 ];
let indices = [0, 1, 2, 0, 3, 1];
Ok(TerminalBuffers {
vao,
vertices: create_buffer_f32(gl, glow::ARRAY_BUFFER, &vertices, glow::STATIC_DRAW)?,
instance_pos: create_static_instance_buffer(gl, cell_pos)?,
instance_cell: create_dynamic_instance_buffer(gl, cell_data)?,
indices: create_buffer_u8(gl, glow::ELEMENT_ARRAY_BUFFER, &indices, glow::STATIC_DRAW)?,
})
}
fn create_buffer_u8(
gl: &glow::Context,
target: u32,
data: &[u8],
usage: u32,
) -> Result<glow::Buffer, Error> {
let buffer =
unsafe { gl.create_buffer() }.map_err(|e| Error::buffer_creation_failed("vbo-u8", e))?;
unsafe {
gl.bind_buffer(target, Some(buffer));
gl.buffer_data_u8_slice(target, data, usage);
}
Ok(buffer)
}
fn create_buffer_f32(
gl: &glow::Context,
target: u32,
data: &[f32],
usage: u32,
) -> Result<glow::Buffer, Error> {
let buffer =
unsafe { gl.create_buffer() }.map_err(|e| Error::buffer_creation_failed("vbo-f32", e))?;
unsafe {
gl.bind_buffer(target, Some(buffer));
let bytes =
std::slice::from_raw_parts(data.as_ptr() as *const u8, std::mem::size_of_val(data));
gl.buffer_data_u8_slice(target, bytes, usage);
}
const STRIDE: i32 = (2 + 2) * 4; enable_vertex_attrib(gl, attrib::POS, 2, glow::FLOAT, 0, STRIDE);
enable_vertex_attrib(gl, attrib::UV, 2, glow::FLOAT, 8, STRIDE);
Ok(buffer)
}
fn create_static_instance_buffer(
gl: &glow::Context,
instance_data: &[CellStatic],
) -> Result<glow::Buffer, Error> {
let buffer = unsafe { gl.create_buffer() }
.map_err(|e| Error::buffer_creation_failed("static-instance-buffer", e))?;
unsafe {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer));
buffer_upload_array(gl, glow::ARRAY_BUFFER, instance_data, glow::STATIC_DRAW);
}
let stride = size_of::<CellStatic>() as i32;
enable_vertex_attrib_array(gl, attrib::GRID_XY, 2, glow::UNSIGNED_SHORT, 0, stride);
Ok(buffer)
}
fn create_dynamic_instance_buffer(
gl: &glow::Context,
instance_data: &[CellDynamic],
) -> Result<glow::Buffer, Error> {
let buffer = unsafe { gl.create_buffer() }
.map_err(|e| Error::buffer_creation_failed("dynamic-instance-buffer", e))?;
unsafe {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer));
buffer_upload_array(gl, glow::ARRAY_BUFFER, instance_data, glow::DYNAMIC_DRAW);
}
let stride = size_of::<CellDynamic>() as i32;
enable_vertex_attrib_array(
gl,
attrib::PACKED_DEPTH_FG_BG,
2,
glow::UNSIGNED_INT,
0,
stride,
);
Ok(buffer)
}
fn enable_vertex_attrib_array(
gl: &glow::Context,
index: u32,
size: i32,
type_: u32,
offset: i32,
stride: i32,
) {
enable_vertex_attrib(gl, index, size, type_, offset, stride);
unsafe { gl.vertex_attrib_divisor(index, 1) };
}
fn enable_vertex_attrib(
gl: &glow::Context,
index: u32,
size: i32,
type_: u32,
offset: i32,
stride: i32,
) {
unsafe {
gl.enable_vertex_attrib_array(index);
if type_ == glow::FLOAT {
gl.vertex_attrib_pointer_f32(index, size, type_, false, stride, offset);
} else {
gl.vertex_attrib_pointer_i32(index, size, type_, stride, offset);
}
}
}
impl Drawable for TerminalGrid {
fn prepare(&self, context: &mut RenderContext) -> Result<(), crate::Error> {
let gl = context.gl;
self.gpu.shader.use_program(gl);
unsafe { gl.bind_vertex_array(Some(self.gpu.buffers.vao)) };
context.state.active_texture(gl, glow::TEXTURE0);
self.atlas.bind(gl);
self.gpu.ubo_vertex.bind(context.gl);
self.gpu.ubo_fragment.bind(context.gl);
unsafe { gl.uniform_1_i32(Some(&self.gpu.sampler_loc), 0) };
Ok(())
}
fn draw(&self, context: &mut RenderContext) {
let gl = context.gl;
let cell_count = self.cells.len() as i32;
unsafe {
gl.draw_elements_instanced(glow::TRIANGLES, 6, glow::UNSIGNED_BYTE, 0, cell_count);
}
}
fn cleanup(&self, context: &mut RenderContext) {
let gl = context.gl;
unsafe {
gl.bind_vertex_array(None);
gl.bind_texture(glow::TEXTURE_2D_ARRAY, None);
gl.use_program(None);
}
self.gpu.ubo_vertex.unbind(gl);
self.gpu.ubo_fragment.unbind(gl);
}
}
#[derive(Debug, Copy, Clone)]
pub struct CellData<'a> {
symbol: &'a str,
style_bits: u16,
fg: u32,
bg: u32,
}
impl<'a> CellData<'a> {
pub fn new(symbol: &'a str, style: FontStyle, effect: GlyphEffect, fg: u32, bg: u32) -> Self {
let style_bits = style.style_mask() | effect as u16;
debug_assert!(
0x81FF & style_bits == 0,
"Invalid style bits: {style_bits:#04x}"
);
Self::new_with_style_bits(symbol, style_bits, fg, bg)
}
pub const fn new_with_style_bits(symbol: &'a str, style_bits: u16, fg: u32, bg: u32) -> Self {
Self { symbol, style_bits, fg, bg }
}
}
#[derive(Clone, Copy)]
#[repr(C, align(4))]
struct CellStatic {
pub grid_xy: [u16; 2],
}
#[derive(Debug, Clone, Copy, Hash)]
#[repr(C, align(4))]
pub struct CellDynamic {
data: [u8; 8], }
impl CellStatic {
fn create_grid(cols: i32, rows: i32) -> Vec<Self> {
debug_assert!(cols > 0 && cols < u16::MAX as i32, "cols: {cols}");
debug_assert!(rows > 0 && rows < u16::MAX as i32, "rows: {rows}");
(0..rows)
.flat_map(|row| (0..cols).map(move |col| (col, row)))
.map(|(col, row)| Self { grid_xy: [col as u16, row as u16] })
.collect()
}
}
impl CellDynamic {
const SIZE: usize = size_of::<Self>();
const GLYPH_STYLE_MASK: u16 =
Glyph::BOLD_FLAG | Glyph::ITALIC_FLAG | Glyph::UNDERLINE_FLAG | Glyph::STRIKETHROUGH_FLAG;
#[inline]
pub fn new(glyph_id: u16, fg: u32, bg: u32) -> Self {
let mut data = [0; 8];
let glyph_id = glyph_id.to_le_bytes();
data[0] = glyph_id[0];
data[1] = glyph_id[1];
let fg = fg.to_le_bytes();
data[2] = fg[2]; data[3] = fg[1]; data[4] = fg[0];
let bg = bg.to_le_bytes();
data[5] = bg[2]; data[6] = bg[1]; data[7] = bg[0];
Self { data }
}
pub fn style(&mut self, style_bits: u16) {
let glyph_id = (self.glyph_id() & !Self::GLYPH_STYLE_MASK) | style_bits;
self.data[..2].copy_from_slice(&glyph_id.to_le_bytes());
}
pub fn flip_colors(&mut self) {
let fg = [self.data[2], self.data[3], self.data[4]];
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]; }
pub fn fg_color(&mut self, fg: u32) {
let fg = fg.to_le_bytes();
self.data[2] = fg[2]; self.data[3] = fg[1]; self.data[4] = fg[0]; }
pub fn bg_color(&mut self, bg: u32) {
let bg = bg.to_le_bytes();
self.data[5] = bg[2]; self.data[6] = bg[1]; self.data[7] = bg[0]; }
pub fn get_fg_color(&self) -> u32 {
((self.data[2] as u32) << 16) | ((self.data[3] as u32) << 8) | (self.data[4] as u32)
}
pub fn get_bg_color(&self) -> u32 {
((self.data[5] as u32) << 16) | ((self.data[6] as u32) << 8) | (self.data[7] as u32)
}
pub fn get_style(&self) -> u16 {
self.glyph_id() & Self::GLYPH_STYLE_MASK
}
#[inline]
fn glyph_id(&self) -> u16 {
u16::from_le_bytes([self.data[0], self.data[1]])
}
fn set_glyph_id(&mut self, glyph_id: u16) {
let bytes = glyph_id.to_le_bytes();
self.data[0] = bytes[0];
self.data[1] = bytes[1];
}
}
#[derive(Clone, Copy)]
#[repr(C, align(16))] struct CellVertexUbo {
pub projection: [f32; 16], pub cell_size: [f32; 2], pub _padding: [f32; 2],
}
#[derive(Clone, Copy)]
#[repr(C, align(16))] struct CellFragmentUbo {
pub padding_frac: [f32; 2], pub underline_pos: f32, pub underline_thickness: f32, pub strikethrough_pos: f32, pub strikethrough_thickness: f32, pub emoji_bit: u32, pub bg_alpha: f32, }
impl CellVertexUbo {
pub const BINDING_POINT: u32 = 0;
fn new(canvas_size: (i32, i32), cell_size: CellSize) -> Self {
let projection =
Mat4::orthographic_from_size(canvas_size.0 as f32, canvas_size.1 as f32).data;
Self {
projection,
cell_size: [cell_size.width as f32, cell_size.height as f32],
_padding: [0.0; 2], }
}
}
impl CellFragmentUbo {
pub const BINDING_POINT: u32 = 1;
fn new(atlas: &FontAtlas, bg_alpha: f32) -> Self {
let tcs = atlas.texture_cell_size();
let underline = atlas.underline();
let strikethrough = atlas.strikethrough();
Self {
padding_frac: [
FontAtlasData::PADDING as f32 / tcs.width as f32,
FontAtlasData::PADDING as f32 / tcs.height as f32,
],
underline_pos: underline.position(),
underline_thickness: underline.thickness(),
strikethrough_pos: strikethrough.position(),
strikethrough_thickness: strikethrough.thickness(),
emoji_bit: atlas.emoji_bit(),
bg_alpha,
}
}
}
fn create_terminal_cell_data(cols: i32, rows: i32, fill_glyph: u16) -> Vec<CellDynamic> {
(0..cols * rows)
.map(|_i| CellDynamic::new(fill_glyph, 0x00ff_ffff, 0x0000_0000))
.collect()
}
mod attrib {
pub const POS: u32 = 0;
pub const UV: u32 = 1;
pub const GRID_XY: u32 = 2;
pub const PACKED_DEPTH_FG_BG: u32 = 3;
}