From ade8faa3456eff2262b9486cbd8e47d0b9957629 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 30 Oct 2016 13:06:13 +0530 Subject: [PATCH] Start work on implementing the char grid --- kitty/boss.py | 40 ++++--- kitty/char_grid.py | 246 +++++++++++++++++++++++++++++++++++++------- kitty/develop_gl.py | 76 ++------------ kitty/main.py | 15 ++- kitty/shaders.py | 5 +- 5 files changed, 257 insertions(+), 125 deletions(-) diff --git a/kitty/boss.py b/kitty/boss.py index d0a2e1851..76bfb8756 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -23,6 +23,7 @@ class Boss(Thread): shutting_down = False pending_title_change = pending_icon_change = None pending_color_changes = {} + pending_update_screen = None def __init__(self, window, window_width, window_height, opts, args): Thread.__init__(self, name='ChildMonitor') @@ -36,16 +37,22 @@ class Boss(Thread): self.tracker = ChangeTracker(self.mark_dirtied) self.screen = Screen(self.opts, self.tracker, self) self.char_grid = CharGrid(self.screen, opts, window_width, window_height) + self.queue_action(self.initialize) sclass = DebugStream if args.dump_commands else Stream self.stream = sclass(self.screen) self.write_buf = memoryview(b'') resize_pty(80, 24) - def on_window_resize(self, window, w, h): - self.queue_action(self.resize_screen, w, h) + def initialize(self): + self.char_grid.initialize() + glfw.glfwPostEmptyEvent() - def resize_screen(self, w, h): + def on_window_resize(self, window, w, h): + self.queue_action(self.apply_resize_screen, w, h) + + def apply_resize_screen(self, w, h): self.char_grid.resize_screen(w, h) + glfw.glfwPostEmptyEvent() def apply_opts(self, opts): self.opts = opts @@ -65,10 +72,16 @@ class Boss(Thread): self.char_grid.render() def run(self): - try: - self.loop.run_forever() - finally: - self.loop.close() + self.loop.run_forever() + + def close(self): + if not self.shutting_down: + self.queue_action(self.shutdown) + + def destroy(self): + # Must be called in the main thread + self.loop.close() + del self.loop def shutdown(self): self.shutting_down = True @@ -114,11 +127,14 @@ class Boss(Thread): self.loop.add_writer(self.child_fd, self.write_ready) def mark_dirtied(self): - self.queue_action(self.update_screen) + # Batch screen updates + if self.pending_update_screen is None: + self.pending_update_screen = self.loop.call_later(0.02, self.apply_update_screen) - def update_screen(self): + def apply_update_screen(self): + self.pending_update_screen = None changes = self.tracker.consolidate_changes() - self.char_grid.update_screen(changes) + self.char_grid.update_cell_data(changes) glfw.glfwPostEmptyEvent() def title_changed(self, new_title): @@ -131,9 +147,9 @@ class Boss(Thread): def change_default_color(self, which, value): self.pending_color_changes[which] = value - self.queue_action(self.change_colors) + self.queue_action(self.apply_change_colors) - def change_colors(self): + def apply_change_colors(self): self.char_grid.change_colors(self.pending_color_changes) self.pending_color_changes = {} glfw.glfwPostEmptyEvent() diff --git a/kitty/char_grid.py b/kitty/char_grid.py index 997e84287..7ec983c75 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -2,29 +2,122 @@ # vim:fileencoding=utf-8 # License: GPL v3 Copyright: 2016, Kovid Goyal -from threading import Lock +from collections import namedtuple +from copy import copy +from itertools import chain, repeat +from queue import Queue, Empty -from .config import build_ansi_color_tables, to_color +from .config import build_ansi_color_tables, to_color, fg_color_table, bg_color_table +from .data_types import COL_MASK, COL_SHIFT, ITALIC_MASK, BOLD_MASK, REVERSE_MASK, as_color from .fonts import set_font_family +from .shaders import Sprites, ShaderProgram import OpenGL.GL as gl +Size = namedtuple('Size', 'width height') +ScreenGeometry = namedtuple('ScreenGeometry', 'xstart ystart xnum ynum dx dy') + +# cell shader {{{ + +cell_shader = ( + '''\ +uniform uvec2 dimensions; // xnum, ynum +uniform vec4 steps; // xstart, ystart, dx, dy +uniform vec2 sprite_layout; // dx, dy +uniform usamplerBuffer sprite_map; // gl_InstanceID -> x, y, z +out vec3 sprite_pos; +out vec4 foreground; +out vec4 background; + +const uvec2 pos_map[] = uvec2[4]( + uvec2(1, 0), // right, top + uvec2(1, 1), // right, bottom + uvec2(0, 1), // left, bottom + uvec2(0, 0) // left, top +); + +void main() { + uint instance_id = uint(gl_InstanceID); + uint r = instance_id / dimensions[0]; + uint c = instance_id - r * dimensions[0]; + float left = steps[0] + c * steps[2]; + float top = steps[1] - r * steps[3]; + vec2 xpos = vec2(left, left + steps[2]); + vec2 ypos = vec2(top, top - steps[3]); + uvec2 pos = pos_map[gl_VertexID]; + gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1); + + int sprite_id = int(instance_id) * 3; + uvec4 spos = texelFetch(sprite_map, sprite_id); + vec2 s_xpos = vec2(spos[0], spos[0] + 1.0) * sprite_layout[0]; + vec2 s_ypos = vec2(spos[1], spos[1] + 1.0) * sprite_layout[1]; + sprite_pos = vec3(s_xpos[pos[0]], s_ypos[pos[1]], spos[2]); + foreground = texelFetch(sprite_map, sprite_id + 1) / 255.0; + background = texelFetch(sprite_map, sprite_id + 2) / 255.0; +} +''', + + '''\ +uniform sampler2DArray sprites; +in vec3 sprite_pos; +in vec4 foreground; +in vec4 background; +out vec4 final_color; + +void main() { + float alpha = texture(sprites, sprite_pos).r; + final_color = background * (1 - alpha) + foreground * alpha; +} +''') +# }}} + + +def calculate_vertices(cell_width, cell_height, screen_width, screen_height): + xnum = screen_width // cell_width + ynum = screen_height // cell_height + dx, dy = 2 * cell_width / screen_width, 2 * cell_height / screen_height + xmargin = (screen_width - (xnum * cell_width)) / screen_width + ymargin = (screen_height - (ynum * cell_height)) / screen_height + xstart = -1 + xmargin + ystart = 1 - ymargin + return ScreenGeometry(xstart, ystart, xnum, ynum, dx, dy) + + +class RenderData: + + __slots__ = 'viewport clear_color cell_data screen_geometry sprite_layout'.split() + + def __init__(self, viewport=None, clear_color=None, cell_data=None, screen_geometry=None, sprite_layout=None): + self.viewport, self.clear_color, self.cell_data = viewport, clear_color, cell_data + self.screen_geometry = screen_geometry + self.sprite_layout = sprite_layout + + def update(self, other): + for k in self.__slots__: + val = getattr(other, k) + if val is not None: + setattr(self, k, val) + +empty_cell = (' ', 0) + class CharGrid: def __init__(self, screen, opts, window_width, window_height): self.width, self.height = window_width, window_height self.screen = screen - self.apply_opts(opts) - self.dirty_everything() - self.default_bg, self.default_fg = self.original_bg, self.original_fg - self.resize_lock = Lock() - self.apply_resize_to_screen(self.width, self.height) + self.opts = opts + self.original_bg = opts.background + self.original_fg = opts.foreground + self.render_queue = Queue() + self.program = ShaderProgram(*cell_shader) + self.sprites = Sprites() + self.last_render_data = RenderData() + self.render_queue.put(RenderData(viewport=Size(self.width, self.height), clear_color=self.original_bg)) - def dirty_everything(self): - self.cell_resize_pending = True - self.clear_color_changed = True - self.resize_pending = self.width, self.height + def initialize(self): + self.apply_opts(self.opts) + self.default_bg, self.default_fg = self.original_bg, self.original_fg def apply_opts(self, opts): self.opts = opts @@ -33,36 +126,19 @@ class CharGrid: self.original_bg = opts.background self.original_fg = opts.foreground self.cell_width, self.cell_height = set_font_family(opts.font_family, opts.font_size) - - def apply_resize_to_screen(self, w, h): - cells_per_line = w // self.cell_width - lines_per_screen = h // self.cell_height - self.screen.resize(lines_per_screen, cells_per_line) + self.do_layout(self.width, self.height) def resize_screen(self, w, h): ' Screen was resized by the user (called in non-UI thread) ' - with self.resize_lock: - self.apply_resize_to_screen(w, h) - self.resize_pending = w, h + self.do_layout(w, h) def do_layout(self, w, h): self.width, self.height = w, h - self.cells_per_line = w // self.cell_width - self.lines_per_screen = h // self.cell_height - if self.cell_resize_pending: - self.cell_resize_pending = False - - def render(self): - with self.resize_lock: - if self.resize_pending: - self.do_layout(*self.resize_pending) - gl.glViewport(0, 0, self.width, self.height) - self.resize_pending = None - if self.clear_color_changed: - bg = self.default_bg - self.clear_color_changed = False - gl.glClearColor(bg[0]/255, bg[1]/255, bg[2]/255, 1) - gl.glClear(gl.GL_COLOR_BUFFER_BIT) + self.screen_geometry = sg = calculate_vertices(self.cell_width, self.cell_height, self.width, self.height) + self.screen.resize(sg.ynum, sg.xnum) + self.sprite_map = (gl.GLuint * (sg.ynum * sg.xnum * 9))() + self.sprite_text = list(repeat(empty_cell, sg.xnum * sg.ynum)) + self.update_cell_data(add_viewport_data=True) def change_colors(self, changes): dirtied = False @@ -77,9 +153,103 @@ class CharGrid: setattr(self, 'default_' + which, val) dirtied = True if dirtied: - self.clear_color_changed = True - self.update_screen() + self.render_queue.put(RenderData(clear_color=self.default_bg)) - def update_screen(self, changes=None): + def update_cell_data(self, changes=None, add_viewport_data=False): + rd = RenderData(sprite_layout=self.sprites.layout) + if add_viewport_data: + rd.viewport = Size(self.width, self.height) + rd.screen_geometry = self.screen_geometry if changes is None: changes = {'screen': True} + sg = self.screen_geometry + cell_data_changed = changes['screen'] or changes['cells'] or changes['lines'] + if cell_data_changed: + if changes['screen']: + lines = range(sg.ynum) + cell_ranges = {} + else: + lines = changes['lines'] + cell_ranges = changes['cells'] + + fgct = fg_color_table() + bgct = bg_color_table() + dfbg = self.default_bg + dffg = self.default_fg + + for y in lines: + self.update_line(y, range(sg.xnum), fgct, bgct, dffg, dfbg) + + for y, ranges in cell_ranges.items(): + self.update_line(y, chain.from_iterable(range(start, stop + 1) for start, stop in ranges), + fgct, bgct, dffg, dfbg) + + rd.cell_data = copy(self.sprite_map), self.sprite_text[:] + rd.sprite_layout = self.sprites.layout + self.render_queue.put(rd) + + def update_line(self, y, cell_range, fgct, bgct, dffg, dfbg): + line = self.screen.line(y) + for x in cell_range: + self.update_cell(line, x, y, fgct, bgct, dffg, dfbg) + + def update_cell(self, line, x, y, fgct, bgct, dffg, dfbg): + idx = x + y * self.screen_geometry.xnum + offset = idx * 9 + ch, attrs, colors = line.basic_cell_data(x) + bgcol = colors >> COL_SHIFT + if bgcol: + bgcol = as_color(bgcol, bgct) or dfbg + else: + bgcol = dfbg + fgcol = colors & COL_MASK + if fgcol: + fgcol = as_color(fgcol, fgct) or dffg + else: + fgcol = dffg + if attrs & REVERSE_MASK: + self.sprite_map[offset + 3:offset + 6] = bgcol + self.sprite_map[offset + 6:offset + 9] = fgcol + else: + self.sprite_map[offset + 3:offset + 6] = fgcol + self.sprite_map[offset + 6:offset + 9] = bgcol + if ch == 0 or ch == 32: + self.sprite_text[idx] = empty_cell + else: + self.sprite_text[idx] = line.text_at(x), attrs + + def render(self): + ' This is the only method in this class called in the UI thread (apart from __init__) ' + cell_data_changed = False + while True: + try: + rd = self.render_queue.get_nowait() + except Empty: + break + cell_data_changed |= rd.cell_data is not None + if rd.clear_color is not None: + bg = rd.clear_color + gl.glClearColor(bg[0] / 255, bg[1] / 255, bg[2] / 255, 1) + if rd.viewport is not None: + gl.glViewport(0, 0, self.width, self.height) + self.last_render_data.update(rd) + if cell_data_changed: + spmap, sptext = rd.cell_data + for i, (text, attrs) in enumerate(sptext): + f = i * 9 + spmap[f:f + 3] = self.sprites.primary_sprite_position(text, attrs & BOLD_MASK, attrs & ITALIC_MASK) + self.sprites.set_sprite_map(spmap) + + gl.glClear(gl.GL_COLOR_BUFFER_BIT) + if self.last_render_data.screen_geometry is None: + return + with self.program: + ul = self.program.uniform_location + sg = self.last_render_data.screen_geometry + gl.glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum) + gl.glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy) + gl.glUniform1i(ul('sprites'), self.sprites.sampler_num) + gl.glUniform1i(ul('sprite_map'), self.sprites.buffer_sampler_num) + gl.glUniform2f(ul('sprite_layout'), *self.last_render_data.sprite_layout) + with self.sprites: + gl.glDrawArraysInstanced(gl.GL_TRIANGLE_FAN, 0, 4, sg.xnum * sg.ynum) diff --git a/kitty/develop_gl.py b/kitty/develop_gl.py index ce8c5cd31..a7dfdb47f 100644 --- a/kitty/develop_gl.py +++ b/kitty/develop_gl.py @@ -8,57 +8,7 @@ import sys from kitty.shaders import ShaderProgram, GL_VERSION, Sprites, check_for_required_extensions from kitty.fonts import set_font_family, cell_size - -textured_shaders = ( - '''\ -uniform uvec2 dimensions; // xnum, ynum -uniform vec4 steps; // xstart, ystart, dx, dy -uniform vec2 sprite_layout; // dx, dy -uniform usamplerBuffer sprite_map; // gl_InstanceID -> x, y, z -out vec3 sprite_pos; -out vec4 foreground; -out vec4 background; - -const uvec2 pos_map[] = uvec2[4]( - uvec2(1, 0), // right, top - uvec2(1, 1), // right, bottom - uvec2(0, 1), // left, bottom - uvec2(0, 0) // left, top -); - -void main() { - uint instance_id = uint(gl_InstanceID); - uint r = instance_id / dimensions[0]; - uint c = instance_id - r * dimensions[0]; - float left = steps[0] + c * steps[2]; - float top = steps[1] - r * steps[3]; - vec2 xpos = vec2(left, left + steps[2]); - vec2 ypos = vec2(top, top - steps[3]); - uvec2 pos = pos_map[gl_VertexID]; - gl_Position = vec4(xpos[pos[0]], ypos[pos[1]], 0, 1); - - int sprite_id = int(instance_id) * 3; - uvec4 spos = texelFetch(sprite_map, sprite_id); - vec2 s_xpos = vec2(spos[0], spos[0] + 1.0) * sprite_layout[0]; - vec2 s_ypos = vec2(spos[1], spos[1] + 1.0) * sprite_layout[1]; - sprite_pos = vec3(s_xpos[pos[0]], s_ypos[pos[1]], spos[2]); - foreground = texelFetch(sprite_map, sprite_id + 1) / 255.0; - background = texelFetch(sprite_map, sprite_id + 2) / 255.0; -} -''', - - '''\ -uniform sampler2DArray sprites; -in vec3 sprite_pos; -in vec4 foreground; -in vec4 background; -out vec4 final_color; - -void main() { - float alpha = texture(sprites, sprite_pos).r; - final_color = background * (1 - alpha) + foreground * alpha; -} -''') +from kitty.char_grid import calculate_vertices, cell_shader def rectangle_uv(left=0, top=0, right=1, bottom=1): @@ -72,17 +22,6 @@ def rectangle_uv(left=0, top=0, right=1, bottom=1): ) -def calculate_vertices(cell_width, cell_height, screen_width, screen_height): - xnum = screen_width // cell_width - ynum = screen_height // cell_height - dx, dy = 2 * cell_width / screen_width, 2 * cell_height / screen_height - xmargin = (screen_width - (xnum * cell_width)) / screen_width - ymargin = (screen_height - (ynum * cell_height)) / screen_height - xstart = -1 + xmargin - ystart = 1 - ymargin - return xnum, ynum, xstart, ystart, dx, dy - - class Renderer: def __init__(self, w, h): @@ -92,7 +31,7 @@ class Renderer: ((0, 0, 0), (255, 255, 255)), ((255, 255, 0), (0, 0, 255)), ] - self.program = ShaderProgram(*textured_shaders) + self.program = ShaderProgram(*cell_shader) self.sprites = Sprites() self.do_layout() @@ -104,8 +43,8 @@ class Renderer: def do_layout(self): # Divide into cells cell_width, cell_height = cell_size() - self.xnum, self.ynum, self.xstart, self.ystart, self.dx, self.dy = calculate_vertices(cell_width, cell_height, self.w, self.h) - data = (gl.GLuint * (self.xnum * self.ynum * 9))() + self.screen_geometry = sg = calculate_vertices(cell_width, cell_height, self.w, self.h) + data = (gl.GLuint * (sg.xnum * sg.ynum * 9))() for i in range(0, len(data), 9): idx = i // 9 c = '%d' % (idx % 10) @@ -117,13 +56,14 @@ class Renderer: def render(self): with self.program: ul = self.program.uniform_location - gl.glUniform2ui(ul('dimensions'), self.xnum, self.ynum) - gl.glUniform4f(ul('steps'), self.xstart, self.ystart, self.dx, self.dy) + sg = self.screen_geometry + gl.glUniform2ui(ul('dimensions'), sg.xnum, sg.ynum) + gl.glUniform4f(ul('steps'), sg.xstart, sg.ystart, sg.dx, sg.dy) gl.glUniform1i(ul('sprites'), self.sprites.sampler_num) gl.glUniform1i(ul('sprite_map'), self.sprites.buffer_sampler_num) gl.glUniform2f(ul('sprite_layout'), *self.sprites.layout) with self.sprites: - gl.glDrawArraysInstanced(gl.GL_TRIANGLE_FAN, 0, 4, self.xnum * self.ynum) + gl.glDrawArraysInstanced(gl.GL_TRIANGLE_FAN, 0, 4, sg.xnum * sg.ynum) # window setup {{{ diff --git a/kitty/main.py b/kitty/main.py index 4e887aba5..e7a99a5b2 100644 --- a/kitty/main.py +++ b/kitty/main.py @@ -57,11 +57,16 @@ def run_app(opts, args): boss = Boss(window, window_width, window_height, opts, args) glfw.glfwSetFramebufferSizeCallback(window, boss.on_window_resize) boss.start() - - while not glfw.glfwWindowShouldClose(window): - boss.render() - glfw.glfwSwapBuffers(window) - glfw.glfwWaitEvents() + try: + while not glfw.glfwWindowShouldClose(window): + boss.render() + glfw.glfwSwapBuffers(window) + glfw.glfwWaitEvents() + finally: + if boss.is_alive(): + boss.close() + boss.join() + boss.destroy() finally: glfw.glfwDestroyWindow(window) diff --git a/kitty/shaders.py b/kitty/shaders.py index 56063cbe4..7e7ec8853 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -40,6 +40,7 @@ class Sprites: # extensions one they become available. def __init__(self, texture_unit=0): + self.xnum = self.ynum = 1 self.sampler_num = texture_unit self.buffer_sampler_num = texture_unit + 1 self.first_cell_cache = {} @@ -53,8 +54,8 @@ class Sprites: self.max_array_len = gl.glGetIntegerv(gl.GL_MAX_ARRAY_TEXTURE_LAYERS) 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.max_y = self.max_texture_size // self.cell_height + 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 @property