diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b9a41c..188276c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Track and report surface damage information to Wayland compositors - Escape sequence for undercurl, dotted and dashed underlines (`CSI 4 : [3-5] m`) - `ToggleMaximized` key binding action to (un-)maximize the active window, not bound by default +- Support for OpenGL ES 2.0 ### Changed diff --git a/README.md b/README.md index e4af703b..9a8546ba 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,7 @@ For everyone else, the detailed instructions to install Alacritty can be found ### Requirements -- OpenGL 3.3 or higher +- At least OpenGL ES 2.0 - [Windows] ConPTY support (Windows 10 version 1809 or higher) ## Configuration diff --git a/alacritty/res/gles2/text.f.glsl b/alacritty/res/gles2/text.f.glsl new file mode 100644 index 00000000..cc5943c9 --- /dev/null +++ b/alacritty/res/gles2/text.f.glsl @@ -0,0 +1,57 @@ +varying mediump vec2 TexCoords; +varying mediump vec3 fg; +varying highp float colored; +varying mediump vec4 bg; + +uniform highp int renderingPass; +uniform sampler2D mask; + +#define COLORED 1 + +mediump float max_rgb(mediump vec3 mask) { + return max(max(mask.r, mask.g), mask.b); +} + +void render_text() { + mediump vec4 mask = texture2D(mask, TexCoords); + mediump float m_rgb = max_rgb(mask.rgb); + + if (renderingPass == 1) { + gl_FragColor = vec4(mask.rgb, m_rgb); + } else if (renderingPass == 2) { + gl_FragColor = bg * (vec4(m_rgb) - vec4(mask.rgb, m_rgb)); + } else { + gl_FragColor = vec4(fg, 1.) * vec4(mask.rgb, m_rgb); + } +} + +// Render colored bitmaps. +void render_bitmap() { + if (renderingPass == 2) { + discard; + } + mediump vec4 mask = texture2D(mask, TexCoords); + if (renderingPass == 1) { + gl_FragColor = mask.aaaa; + } else { + gl_FragColor = mask; + } +} + +void main() { + // Handle background pass drawing before anything else. + if (renderingPass == 0) { + if (bg.a == 0.0) { + discard; + } + + gl_FragColor = vec4(bg.rgb * bg.a, bg.a); + return; + } + + if (int(colored) == COLORED) { + render_bitmap(); + } else { + render_text(); + } +} diff --git a/alacritty/res/gles2/text.v.glsl b/alacritty/res/gles2/text.v.glsl new file mode 100644 index 00000000..009094cc --- /dev/null +++ b/alacritty/res/gles2/text.v.glsl @@ -0,0 +1,46 @@ +// Cell coords. +attribute vec2 cellCoords; + +// Glyph coords. +attribute vec2 glyphCoords; + +// uv mapping. +attribute vec2 uv; + +// Text foreground rgb packed together with cell flags. textColor.a +// are the bitflags; consult RenderingGlyphFlags in renderer/mod.rs +// for the possible values. +attribute vec4 textColor; + +// Background color. +attribute vec4 backgroundColor; + +varying vec2 TexCoords; +varying vec3 fg; +varying float colored; +varying vec4 bg; + +uniform highp int renderingPass; +uniform vec4 projection; + +void main() { + vec2 projectionOffset = projection.xy; + vec2 projectionScale = projection.zw; + + vec2 position; + if (renderingPass == 0) { + TexCoords = vec2(0, 0); + position = cellCoords; + } else { + TexCoords = uv; + position = glyphCoords; + } + + fg = vec3(float(textColor.r), float(textColor.g), float(textColor.b)) / 255.; + colored = float(textColor.a); + bg = vec4(float(backgroundColor.r), float(backgroundColor.g), float(backgroundColor.b), + float(backgroundColor.a)) / 255.; + + vec2 finalPosition = projectionOffset + position * projectionScale; + gl_Position = vec4(finalPosition, 0., 1.); +} diff --git a/alacritty/res/text.f.glsl b/alacritty/res/glsl3/text.f.glsl similarity index 98% rename from alacritty/res/text.f.glsl rename to alacritty/res/glsl3/text.f.glsl index 3b8cd67c..eddc1734 100644 --- a/alacritty/res/text.f.glsl +++ b/alacritty/res/glsl3/text.f.glsl @@ -1,4 +1,3 @@ -#version 330 core in vec2 TexCoords; flat in vec4 fg; flat in vec4 bg; diff --git a/alacritty/res/text.v.glsl b/alacritty/res/glsl3/text.v.glsl similarity index 99% rename from alacritty/res/text.v.glsl rename to alacritty/res/glsl3/text.v.glsl index a4a31382..5c22e0e6 100644 --- a/alacritty/res/text.v.glsl +++ b/alacritty/res/glsl3/text.v.glsl @@ -1,4 +1,3 @@ -#version 330 core // Cell properties. layout(location = 0) in vec2 gridCoords; diff --git a/alacritty/res/rect.f.glsl b/alacritty/res/rect.f.glsl index aad8b418..10c7983a 100644 --- a/alacritty/res/rect.f.glsl +++ b/alacritty/res/rect.f.glsl @@ -1,18 +1,32 @@ -#version 330 core +#if defined(GLES2_RENDERER) +#define float_t mediump float +#define color_t mediump vec4 +#define FRAG_COLOR gl_FragColor + +varying color_t color; + +#else +#define float_t float +#define int_t int +#define color_t vec4 -flat in vec4 color; out vec4 FragColor; +#define FRAG_COLOR FragColor + +flat in color_t color; + +#endif + uniform int rectKind; +uniform float_t cellWidth; +uniform float_t cellHeight; +uniform float_t paddingY; +uniform float_t paddingX; -uniform float cellWidth; -uniform float cellHeight; -uniform float paddingY; -uniform float paddingX; +uniform float_t underlinePosition; +uniform float_t underlineThickness; -uniform float underlinePosition; -uniform float underlineThickness; - -uniform float undercurlPosition; +uniform float_t undercurlPosition; #define UNDERCURL 1 #define DOTTED 2 @@ -20,19 +34,18 @@ uniform float undercurlPosition; #define PI 3.1415926538 -vec4 draw_undercurl(int x, int y) { +color_t draw_undercurl(float_t x, float_t y) { // We use `undercurlPosition` as an amplitude, since it's half of the descent // value. - float undercurl = - undercurlPosition / 2. * cos(float(x) * 2 * PI / cellWidth) + - + undercurlPosition - 1.; + float_t undercurl = undercurlPosition / 2. * cos(x * 2. * PI / cellWidth) + + undercurlPosition - 1.; - float undercurlTop = undercurl + max((underlineThickness - 1), 0); - float undercurlBottom = undercurl - max((underlineThickness - 1), 0); + float_t undercurlTop = undercurl + max((underlineThickness - 1.), 0.); + float_t undercurlBottom = undercurl - max((underlineThickness - 1.), 0.); // Compute resulted alpha based on distance from `gl_FragCoord.y` to the // cosine curve. - float alpha = 1.; + float_t alpha = 1.; if (y > undercurlTop || y < undercurlBottom) { alpha = 1. - min(abs(undercurlTop - y), abs(undercurlBottom - y)); } @@ -43,54 +56,54 @@ vec4 draw_undercurl(int x, int y) { // When the dot size increases we can use AA to make spacing look even and the // dots rounded. -vec4 draw_dotted_aliased(float x, float y) { - int dotNumber = int(x / underlineThickness); +color_t draw_dotted_aliased(float_t x, float_t y) { + float_t dotNumber = floor(x / underlineThickness); - float radius = underlineThickness / 2.; - float centerY = underlinePosition - 1.; + float_t radius = underlineThickness / 2.; + float_t centerY = underlinePosition - 1.; - float leftCenter = (dotNumber - dotNumber % 2) * underlineThickness + radius; - float rightCenter = leftCenter + 2 * underlineThickness; + float_t leftCenter = (dotNumber - mod(dotNumber, 2.)) * underlineThickness + radius; + float_t rightCenter = leftCenter + 2. * underlineThickness; - float distanceLeft = sqrt(pow(x - leftCenter, 2) + pow(y - centerY, 2)); - float distanceRight = sqrt(pow(x - rightCenter, 2) + pow(y - centerY, 2)); + float_t distanceLeft = sqrt(pow(x - leftCenter, 2.) + pow(y - centerY, 2.)); + float_t distanceRight = sqrt(pow(x - rightCenter, 2.) + pow(y - centerY, 2.)); - float alpha = max(1 - (min(distanceLeft, distanceRight) - radius), 0); + float_t alpha = max(1. - (min(distanceLeft, distanceRight) - radius), 0.); return vec4(color.rgb, alpha); } /// Draw dotted line when dot is just a single pixel. -vec4 draw_dotted(int x, int y) { - int cellEven = 0; +color_t draw_dotted(float_t x, float_t y) { + float_t cellEven = 0.; // Since the size of the dot and its gap combined is 2px we should ensure that // spacing will be even. If the cellWidth is even it'll work since we start // with dot and end with gap. However if cellWidth is odd, the cell will start // and end with a dot, creating a dash. To resolve this issue, we invert the // pattern every two cells. - if (int(cellWidth) % 2 != 0) { - cellEven = int((gl_FragCoord.x - paddingX) / cellWidth) % 2; + if (int(mod(cellWidth, 2.)) != 0) { + cellEven = mod((gl_FragCoord.x - paddingX) / cellWidth, 2.); } // Since we use the entire descent area for dotted underlines, we limit its // height to a single pixel so we don't draw bars instead of dots. - float alpha = 1. - abs(round(underlinePosition - 1.) - y); - if (x % 2 != cellEven) { - alpha = 0; + float_t alpha = 1. - abs(floor(underlinePosition - 0.5) - y); + if (int(mod(x, 2.)) != int(cellEven)) { + alpha = 0.; } return vec4(color.rgb, alpha); } -vec4 draw_dashed(int x) { +color_t draw_dashed(float_t x) { // Since dashes of adjacent cells connect with each other our dash length is // half of the desired total length. - int halfDashLen = int(cellWidth) / 4; + float_t halfDashLen = floor(cellWidth / 4.); - float alpha = 1.; + float_t alpha = 1.; // Check if `x` coordinate is where we should draw gap. - if (x > halfDashLen && x < cellWidth - halfDashLen - 1) { + if (x > halfDashLen && x < cellWidth - halfDashLen - 1.) { alpha = 0.; } @@ -98,25 +111,20 @@ vec4 draw_dashed(int x) { } void main() { - int x = int(gl_FragCoord.x - paddingX) % int(cellWidth); - int y = int(gl_FragCoord.y - paddingY) % int(cellHeight); + float_t x = floor(mod(gl_FragCoord.x - paddingX, cellWidth)); + float_t y = floor(mod(gl_FragCoord.y - paddingY, cellHeight)); - switch (rectKind) { - case UNDERCURL: - FragColor = draw_undercurl(x, y); - break; - case DOTTED: - if (underlineThickness < 2) { - FragColor = draw_dotted(x, y); + if (rectKind == UNDERCURL) { + FRAG_COLOR = draw_undercurl(x, y); + } else if (rectKind == DOTTED) { + if (underlineThickness < 2.) { + FRAG_COLOR = draw_dotted(x, y); } else { - FragColor = draw_dotted_aliased(x, y); + FRAG_COLOR = draw_dotted_aliased(x, y); } - break; - case DASHED: - FragColor = draw_dashed(x); - break; - default: - FragColor = color; - break; + } else if (rectKind == DASHED) { + FRAG_COLOR = draw_dashed(x); + } else { + FRAG_COLOR = color; } } diff --git a/alacritty/res/rect.v.glsl b/alacritty/res/rect.v.glsl index bf9a97d3..746eab74 100644 --- a/alacritty/res/rect.v.glsl +++ b/alacritty/res/rect.v.glsl @@ -1,11 +1,16 @@ -#version 330 core +#if defined(GLES2_RENDERER) +attribute vec2 aPos; +attribute vec4 aColor; + +varying mediump vec4 color; +#else layout (location = 0) in vec2 aPos; layout (location = 1) in vec4 aColor; flat out vec4 color; +#endif -void main() -{ +void main() { color = aColor; gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); } diff --git a/alacritty/src/display/mod.rs b/alacritty/src/display/mod.rs index dfd43d04..7e8cfc86 100644 --- a/alacritty/src/display/mod.rs +++ b/alacritty/src/display/mod.rs @@ -50,7 +50,7 @@ use crate::display::window::Window; use crate::event::{Mouse, SearchState}; use crate::message_bar::{MessageBuffer, MessageType}; use crate::renderer::rects::{RenderLines, RenderRect}; -use crate::renderer::{self, GlyphCache, QuadRenderer}; +use crate::renderer::{self, GlyphCache, Renderer}; pub mod content; pub mod cursor; @@ -204,7 +204,7 @@ pub struct Display { is_damage_supported: bool, debug_damage: bool, damage_rects: Vec, - renderer: QuadRenderer, + renderer: Renderer, glyph_cache: GlyphCache, meter: Meter, } @@ -259,7 +259,7 @@ impl Display { )?; // Create renderer. - let mut renderer = QuadRenderer::new()?; + let mut renderer = Renderer::new()?; let scale_factor = window.scale_factor; info!("Display scale factor: {}", scale_factor); @@ -308,9 +308,7 @@ impl Display { // Clear screen. let background_color = config.colors.primary.background; - renderer.with_api(config, &size_info, |api| { - api.clear(background_color); - }); + renderer.clear(background_color, config.window_opacity()); // Set subpixel anti-aliasing. #[cfg(target_os = "macos")] @@ -325,9 +323,7 @@ impl Display { #[cfg(not(any(target_os = "macos", windows)))] if is_x11 { window.swap_buffers(); - renderer.with_api(config, &size_info, |api| { - api.finish(); - }); + renderer.finish(); } window.set_visible(true); @@ -376,7 +372,7 @@ impl Display { /// /// This will return a tuple of the cell width and height. fn update_glyph_cache( - renderer: &mut QuadRenderer, + renderer: &mut Renderer, glyph_cache: &mut GlyphCache, scale_factor: f64, config: &UiConfig, @@ -573,10 +569,7 @@ impl Display { // Make sure this window's OpenGL context is active. self.window.make_current(); - self.renderer.with_api(config, &size_info, |api| { - api.clear(background_color); - }); - + self.renderer.clear(background_color, config.window_opacity()); let mut lines = RenderLines::new(); // Draw grid. @@ -590,9 +583,10 @@ impl Display { let glyph_cache = &mut self.glyph_cache; let highlighted_hint = &self.highlighted_hint; let vi_highlighted_hint = &self.vi_highlighted_hint; - self.renderer.with_api(config, &size_info, |mut api| { - // Iterate over all non-empty cells in the grid. - for mut cell in grid_cells { + self.renderer.draw_cells( + &size_info, + glyph_cache, + grid_cells.into_iter().map(|mut cell| { // Underline hints hovered by mouse or vi mode cursor. let point = viewport_to_point(display_offset, cell.point); if highlighted_hint.as_ref().map_or(false, |h| h.bounds.contains(&point)) @@ -604,10 +598,9 @@ impl Display { // Update underline/strikeout. lines.update(&cell); - // Draw the cell. - api.draw_cell(cell, glyph_cache); - } - }); + cell + }), + ); } let mut rects = lines.rects(&metrics, &size_info); @@ -675,9 +668,7 @@ impl Display { let fg = config.colors.primary.background; for (i, message_text) in text.iter().enumerate() { let point = Point::new(start_line + i, Column(0)); - self.renderer.with_api(config, &size_info, |mut api| { - api.draw_string(glyph_cache, point, fg, bg, message_text); - }); + self.renderer.draw_string(point, fg, bg, message_text, &size_info, glyph_cache); } } else { // Draw rectangles. @@ -726,9 +717,7 @@ impl Display { // On X11 `swap_buffers` does not block for vsync. However the next OpenGl command // will block to synchronize (this is `glClear` in Alacritty), which causes a // permanent one frame delay. - self.renderer.with_api(config, &size_info, |api| { - api.finish(); - }); + self.renderer.finish(); } self.damage_rects.clear(); @@ -833,9 +822,7 @@ impl Display { let fg = config.colors.search_bar_foreground(); let bg = config.colors.search_bar_background(); - self.renderer.with_api(config, size_info, |mut api| { - api.draw_string(glyph_cache, point, fg, bg, &text); - }); + self.renderer.draw_string(point, fg, bg, &text, size_info, glyph_cache); } /// Draw render timer. @@ -853,9 +840,7 @@ impl Display { self.damage_from_point(point, self.size_info.columns() as u32); let glyph_cache = &mut self.glyph_cache; - self.renderer.with_api(config, size_info, |mut api| { - api.draw_string(glyph_cache, point, fg, bg, &timing); - }); + self.renderer.draw_string(point, fg, bg, &timing, size_info, glyph_cache); } /// Draw an indicator for the position of a line in history. @@ -894,9 +879,7 @@ impl Display { // Do not render anything if it would obscure the vi mode cursor. if obstructed_column.map_or(true, |obstructed_column| obstructed_column < column) { let glyph_cache = &mut self.glyph_cache; - self.renderer.with_api(config, size_info, |mut api| { - api.draw_string(glyph_cache, point, fg, bg, &text); - }); + self.renderer.draw_string(point, fg, bg, &text, size_info, glyph_cache); } } diff --git a/alacritty/src/renderer/mod.rs b/alacritty/src/renderer/mod.rs index 4aa562ad..cf9ee149 100644 --- a/alacritty/src/renderer/mod.rs +++ b/alacritty/src/renderer/mod.rs @@ -1,33 +1,27 @@ -use std::collections::HashMap; -use std::hash::BuildHasherDefault; -use std::mem::size_of; -use std::{fmt, ptr}; +use std::ffi::CStr; +use std::fmt; -use bitflags::bitflags; -use crossfont::{ - BitmapBuffer, Error as RasterizerError, FontDesc, FontKey, GlyphKey, Metrics, Rasterize, - RasterizedGlyph, Rasterizer, Size, Slant, Style, Weight, -}; -use fnv::FnvHasher; -use log::{error, info}; -use unicode_width::UnicodeWidthChar; +use crossfont::Metrics; +use log::info; use alacritty_terminal::index::Point; use alacritty_terminal::term::cell::Flags; use alacritty_terminal::term::color::Rgb; use alacritty_terminal::term::SizeInfo; -use crate::config::font::{Font, FontDescription}; -use crate::config::ui_config::{Delta, UiConfig}; use crate::display::content::RenderableCell; use crate::gl; -use crate::gl::types::*; use crate::renderer::rects::{RectRenderer, RenderRect}; -use crate::renderer::shader::{ShaderError, ShaderProgram}; +use crate::renderer::shader::ShaderError; -pub mod builtin_font; pub mod rects; mod shader; +mod text; + +pub use text::{GlyphCache, LoaderApi}; + +use shader::ShaderVersion; +use text::{Gles2Renderer, Glsl3Renderer, TextRenderer}; macro_rules! cstr { ($s:literal) => { @@ -37,21 +31,6 @@ macro_rules! cstr { } pub(crate) use cstr; -// Shader source. -static TEXT_SHADER_F: &str = include_str!("../../res/text.f.glsl"); -static TEXT_SHADER_V: &str = include_str!("../../res/text.v.glsl"); - -/// `LoadGlyph` allows for copying a rasterized glyph into graphics memory. -pub trait LoadGlyph { - /// Load the rasterized glyph into GPU memory. - fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph; - - /// Clear any state accumulated from previous loaded glyphs. - /// - /// This can, for instance, be used to reset the texture Atlas. - fn clear(&mut self); -} - #[derive(Debug)] pub enum Error { /// Shader error. @@ -82,588 +61,93 @@ impl From for Error { } } -/// Text drawing program. -/// -/// Uniforms are prefixed with "u", and vertex attributes are prefixed with "a". #[derive(Debug)] -pub struct TextShaderProgram { - /// Shader program. - program: ShaderProgram, - - /// Projection scale and offset uniform. - u_projection: GLint, - - /// Cell dimensions (pixels). - u_cell_dim: GLint, - - /// Background pass flag. - /// - /// Rendering is split into two passes; 1 for backgrounds, and one for text. - u_background: GLint, -} - -#[derive(Copy, Clone, Debug)] -pub struct Glyph { - tex_id: GLuint, - multicolor: bool, - top: i16, - left: i16, - width: i16, - height: i16, - uv_bot: f32, - uv_left: f32, - uv_width: f32, - uv_height: f32, -} - -/// Naïve glyph cache. -/// -/// Currently only keyed by `char`, and thus not possible to hold different -/// representations of the same code point. -pub struct GlyphCache { - /// Cache of buffered glyphs. - cache: HashMap>, - - /// Rasterizer for loading new glyphs. - rasterizer: Rasterizer, - - /// Regular font. - font_key: FontKey, - - /// Bold font. - bold_key: FontKey, - - /// Italic font. - italic_key: FontKey, - - /// Bold italic font. - bold_italic_key: FontKey, - - /// Font size. - font_size: crossfont::Size, - - /// Font offset. - font_offset: Delta, - - /// Glyph offset. - glyph_offset: Delta, - - /// Font metrics. - metrics: Metrics, - - /// Whether to use the built-in font for box drawing characters. - builtin_box_drawing: bool, -} - -impl GlyphCache { - pub fn new(mut rasterizer: Rasterizer, font: &Font) -> Result { - let (regular, bold, italic, bold_italic) = Self::compute_font_keys(font, &mut rasterizer)?; - - // Need to load at least one glyph for the face before calling metrics. - // The glyph requested here ('m' at the time of writing) has no special - // meaning. - rasterizer.get_glyph(GlyphKey { font_key: regular, character: 'm', size: font.size() })?; - - let metrics = rasterizer.metrics(regular, font.size())?; - - Ok(Self { - cache: HashMap::default(), - rasterizer, - font_size: font.size(), - font_key: regular, - bold_key: bold, - italic_key: italic, - bold_italic_key: bold_italic, - font_offset: font.offset, - glyph_offset: font.glyph_offset, - metrics, - builtin_box_drawing: font.builtin_box_drawing, - }) - } - - fn load_glyphs_for_font(&mut self, font: FontKey, loader: &mut L) { - let size = self.font_size; - - // Cache all ascii characters. - for i in 32u8..=126u8 { - self.get(GlyphKey { font_key: font, character: i as char, size }, loader, true); - } - } - - /// Computes font keys for (Regular, Bold, Italic, Bold Italic). - fn compute_font_keys( - font: &Font, - rasterizer: &mut Rasterizer, - ) -> Result<(FontKey, FontKey, FontKey, FontKey), crossfont::Error> { - let size = font.size(); - - // Load regular font. - let regular_desc = Self::make_desc(font.normal(), Slant::Normal, Weight::Normal); - - let regular = Self::load_regular_font(rasterizer, ®ular_desc, size)?; - - // Helper to load a description if it is not the `regular_desc`. - let mut load_or_regular = |desc: FontDesc| { - if desc == regular_desc { - regular - } else { - rasterizer.load_font(&desc, size).unwrap_or(regular) - } - }; - - // Load bold font. - let bold_desc = Self::make_desc(&font.bold(), Slant::Normal, Weight::Bold); - - let bold = load_or_regular(bold_desc); - - // Load italic font. - let italic_desc = Self::make_desc(&font.italic(), Slant::Italic, Weight::Normal); - - let italic = load_or_regular(italic_desc); - - // Load bold italic font. - let bold_italic_desc = Self::make_desc(&font.bold_italic(), Slant::Italic, Weight::Bold); - - let bold_italic = load_or_regular(bold_italic_desc); - - Ok((regular, bold, italic, bold_italic)) - } - - fn load_regular_font( - rasterizer: &mut Rasterizer, - description: &FontDesc, - size: Size, - ) -> Result { - match rasterizer.load_font(description, size) { - Ok(font) => Ok(font), - Err(err) => { - error!("{}", err); - - let fallback_desc = - Self::make_desc(Font::default().normal(), Slant::Normal, Weight::Normal); - rasterizer.load_font(&fallback_desc, size) - }, - } - } - - fn make_desc(desc: &FontDescription, slant: Slant, weight: Weight) -> FontDesc { - let style = if let Some(ref spec) = desc.style { - Style::Specific(spec.to_owned()) - } else { - Style::Description { slant, weight } - }; - FontDesc::new(desc.family.clone(), style) - } - - /// Get a glyph from the font. - /// - /// If the glyph has never been loaded before, it will be rasterized and inserted into the - /// cache. - /// - /// # Errors - /// - /// This will fail when the glyph could not be rasterized. Usually this is due to the glyph - /// not being present in any font. - fn get(&mut self, glyph_key: GlyphKey, loader: &mut L, show_missing: bool) -> Glyph - where - L: LoadGlyph, - { - // Try to load glyph from cache. - if let Some(glyph) = self.cache.get(&glyph_key) { - return *glyph; - }; - - // Rasterize the glyph using the built-in font for special characters or the user's font - // for everything else. - let rasterized = self - .builtin_box_drawing - .then(|| { - builtin_font::builtin_glyph( - glyph_key.character, - &self.metrics, - &self.font_offset, - &self.glyph_offset, - ) - }) - .flatten() - .map_or_else(|| self.rasterizer.get_glyph(glyph_key), Ok); - - let glyph = match rasterized { - Ok(rasterized) => self.load_glyph(loader, rasterized), - // Load fallback glyph. - Err(RasterizerError::MissingGlyph(rasterized)) if show_missing => { - // Use `\0` as "missing" glyph to cache it only once. - let missing_key = GlyphKey { character: '\0', ..glyph_key }; - if let Some(glyph) = self.cache.get(&missing_key) { - *glyph - } else { - // If no missing glyph was loaded yet, insert it as `\0`. - let glyph = self.load_glyph(loader, rasterized); - self.cache.insert(missing_key, glyph); - - glyph - } - }, - Err(_) => self.load_glyph(loader, Default::default()), - }; - - // Cache rasterized glyph. - *self.cache.entry(glyph_key).or_insert(glyph) - } - - /// Load glyph into the atlas. - /// - /// This will apply all transforms defined for the glyph cache to the rasterized glyph before - /// insertion. - fn load_glyph(&self, loader: &mut L, mut glyph: RasterizedGlyph) -> Glyph - where - L: LoadGlyph, - { - glyph.left += i32::from(self.glyph_offset.x); - glyph.top += i32::from(self.glyph_offset.y); - glyph.top -= self.metrics.descent as i32; - - // The metrics of zero-width characters are based on rendering - // the character after the current cell, with the anchor at the - // right side of the preceding character. Since we render the - // zero-width characters inside the preceding character, the - // anchor has been moved to the right by one cell. - if glyph.character.width() == Some(0) { - glyph.left += self.metrics.average_advance as i32; - } - - // Add glyph to cache. - loader.load_glyph(&glyph) - } - - /// Clear currently cached data in both GL and the registry. - pub fn clear_glyph_cache(&mut self, loader: &mut L) { - loader.clear(); - self.cache = HashMap::default(); - - self.load_common_glyphs(loader); - } - - pub fn update_font_size( - &mut self, - font: &Font, - scale_factor: f64, - loader: &mut L, - ) -> Result<(), crossfont::Error> { - // Update dpi scaling. - self.rasterizer.update_dpr(scale_factor as f32); - self.font_offset = font.offset; - - // Recompute font keys. - let (regular, bold, italic, bold_italic) = - Self::compute_font_keys(font, &mut self.rasterizer)?; - - self.rasterizer.get_glyph(GlyphKey { - font_key: regular, - character: 'm', - size: font.size(), - })?; - let metrics = self.rasterizer.metrics(regular, font.size())?; - - info!("Font size changed to {:?} with scale factor of {}", font.size(), scale_factor); - - self.font_size = font.size(); - self.font_key = regular; - self.bold_key = bold; - self.italic_key = italic; - self.bold_italic_key = bold_italic; - self.metrics = metrics; - self.builtin_box_drawing = font.builtin_box_drawing; - - self.clear_glyph_cache(loader); - - Ok(()) - } - - pub fn font_metrics(&self) -> crossfont::Metrics { - self.metrics - } - - /// Prefetch glyphs that are almost guaranteed to be loaded anyways. - pub fn load_common_glyphs(&mut self, loader: &mut L) { - self.load_glyphs_for_font(self.font_key, loader); - self.load_glyphs_for_font(self.bold_key, loader); - self.load_glyphs_for_font(self.italic_key, loader); - self.load_glyphs_for_font(self.bold_italic_key, loader); - } -} - -// NOTE: These flags must be in sync with their usage in the text.*.glsl shaders. -bitflags! { - #[repr(C)] - struct RenderingGlyphFlags: u8 { - const WIDE_CHAR = 0b0000_0001; - const COLORED = 0b0000_0010; - } +enum TextRendererProvider { + Gles2(Gles2Renderer), + Glsl3(Glsl3Renderer), } #[derive(Debug)] -#[repr(C)] -struct InstanceData { - // Coords. - col: u16, - row: u16, - - // Glyph offset. - left: i16, - top: i16, - - // Glyph size. - width: i16, - height: i16, - - // UV offset. - uv_left: f32, - uv_bot: f32, - - // uv scale. - uv_width: f32, - uv_height: f32, - - // Color. - r: u8, - g: u8, - b: u8, - - // Cell flags like multicolor or fullwidth character. - cell_flags: RenderingGlyphFlags, - - // Background color. - bg_r: u8, - bg_g: u8, - bg_b: u8, - bg_a: u8, -} - -#[derive(Debug)] -pub struct QuadRenderer { - program: TextShaderProgram, - vao: GLuint, - ebo: GLuint, - vbo_instance: GLuint, - atlas: Vec, - current_atlas: usize, - active_tex: GLuint, - batch: Batch, - +pub struct Renderer { + text_renderer: TextRendererProvider, rect_renderer: RectRenderer, } -#[derive(Debug)] -pub struct RenderApi<'a> { - active_tex: &'a mut GLuint, - batch: &'a mut Batch, - atlas: &'a mut Vec, - current_atlas: &'a mut usize, - program: &'a mut TextShaderProgram, - config: &'a UiConfig, -} - -#[derive(Debug)] -pub struct LoaderApi<'a> { - active_tex: &'a mut GLuint, - atlas: &'a mut Vec, - current_atlas: &'a mut usize, -} - -#[derive(Debug, Default)] -pub struct Batch { - tex: GLuint, - instances: Vec, -} - -impl Batch { - #[inline] - pub fn new() -> Self { - Self { tex: 0, instances: Vec::with_capacity(BATCH_MAX) } - } - - pub fn add_item(&mut self, cell: &RenderableCell, glyph: &Glyph) { - if self.is_empty() { - self.tex = glyph.tex_id; - } - - let mut cell_flags = RenderingGlyphFlags::empty(); - cell_flags.set(RenderingGlyphFlags::COLORED, glyph.multicolor); - cell_flags.set(RenderingGlyphFlags::WIDE_CHAR, cell.flags.contains(Flags::WIDE_CHAR)); - - self.instances.push(InstanceData { - col: cell.point.column.0 as u16, - row: cell.point.line as u16, - - top: glyph.top, - left: glyph.left, - width: glyph.width, - height: glyph.height, - - uv_bot: glyph.uv_bot, - uv_left: glyph.uv_left, - uv_width: glyph.uv_width, - uv_height: glyph.uv_height, - - r: cell.fg.r, - g: cell.fg.g, - b: cell.fg.b, - cell_flags, - - bg_r: cell.bg.r, - bg_g: cell.bg.g, - bg_b: cell.bg.b, - bg_a: (cell.bg_alpha * 255.0) as u8, - }); - } - - #[inline] - pub fn full(&self) -> bool { - self.capacity() == self.len() - } - - #[inline] - pub fn len(&self) -> usize { - self.instances.len() - } - - #[inline] - pub fn capacity(&self) -> usize { - BATCH_MAX - } - - #[inline] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - #[inline] - pub fn size(&self) -> usize { - self.len() * size_of::() - } - - pub fn clear(&mut self) { - self.tex = 0; - self.instances.clear(); - } -} - -/// Maximum items to be drawn in a batch. -const BATCH_MAX: usize = 0x1_0000; -const ATLAS_SIZE: i32 = 1024; - -impl QuadRenderer { - pub fn new() -> Result { - let program = TextShaderProgram::new()?; - - let mut vao: GLuint = 0; - let mut ebo: GLuint = 0; - - let mut vbo_instance: GLuint = 0; - - unsafe { - gl::Enable(gl::BLEND); - gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); - gl::Enable(gl::MULTISAMPLE); - - // Disable depth mask, as the renderer never uses depth tests. - gl::DepthMask(gl::FALSE); - - gl::GenVertexArrays(1, &mut vao); - gl::GenBuffers(1, &mut ebo); - gl::GenBuffers(1, &mut vbo_instance); - gl::BindVertexArray(vao); - - // --------------------- - // Set up element buffer - // --------------------- - let indices: [u32; 6] = [0, 1, 3, 1, 2, 3]; - - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); - gl::BufferData( - gl::ELEMENT_ARRAY_BUFFER, - (6 * size_of::()) as isize, - indices.as_ptr() as *const _, - gl::STATIC_DRAW, - ); - - // ---------------------------- - // Setup vertex instance buffer - // ---------------------------- - gl::BindBuffer(gl::ARRAY_BUFFER, vbo_instance); - gl::BufferData( - gl::ARRAY_BUFFER, - (BATCH_MAX * size_of::()) as isize, - ptr::null(), - gl::STREAM_DRAW, - ); - - let mut index = 0; - let mut size = 0; - - macro_rules! add_attr { - ($count:expr, $gl_type:expr, $type:ty) => { - gl::VertexAttribPointer( - index, - $count, - $gl_type, - gl::FALSE, - size_of::() as i32, - size as *const _, - ); - gl::EnableVertexAttribArray(index); - gl::VertexAttribDivisor(index, 1); - - #[allow(unused_assignments)] - { - size += $count * size_of::<$type>(); - index += 1; - } - }; - } - - // Coords. - add_attr!(2, gl::UNSIGNED_SHORT, u16); - - // Glyph offset and size. - add_attr!(4, gl::SHORT, i16); - - // UV offset. - add_attr!(4, gl::FLOAT, f32); - - // Color and cell flags. - // - // These are packed together because of an OpenGL driver issue on macOS, which caused a - // `vec3(u8)` text color and a `u8` cell flags to increase the rendering time by a - // huge margin. - add_attr!(4, gl::UNSIGNED_BYTE, u8); - - // Background color. - add_attr!(4, gl::UNSIGNED_BYTE, u8); - - // Cleanup. - gl::BindVertexArray(0); - gl::BindBuffer(gl::ARRAY_BUFFER, 0); - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); - } - - let mut renderer = Self { - program, - rect_renderer: RectRenderer::new()?, - vao, - ebo, - vbo_instance, - atlas: Vec::new(), - current_atlas: 0, - active_tex: 0, - batch: Batch::new(), +impl Renderer { + /// Create a new renderer. + /// + /// This will automatically pick between the GLES2 and GLSL3 renderer based on the GPU's + /// supported OpenGL version. + pub fn new() -> Result { + let (version, renderer) = unsafe { + let renderer = CStr::from_ptr(gl::GetString(gl::RENDERER) as *mut _); + let version = CStr::from_ptr(gl::GetString(gl::SHADING_LANGUAGE_VERSION) as *mut _); + (version.to_string_lossy(), renderer.to_string_lossy()) }; - let atlas = Atlas::new(ATLAS_SIZE); - renderer.atlas.push(atlas); + info!("Running on {}", renderer); - Ok(renderer) + let (text_renderer, rect_renderer) = if version.as_ref() >= "3.3" { + let text_renderer = TextRendererProvider::Glsl3(Glsl3Renderer::new()?); + let rect_renderer = RectRenderer::new(ShaderVersion::Glsl3)?; + (text_renderer, rect_renderer) + } else { + let text_renderer = TextRendererProvider::Gles2(Gles2Renderer::new()?); + let rect_renderer = RectRenderer::new(ShaderVersion::Gles2)?; + (text_renderer, rect_renderer) + }; + + Ok(Self { text_renderer, rect_renderer }) + } + + pub fn draw_cells>( + &mut self, + size_info: &SizeInfo, + glyph_cache: &mut GlyphCache, + cells: I, + ) { + match &mut self.text_renderer { + TextRendererProvider::Gles2(renderer) => { + renderer.draw_cells(size_info, glyph_cache, cells) + }, + TextRendererProvider::Glsl3(renderer) => { + renderer.draw_cells(size_info, glyph_cache, cells) + }, + } + } + + /// Draw a string in a variable location. Used for printing the render timer, warnings and + /// errors. + pub fn draw_string( + &mut self, + point: Point, + fg: Rgb, + bg: Rgb, + string: &str, + size_info: &SizeInfo, + glyph_cache: &mut GlyphCache, + ) { + let cells = string.chars().enumerate().map(|(i, character)| RenderableCell { + point: Point::new(point.line, point.column + i), + character, + zerowidth: None, + flags: Flags::empty(), + bg_alpha: 1.0, + fg, + bg, + }); + + self.draw_cells(size_info, glyph_cache, cells); + } + + pub fn with_loader(&mut self, func: F) -> T + where + F: FnOnce(LoaderApi<'_>) -> T, + { + match &mut self.text_renderer { + TextRendererProvider::Gles2(renderer) => renderer.with_loader(func), + TextRendererProvider::Glsl3(renderer) => renderer.with_loader(func), + } } /// Draw all rectangles simultaneously to prevent excessive program swaps. @@ -691,99 +175,9 @@ impl QuadRenderer { } } - pub fn with_api(&mut self, config: &UiConfig, props: &SizeInfo, func: F) -> T - where - F: FnOnce(RenderApi<'_>) -> T, - { + /// Fill the window with `color` and `alpha`. + pub fn clear(&self, color: Rgb, alpha: f32) { unsafe { - gl::UseProgram(self.program.id()); - self.program.set_term_uniforms(props); - - gl::BindVertexArray(self.vao); - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, self.ebo); - gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo_instance); - gl::ActiveTexture(gl::TEXTURE0); - } - - let res = func(RenderApi { - active_tex: &mut self.active_tex, - batch: &mut self.batch, - atlas: &mut self.atlas, - current_atlas: &mut self.current_atlas, - program: &mut self.program, - config, - }); - - unsafe { - gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); - gl::BindBuffer(gl::ARRAY_BUFFER, 0); - gl::BindVertexArray(0); - - gl::UseProgram(0); - } - - res - } - - pub fn with_loader(&mut self, func: F) -> T - where - F: FnOnce(LoaderApi<'_>) -> T, - { - unsafe { - gl::ActiveTexture(gl::TEXTURE0); - } - - func(LoaderApi { - active_tex: &mut self.active_tex, - atlas: &mut self.atlas, - current_atlas: &mut self.current_atlas, - }) - } - - pub fn resize(&self, size: &SizeInfo) { - unsafe { - self.set_viewport(size); - - // Update projection. - gl::UseProgram(self.program.id()); - self.program.update_projection( - size.width(), - size.height(), - size.padding_x(), - size.padding_y(), - ); - gl::UseProgram(0); - } - } - - /// Set the viewport for cell rendering. - #[inline] - pub fn set_viewport(&self, size: &SizeInfo) { - unsafe { - gl::Viewport( - size.padding_x() as i32, - size.padding_y() as i32, - size.width() as i32 - 2 * size.padding_x() as i32, - size.height() as i32 - 2 * size.padding_y() as i32, - ); - } - } -} - -impl Drop for QuadRenderer { - fn drop(&mut self) { - unsafe { - gl::DeleteBuffers(1, &self.vbo_instance); - gl::DeleteBuffers(1, &self.ebo); - gl::DeleteVertexArrays(1, &self.vao); - } - } -} - -impl<'a> RenderApi<'a> { - pub fn clear(&self, color: Rgb) { - unsafe { - let alpha = self.config.window_opacity(); gl::ClearColor( (f32::from(color.r) / 255.0).min(1.0) * alpha, (f32::from(color.g) / 255.0).min(1.0) * alpha, @@ -801,458 +195,25 @@ impl<'a> RenderApi<'a> { } } - fn render_batch(&mut self) { - unsafe { - gl::BufferSubData( - gl::ARRAY_BUFFER, - 0, - self.batch.size() as isize, - self.batch.instances.as_ptr() as *const _, - ); - } - - // Bind texture if necessary. - if *self.active_tex != self.batch.tex { - unsafe { - gl::BindTexture(gl::TEXTURE_2D, self.batch.tex); - } - *self.active_tex = self.batch.tex; - } - - unsafe { - self.program.set_background_pass(true); - gl::DrawElementsInstanced( - gl::TRIANGLES, - 6, - gl::UNSIGNED_INT, - ptr::null(), - self.batch.len() as GLsizei, - ); - self.program.set_background_pass(false); - gl::DrawElementsInstanced( - gl::TRIANGLES, - 6, - gl::UNSIGNED_INT, - ptr::null(), - self.batch.len() as GLsizei, - ); - } - - self.batch.clear(); - } - - /// Draw a string in a variable location. Used for printing the render timer, warnings and - /// errors. - pub fn draw_string( - &mut self, - glyph_cache: &mut GlyphCache, - point: Point, - fg: Rgb, - bg: Rgb, - string: &str, - ) { - let cells = string - .chars() - .enumerate() - .map(|(i, character)| RenderableCell { - point: Point::new(point.line, point.column + i), - character, - zerowidth: None, - flags: Flags::empty(), - bg_alpha: 1.0, - fg, - bg, - }) - .collect::>(); - - for cell in cells { - self.draw_cell(cell, glyph_cache); - } - } - + /// Set the viewport for cell rendering. #[inline] - fn add_render_item(&mut self, cell: &RenderableCell, glyph: &Glyph) { - // Flush batch if tex changing. - if !self.batch.is_empty() && self.batch.tex != glyph.tex_id { - self.render_batch(); - } - - self.batch.add_item(cell, glyph); - - // Render batch and clear if it's full. - if self.batch.full() { - self.render_batch(); - } - } - - pub fn draw_cell(&mut self, mut cell: RenderableCell, glyph_cache: &mut GlyphCache) { - // Get font key for cell. - let font_key = match cell.flags & Flags::BOLD_ITALIC { - Flags::BOLD_ITALIC => glyph_cache.bold_italic_key, - Flags::ITALIC => glyph_cache.italic_key, - Flags::BOLD => glyph_cache.bold_key, - _ => glyph_cache.font_key, - }; - - // Ignore hidden cells and render tabs as spaces to prevent font issues. - let hidden = cell.flags.contains(Flags::HIDDEN); - if cell.character == '\t' || hidden { - cell.character = ' '; - } - - let mut glyph_key = - GlyphKey { font_key, size: glyph_cache.font_size, character: cell.character }; - - // Add cell to batch. - let glyph = glyph_cache.get(glyph_key, self, true); - self.add_render_item(&cell, &glyph); - - // Render visible zero-width characters. - if let Some(zerowidth) = cell.zerowidth.take().filter(|_| !hidden) { - for character in zerowidth { - glyph_key.character = character; - let glyph = glyph_cache.get(glyph_key, self, false); - self.add_render_item(&cell, &glyph); - } - } - } -} - -/// Load a glyph into a texture atlas. -/// -/// If the current atlas is full, a new one will be created. -#[inline] -fn load_glyph( - active_tex: &mut GLuint, - atlas: &mut Vec, - current_atlas: &mut usize, - rasterized: &RasterizedGlyph, -) -> Glyph { - // At least one atlas is guaranteed to be in the `self.atlas` list; thus - // the unwrap. - match atlas[*current_atlas].insert(rasterized, active_tex) { - Ok(glyph) => glyph, - Err(AtlasInsertError::Full) => { - *current_atlas += 1; - if *current_atlas == atlas.len() { - let new = Atlas::new(ATLAS_SIZE); - *active_tex = 0; // Atlas::new binds a texture. Ugh this is sloppy. - atlas.push(new); - } - load_glyph(active_tex, atlas, current_atlas, rasterized) - }, - Err(AtlasInsertError::GlyphTooLarge) => Glyph { - tex_id: atlas[*current_atlas].id, - multicolor: false, - top: 0, - left: 0, - width: 0, - height: 0, - uv_bot: 0., - uv_left: 0., - uv_width: 0., - uv_height: 0., - }, - } -} - -#[inline] -fn clear_atlas(atlas: &mut Vec, current_atlas: &mut usize) { - for atlas in atlas.iter_mut() { - atlas.clear(); - } - *current_atlas = 0; -} - -impl<'a> LoadGlyph for LoaderApi<'a> { - fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { - load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) - } - - fn clear(&mut self) { - clear_atlas(self.atlas, self.current_atlas) - } -} - -impl<'a> LoadGlyph for RenderApi<'a> { - fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { - load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) - } - - fn clear(&mut self) { - clear_atlas(self.atlas, self.current_atlas) - } -} - -impl<'a> Drop for RenderApi<'a> { - fn drop(&mut self) { - if !self.batch.is_empty() { - self.render_batch(); - } - } -} - -impl TextShaderProgram { - pub fn new() -> Result { - let program = ShaderProgram::new(TEXT_SHADER_V, TEXT_SHADER_F)?; - Ok(Self { - u_projection: program.get_uniform_location(cstr!("projection"))?, - u_cell_dim: program.get_uniform_location(cstr!("cellDim"))?, - u_background: program.get_uniform_location(cstr!("backgroundPass"))?, - program, - }) - } - - fn id(&self) -> GLuint { - self.program.id() - } - - fn update_projection(&self, width: f32, height: f32, padding_x: f32, padding_y: f32) { - // Bounds check. - if (width as u32) < (2 * padding_x as u32) || (height as u32) < (2 * padding_y as u32) { - return; - } - - // Compute scale and offset factors, from pixel to ndc space. Y is inverted. - // [0, width - 2 * padding_x] to [-1, 1] - // [height - 2 * padding_y, 0] to [-1, 1] - let scale_x = 2. / (width - 2. * padding_x); - let scale_y = -2. / (height - 2. * padding_y); - let offset_x = -1.; - let offset_y = 1.; - + pub fn set_viewport(&self, size: &SizeInfo) { unsafe { - gl::Uniform4f(self.u_projection, offset_x, offset_y, scale_x, scale_y); - } - } - - fn set_term_uniforms(&self, props: &SizeInfo) { - unsafe { - gl::Uniform2f(self.u_cell_dim, props.cell_width(), props.cell_height()); - } - } - - fn set_background_pass(&self, background_pass: bool) { - let value = if background_pass { 1 } else { 0 }; - - unsafe { - gl::Uniform1i(self.u_background, value); - } - } -} - -/// Manages a single texture atlas. -/// -/// The strategy for filling an atlas looks roughly like this: -/// -/// ```text -/// (width, height) -/// ┌─────┬─────┬─────┬─────┬─────┐ -/// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while -/// │ │ │ │ │ │ glyph_height < height - row_baseline -/// ├─────┼─────┼─────┼─────┼─────┤ -/// │ 5 │ 6 │ 7 │ 8 │ 9 │ -/// │ │ │ │ │ │ -/// ├─────┼─────┼─────┼─────┴─────┤ <- Row height is tallest glyph in row; this is -/// │ 1 │ 2 │ 3 │ 4 │ used as the baseline for the following row. -/// │ │ │ │ │ <- Row considered full when next glyph doesn't -/// └─────┴─────┴─────┴───────────┘ fit in the row. -/// (0, 0) x-> -/// ``` -#[derive(Debug)] -struct Atlas { - /// Texture id for this atlas. - id: GLuint, - - /// Width of atlas. - width: i32, - - /// Height of atlas. - height: i32, - - /// Left-most free pixel in a row. - /// - /// This is called the extent because it is the upper bound of used pixels - /// in a row. - row_extent: i32, - - /// Baseline for glyphs in the current row. - row_baseline: i32, - - /// Tallest glyph in current row. - /// - /// This is used as the advance when end of row is reached. - row_tallest: i32, -} - -/// Error that can happen when inserting a texture to the Atlas. -enum AtlasInsertError { - /// Texture atlas is full. - Full, - - /// The glyph cannot fit within a single texture. - GlyphTooLarge, -} - -impl Atlas { - fn new(size: i32) -> Self { - let mut id: GLuint = 0; - unsafe { - gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); - gl::GenTextures(1, &mut id); - gl::BindTexture(gl::TEXTURE_2D, id); - // Use RGBA texture for both normal and emoji glyphs, since it has no performance - // impact. - gl::TexImage2D( - gl::TEXTURE_2D, - 0, - gl::RGBA as i32, - size, - size, - 0, - gl::RGBA, - gl::UNSIGNED_BYTE, - ptr::null(), + gl::Viewport( + size.padding_x() as i32, + size.padding_y() as i32, + size.width() as i32 - 2 * size.padding_x() as i32, + size.height() as i32 - 2 * size.padding_y() as i32, ); - - gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32); - gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32); - gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); - gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32); - - gl::BindTexture(gl::TEXTURE_2D, 0); - } - - Self { id, width: size, height: size, row_extent: 0, row_baseline: 0, row_tallest: 0 } - } - - pub fn clear(&mut self) { - self.row_extent = 0; - self.row_baseline = 0; - self.row_tallest = 0; - } - - /// Insert a RasterizedGlyph into the texture atlas. - pub fn insert( - &mut self, - glyph: &RasterizedGlyph, - active_tex: &mut u32, - ) -> Result { - if glyph.width > self.width || glyph.height > self.height { - return Err(AtlasInsertError::GlyphTooLarge); - } - - // If there's not enough room in current row, go onto next one. - if !self.room_in_row(glyph) { - self.advance_row()?; - } - - // If there's still not room, there's nothing that can be done here.. - if !self.room_in_row(glyph) { - return Err(AtlasInsertError::Full); - } - - // There appears to be room; load the glyph. - Ok(self.insert_inner(glyph, active_tex)) - } - - /// Insert the glyph without checking for room. - /// - /// Internal function for use once atlas has been checked for space. GL - /// errors could still occur at this point if we were checking for them; - /// hence, the Result. - fn insert_inner(&mut self, glyph: &RasterizedGlyph, active_tex: &mut u32) -> Glyph { - let offset_y = self.row_baseline; - let offset_x = self.row_extent; - let height = glyph.height as i32; - let width = glyph.width as i32; - let multicolor; - - unsafe { - gl::BindTexture(gl::TEXTURE_2D, self.id); - - // Load data into OpenGL. - let (format, buffer) = match &glyph.buffer { - BitmapBuffer::Rgb(buffer) => { - multicolor = false; - (gl::RGB, buffer) - }, - BitmapBuffer::Rgba(buffer) => { - multicolor = true; - (gl::RGBA, buffer) - }, - }; - - gl::TexSubImage2D( - gl::TEXTURE_2D, - 0, - offset_x, - offset_y, - width, - height, - format, - gl::UNSIGNED_BYTE, - buffer.as_ptr() as *const _, - ); - - gl::BindTexture(gl::TEXTURE_2D, 0); - *active_tex = 0; - } - - // Update Atlas state. - self.row_extent = offset_x + width; - if height > self.row_tallest { - self.row_tallest = height; - } - - // Generate UV coordinates. - let uv_bot = offset_y as f32 / self.height as f32; - let uv_left = offset_x as f32 / self.width as f32; - let uv_height = height as f32 / self.height as f32; - let uv_width = width as f32 / self.width as f32; - - Glyph { - tex_id: self.id, - multicolor, - top: glyph.top as i16, - left: glyph.left as i16, - width: width as i16, - height: height as i16, - uv_bot, - uv_left, - uv_width, - uv_height, } } - /// Check if there's room in the current row for given glyph. - fn room_in_row(&self, raw: &RasterizedGlyph) -> bool { - let next_extent = self.row_extent + raw.width as i32; - let enough_width = next_extent <= self.width; - let enough_height = (raw.height as i32) < (self.height - self.row_baseline); - - enough_width && enough_height - } - - /// Mark current row as finished and prepare to insert into the next row. - fn advance_row(&mut self) -> Result<(), AtlasInsertError> { - let advance_to = self.row_baseline + self.row_tallest; - if self.height - advance_to <= 0 { - return Err(AtlasInsertError::Full); - } - - self.row_baseline = advance_to; - self.row_extent = 0; - self.row_tallest = 0; - - Ok(()) - } -} - -impl Drop for Atlas { - fn drop(&mut self) { - unsafe { - gl::DeleteTextures(1, &self.id); + /// Resize the renderer. + pub fn resize(&self, size_info: &SizeInfo) { + self.set_viewport(size_info); + match &self.text_renderer { + TextRendererProvider::Gles2(renderer) => renderer.resize(size_info), + TextRendererProvider::Glsl3(renderer) => renderer.resize(size_info), } } } diff --git a/alacritty/src/renderer/rects.rs b/alacritty/src/renderer/rects.rs index 3c74b10b..8c600b6b 100644 --- a/alacritty/src/renderer/rects.rs +++ b/alacritty/src/renderer/rects.rs @@ -12,7 +12,7 @@ use alacritty_terminal::term::SizeInfo; use crate::display::content::RenderableCell; use crate::gl; use crate::gl::types::*; -use crate::renderer::shader::{ShaderError, ShaderProgram}; +use crate::renderer::shader::{ShaderError, ShaderProgram, ShaderVersion}; use crate::renderer::{self, cstr}; #[derive(Debug, Copy, Clone)] @@ -252,10 +252,10 @@ pub struct RectRenderer { } impl RectRenderer { - pub fn new() -> Result { + pub fn new(shader_version: ShaderVersion) -> Result { let mut vao: GLuint = 0; let mut vbo: GLuint = 0; - let program = RectShaderProgram::new()?; + let program = RectShaderProgram::new(shader_version)?; unsafe { // Allocate buffers. @@ -422,8 +422,8 @@ pub struct RectShaderProgram { } impl RectShaderProgram { - pub fn new() -> Result { - let program = ShaderProgram::new(RECT_SHADER_V, RECT_SHADER_F)?; + pub fn new(shader_version: ShaderVersion) -> Result { + let program = ShaderProgram::new(shader_version, RECT_SHADER_V, RECT_SHADER_F)?; Ok(Self { u_rect_kind: program.get_uniform_location(cstr!("rectKind"))?, diff --git a/alacritty/src/renderer/shader.rs b/alacritty/src/renderer/shader.rs index edb01277..9326796b 100644 --- a/alacritty/src/renderer/shader.rs +++ b/alacritty/src/renderer/shader.rs @@ -8,13 +8,33 @@ use crate::gl::types::*; #[derive(Debug)] pub struct ShaderProgram(GLuint); +#[derive(Copy, Clone, Debug)] +pub enum ShaderVersion { + /// OpenGL 3.3 core shaders. + Glsl3, + + /// OpenGL ES 2.0 shaders. + Gles2, +} + +impl ShaderVersion { + // Header to which we concatenate the entire shader. The newlines are required. + fn shader_header(&self) -> &'static str { + match self { + Self::Glsl3 => "#version 330 core\n", + Self::Gles2 => "#version 100\n#define GLES2_RENDERER\n", + } + } +} + impl ShaderProgram { pub fn new( + shader_version: ShaderVersion, vertex_shader: &'static str, fragment_shader: &'static str, ) -> Result { - let vertex_shader = Shader::new(gl::VERTEX_SHADER, vertex_shader)?; - let fragment_shader = Shader::new(gl::FRAGMENT_SHADER, fragment_shader)?; + let vertex_shader = Shader::new(shader_version, gl::VERTEX_SHADER, vertex_shader)?; + let fragment_shader = Shader::new(shader_version, gl::FRAGMENT_SHADER, fragment_shader)?; let program = unsafe { Self(gl::CreateProgram()) }; @@ -60,23 +80,34 @@ impl Drop for ShaderProgram { struct Shader(GLuint); impl Shader { - fn new(kind: GLenum, source: &'static str) -> Result { - let len: [GLint; 1] = [source.len() as GLint]; + fn new( + shader_version: ShaderVersion, + kind: GLenum, + source: &'static str, + ) -> Result { + let header = shader_version.shader_header(); + let len: [GLint; 2] = [header.len() as GLint, source.len() as GLint]; + let source = [header.as_ptr() as *const _, source.as_ptr() as *const _]; let shader = unsafe { Self(gl::CreateShader(kind)) }; let mut success: GLint = 0; unsafe { - gl::ShaderSource(shader.id(), 1, &(source.as_ptr() as *const _), len.as_ptr()); + gl::ShaderSource( + shader.id(), + len.len() as GLint, + source.as_ptr() as *const _, + len.as_ptr(), + ); gl::CompileShader(shader.id()); gl::GetShaderiv(shader.id(), gl::COMPILE_STATUS, &mut success); } - if success != GLint::from(gl::TRUE) { - return Err(ShaderError::Compile(get_shader_info_log(shader.id()))); + if success == GLint::from(gl::TRUE) { + Ok(shader) + } else { + Err(ShaderError::Compile(get_shader_info_log(shader.id()))) } - - Ok(shader) } fn id(&self) -> GLuint { diff --git a/alacritty/src/renderer/text/atlas.rs b/alacritty/src/renderer/text/atlas.rs new file mode 100644 index 00000000..97c8b0b4 --- /dev/null +++ b/alacritty/src/renderer/text/atlas.rs @@ -0,0 +1,273 @@ +use std::ptr; + +use crossfont::{BitmapBuffer, RasterizedGlyph}; + +use crate::gl; +use crate::gl::types::*; + +use super::Glyph; + +/// Size of the Atlas. +pub const ATLAS_SIZE: i32 = 1024; + +/// Manages a single texture atlas. +/// +/// The strategy for filling an atlas looks roughly like this: +/// +/// ```text +/// (width, height) +/// ┌─────┬─────┬─────┬─────┬─────┐ +/// │ 10 │ │ │ │ │ <- Empty spaces; can be filled while +/// │ │ │ │ │ │ glyph_height < height - row_baseline +/// ├─────┼─────┼─────┼─────┼─────┤ +/// │ 5 │ 6 │ 7 │ 8 │ 9 │ +/// │ │ │ │ │ │ +/// ├─────┼─────┼─────┼─────┴─────┤ <- Row height is tallest glyph in row; this is +/// │ 1 │ 2 │ 3 │ 4 │ used as the baseline for the following row. +/// │ │ │ │ │ <- Row considered full when next glyph doesn't +/// └─────┴─────┴─────┴───────────┘ fit in the row. +/// (0, 0) x-> +/// ``` +#[derive(Debug)] +pub struct Atlas { + /// Texture id for this atlas. + id: GLuint, + + /// Width of atlas. + width: i32, + + /// Height of atlas. + height: i32, + + /// Left-most free pixel in a row. + /// + /// This is called the extent because it is the upper bound of used pixels + /// in a row. + row_extent: i32, + + /// Baseline for glyphs in the current row. + row_baseline: i32, + + /// Tallest glyph in current row. + /// + /// This is used as the advance when end of row is reached. + row_tallest: i32, +} + +/// Error that can happen when inserting a texture to the Atlas. +pub enum AtlasInsertError { + /// Texture atlas is full. + Full, + + /// The glyph cannot fit within a single texture. + GlyphTooLarge, +} + +impl Atlas { + pub fn new(size: i32) -> Self { + let mut id: GLuint = 0; + unsafe { + gl::PixelStorei(gl::UNPACK_ALIGNMENT, 1); + gl::GenTextures(1, &mut id); + gl::BindTexture(gl::TEXTURE_2D, id); + // Use RGBA texture for both normal and emoji glyphs, since it has no performance + // impact. + gl::TexImage2D( + gl::TEXTURE_2D, + 0, + gl::RGBA as i32, + size, + size, + 0, + gl::RGBA, + gl::UNSIGNED_BYTE, + ptr::null(), + ); + + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as i32); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as i32); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MAG_FILTER, gl::LINEAR as i32); + + gl::BindTexture(gl::TEXTURE_2D, 0); + } + + Self { id, width: size, height: size, row_extent: 0, row_baseline: 0, row_tallest: 0 } + } + + pub fn clear(&mut self) { + self.row_extent = 0; + self.row_baseline = 0; + self.row_tallest = 0; + } + + /// Insert a RasterizedGlyph into the texture atlas. + pub fn insert( + &mut self, + glyph: &RasterizedGlyph, + active_tex: &mut u32, + ) -> Result { + if glyph.width > self.width || glyph.height > self.height { + return Err(AtlasInsertError::GlyphTooLarge); + } + + // If there's not enough room in current row, go onto next one. + if !self.room_in_row(glyph) { + self.advance_row()?; + } + + // If there's still not room, there's nothing that can be done here.. + if !self.room_in_row(glyph) { + return Err(AtlasInsertError::Full); + } + + // There appears to be room; load the glyph. + Ok(self.insert_inner(glyph, active_tex)) + } + + /// Insert the glyph without checking for room. + /// + /// Internal function for use once atlas has been checked for space. GL + /// errors could still occur at this point if we were checking for them; + /// hence, the Result. + fn insert_inner(&mut self, glyph: &RasterizedGlyph, active_tex: &mut u32) -> Glyph { + let offset_y = self.row_baseline; + let offset_x = self.row_extent; + let height = glyph.height as i32; + let width = glyph.width as i32; + let multicolor; + + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.id); + + // Load data into OpenGL. + let (format, buffer) = match &glyph.buffer { + BitmapBuffer::Rgb(buffer) => { + multicolor = false; + (gl::RGB, buffer) + }, + BitmapBuffer::Rgba(buffer) => { + multicolor = true; + (gl::RGBA, buffer) + }, + }; + + gl::TexSubImage2D( + gl::TEXTURE_2D, + 0, + offset_x, + offset_y, + width, + height, + format, + gl::UNSIGNED_BYTE, + buffer.as_ptr() as *const _, + ); + + gl::BindTexture(gl::TEXTURE_2D, 0); + *active_tex = 0; + } + + // Update Atlas state. + self.row_extent = offset_x + width; + if height > self.row_tallest { + self.row_tallest = height; + } + + // Generate UV coordinates. + let uv_bot = offset_y as f32 / self.height as f32; + let uv_left = offset_x as f32 / self.width as f32; + let uv_height = height as f32 / self.height as f32; + let uv_width = width as f32 / self.width as f32; + + Glyph { + tex_id: self.id, + multicolor, + top: glyph.top as i16, + left: glyph.left as i16, + width: width as i16, + height: height as i16, + uv_bot, + uv_left, + uv_width, + uv_height, + } + } + + /// Check if there's room in the current row for given glyph. + pub fn room_in_row(&self, raw: &RasterizedGlyph) -> bool { + let next_extent = self.row_extent + raw.width as i32; + let enough_width = next_extent <= self.width; + let enough_height = (raw.height as i32) < (self.height - self.row_baseline); + + enough_width && enough_height + } + + /// Mark current row as finished and prepare to insert into the next row. + pub fn advance_row(&mut self) -> Result<(), AtlasInsertError> { + let advance_to = self.row_baseline + self.row_tallest; + if self.height - advance_to <= 0 { + return Err(AtlasInsertError::Full); + } + + self.row_baseline = advance_to; + self.row_extent = 0; + self.row_tallest = 0; + + Ok(()) + } + + /// Load a glyph into a texture atlas. + /// + /// If the current atlas is full, a new one will be created. + #[inline] + pub fn load_glyph( + active_tex: &mut GLuint, + atlas: &mut Vec, + current_atlas: &mut usize, + rasterized: &RasterizedGlyph, + ) -> Glyph { + // At least one atlas is guaranteed to be in the `self.atlas` list; thus + // the unwrap. + match atlas[*current_atlas].insert(rasterized, active_tex) { + Ok(glyph) => glyph, + Err(AtlasInsertError::Full) => { + *current_atlas += 1; + if *current_atlas == atlas.len() { + let new = Atlas::new(ATLAS_SIZE); + *active_tex = 0; // Atlas::new binds a texture. Ugh this is sloppy. + atlas.push(new); + } + Atlas::load_glyph(active_tex, atlas, current_atlas, rasterized) + }, + Err(AtlasInsertError::GlyphTooLarge) => Glyph { + tex_id: atlas[*current_atlas].id, + multicolor: false, + top: 0, + left: 0, + width: 0, + height: 0, + uv_bot: 0., + uv_left: 0., + uv_width: 0., + uv_height: 0., + }, + } + } + + #[inline] + pub fn clear_atlas(atlas: &mut Vec, current_atlas: &mut usize) { + for atlas in atlas.iter_mut() { + atlas.clear(); + } + *current_atlas = 0; + } +} + +impl Drop for Atlas { + fn drop(&mut self) { + unsafe { + gl::DeleteTextures(1, &self.id); + } + } +} diff --git a/alacritty/src/renderer/builtin_font.rs b/alacritty/src/renderer/text/builtin_font.rs similarity index 100% rename from alacritty/src/renderer/builtin_font.rs rename to alacritty/src/renderer/text/builtin_font.rs diff --git a/alacritty/src/renderer/text/gles2.rs b/alacritty/src/renderer/text/gles2.rs new file mode 100644 index 00000000..9b161081 --- /dev/null +++ b/alacritty/src/renderer/text/gles2.rs @@ -0,0 +1,478 @@ +use std::mem::size_of; +use std::ptr; + +use crossfont::RasterizedGlyph; +use log::info; + +use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::term::SizeInfo; + +use crate::display::content::RenderableCell; +use crate::gl; +use crate::gl::types::*; +use crate::renderer::shader::{ShaderProgram, ShaderVersion}; +use crate::renderer::{cstr, Error}; + +use super::atlas::{Atlas, ATLAS_SIZE}; +use super::{ + Glyph, LoadGlyph, LoaderApi, TextRenderApi, TextRenderBatch, TextRenderer, TextShader, +}; + +// Shader source. +static TEXT_SHADER_F: &str = include_str!("../../../res/gles2/text.f.glsl"); +static TEXT_SHADER_V: &str = include_str!("../../../res/gles2/text.v.glsl"); + +#[derive(Debug)] +pub struct Gles2Renderer { + program: TextShaderProgram, + vao: GLuint, + vbo: GLuint, + ebo: GLuint, + atlas: Vec, + batch: Batch, + current_atlas: usize, + active_tex: GLuint, +} + +impl Gles2Renderer { + pub fn new() -> Result { + info!("Using OpenGL ES 2.0 renderer"); + + let program = TextShaderProgram::new(ShaderVersion::Gles2)?; + let mut vao: GLuint = 0; + let mut vbo: GLuint = 0; + let mut ebo: GLuint = 0; + + let mut vertex_indices = Vec::with_capacity(BATCH_MAX / 4 * 6); + for index in 0..(BATCH_MAX / 4) as u16 { + let index = index * 4; + vertex_indices.push(index); + vertex_indices.push(index + 1); + vertex_indices.push(index + 3); + + vertex_indices.push(index + 1); + vertex_indices.push(index + 2); + vertex_indices.push(index + 3); + } + + unsafe { + gl::Enable(gl::BLEND); + + gl::DepthMask(gl::FALSE); + + gl::GenVertexArrays(1, &mut vao); + gl::GenBuffers(1, &mut ebo); + gl::GenBuffers(1, &mut vbo); + gl::BindVertexArray(vao); + + // Elements buffer. + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); + gl::BufferData( + gl::ELEMENT_ARRAY_BUFFER, + (vertex_indices.capacity() * size_of::()) as isize, + vertex_indices.as_ptr() as *const _, + gl::STATIC_DRAW, + ); + + // Vertex buffer. + gl::BindBuffer(gl::ARRAY_BUFFER, vbo); + gl::BufferData( + gl::ARRAY_BUFFER, + (BATCH_MAX * size_of::()) as isize, + ptr::null(), + gl::STREAM_DRAW, + ); + + let mut index = 0; + let mut size = 0; + + macro_rules! add_attr { + ($count:expr, $gl_type:expr, $type:ty) => { + gl::VertexAttribPointer( + index, + $count, + $gl_type, + gl::FALSE, + size_of::() as i32, + size as *const _, + ); + gl::EnableVertexAttribArray(index); + + #[allow(unused_assignments)] + { + size += $count * size_of::<$type>(); + index += 1; + } + }; + } + + // Cell coords. + add_attr!(2, gl::SHORT, i16); + + // Glyph coords. + add_attr!(2, gl::SHORT, i16); + + // UV. + add_attr!(2, gl::FLOAT, u32); + + // Color and bitmap color. + // + // These are packed together because of an OpenGL driver issue on macOS, which caused a + // `vec3(u8)` text color and a `u8` for glyph color to cause performance regressions. + add_attr!(4, gl::UNSIGNED_BYTE, u8); + + // Background color. + add_attr!(4, gl::UNSIGNED_BYTE, u8); + + // Cleanup. + gl::BindVertexArray(0); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + } + + Ok(Self { + program, + vao, + vbo, + ebo, + atlas: vec![Atlas::new(ATLAS_SIZE)], + batch: Batch::new(), + current_atlas: 0, + active_tex: 0, + }) + } +} + +impl Drop for Gles2Renderer { + fn drop(&mut self) { + unsafe { + gl::DeleteBuffers(1, &self.vbo); + gl::DeleteBuffers(1, &self.ebo); + gl::DeleteVertexArrays(1, &self.vao); + } + } +} + +impl<'a> TextRenderer<'a> for Gles2Renderer { + type RenderApi = RenderApi<'a>; + type RenderBatch = Batch; + type Shader = TextShaderProgram; + + fn program(&self) -> &Self::Shader { + &self.program + } + + fn with_api<'b: 'a, F, T>(&'b mut self, _: &'b SizeInfo, func: F) -> T + where + F: FnOnce(Self::RenderApi) -> T, + { + unsafe { + gl::UseProgram(self.program.id()); + gl::BindVertexArray(self.vao); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, self.ebo); + gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo); + gl::ActiveTexture(gl::TEXTURE0); + } + + let res = func(RenderApi { + active_tex: &mut self.active_tex, + batch: &mut self.batch, + atlas: &mut self.atlas, + current_atlas: &mut self.current_atlas, + program: &mut self.program, + }); + + unsafe { + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindVertexArray(0); + + gl::UseProgram(0); + } + + res + } + + fn loader_api(&mut self) -> LoaderApi<'_> { + LoaderApi { + active_tex: &mut self.active_tex, + atlas: &mut self.atlas, + current_atlas: &mut self.current_atlas, + } + } +} + +/// Maximum items to be drawn in a batch. +/// +/// We use the closest number to `u16::MAX` dividable by 4 (amount of vertices we push for a glyph), +/// since it's the maximum possible index in `glDrawElements` in gles2. +const BATCH_MAX: usize = (u16::MAX - u16::MAX % 4) as usize; + +#[derive(Debug)] +pub struct Batch { + tex: GLuint, + vertices: Vec, +} + +impl Batch { + fn new() -> Self { + Self { tex: 0, vertices: Vec::with_capacity(BATCH_MAX) } + } + + #[inline] + fn len(&self) -> usize { + self.vertices.len() + } + + #[inline] + fn capacity(&self) -> usize { + BATCH_MAX + } + + #[inline] + fn size(&self) -> usize { + self.len() * size_of::() + } + + #[inline] + fn clear(&mut self) { + self.vertices.clear(); + } +} + +impl TextRenderBatch for Batch { + #[inline] + fn tex(&self) -> GLuint { + self.tex + } + + #[inline] + fn full(&self) -> bool { + self.capacity() == self.len() + } + + #[inline] + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn add_item(&mut self, cell: &RenderableCell, glyph: &Glyph, size_info: &SizeInfo) { + if self.is_empty() { + self.tex = glyph.tex_id; + } + + // Calculate the cell position. + let x = cell.point.column.0 as i16 * size_info.cell_width() as i16; + let y = cell.point.line as i16 * size_info.cell_height() as i16; + + // Calculate the glyph position. + let glyph_x = cell.point.column.0 as i16 * size_info.cell_width() as i16 + glyph.left; + let glyph_y = (cell.point.line + 1) as i16 * size_info.cell_height() as i16 - glyph.top; + + let colored = if glyph.multicolor { 1 } else { 0 }; + let is_wide = if cell.flags.contains(Flags::WIDE_CHAR) { 2 } else { 1 }; + + let mut vertex = TextVertex { + x, + y: y + size_info.cell_height() as i16, + + glyph_x, + glyph_y: glyph_y + glyph.height, + + u: glyph.uv_left, + v: glyph.uv_bot + glyph.uv_height, + r: cell.fg.r, + g: cell.fg.g, + b: cell.fg.b, + colored, + bg_r: cell.bg.r, + bg_g: cell.bg.g, + bg_b: cell.bg.b, + bg_a: (cell.bg_alpha * 255.0) as u8, + }; + + self.vertices.push(vertex); + + vertex.y = y; + vertex.glyph_y = glyph_y; + vertex.u = glyph.uv_left; + vertex.v = glyph.uv_bot; + self.vertices.push(vertex); + + vertex.x = x + is_wide * size_info.cell_width() as i16; + vertex.glyph_x = glyph_x + glyph.width; + vertex.u = glyph.uv_left + glyph.uv_width; + vertex.v = glyph.uv_bot; + self.vertices.push(vertex); + + vertex.x = x + is_wide * size_info.cell_width() as i16; + vertex.y = y + size_info.cell_height() as i16; + vertex.glyph_x = glyph_x + glyph.width; + vertex.glyph_y = glyph_y + glyph.height; + vertex.u = glyph.uv_left + glyph.uv_width; + vertex.v = glyph.uv_bot + glyph.uv_height; + self.vertices.push(vertex); + } +} + +#[derive(Debug)] +pub struct RenderApi<'a> { + active_tex: &'a mut GLuint, + batch: &'a mut Batch, + atlas: &'a mut Vec, + current_atlas: &'a mut usize, + program: &'a mut TextShaderProgram, +} + +impl<'a> Drop for RenderApi<'a> { + fn drop(&mut self) { + if !self.batch.is_empty() { + self.render_batch(); + } + } +} + +impl<'a> LoadGlyph for RenderApi<'a> { + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { + Atlas::load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) + } + + fn clear(&mut self) { + Atlas::clear_atlas(self.atlas, self.current_atlas) + } +} + +impl<'a> TextRenderApi for RenderApi<'a> { + fn batch(&mut self) -> &mut Batch { + self.batch + } + + fn render_batch(&mut self) { + unsafe { + gl::BufferSubData( + gl::ARRAY_BUFFER, + 0, + self.batch.size() as isize, + self.batch.vertices.as_ptr() as *const _, + ); + } + + if *self.active_tex != self.batch.tex() { + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.batch.tex()); + } + *self.active_tex = self.batch.tex(); + } + + unsafe { + let num_indices = (self.batch.len() / 4 * 6) as i32; + + // The rendering is inspired by + // https://github.com/servo/webrender/blob/master/webrender/doc/text-rendering.md. + + // Draw background. + self.program.set_rendering_pass(RenderingPass::Background); + gl::BlendFunc(gl::ONE, gl::ZERO); + gl::DrawElements(gl::TRIANGLES, num_indices, gl::UNSIGNED_SHORT, ptr::null()); + + // First text rendering pass. + self.program.set_rendering_pass(RenderingPass::SubpixelPass1); + gl::BlendFuncSeparate(gl::ZERO, gl::ONE_MINUS_SRC_COLOR, gl::ZERO, gl::ONE); + gl::DrawElements(gl::TRIANGLES, num_indices, gl::UNSIGNED_SHORT, ptr::null()); + + // Second text rendering pass. + self.program.set_rendering_pass(RenderingPass::SubpixelPass2); + gl::BlendFuncSeparate(gl::ONE_MINUS_DST_ALPHA, gl::ONE, gl::ZERO, gl::ONE); + gl::DrawElements(gl::TRIANGLES, num_indices, gl::UNSIGNED_SHORT, ptr::null()); + + // Third pass. + self.program.set_rendering_pass(RenderingPass::SubpixelPass3); + gl::BlendFuncSeparate(gl::ONE, gl::ONE, gl::ONE, gl::ONE_MINUS_SRC_ALPHA); + gl::DrawElements(gl::TRIANGLES, num_indices, gl::UNSIGNED_SHORT, ptr::null()); + } + + self.batch.clear(); + } +} + +#[repr(C)] +#[derive(Debug, Copy, Clone)] +struct TextVertex { + // Cell coordinates. + x: i16, + y: i16, + + // Glyph coordinates. + glyph_x: i16, + glyph_y: i16, + + // Offsets into Atlas. + u: f32, + v: f32, + + // Color. + r: u8, + g: u8, + b: u8, + + // Whether the glyph is colored. + colored: u8, + + // Background color. + bg_r: u8, + bg_g: u8, + bg_b: u8, + bg_a: u8, +} + +// NOTE: These flags must be in sync with their usage in the gles2/text.*.glsl shaders. +#[repr(u8)] +enum RenderingPass { + Background = 0, + SubpixelPass1 = 1, + SubpixelPass2 = 2, + SubpixelPass3 = 3, +} + +#[derive(Debug)] +pub struct TextShaderProgram { + /// Shader program. + program: ShaderProgram, + + /// Projection scale and offset uniform. + u_projection: GLint, + + /// Rendering pass. + /// + /// The rendering is split into 4 passes. One is used for the background and the rest to + /// perform subpixel text rendering according to + /// https://github.com/servo/webrender/blob/master/webrender/doc/text-rendering.md. + /// + /// Rendering is split into three passes. + u_rendering_pass: GLint, +} + +impl TextShaderProgram { + pub fn new(shader_version: ShaderVersion) -> Result { + let program = ShaderProgram::new(shader_version, TEXT_SHADER_V, TEXT_SHADER_F)?; + Ok(Self { + u_projection: program.get_uniform_location(cstr!("projection"))?, + u_rendering_pass: program.get_uniform_location(cstr!("renderingPass"))?, + program, + }) + } + + fn set_rendering_pass(&self, rendering_pass: RenderingPass) { + unsafe { gl::Uniform1i(self.u_rendering_pass, rendering_pass as i32) } + } +} + +impl TextShader for TextShaderProgram { + fn id(&self) -> GLuint { + self.program.id() + } + + fn projection_uniform(&self) -> GLint { + self.u_projection + } +} diff --git a/alacritty/src/renderer/text/glsl3.rs b/alacritty/src/renderer/text/glsl3.rs new file mode 100644 index 00000000..6701cf3d --- /dev/null +++ b/alacritty/src/renderer/text/glsl3.rs @@ -0,0 +1,460 @@ +use std::mem::size_of; +use std::ptr; + +use crossfont::RasterizedGlyph; +use log::info; + +use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::term::SizeInfo; + +use crate::display::content::RenderableCell; +use crate::gl; +use crate::gl::types::*; +use crate::renderer::shader::{ShaderProgram, ShaderVersion}; +use crate::renderer::{cstr, Error}; + +use super::atlas::{Atlas, ATLAS_SIZE}; +use super::{ + Glyph, LoadGlyph, LoaderApi, RenderingGlyphFlags, TextRenderApi, TextRenderBatch, TextRenderer, + TextShader, +}; + +// Shader source. +static TEXT_SHADER_F: &str = include_str!("../../../res/glsl3/text.f.glsl"); +static TEXT_SHADER_V: &str = include_str!("../../../res/glsl3/text.v.glsl"); + +/// Maximum items to be drawn in a batch. +const BATCH_MAX: usize = 0x1_0000; + +#[derive(Debug)] +pub struct Glsl3Renderer { + program: TextShaderProgram, + vao: GLuint, + ebo: GLuint, + vbo_instance: GLuint, + atlas: Vec, + current_atlas: usize, + active_tex: GLuint, + batch: Batch, +} + +impl Glsl3Renderer { + pub fn new() -> Result { + info!("Using OpenGL 3.3 renderer"); + + let program = TextShaderProgram::new(ShaderVersion::Glsl3)?; + let mut vao: GLuint = 0; + let mut ebo: GLuint = 0; + let mut vbo_instance: GLuint = 0; + + unsafe { + gl::Enable(gl::BLEND); + gl::BlendFunc(gl::SRC1_COLOR, gl::ONE_MINUS_SRC1_COLOR); + gl::Enable(gl::MULTISAMPLE); + + // Disable depth mask, as the renderer never uses depth tests. + gl::DepthMask(gl::FALSE); + + gl::GenVertexArrays(1, &mut vao); + gl::GenBuffers(1, &mut ebo); + gl::GenBuffers(1, &mut vbo_instance); + gl::BindVertexArray(vao); + + // --------------------- + // Set up element buffer + // --------------------- + let indices: [u32; 6] = [0, 1, 3, 1, 2, 3]; + + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); + gl::BufferData( + gl::ELEMENT_ARRAY_BUFFER, + (6 * size_of::()) as isize, + indices.as_ptr() as *const _, + gl::STATIC_DRAW, + ); + + // ---------------------------- + // Setup vertex instance buffer + // ---------------------------- + gl::BindBuffer(gl::ARRAY_BUFFER, vbo_instance); + gl::BufferData( + gl::ARRAY_BUFFER, + (BATCH_MAX * size_of::()) as isize, + ptr::null(), + gl::STREAM_DRAW, + ); + + let mut index = 0; + let mut size = 0; + + macro_rules! add_attr { + ($count:expr, $gl_type:expr, $type:ty) => { + gl::VertexAttribPointer( + index, + $count, + $gl_type, + gl::FALSE, + size_of::() as i32, + size as *const _, + ); + gl::EnableVertexAttribArray(index); + gl::VertexAttribDivisor(index, 1); + + #[allow(unused_assignments)] + { + size += $count * size_of::<$type>(); + index += 1; + } + }; + } + + // Coords. + add_attr!(2, gl::UNSIGNED_SHORT, u16); + + // Glyph offset and size. + add_attr!(4, gl::SHORT, i16); + + // UV offset. + add_attr!(4, gl::FLOAT, f32); + + // Color and cell flags. + // + // These are packed together because of an OpenGL driver issue on macOS, which caused a + // `vec3(u8)` text color and a `u8` cell flags to increase the rendering time by a + // huge margin. + add_attr!(4, gl::UNSIGNED_BYTE, u8); + + // Background color. + add_attr!(4, gl::UNSIGNED_BYTE, u8); + + // Cleanup. + gl::BindVertexArray(0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); + } + + Ok(Self { + program, + vao, + ebo, + vbo_instance, + atlas: vec![Atlas::new(ATLAS_SIZE)], + current_atlas: 0, + active_tex: 0, + batch: Batch::new(), + }) + } +} + +impl<'a> TextRenderer<'a> for Glsl3Renderer { + type RenderApi = RenderApi<'a>; + type RenderBatch = Batch; + type Shader = TextShaderProgram; + + fn with_api<'b: 'a, F, T>(&'b mut self, size_info: &'b SizeInfo, func: F) -> T + where + F: FnOnce(Self::RenderApi) -> T, + { + unsafe { + gl::UseProgram(self.program.id()); + self.program.set_term_uniforms(size_info); + + gl::BindVertexArray(self.vao); + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, self.ebo); + gl::BindBuffer(gl::ARRAY_BUFFER, self.vbo_instance); + gl::ActiveTexture(gl::TEXTURE0); + } + + let res = func(RenderApi { + active_tex: &mut self.active_tex, + batch: &mut self.batch, + atlas: &mut self.atlas, + current_atlas: &mut self.current_atlas, + program: &mut self.program, + }); + + unsafe { + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, 0); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + gl::BindVertexArray(0); + + gl::UseProgram(0); + } + + res + } + + fn program(&self) -> &Self::Shader { + &self.program + } + + fn loader_api(&mut self) -> LoaderApi<'_> { + LoaderApi { + active_tex: &mut self.active_tex, + atlas: &mut self.atlas, + current_atlas: &mut self.current_atlas, + } + } +} + +impl Drop for Glsl3Renderer { + fn drop(&mut self) { + unsafe { + gl::DeleteBuffers(1, &self.vbo_instance); + gl::DeleteBuffers(1, &self.ebo); + gl::DeleteVertexArrays(1, &self.vao); + } + } +} + +#[derive(Debug)] +pub struct RenderApi<'a> { + active_tex: &'a mut GLuint, + batch: &'a mut Batch, + atlas: &'a mut Vec, + current_atlas: &'a mut usize, + program: &'a mut TextShaderProgram, +} + +impl<'a> TextRenderApi for RenderApi<'a> { + fn batch(&mut self) -> &mut Batch { + self.batch + } + + fn render_batch(&mut self) { + unsafe { + gl::BufferSubData( + gl::ARRAY_BUFFER, + 0, + self.batch.size() as isize, + self.batch.instances.as_ptr() as *const _, + ); + } + + // Bind texture if necessary. + if *self.active_tex != self.batch.tex() { + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.batch.tex()); + } + *self.active_tex = self.batch.tex(); + } + + unsafe { + self.program.set_background_pass(true); + gl::DrawElementsInstanced( + gl::TRIANGLES, + 6, + gl::UNSIGNED_INT, + ptr::null(), + self.batch.len() as GLsizei, + ); + self.program.set_background_pass(false); + gl::DrawElementsInstanced( + gl::TRIANGLES, + 6, + gl::UNSIGNED_INT, + ptr::null(), + self.batch.len() as GLsizei, + ); + } + + self.batch.clear(); + } +} + +impl<'a> LoadGlyph for RenderApi<'a> { + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { + Atlas::load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) + } + + fn clear(&mut self) { + Atlas::clear_atlas(self.atlas, self.current_atlas) + } +} + +impl<'a> Drop for RenderApi<'a> { + fn drop(&mut self) { + if !self.batch.is_empty() { + self.render_batch(); + } + } +} + +#[derive(Debug)] +#[repr(C)] +struct InstanceData { + // Coords. + col: u16, + row: u16, + + // Glyph offset. + left: i16, + top: i16, + + // Glyph size. + width: i16, + height: i16, + + // UV offset. + uv_left: f32, + uv_bot: f32, + + // uv scale. + uv_width: f32, + uv_height: f32, + + // Color. + r: u8, + g: u8, + b: u8, + + // Cell flags like multicolor or fullwidth character. + cell_flags: RenderingGlyphFlags, + + // Background color. + bg_r: u8, + bg_g: u8, + bg_b: u8, + bg_a: u8, +} + +#[derive(Debug, Default)] +pub struct Batch { + tex: GLuint, + instances: Vec, +} + +impl TextRenderBatch for Batch { + #[inline] + fn tex(&self) -> GLuint { + self.tex + } + + #[inline] + fn full(&self) -> bool { + self.capacity() == self.len() + } + + #[inline] + fn is_empty(&self) -> bool { + self.len() == 0 + } + + fn add_item(&mut self, cell: &RenderableCell, glyph: &Glyph, _: &SizeInfo) { + if self.is_empty() { + self.tex = glyph.tex_id; + } + + let mut cell_flags = RenderingGlyphFlags::empty(); + cell_flags.set(RenderingGlyphFlags::COLORED, glyph.multicolor); + cell_flags.set(RenderingGlyphFlags::WIDE_CHAR, cell.flags.contains(Flags::WIDE_CHAR)); + + self.instances.push(InstanceData { + col: cell.point.column.0 as u16, + row: cell.point.line as u16, + + top: glyph.top, + left: glyph.left, + width: glyph.width, + height: glyph.height, + + uv_bot: glyph.uv_bot, + uv_left: glyph.uv_left, + uv_width: glyph.uv_width, + uv_height: glyph.uv_height, + + r: cell.fg.r, + g: cell.fg.g, + b: cell.fg.b, + cell_flags, + + bg_r: cell.bg.r, + bg_g: cell.bg.g, + bg_b: cell.bg.b, + bg_a: (cell.bg_alpha * 255.0) as u8, + }); + } +} + +impl Batch { + #[inline] + pub fn new() -> Self { + Self { tex: 0, instances: Vec::with_capacity(BATCH_MAX) } + } + + #[inline] + pub fn len(&self) -> usize { + self.instances.len() + } + + #[inline] + pub fn capacity(&self) -> usize { + BATCH_MAX + } + + #[inline] + pub fn size(&self) -> usize { + self.len() * size_of::() + } + + pub fn clear(&mut self) { + self.tex = 0; + self.instances.clear(); + } +} + +/// Text drawing program. +/// +/// Uniforms are prefixed with "u", and vertex attributes are prefixed with "a". +#[derive(Debug)] +pub struct TextShaderProgram { + /// Shader program. + program: ShaderProgram, + + /// Projection scale and offset uniform. + u_projection: GLint, + + /// Cell dimensions (pixels). + u_cell_dim: GLint, + + /// Background pass flag. + /// + /// Rendering is split into two passes; 1 for backgrounds, and one for text. + u_background: GLint, +} + +impl TextShaderProgram { + pub fn new(shader_version: ShaderVersion) -> Result { + let program = ShaderProgram::new(shader_version, TEXT_SHADER_V, TEXT_SHADER_F)?; + Ok(Self { + u_projection: program.get_uniform_location(cstr!("projection"))?, + u_cell_dim: program.get_uniform_location(cstr!("cellDim"))?, + u_background: program.get_uniform_location(cstr!("backgroundPass"))?, + program, + }) + } + + fn set_term_uniforms(&self, props: &SizeInfo) { + unsafe { + gl::Uniform2f(self.u_cell_dim, props.cell_width(), props.cell_height()); + } + } + + fn set_background_pass(&self, background_pass: bool) { + let value = if background_pass { 1 } else { 0 }; + + unsafe { + gl::Uniform1i(self.u_background, value); + } + } +} + +impl TextShader for TextShaderProgram { + fn id(&self) -> GLuint { + self.program.id() + } + + fn projection_uniform(&self) -> GLint { + self.u_projection + } +} diff --git a/alacritty/src/renderer/text/glyph_cache.rs b/alacritty/src/renderer/text/glyph_cache.rs new file mode 100644 index 00000000..c75cad7a --- /dev/null +++ b/alacritty/src/renderer/text/glyph_cache.rs @@ -0,0 +1,322 @@ +use std::collections::HashMap; +use std::hash::BuildHasherDefault; + +use crossfont::{ + Error as RasterizerError, FontDesc, FontKey, GlyphKey, Metrics, Rasterize, RasterizedGlyph, + Rasterizer, Size, Slant, Style, Weight, +}; +use fnv::FnvHasher; +use log::{error, info}; +use unicode_width::UnicodeWidthChar; + +use crate::config::font::{Font, FontDescription}; +use crate::config::ui_config::Delta; +use crate::gl::types::*; + +use super::builtin_font; + +/// `LoadGlyph` allows for copying a rasterized glyph into graphics memory. +pub trait LoadGlyph { + /// Load the rasterized glyph into GPU memory. + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph; + + /// Clear any state accumulated from previous loaded glyphs. + /// + /// This can, for instance, be used to reset the texture Atlas. + fn clear(&mut self); +} + +#[derive(Copy, Clone, Debug)] +pub struct Glyph { + pub tex_id: GLuint, + pub multicolor: bool, + pub top: i16, + pub left: i16, + pub width: i16, + pub height: i16, + pub uv_bot: f32, + pub uv_left: f32, + pub uv_width: f32, + pub uv_height: f32, +} + +/// Naïve glyph cache. +/// +/// Currently only keyed by `char`, and thus not possible to hold different +/// representations of the same code point. +pub struct GlyphCache { + /// Cache of buffered glyphs. + cache: HashMap>, + + /// Rasterizer for loading new glyphs. + rasterizer: Rasterizer, + + /// Regular font. + pub font_key: FontKey, + + /// Bold font. + pub bold_key: FontKey, + + /// Italic font. + pub italic_key: FontKey, + + /// Bold italic font. + pub bold_italic_key: FontKey, + + /// Font size. + pub font_size: crossfont::Size, + + /// Font offset. + font_offset: Delta, + + /// Glyph offset. + glyph_offset: Delta, + + /// Font metrics. + metrics: Metrics, + + /// Whether to use the built-in font for box drawing characters. + builtin_box_drawing: bool, +} + +impl GlyphCache { + pub fn new(mut rasterizer: Rasterizer, font: &Font) -> Result { + let (regular, bold, italic, bold_italic) = Self::compute_font_keys(font, &mut rasterizer)?; + + // Need to load at least one glyph for the face before calling metrics. + // The glyph requested here ('m' at the time of writing) has no special + // meaning. + rasterizer.get_glyph(GlyphKey { font_key: regular, character: 'm', size: font.size() })?; + + let metrics = rasterizer.metrics(regular, font.size())?; + + Ok(Self { + cache: HashMap::default(), + rasterizer, + font_size: font.size(), + font_key: regular, + bold_key: bold, + italic_key: italic, + bold_italic_key: bold_italic, + font_offset: font.offset, + glyph_offset: font.glyph_offset, + metrics, + builtin_box_drawing: font.builtin_box_drawing, + }) + } + + fn load_glyphs_for_font(&mut self, font: FontKey, loader: &mut L) { + let size = self.font_size; + + // Cache all ascii characters. + for i in 32u8..=126u8 { + self.get(GlyphKey { font_key: font, character: i as char, size }, loader, true); + } + } + + /// Computes font keys for (Regular, Bold, Italic, Bold Italic). + fn compute_font_keys( + font: &Font, + rasterizer: &mut Rasterizer, + ) -> Result<(FontKey, FontKey, FontKey, FontKey), crossfont::Error> { + let size = font.size(); + + // Load regular font. + let regular_desc = Self::make_desc(font.normal(), Slant::Normal, Weight::Normal); + + let regular = Self::load_regular_font(rasterizer, ®ular_desc, size)?; + + // Helper to load a description if it is not the `regular_desc`. + let mut load_or_regular = |desc: FontDesc| { + if desc == regular_desc { + regular + } else { + rasterizer.load_font(&desc, size).unwrap_or(regular) + } + }; + + // Load bold font. + let bold_desc = Self::make_desc(&font.bold(), Slant::Normal, Weight::Bold); + + let bold = load_or_regular(bold_desc); + + // Load italic font. + let italic_desc = Self::make_desc(&font.italic(), Slant::Italic, Weight::Normal); + + let italic = load_or_regular(italic_desc); + + // Load bold italic font. + let bold_italic_desc = Self::make_desc(&font.bold_italic(), Slant::Italic, Weight::Bold); + + let bold_italic = load_or_regular(bold_italic_desc); + + Ok((regular, bold, italic, bold_italic)) + } + + fn load_regular_font( + rasterizer: &mut Rasterizer, + description: &FontDesc, + size: Size, + ) -> Result { + match rasterizer.load_font(description, size) { + Ok(font) => Ok(font), + Err(err) => { + error!("{}", err); + + let fallback_desc = + Self::make_desc(Font::default().normal(), Slant::Normal, Weight::Normal); + rasterizer.load_font(&fallback_desc, size) + }, + } + } + + fn make_desc(desc: &FontDescription, slant: Slant, weight: Weight) -> FontDesc { + let style = if let Some(ref spec) = desc.style { + Style::Specific(spec.to_owned()) + } else { + Style::Description { slant, weight } + }; + FontDesc::new(desc.family.clone(), style) + } + + /// Get a glyph from the font. + /// + /// If the glyph has never been loaded before, it will be rasterized and inserted into the + /// cache. + /// + /// # Errors + /// + /// This will fail when the glyph could not be rasterized. Usually this is due to the glyph + /// not being present in any font. + pub fn get( + &mut self, + glyph_key: GlyphKey, + loader: &mut L, + show_missing: bool, + ) -> Glyph + where + L: LoadGlyph, + { + // Try to load glyph from cache. + if let Some(glyph) = self.cache.get(&glyph_key) { + return *glyph; + }; + + // Rasterize the glyph using the built-in font for special characters or the user's font + // for everything else. + let rasterized = self + .builtin_box_drawing + .then(|| { + builtin_font::builtin_glyph( + glyph_key.character, + &self.metrics, + &self.font_offset, + &self.glyph_offset, + ) + }) + .flatten() + .map_or_else(|| self.rasterizer.get_glyph(glyph_key), Ok); + + let glyph = match rasterized { + Ok(rasterized) => self.load_glyph(loader, rasterized), + // Load fallback glyph. + Err(RasterizerError::MissingGlyph(rasterized)) if show_missing => { + // Use `\0` as "missing" glyph to cache it only once. + let missing_key = GlyphKey { character: '\0', ..glyph_key }; + if let Some(glyph) = self.cache.get(&missing_key) { + *glyph + } else { + // If no missing glyph was loaded yet, insert it as `\0`. + let glyph = self.load_glyph(loader, rasterized); + self.cache.insert(missing_key, glyph); + + glyph + } + }, + Err(_) => self.load_glyph(loader, Default::default()), + }; + + // Cache rasterized glyph. + *self.cache.entry(glyph_key).or_insert(glyph) + } + + /// Load glyph into the atlas. + /// + /// This will apply all transforms defined for the glyph cache to the rasterized glyph before + pub fn load_glyph(&self, loader: &mut L, mut glyph: RasterizedGlyph) -> Glyph + where + L: LoadGlyph, + { + glyph.left += i32::from(self.glyph_offset.x); + glyph.top += i32::from(self.glyph_offset.y); + glyph.top -= self.metrics.descent as i32; + + // The metrics of zero-width characters are based on rendering + // the character after the current cell, with the anchor at the + // right side of the preceding character. Since we render the + // zero-width characters inside the preceding character, the + // anchor has been moved to the right by one cell. + if glyph.character.width() == Some(0) { + glyph.left += self.metrics.average_advance as i32; + } + + // Add glyph to cache. + loader.load_glyph(&glyph) + } + + /// Clear currently cached data in both GL and the registry. + pub fn clear_glyph_cache(&mut self, loader: &mut L) { + loader.clear(); + self.cache = HashMap::default(); + + self.load_common_glyphs(loader); + } + + pub fn update_font_size( + &mut self, + font: &Font, + scale_factor: f64, + loader: &mut L, + ) -> Result<(), crossfont::Error> { + // Update dpi scaling. + self.rasterizer.update_dpr(scale_factor as f32); + self.font_offset = font.offset; + + // Recompute font keys. + let (regular, bold, italic, bold_italic) = + Self::compute_font_keys(font, &mut self.rasterizer)?; + + self.rasterizer.get_glyph(GlyphKey { + font_key: regular, + character: 'm', + size: font.size(), + })?; + let metrics = self.rasterizer.metrics(regular, font.size())?; + + info!("Font size changed to {:?} with scale factor of {}", font.size(), scale_factor); + + self.font_size = font.size(); + self.font_key = regular; + self.bold_key = bold; + self.italic_key = italic; + self.bold_italic_key = bold_italic; + self.metrics = metrics; + self.builtin_box_drawing = font.builtin_box_drawing; + + self.clear_glyph_cache(loader); + + Ok(()) + } + + pub fn font_metrics(&self) -> crossfont::Metrics { + self.metrics + } + + /// Prefetch glyphs that are almost guaranteed to be loaded anyways. + pub fn load_common_glyphs(&mut self, loader: &mut L) { + self.load_glyphs_for_font(self.font_key, loader); + self.load_glyphs_for_font(self.bold_key, loader); + self.load_glyphs_for_font(self.italic_key, loader); + self.load_glyphs_for_font(self.bold_italic_key, loader); + } +} diff --git a/alacritty/src/renderer/text/mod.rs b/alacritty/src/renderer/text/mod.rs new file mode 100644 index 00000000..05ac59e8 --- /dev/null +++ b/alacritty/src/renderer/text/mod.rs @@ -0,0 +1,202 @@ +use bitflags::bitflags; +use crossfont::{GlyphKey, RasterizedGlyph}; + +use alacritty_terminal::term::cell::Flags; +use alacritty_terminal::term::SizeInfo; + +use crate::display::content::RenderableCell; +use crate::gl; +use crate::gl::types::*; + +mod atlas; +mod builtin_font; +mod gles2; +mod glsl3; +pub mod glyph_cache; + +use atlas::Atlas; +pub use gles2::Gles2Renderer; +pub use glsl3::Glsl3Renderer; +pub use glyph_cache::GlyphCache; +use glyph_cache::{Glyph, LoadGlyph}; + +// NOTE: These flags must be in sync with their usage in the text.*.glsl shaders. +bitflags! { + #[repr(C)] + struct RenderingGlyphFlags: u8 { + const WIDE_CHAR = 0b0000_0001; + const COLORED = 0b0000_0010; + } +} + +pub trait TextRenderer<'a> { + type Shader: TextShader; + type RenderBatch: TextRenderBatch; + type RenderApi: TextRenderApi; + + /// Get loader API for the renderer. + fn loader_api(&mut self) -> LoaderApi<'_>; + + /// Draw cells. + fn draw_cells<'b: 'a, I: Iterator>( + &'b mut self, + size_info: &'b SizeInfo, + glyph_cache: &'a mut GlyphCache, + cells: I, + ) { + self.with_api(size_info, |mut api| { + for cell in cells { + api.draw_cell(cell, glyph_cache, size_info); + } + }) + } + + fn with_api<'b: 'a, F, T>(&'b mut self, size_info: &'b SizeInfo, func: F) -> T + where + F: FnOnce(Self::RenderApi) -> T; + + fn program(&self) -> &Self::Shader; + + /// Resize the text rendering. + fn resize(&self, size: &SizeInfo) { + unsafe { + let program = self.program(); + gl::UseProgram(program.id()); + update_projection(program.projection_uniform(), size); + gl::UseProgram(0); + } + } + + /// Invoke renderer with the loader. + fn with_loader) -> T, T>(&mut self, func: F) -> T { + unsafe { + gl::ActiveTexture(gl::TEXTURE0); + } + + func(self.loader_api()) + } +} + +pub trait TextRenderBatch { + /// Check if `Batch` is empty. + fn is_empty(&self) -> bool; + + /// Check whether the `Batch` is full. + fn full(&self) -> bool; + + /// Get texture `Batch` is using. + fn tex(&self) -> GLuint; + + /// Add item to the batch. + fn add_item(&mut self, cell: &RenderableCell, glyph: &Glyph, size_info: &SizeInfo); +} + +pub trait TextRenderApi: LoadGlyph { + /// Get `Batch` the api is using. + fn batch(&mut self) -> &mut T; + + /// Render the underlying data. + fn render_batch(&mut self); + + /// Add item to the rendering queue. + #[inline] + fn add_render_item(&mut self, cell: &RenderableCell, glyph: &Glyph, size_info: &SizeInfo) { + // Flush batch if tex changing. + if !self.batch().is_empty() && self.batch().tex() != glyph.tex_id { + self.render_batch(); + } + + self.batch().add_item(cell, glyph, size_info); + + // Render batch and clear if it's full. + if self.batch().full() { + self.render_batch(); + } + } + + /// Draw cell. + fn draw_cell( + &mut self, + mut cell: RenderableCell, + glyph_cache: &mut GlyphCache, + size_info: &SizeInfo, + ) { + // Get font key for cell. + let font_key = match cell.flags & Flags::BOLD_ITALIC { + Flags::BOLD_ITALIC => glyph_cache.bold_italic_key, + Flags::ITALIC => glyph_cache.italic_key, + Flags::BOLD => glyph_cache.bold_key, + _ => glyph_cache.font_key, + }; + + // Ignore hidden cells and render tabs as spaces to prevent font issues. + let hidden = cell.flags.contains(Flags::HIDDEN); + if cell.character == '\t' || hidden { + cell.character = ' '; + } + + let mut glyph_key = + GlyphKey { font_key, size: glyph_cache.font_size, character: cell.character }; + + // Add cell to batch. + let glyph = glyph_cache.get(glyph_key, self, true); + self.add_render_item(&cell, &glyph, size_info); + + // Render visible zero-width characters. + if let Some(zerowidth) = cell.zerowidth.take().filter(|_| !hidden) { + for character in zerowidth { + glyph_key.character = character; + let glyph = glyph_cache.get(glyph_key, self, false); + self.add_render_item(&cell, &glyph, size_info); + } + } + } +} + +pub trait TextShader { + fn id(&self) -> GLuint; + + /// Id of the projection uniform. + fn projection_uniform(&self) -> GLint; +} + +#[derive(Debug)] +pub struct LoaderApi<'a> { + active_tex: &'a mut GLuint, + atlas: &'a mut Vec, + current_atlas: &'a mut usize, +} + +impl<'a> LoadGlyph for LoaderApi<'a> { + fn load_glyph(&mut self, rasterized: &RasterizedGlyph) -> Glyph { + Atlas::load_glyph(self.active_tex, self.atlas, self.current_atlas, rasterized) + } + + fn clear(&mut self) { + Atlas::clear_atlas(self.atlas, self.current_atlas) + } +} + +fn update_projection(u_projection: GLint, size: &SizeInfo) { + let width = size.width(); + let height = size.height(); + let padding_x = size.padding_x(); + let padding_y = size.padding_y(); + + // Bounds check. + if (width as u32) < (2 * padding_x as u32) || (height as u32) < (2 * padding_y as u32) { + return; + } + + // Compute scale and offset factors, from pixel to ndc space. Y is inverted. + // [0, width - 2 * padding_x] to [-1, 1] + // [height - 2 * padding_y, 0] to [-1, 1] + let scale_x = 2. / (width - 2. * padding_x); + let scale_y = -2. / (height - 2. * padding_y); + let offset_x = -1.; + let offset_y = 1.; + + unsafe { + gl::Uniform4f(u_projection, offset_x, offset_y, scale_x, scale_y); + } +}