mirror of
https://github.com/kovidgoyal/kitty
synced 2026-06-08 22:28:24 +02:00
251 lines
9.6 KiB
Python
251 lines
9.6 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=utf-8
|
|
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from ctypes import addressof, sizeof
|
|
from functools import lru_cache
|
|
|
|
from .fonts import render_cell
|
|
from .fast_data_types import (
|
|
glCreateProgram, glAttachShader, GL_FRAGMENT_SHADER, GL_VERTEX_SHADER,
|
|
glLinkProgram, GL_TRUE, GL_LINK_STATUS, glGetProgramiv,
|
|
glGetProgramInfoLog, glDeleteShader, glDeleteProgram, glGenVertexArrays,
|
|
glCreateShader, glShaderSource, glCompileShader, glGetShaderiv,
|
|
GL_COMPILE_STATUS, glGetShaderInfoLog, glGetUniformLocation,
|
|
glGetAttribLocation, glUseProgram, glBindVertexArray, GL_TEXTURE0,
|
|
GL_TEXTURE1, glGetIntegerv, GL_MAX_ARRAY_TEXTURE_LAYERS, glBufferData,
|
|
GL_MAX_TEXTURE_SIZE, glDeleteTexture, GL_TEXTURE_2D_ARRAY, glGenTextures,
|
|
glBindTexture, glTexParameteri, GL_CLAMP_TO_EDGE, glDeleteBuffer,
|
|
GL_TEXTURE_MAG_FILTER, GL_TEXTURE_MIN_FILTER, GL_TEXTURE_WRAP_S,
|
|
GL_NEAREST, GL_TEXTURE_WRAP_T, glGenBuffers, GL_R8, GL_RED,
|
|
GL_UNPACK_ALIGNMENT, GL_UNSIGNED_BYTE, GL_STATIC_DRAW, GL_TEXTURE_BUFFER,
|
|
GL_RGB32UI, glBindBuffer, glPixelStorei, glTexBuffer, glActiveTexture,
|
|
glTexStorage3D, glCopyImageSubData, glTexSubImage3D, ITALIC, BOLD
|
|
)
|
|
|
|
GL_VERSION = (3, 3)
|
|
VERSION = GL_VERSION[0] * 100 + GL_VERSION[1] * 10
|
|
ITALIC_MASK = 1 << ITALIC
|
|
BOLD_MASK = 1 << BOLD
|
|
|
|
|
|
class Sprites:
|
|
''' Maintain sprite sheets of all rendered characters on the GPU as a texture
|
|
array with each texture being a sprite sheet. '''
|
|
|
|
# TODO: Rewrite this class using the ARB_shader_image_load_store and ARB_shader_storage_buffer_object
|
|
# extensions one they become available.
|
|
|
|
def __init__(self):
|
|
self.xnum = self.ynum = 1
|
|
self.sampler_num = 0
|
|
self.buffer_sampler_num = 1
|
|
self.first_cell_cache = {}
|
|
self.second_cell_cache = {}
|
|
self.x = self.y = self.z = 0
|
|
self.texture_id = self.buffer_id = self.buffer_texture_id = None
|
|
self.last_num_of_layers = 1
|
|
|
|
def initialize(self):
|
|
self.texture_unit = GL_TEXTURE0
|
|
self.max_array_len = glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS)
|
|
self.max_texture_size = glGetIntegerv(GL_MAX_TEXTURE_SIZE)
|
|
self.do_layout(getattr(self, 'cell_width', 1), getattr(self, 'cell_height', 1))
|
|
|
|
def do_layout(self, cell_width=1, cell_height=1):
|
|
self.cell_width, self.cell_height = cell_width or 1, cell_height or 1
|
|
self.first_cell_cache = {}
|
|
self.second_cell_cache = {}
|
|
self.xnum = max(1, self.max_texture_size // self.cell_width)
|
|
self.max_y = max(1, self.max_texture_size // self.cell_height)
|
|
self.ynum = 1
|
|
if self.texture_id is not None:
|
|
glDeleteTexture(self.texture_id)
|
|
self.texture_id = None
|
|
|
|
@property
|
|
def layout(self):
|
|
return 1 / self.xnum, 1 / self.ynum
|
|
|
|
def realloc_texture(self):
|
|
if self.texture_id is None:
|
|
self.initialize()
|
|
tgt = GL_TEXTURE_2D_ARRAY
|
|
tex = glGenTextures(1)
|
|
glBindTexture(tgt, tex)
|
|
# We use GL_NEAREST otherwise glyphs that touch the edge of the cell
|
|
# often show a border between cells
|
|
glTexParameteri(tgt, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
|
|
glTexParameteri(tgt, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
|
|
glTexParameteri(tgt, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
|
glTexParameteri(tgt, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
|
znum = self.z + 1
|
|
width, height = self.xnum * self.cell_width, self.ynum * self.cell_height
|
|
glTexStorage3D(tgt, 1, GL_R8, width, height, znum)
|
|
if self.texture_id is not None:
|
|
ynum = self.ynum
|
|
if self.z == 0:
|
|
ynum -= 1 # Only copy the previous rows
|
|
glCopyImageSubData(self.texture_id, tgt, 0, 0, 0, 0, tex, tgt, 0, 0, 0, 0,
|
|
width, ynum * self.cell_height, self.last_num_of_layers)
|
|
glDeleteTexture(self.texture_id)
|
|
self.last_num_of_layers = znum
|
|
self.texture_id = tex
|
|
glBindTexture(tgt, 0)
|
|
|
|
def destroy(self):
|
|
if self.texture_id is not None:
|
|
glDeleteTexture(self.texture_id)
|
|
self.texture_id = None
|
|
if self.buffer_texture_id is not None:
|
|
glDeleteTexture(self.buffer_texture_id)
|
|
if self.buffer_id is not None:
|
|
glDeleteBuffer(self.buffer_id)
|
|
|
|
def ensure_state(self):
|
|
if self.texture_id is None:
|
|
self.realloc_texture()
|
|
self.buffer_id = glGenBuffers(1)
|
|
self.buffer_texture_id = glGenTextures(1)
|
|
self.buffer_texture_unit = GL_TEXTURE1
|
|
|
|
def add_sprite(self, buf):
|
|
tgt = GL_TEXTURE_2D_ARRAY
|
|
glBindTexture(tgt, self.texture_id)
|
|
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
|
|
x, y = self.x * self.cell_width, self.y * self.cell_height
|
|
glTexSubImage3D(tgt, 0, x, y, self.z, self.cell_width, self.cell_height, 1, GL_RED, GL_UNSIGNED_BYTE, addressof(buf))
|
|
glBindTexture(tgt, 0)
|
|
|
|
# co-ordinates for this sprite in the sprite sheet
|
|
x, y, z = self.x, self.y, self.z
|
|
|
|
# Now increment the current cell position
|
|
self.x += 1
|
|
if self.x >= self.xnum:
|
|
self.x = 0
|
|
self.y += 1
|
|
self.ynum = min(max(self.ynum, self.y + 1), self.max_y)
|
|
if self.y >= self.max_y:
|
|
self.y = 0
|
|
self.z += 1
|
|
self.realloc_texture() # we allocate a row at a time
|
|
return x, y, z
|
|
|
|
def set_sprite_map(self, data):
|
|
tgt = GL_TEXTURE_BUFFER
|
|
glBindBuffer(tgt, self.buffer_id)
|
|
glBufferData(tgt, sizeof(data), addressof(data), GL_STATIC_DRAW)
|
|
glBindBuffer(tgt, 0)
|
|
|
|
def primary_sprite_position(self, key):
|
|
' Return a 3-tuple (x, y, z) giving the position of this sprite on the sprite sheet '
|
|
try:
|
|
return self.first_cell_cache[key]
|
|
except KeyError:
|
|
pass
|
|
text, attrs = key
|
|
bold, italic = bool(attrs & BOLD_MASK), bool(attrs & ITALIC_MASK)
|
|
first, second = render_cell(text, bold, italic)
|
|
self.first_cell_cache[key] = first = self.add_sprite(first)
|
|
if second is not None:
|
|
self.second_cell_cache[key] = self.add_sprite(second)
|
|
return first
|
|
|
|
def secondary_sprite_position(self, key):
|
|
ans = self.second_cell_cache.get(key)
|
|
if ans is None:
|
|
self.primary_sprite_position(key)
|
|
ans = self.second_cell_cache.get(key)
|
|
if ans is None:
|
|
return 0, 0, 0
|
|
return ans
|
|
|
|
def __enter__(self):
|
|
self.ensure_state()
|
|
glActiveTexture(self.texture_unit)
|
|
glBindTexture(GL_TEXTURE_2D_ARRAY, self.texture_id)
|
|
|
|
glActiveTexture(self.buffer_texture_unit)
|
|
glBindTexture(GL_TEXTURE_BUFFER, self.buffer_texture_id)
|
|
glBindBuffer(GL_TEXTURE_BUFFER, self.buffer_id)
|
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32UI, self.buffer_id)
|
|
|
|
def __exit__(self, *a):
|
|
glBindTexture(GL_TEXTURE_2D_ARRAY, 0)
|
|
glBindTexture(GL_TEXTURE_BUFFER, 0)
|
|
glBindBuffer(GL_TEXTURE_BUFFER, 0)
|
|
glTexBuffer(GL_TEXTURE_BUFFER, GL_RGB32UI, 0)
|
|
|
|
|
|
class ShaderProgram:
|
|
""" Helper class for using GLSL shader programs """
|
|
|
|
def __init__(self, vertex, fragment):
|
|
"""
|
|
Create a shader program.
|
|
|
|
"""
|
|
self.program_id = glCreateProgram()
|
|
self.is_active = False
|
|
vs_id = self.add_shader(vertex, GL_VERTEX_SHADER)
|
|
glAttachShader(self.program_id, vs_id)
|
|
|
|
frag_id = self.add_shader(fragment, GL_FRAGMENT_SHADER)
|
|
glAttachShader(self.program_id, frag_id)
|
|
|
|
glLinkProgram(self.program_id)
|
|
if glGetProgramiv(self.program_id, GL_LINK_STATUS) != GL_TRUE:
|
|
info = glGetProgramInfoLog(self.program_id)
|
|
glDeleteProgram(self.program_id)
|
|
glDeleteShader(vs_id)
|
|
glDeleteShader(frag_id)
|
|
raise ValueError('Error linking shader program: \n%s' % info.decode('utf-8'))
|
|
glDeleteShader(vs_id)
|
|
glDeleteShader(frag_id)
|
|
self.vao_id = glGenVertexArrays(1)
|
|
|
|
def __hash__(self) -> int:
|
|
return self.program_id
|
|
|
|
def __eq__(self, other) -> bool:
|
|
return isinstance(other, ShaderProgram) and other.program_id == self.program_id
|
|
|
|
def __ne__(self, other) -> bool:
|
|
return not self.__eq__(other)
|
|
|
|
def add_shader(self, source: str, shader_type: int) -> int:
|
|
' Compile a shader and return its id, or raise an exception if compilation fails '
|
|
shader_id = glCreateShader(shader_type)
|
|
source = '#version {}\n{}'.format(VERSION, source)
|
|
try:
|
|
glShaderSource(shader_id, source)
|
|
glCompileShader(shader_id)
|
|
if glGetShaderiv(shader_id, GL_COMPILE_STATUS) != GL_TRUE:
|
|
info = glGetShaderInfoLog(shader_id)
|
|
raise ValueError('GLSL {} compilation failed: \n{}'.format(shader_type, info.decode('utf-8')))
|
|
return shader_id
|
|
except Exception:
|
|
glDeleteShader(shader_id)
|
|
raise
|
|
|
|
@lru_cache(maxsize=2**6)
|
|
def uniform_location(self, name: str) -> int:
|
|
' Return the id for the uniform variable `name` or -1 if not found. '
|
|
return glGetUniformLocation(self.program_id, name)
|
|
|
|
@lru_cache(maxsize=2**6)
|
|
def attribute_location(self, name: str) -> int:
|
|
' Return the id for the attribute variable `name` or -1 if not found. '
|
|
return glGetAttribLocation(self.program_id, name)
|
|
|
|
def __enter__(self):
|
|
glUseProgram(self.program_id)
|
|
glBindVertexArray(self.vao_id)
|
|
self.is_active = True
|
|
|
|
def __exit__(self, *args):
|
|
glUseProgram(0)
|
|
glBindVertexArray(0)
|
|
self.is_active = False
|