From 321373056c98838f8ddba927b38b5a9a51f677dc Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 26 Oct 2016 15:44:27 +0530 Subject: [PATCH] Start work on a class to manage cell textures on the GPU --- kitty/develop_gl.py | 2 + kitty/fonts.py | 35 ++--------------- kitty/shaders.py | 91 +++++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 93 insertions(+), 35 deletions(-) diff --git a/kitty/develop_gl.py b/kitty/develop_gl.py index e5fbcf856..9b3dd84f1 100644 --- a/kitty/develop_gl.py +++ b/kitty/develop_gl.py @@ -34,6 +34,8 @@ class Renderer: print(gl.glGetIntegerv(gl.GL_MAX_VERTEX_UNIFORM_COMPONENTS)) print(gl.glGetIntegerv(gl.GL_MAX_UNIFORM_BLOCK_SIZE)) print(gl.glGetIntegerv(gl.GL_MAX_ARRAY_TEXTURE_LAYERS)) + print(gl.glGetIntegerv(gl.GL_MAX_TEXTURE_IMAGE_UNITS)) + print(gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE)) def on_resize(self, window, w, h): gl.glViewport(0, 0, w, h) diff --git a/kitty/fonts.py b/kitty/fonts.py index 32a839618..0ce74e08e 100644 --- a/kitty/fonts.py +++ b/kitty/fonts.py @@ -8,7 +8,6 @@ import re import ctypes from collections import namedtuple from functools import lru_cache -from threading import Lock from freetype import ( Face, FT_LOAD_RENDER, FT_LOAD_TARGET_NORMAL, FT_LOAD_TARGET_LIGHT, @@ -93,7 +92,7 @@ def set_font_family(family, size_in_pts): underline_position = baseline - font_units_to_pixels(face.underline_position, face.units_per_EM, size_in_pts, dpi[1]) underline_thickness = font_units_to_pixels(face.underline_thickness, face.units_per_EM, size_in_pts, dpi[1]) CharTexture = ctypes.c_ubyte * (cell_width * cell_height) - glyph_cache = GlyphCache() + glyph_cache = GlyphWidthCache() set_current_font_metrics(glyph_cache.width) return cell_width, cell_height @@ -214,34 +213,17 @@ def create_cell_buffer(bitmap_char, src_start_row, dest_start_row, row_count, sr return dest -class GlyphCache: +class GlyphWidthCache: def __init__(self): - self.lock = Lock() self.clear() - self.prepopulate() def render(self, text, bold=False, italic=False): first, second = render_cell(text, bold, italic) self.width_map[text] = 1 if second is None else 2 - self.char_map[text] = self.add_cell(first) - if second is not None: - self.second_char_map[text] = self.add_cell(second) - - def add_cell(self, data): - with self.lock: - i = len(self.texture_array) - self.texture_array.append(data) - self.to_sync.append(i) - return i def clear(self): - with self.lock: - self.char_map = {} - self.second_char_map = {} - self.texture_array = [] - self.to_sync = [] - self.width_map = {} + self.width_map = {} def width(self, text): try: @@ -251,17 +233,6 @@ class GlyphCache: self.render(text) return self.width_map[text] - def prepopulate(self): - # Pre-render the basic ASCII chars in the current font - for i in range(32, 128): - self.render(chr(i)) - - def __iter__(self): - with self.lock: - for f in self.to_sync: - yield f, self.texture_array[f] - self.to_sync = [] - def join_cells(*cells): dstride = len(cells) * cell_width diff --git a/kitty/shaders.py b/kitty/shaders.py index 1400de049..5d6c504a8 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -7,10 +7,91 @@ from functools import lru_cache from OpenGL.arrays import ArrayDatatype import OpenGL.GL as gl +from .fonts import render_cell, cell_size + GL_VERSION = (4, 1) VERSION = GL_VERSION[0] * 100 + GL_VERSION[1] * 10 +class TextureManager: + + def __init__(self): + self.textures = [] + self.current_texture_array = None + self.current_array_dirty = False + self.current_array_data = None + self.arraylengths = {} + self.first_cell_cache = {} + self.second_cell_cache = {} + self.max_array_len = gl.glGetIntegerv(gl.GL_MAX_ARRAY_TEXTURE_LAYERS) + self.max_active_textures = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_IMAGE_UNITS) + + def ensure_texture_array(self, amt=2): + if self.current_texture_array is None or self.arraylengths[self.current_texture_array] > self.max_array_len - amt: + if self.current_texture_array is not None and len(self.textures) >= self.max_active_textures: + raise RuntimeError('No space left to allocate character textures') + if self.current_array_dirty: + self.commit_current_array() + self.current_texture_array = gl.glGenTextures(1) + self.current_array_data = None + self.current_array_dirty = False + self.textures.append(self.current_texture_array) + self.arraylengths[self.current_texture_array] = 0 + + def texture_ids_for(self, items): + for key in items: + first = self.first_cell_cache.get(key) + if first is None: + self.ensure_texture_array() + first, second = render_cell(*key) + items = (first, second) if second is not None else (first,) + texture_unit = len(self.textures) - 1 + layerf = len(self.arraylengths[self.current_texture_array]) + self.append_to_current_array(items) + self.first_cell_cache[key] = first = texture_unit, layerf + if second is not None: + self.second_cell_cache[key] = texture_unit, layerf + 1 + yield first, self.second_cell_cache.get(key) + if self.current_array_dirty: + self.commit_current_array() + + def commit_current_array(self): + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.current_texture_array) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE) + gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE) + width, height = cell_size() + gl.glTexStorage3D(gl.GL_TEXTURE_2D_ARRAY, 0, gl.GL_R8, width, height, self.arraylengths[self.current_texture_array]) + gl.glTexSubImage3D(gl.GL_TEXTURE_2D_ARRAY, 0, 0, 0, 0, width, height, self.arraylengths[ + self.current_texture_array], gl.GL_RED, gl.GL_UNSIGNED_BYTE, self.current_array_data) + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, 0) + self.current_array_dirty = False + + def append_to_current_array(self, items): + current_len = len(self.current_array_data or '') + self.arraylengths[self.current_texture_array] += len(items) + new_data = (gl.GLubyte * (current_len + sum(map(len, items))))() + if current_len: + new_data[:current_len] = self.current_array_data + gl.glDeleteTextures([self.current_texture_array]) + pos = current_len + for i in items: + new_data[pos:pos + len(i)] = i + pos += len(i) + self.current_array_data = new_data + self.current_array_dirty = True + + def __enter__(self): + for i, texture_id in enumerate(self.textures): + gl.glActiveTexture(getattr(gl, 'GL_TEXTURE{}'.format(i))) + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, texture_id) + + def __exit__(self, *a): + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, 0) + + class ShaderProgram: """ Helper class for using GLSL shader programs """ @@ -79,11 +160,12 @@ class ShaderProgram: gl.glBindVertexArray(self.vao_id) self.is_active = True if self.texture_id is not None: - gl.glActiveTexture(gl.GL_TEXTURE0) gl.glBindTexture(gl.GL_TEXTURE_2D, self.texture_id) + gl.glActiveTexture(gl.GL_TEXTURE0) gl.glUniform1i(self.texture_var, 0) # 0 because using GL_TEXTURE0 def __exit__(self, *args): + gl.glBindTexture(gl.GL_TEXTURE_2D, 0) gl.glBindVertexArray(0) gl.glUseProgram(0) self.is_active = False @@ -97,8 +179,8 @@ class ShaderProgram: gl.glDeleteTextures([self.texture_id]) texture_id = self.texture_id = gl.glGenTextures(1) self.texture_var = self.uniform_location(var_name) - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1 if data_type == 'red' else 4) gl.glBindTexture(gl.GL_TEXTURE_2D, texture_id) + gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1 if data_type == 'red' else 4) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MIN_FILTER, min_filter) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_MAG_FILTER, mag_filter) gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_S, swrap) @@ -110,9 +192,10 @@ class ShaderProgram: }[data_type] gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, internal_format, width, height, 0, external_format, gl.GL_UNSIGNED_BYTE, data) + gl.glBindTexture(gl.GL_TEXTURE_2D, 0) return texture_id - def set_attribute_data(self, attribute_name, data, items_per_attribute_value=2, normalize=False): + def set_attribute_data(self, attribute_name, data, items_per_attribute_value=2, divisor=None, normalize=False): if not self.is_active: raise RuntimeError('The program must be active before you can add buffers') if len(data) % items_per_attribute_value != 0: @@ -132,6 +215,8 @@ class ShaderProgram: gl.glVertexAttribPointer( loc, items_per_attribute_value, typ, gl.GL_TRUE if normalize else gl.GL_FALSE, 0, None ) + if divisor is not None: + gl.glVertexBindingDivisor(loc, divisor) def array(*args, dtype=gl.GLfloat):