Start work on implementing the char grid

This commit is contained in:
Kovid Goyal
2016-10-30 13:06:13 +05:30
parent 42675c2531
commit ade8faa345
5 changed files with 257 additions and 125 deletions

View File

@@ -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()

View File

@@ -2,29 +2,122 @@
# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
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)

View File

@@ -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 {{{

View File

@@ -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)

View File

@@ -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