From f692776e291c8e571738814dca85dbbf96f1f7c1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 27 Oct 2016 13:58:36 +0530 Subject: [PATCH] Use a sprite sheet for char cells --- kitty/develop_gl.py | 92 ++++++++++---------- kitty/shaders.py | 199 ++++++++++++++++++++++---------------------- 2 files changed, 150 insertions(+), 141 deletions(-) diff --git a/kitty/develop_gl.py b/kitty/develop_gl.py index 9b3dd84f1..6609d1452 100644 --- a/kitty/develop_gl.py +++ b/kitty/develop_gl.py @@ -9,6 +9,32 @@ import sys from kitty.shaders import ShaderProgram, array, GL_VERSION from kitty.fonts import set_font_family, render_cell, cell_size +textured_shaders = ( + '''\ +in vec2 vertex; +in vec3 texture_position; +out vec3 texture_position_for_fs; + +void main() { + gl_Position = vec4(vertex, 0, 1); + texture_position_for_fs = texture_position; +} +''', + + '''\ +uniform sampler2DArray sprites; +in vec3 texture_position_for_fs; +out vec4 final_color; +const vec3 background = vec3(0, 1, 0); +const vec3 foreground = vec3(0, 0, 1); + +void main() { + float alpha = texture(sprites, texture_position_for_fs).r; + vec3 color = background * (1 - alpha) + foreground * alpha; + final_color = vec4(color, 1); +} +''') + def key_callback(key, action): """ Sample keyboard callback function """ @@ -30,12 +56,18 @@ class Renderer: def __init__(self, w, h): self.w, self.h = w, h self.do_layout() - self.program = rectangle_texture() - 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)) + self.program = ShaderProgram(*textured_shaders) + chars = '0123456789' + sprite_vecs = list(s[0] for s in self.program.sprites.positions_for(((x, False, False) for x in chars))) + rv = rectangle_vertices() + # import pprint + # pprint.pprint(sprite_vecs) + # self.program.sprites.display_layer(0) + # raise SystemExit(0) + texc = rectangle_uv(*(sprite_vecs[4])) + with self.program: + self.program.set_attribute_data('vertex', rv) + self.program.set_attribute_data('texture_position', texc, items_per_attribute_value=3) def on_resize(self, window, w, h): gl.glViewport(0, 0, w, h) @@ -49,7 +81,8 @@ class Renderer: self.lines_per_screen = self.h // cell_height def render(self): - rectangle_texture(self.program) + with self.program: + gl.glDrawArrays(gl.GL_TRIANGLES, 0, 6) def _main(): @@ -90,7 +123,7 @@ def _main(): def rectangle_vertices(left=-0.8, top=0.8, right=0.8, bottom=-0.8): - vertex_data = array( + return array( right, top, right, bottom, left, bottom, @@ -99,41 +132,16 @@ def rectangle_vertices(left=-0.8, top=0.8, right=0.8, bottom=-0.8): left, top ) - texture_coords = array( - 1, 0, # right top - 1, 1, # right bottom - 0, 1, # left bottom - 1, 0, # right top - 0, 1, # left bottom - 0, 0) # left top - return vertex_data, texture_coords - -textured_shaders = ( - '''\ -in vec2 vertex; -in vec2 texture_position; -out vec2 texture_position_for_fs; - -void main() { - gl_Position = vec4(vertex, 0, 1); - texture_position_for_fs = texture_position; -} -''', - - '''\ -uniform sampler2D tex; -in vec2 texture_position_for_fs; -out vec4 final_color; -const vec3 background = vec3(0, 1, 0); -const vec3 foreground = vec3(0, 0, 1); - -void main() { - float alpha = texture(tex, texture_position_for_fs).r; - vec3 color = background * (1 - alpha) + foreground * alpha; - final_color = vec4(color, 1); -} -''') +def rectangle_uv(left=0., top=1., right=1., bottom=0., z=0.): + return array( + right, top, z, + right, bottom, z, + left, bottom, z, + right, top, z, + left, bottom, z, + left, top, z + ) def texture_data(): diff --git a/kitty/shaders.py b/kitty/shaders.py index 5d6c504a8..29a803473 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -7,90 +7,122 @@ from functools import lru_cache from OpenGL.arrays import ArrayDatatype import OpenGL.GL as gl -from .fonts import render_cell, cell_size +from .fonts import render_cell, cell_size, display_bitmap GL_VERSION = (4, 1) VERSION = GL_VERSION[0] * 100 + GL_VERSION[1] * 10 -class TextureManager: +def array(*args, dtype=gl.GLfloat): + return (dtype * len(args))(*args) - def __init__(self): - self.textures = [] - self.current_texture_array = None - self.current_array_dirty = False - self.current_array_data = None - self.arraylengths = {} + +class Sprites: + ''' Maintain sprite sheets of all rendered characters on the GPU as a texture + array with each texture being a sprite sheet. ''' + + def __init__(self, texture_unit=0): + self.texture_unit = getattr(gl, 'GL_TEXTURE%d' % texture_unit) + self.sampler_num = texture_unit 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) + self.max_texture_size = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE) + self.cell_width, self.cell_height = cell_size() + self.xnum = self.max_texture_size // self.cell_width + self.ynum = self.max_texture_size // self.cell_height + # self.xnum = self.ynum = 2 + self.width = self.xnum * self.cell_width + self.height = self.ynum * self.cell_height + self.previous_layers = [] + self.current_layer_dirty = False + self.current_layer_buffer = (gl.GLubyte * (self.width * self.height))() + self.x = self.y = 0 + self.dx, self.dy = self.cell_width / self.width, self.cell_height / self.height + self.texture_id = gl.glGenTextures(1) + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.texture_id) 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 + self.commit_all_layers() - 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 positions_for(self, items): + ''' Yield 5-tuples (left, top, right, bottom, z) pointing to the desired sprite ''' + for key in items: + first = self.first_cell_cache.get(key) + if first is None: + first, second = render_cell(*key) + self.first_cell_cache[key] = first = self.add_sprite(first) + if second is not None: + self.second_cell_cache[key] = self.add_sprite(second) + yield first, self.second_cell_cache.get(key) + if self.current_layer_dirty: + self.commit_layer() + + def commit_all_layers(self): + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.texture_id) + gl.glTexStorage3D(gl.GL_TEXTURE_2D_ARRAY, 1, gl.GL_R8, self.width, self.height, len(self.previous_layers) + 1) + for i, buf in enumerate(self.previous_layers): + self.commit_layer(i, buf, bind=False) + self.commit_layer(bind=False) + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, 0) + + def commit_layer(self, num=None, buf=None, bind=True): + if bind: + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.texture_id) + if num is None: + num, buf = len(self.previous_layers), self.current_layer_buffer + self.current_layer_dirty = False + gl.glTexSubImage3D(gl.GL_TEXTURE_2D_ARRAY, 0, 0, 0, num, self.width, self.height, 1, + gl.GL_RED, gl.GL_UNSIGNED_BYTE, buf) + if bind: + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, 0) + + def add_sprite(self, buf): + self.current_layer_dirty = True + pixels_per_line = self.cell_width * self.xnum + pixels_per_row = pixels_per_line * self.cell_height + offset_to_start_of_row = self.y * pixels_per_row + + for y in range(self.cell_height): + doff = offset_to_start_of_row + self.x * self.cell_width + soff = y * self.cell_width + for x in range(self.cell_width): + self.current_layer_buffer[doff + x] = buf[soff + x] + offset_to_start_of_row += pixels_per_line + + # UV space co-ordinates + left, top, z = self.x / self.xnum, self.y / self.ynum, len(self.previous_layers) + + # Now increment the current cell position + self.x += 1 + if self.x >= self.xnum: + self.x = 0 + self.y += 1 + if self.y >= self.ynum: + self.y = 0 + self.previous_layers.append(self.current_layer_buffer) + gl.glDeleteTextures([self.texture_id]) + self.texture_id = gl.glGenTextures(1) + self.current_layer_buffer = (gl.GLubyte * (self.width * self.height))() + self.commit_all_layers() + return left, top, left + self.dx, top + self.dy, z 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) + gl.glActiveTexture(self.texture_unit) + gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, self.texture_id) def __exit__(self, *a): gl.glBindTexture(gl.GL_TEXTURE_2D_ARRAY, 0) + def display_layer(self, num=None): + if num is None: + buf = self.current_layer_buffer + else: + buf = self.previous_layers[num] + display_bitmap(buf, self.width, self.height) + class ShaderProgram: """ Helper class for using GLSL shader programs """ @@ -101,7 +133,6 @@ class ShaderProgram: """ self.program_id = gl.glCreateProgram() - self.texture_id = None self.is_active = False vs_id = self.add_shader(vertex, gl.GL_VERTEX_SHADER) gl.glAttachShader(self.program_id, vs_id) @@ -120,6 +151,7 @@ class ShaderProgram: gl.glDeleteShader(frag_id) self.vao_id = gl.glGenVertexArrays(1) self.attribute_buffers = {} + self.sprites = Sprites() def __hash__(self) -> int: return self.program_id @@ -158,43 +190,16 @@ class ShaderProgram: def __enter__(self): gl.glUseProgram(self.program_id) gl.glBindVertexArray(self.vao_id) + gl.glUniform1i(self.uniform_location('sprites'), self.sprites.sampler_num) + self.sprites.__enter__() self.is_active = True - if self.texture_id is not None: - 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.sprites.__exit__(*args) self.is_active = False - def set_2d_texture(self, var_name, data, width, height, data_type='red', - min_filter=gl.GL_LINEAR, mag_filter=gl.GL_LINEAR, - swrap=gl.GL_CLAMP_TO_EDGE, twrap=gl.GL_CLAMP_TO_EDGE): - if not self.is_active: - raise RuntimeError('The program must be active before you can add buffers') - if self.texture_id is not None: - gl.glDeleteTextures([self.texture_id]) - texture_id = self.texture_id = gl.glGenTextures(1) - self.texture_var = self.uniform_location(var_name) - 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) - gl.glTexParameteri(gl.GL_TEXTURE_2D, gl.GL_TEXTURE_WRAP_T, twrap) - internal_format, external_format = { - 'rgba': (gl.GL_RGBA8, gl.GL_RGBA), - 'rgb': (gl.GL_RGB8, gl.GL_RGB), - 'red': (gl.GL_R8, gl.GL_RED), - }[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, divisor=None, normalize=False): if not self.is_active: raise RuntimeError('The program must be active before you can add buffers') @@ -217,7 +222,3 @@ class ShaderProgram: ) if divisor is not None: gl.glVertexBindingDivisor(loc, divisor) - - -def array(*args, dtype=gl.GLfloat): - return (dtype * len(args))(*args)