use crate::api::AlphaBlending;
use crate::gl;
use crate::gl::types::*;
use crate::image::Image;
use crate::renderer::{DrawCallHandle, Renderer, Shaders, TextureWrapping};
use crate::text::types::*;
use crate::types::*;
use fnv::FnvHashMap;
use std::cell::Cell;
use std::rc::{Rc, Weak};
const GLYPH_CACHE_GAP: i32 = 1;
const TEXT_FRAGMENT_SHADER_110: &str = include_str!("../shaders/legacy/text.frag");
const TEXT_FRAGMENT_SHADER_330: &str = include_str!("../shaders/text.frag");
pub(crate) struct GlyphCache {
pub(crate) call: DrawCallHandle,
width: i32,
height: i32,
max_size: i32,
column_cursor: i32,
columns: Vec<GlyphColumn>,
cache: FnvHashMap<CacheIdentifier, Weak<GlyphSpot>>,
requested_resize: Option<i32>,
}
impl GlyphCache {
pub fn new(renderer: &mut Renderer, smoothed: bool) -> GlyphCache {
let mut max_size = 0 as GLint;
unsafe { gl::GetIntegerv(gl::MAX_TEXTURE_SIZE, &mut max_size) };
let size = if renderer.legacy {
max_size
} else {
64.min(max_size)
};
let cache_image = Image::with_null_texture(size, size, gl::RED);
let mut shaders = Shaders::default();
shaders.shader_110.fragment_shader = TEXT_FRAGMENT_SHADER_110.to_string();
shaders.shader_100_es.fragment_shader = TEXT_FRAGMENT_SHADER_110.to_string();
shaders.shader_330.fragment_shader = TEXT_FRAGMENT_SHADER_330.to_string();
shaders.shader_300_es.fragment_shader = TEXT_FRAGMENT_SHADER_330.to_string();
let call = renderer.create_draw_call(
Some(&cache_image),
&shaders,
AlphaBlending {
blend: true,
sort: true,
},
true,
smoothed,
(TextureWrapping::Clamp, TextureWrapping::Clamp),
false,
);
let cache = GlyphCache {
call,
width: size,
height: size,
max_size,
column_cursor: GLYPH_CACHE_GAP,
columns: Vec::with_capacity(1),
cache: FnvHashMap::default(),
requested_resize: None,
};
cache
}
pub fn resize_if_needed(&mut self, renderer: &mut Renderer) {
if let Some(size) = self.requested_resize {
self.requested_resize = None;
renderer.resize_texture(&self.call, size, size);
self.width = size;
self.height = size;
for col in &mut self.columns {
col.height = self.height - GLYPH_CACHE_GAP * 2;
}
}
}
pub fn reserve(
&mut self,
id: CacheIdentifier,
width: i32,
height: i32,
) -> Result<(RectPx, bool), GlyphRenderingError> {
if let Some(uvs) = self.get_uvs_from_cache(id) {
Ok((uvs.texcoords, false))
} else {
if let Some(uvs) = None
.or_else(|| {
let reserve = |col: &mut GlyphColumn| col.reserve(width, height, false, false);
self.columns.iter_mut().find_map(reserve)
})
.or_else(|| {
self.create_column(width, height)
.and_then(|col| col.reserve(width, height, false, false))
})
.or_else(|| {
let reserve = |col: &mut GlyphColumn| col.reserve(width, height, true, false);
self.columns.iter_mut().find_map(reserve)
})
.or_else(|| {
let reserve = |col: &mut GlyphColumn| col.reserve(width, height, true, true);
self.columns.iter_mut().find_map(reserve)
})
.or_else(|| {
let new_size = if width > height {
(((self.column_cursor + width) as u32).next_power_of_two() as i32)
.min(self.max_size)
} else {
(((self.height + height) as u32).next_power_of_two() as i32)
.min(self.max_size)
};
if self.column_cursor + width + GLYPH_CACHE_GAP < new_size
|| self.height + height + GLYPH_CACHE_GAP * 2 < new_size
{
self.requested_resize = Some(new_size);
}
None
})
{
self.cache.insert(id, Rc::downgrade(&uvs));
crate::profiler::write(|p| p.glyphs_rasterized += 1);
Ok((uvs.texcoords, true))
} else {
Err(GlyphRenderingError::GlyphCacheFull)
}
}
}
pub fn upload_glyph<F: Fn(i32, i32) -> u8>(
&mut self,
renderer: &Renderer,
spot: RectPx,
get_color: F,
) {
let tex_x = spot.x - 1;
let tex_y = spot.y - 1;
let width = spot.width + 2;
let height = spot.height + 2;
let mut pixels = Vec::with_capacity((width * height) as usize);
for y in 0..height {
for x in 0..width {
if x == 0 || y == 0 || x == width - 1 || y == height - 1 {
pixels.push(0u8);
} else {
pixels.push(get_color(x - 1, y - 1));
}
}
}
let image = Image {
pixels,
width,
height,
pixel_type: gl::UNSIGNED_BYTE,
format: gl::RED,
null_data: false,
};
renderer.upload_texture_region(&self.call, (tex_x, tex_y, width, height).into(), &image);
}
pub fn expire_one_step(&mut self) {
let mut removed_spots = Vec::new();
for (key, spot) in self.cache.iter_mut() {
if let Some(spot) = spot.upgrade() {
spot.status.set(spot.status.get().one_step_expired());
} else {
removed_spots.push(key.clone());
}
}
for spot_key in removed_spots {
self.cache.remove(&spot_key);
}
}
fn get_uvs_from_cache(&mut self, id: CacheIdentifier) -> Option<Rc<GlyphSpot>> {
if let Some(spot) = self.cache.get(&id) {
if let Some(spot) = spot.upgrade() {
debug_assert_eq!(Rc::strong_count(&spot), 2);
spot.status.set(ExpiryStatus::UsedDuringThisFrame);
return Some(spot);
} else {
self.cache.remove(&id);
}
}
None
}
fn create_column(&mut self, width: i32, height: i32) -> Option<&mut GlyphColumn> {
if height > self.height - GLYPH_CACHE_GAP * 2 {
None
} else {
let col_width = (((width * 4).max(128) as u32).next_power_of_two() as i32)
.min(self.width - self.column_cursor - GLYPH_CACHE_GAP);
if col_width >= width {
let column = GlyphColumn::new(
self.column_cursor,
GLYPH_CACHE_GAP,
col_width,
self.height - GLYPH_CACHE_GAP * 2,
);
self.columns.push(column);
self.column_cursor += col_width + GLYPH_CACHE_GAP;
let i = self.columns.len() - 1;
Some(&mut self.columns[i])
} else {
None
}
}
}
}
struct GlyphColumn {
x: i32,
y: i32,
width: i32,
height: i32,
lines: Vec<GlyphLine>,
}
impl GlyphColumn {
fn new(x: i32, y: i32, width: i32, height: i32) -> GlyphColumn {
GlyphColumn {
x,
y,
width,
height,
lines: Vec::new(),
}
}
fn reserve(
&mut self,
width: i32,
height: i32,
can_evict_spots: bool,
can_evict_lines: bool,
) -> Option<Rc<GlyphSpot>> {
if width > self.width || height > self.height - GLYPH_CACHE_GAP {
return None;
}
for line in &mut self.lines {
if let Some(spot) = line
.reserve(width, height, can_evict_spots)
.and_then(|spot_weak| spot_weak.upgrade())
{
return Some(spot);
}
}
if can_evict_lines {
if let Some(spot) = self
.evict_line(height)
.and_then(|new_line| new_line.reserve(width, height, false))
.and_then(|spot_weak| spot_weak.upgrade())
{
return Some(spot);
}
} else if let Some(spot) = self
.reserve_line(height)
.and_then(|new_line| new_line.reserve(width, height, false))
.and_then(|spot_weak| spot_weak.upgrade())
{
return Some(spot);
}
None
}
#[allow(clippy::len_zero)]
fn evict_line(&mut self, height: i32) -> Option<&mut GlyphLine> {
let mut top = 0;
let mut bottom = 0;
let mut best_range = None;
let mut collected_range = 0..0;
for i in 0..self.lines.len() {
if self.lines[i].has_been_hit_this_frame() {
collected_range = 0..0;
} else {
if collected_range.len() == 0 {
top = if i > 0 {
self.lines[i - 1].y + self.lines[i - 1].height + GLYPH_CACHE_GAP
} else {
self.y + GLYPH_CACHE_GAP
};
collected_range = i..i;
}
bottom = if i + 1 < self.lines.len() {
self.lines[i + 1].y - GLYPH_CACHE_GAP
} else {
self.y + self.height - GLYPH_CACHE_GAP
};
collected_range.end += 1;
}
'inner: while bottom - top >= height {
let evicted_glyphs: usize = self.lines[collected_range.clone()]
.iter()
.map(|line| line.reserved.len())
.sum();
if let Some((_, _, other_evicted_glyphs)) = best_range {
if evicted_glyphs < other_evicted_glyphs {
best_range = Some((collected_range.clone(), top, evicted_glyphs))
}
} else {
best_range = Some((collected_range.clone(), top, evicted_glyphs))
}
if collected_range.start < collected_range.end {
collected_range.start += 1;
let new_top_line = &self.lines[collected_range.start - 1];
top = new_top_line.y + new_top_line.height + GLYPH_CACHE_GAP;
} else {
break 'inner;
}
}
}
if let Some((range, top, _)) = best_range {
self.lines.splice(range, None);
self.create_line(top, height)
} else {
None
}
}
fn reserve_line(&mut self, height: i32) -> Option<&mut GlyphLine> {
let mut current_best_line = None;
for i in 0..=self.lines.len() {
let previous_line_bottom = if i > 0 {
self.lines[i - 1].y + self.lines[i - 1].height + GLYPH_CACHE_GAP
} else {
self.y + GLYPH_CACHE_GAP
};
let current_line_top = if i < self.lines.len() {
self.lines[i].y - GLYPH_CACHE_GAP
} else {
self.y + self.height - GLYPH_CACHE_GAP
};
let available_height = current_line_top - previous_line_bottom;
let y = previous_line_bottom;
if height <= available_height {
if let Some((_, compare_height)) = current_best_line {
if available_height < compare_height {
current_best_line = Some((y, available_height));
}
} else {
current_best_line = Some((y, available_height));
}
}
}
if let Some((y, _)) = current_best_line {
self.create_line(y, height)
} else {
None
}
}
fn create_line(&mut self, y: i32, height: i32) -> Option<&mut GlyphLine> {
if let Err(i) = self.lines.binary_search_by(|elem| elem.y.cmp(&y)) {
let max_height_cap = if i < self.lines.len() {
self.lines[i].y - y - GLYPH_CACHE_GAP
} else {
self.height - y - GLYPH_CACHE_GAP
};
let line = GlyphLine::new(max_height_cap, self.x, y, self.width, height);
self.lines.splice(i..i, Some(line));
if i > 0 {
self.lines[i - 1].max_height = self.lines[i - 1].height;
}
Some(&mut self.lines[i])
} else {
log::warn!("tried to reserve a line that was already occupied");
None
}
}
}
struct GlyphLine {
x: i32,
y: i32,
width: i32,
height: i32,
min_height: i32,
max_height: i32,
reserved: Vec<Rc<GlyphSpot>>,
}
impl GlyphLine {
fn new(max_height_cap: i32, x: i32, y: i32, width: i32, height: i32) -> GlyphLine {
GlyphLine {
x,
y,
width,
height,
min_height: height / 2,
max_height: (height * 2).min(max_height_cap),
reserved: Vec::new(),
}
}
#[allow(clippy::len_zero)]
fn evict_width(&mut self, width: i32) -> Option<i32> {
let mut left = 0;
let mut right = 0;
let mut collected_range = 0..0;
for i in 0..self.reserved.len() {
let spot = &self.reserved[i];
if spot.status.get() == ExpiryStatus::UsedDuringThisFrame {
collected_range = 0..0;
} else {
if collected_range.len() == 0 {
left = if i > 0 {
let previous = self.reserved[i - 1].texcoords;
previous.x + previous.width + GLYPH_CACHE_GAP
} else {
self.x
};
collected_range = i..i;
}
right = if i + 1 < self.reserved.len() {
self.reserved[i + 1].texcoords.x - GLYPH_CACHE_GAP
} else {
self.x + self.width
};
collected_range.end += 1;
}
if right - left >= width {
break;
}
}
if right - left >= width {
self.reserved.splice(collected_range, None);
Some(left)
} else {
None
}
}
fn reserve_width(&mut self, width: i32) -> Option<i32> {
if self.reserved.is_empty() {
return Some(self.x);
}
for i in 0..=self.reserved.len() {
let previous_spot_right = if i > 0 {
let previous = self.reserved[i - 1].texcoords;
previous.x + previous.width + GLYPH_CACHE_GAP
} else {
self.x
};
let current_spot_left = if i < self.reserved.len() {
self.reserved[i].texcoords.x - GLYPH_CACHE_GAP
} else {
self.x + self.width
};
if current_spot_left >= previous_spot_right + width {
return Some(previous_spot_right);
}
}
None
}
fn reserve(&mut self, width: i32, height: i32, can_evict: bool) -> Option<Weak<GlyphSpot>> {
if height < self.min_height || height > self.max_height {
return None;
}
let x = if can_evict {
self.evict_width(width)?
} else {
self.reserve_width(width)?
};
let spot = Rc::new(GlyphSpot {
texcoords: RectPx {
x,
y: self.y,
width,
height,
},
status: Cell::new(ExpiryStatus::UsedDuringThisFrame),
});
let spot_weak = Rc::downgrade(&spot);
if let Err(i) = self
.reserved
.binary_search_by(|elem| elem.texcoords.x.cmp(&x))
{
self.reserved.splice(i..i, Some(spot));
} else if cfg!(debug_assertions) {
panic!("reserved a glyph in a spot that was already taken");
}
self.height = self.height.max(height);
Some(spot_weak)
}
fn has_been_hit_this_frame(&self) -> bool {
for spot in &self.reserved {
if spot.status.get() == ExpiryStatus::UsedDuringThisFrame {
return true;
}
}
false
}
}
struct GlyphSpot {
texcoords: RectPx,
status: Cell<ExpiryStatus>,
}
#[derive(Clone, Copy, PartialEq, Debug)]
enum ExpiryStatus {
UsedDuringThisFrame,
UsedDuringLastFrame,
Expired,
}
impl ExpiryStatus {
fn one_step_expired(self) -> Self {
use ExpiryStatus::*;
match self {
UsedDuringThisFrame => UsedDuringLastFrame,
UsedDuringLastFrame => Expired,
_ => self,
}
}
}