Speed up the update_cell_data inner loop

This commit is contained in:
Kovid Goyal
2016-11-10 23:20:42 +05:30
parent 44e8a6c3c7
commit 470d88a950
8 changed files with 215 additions and 141 deletions

View File

@@ -4,9 +4,9 @@
from collections import namedtuple
from copy import copy
from ctypes import c_uint
from itertools import chain, repeat
from ctypes import c_uint, addressof
from queue import Queue, Empty
from threading import Lock
from .config import build_ansi_color_table, to_color
from .fonts import set_font_family
@@ -144,6 +144,7 @@ empty_cell = (' ', 0)
class CharGrid:
def __init__(self, screen, opts, window_width, window_height):
self.lock = Lock()
self.dpix, self.dpiy = get_logical_dpi()
self.width, self.height = window_width, window_height
self.color_profile = ColorProfile()
@@ -162,6 +163,7 @@ class CharGrid:
self.render_queue.put(RenderData(
viewport=Size(self.width, self.height), clear_color=self.original_bg,
cursor=self.default_cursor))
self.sprites.ensure_state()
def destroy(self):
self.sprites.destroy()
@@ -191,7 +193,6 @@ class CharGrid:
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 = (c_uint * (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):
@@ -228,58 +229,38 @@ class CharGrid:
dfbg = self.default_bg
dffg = self.default_fg
dfbg = dfbg[0] << 16 | dfbg[1] << 8 | dfbg[2]
dffg = dffg[0] << 16 | dffg[1] << 8 | dffg[2]
ptr = addressof(self.sprite_map)
for y in lines:
self.update_line(y, range(sg.xnum), dffg, dfbg)
with self.lock:
for y in lines:
self.update_line(y, [(0, sg.xnum - 1)], dffg, dfbg, ptr)
for y, ranges in cell_ranges.items():
self.update_line(y, chain.from_iterable(range(start, stop + 1) for start, stop in ranges),
dffg, dfbg)
for y, ranges in cell_ranges.items():
self.update_line(y, ranges, dffg, dfbg, ptr)
rd.cell_data = copy(self.sprite_map), self.sprite_text[:]
rd.cell_data = copy(self.sprite_map)
rd.sprite_layout = self.sprites.layout
c = changes.get('cursor')
if c is not None:
rd.cursor = Cursor(c.x, c.y, c.hidden, c.shape, c.color, c.blink)
self.render_queue.put(rd)
def update_line(self, y, cell_range, dffg, dfbg):
def update_line(self, y, cell_ranges, dffg, dfbg, ptr):
line = self.screen.line(y)
for x in cell_range:
self.update_cell(line, x, y, dffg, dfbg)
def update_cell(self, line, x, y, dffg, dfbg):
ch, attrs, colors = line.basic_cell_data(x)
idx = x + y * self.screen_geometry.xnum
offset = idx * 9
bgcol = colors >> COL_SHIFT
if bgcol:
bgcol = self.as_color(bgcol) or dfbg
else:
bgcol = dfbg
fgcol = colors & COL_MASK
if fgcol:
fgcol = self.as_color(fgcol) 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[x], attrs
for x, xmax in cell_ranges:
self.sprites.update_cell_data(line, x, xmax, self.color_profile, dfbg, dffg, ptr)
def render(self):
' This is the only method in this class called in the UI thread (apart from __init__) '
glClear(GL_COLOR_BUFFER_BIT)
cell_data_changed = self.get_all_render_changes()
with self.sprites:
with self.lock:
self.sprites.render_dirty_cells()
if cell_data_changed:
self.update_sprite_map()
self.sprites.set_sprite_map(self.last_render_data.cell_data)
data = self.last_render_data
if data.screen_geometry is None:
@@ -306,15 +287,6 @@ class CharGrid:
data.update(rd)
return cell_data_changed
def update_sprite_map(self):
spmap, sptext = self.last_render_data.cell_data
psp = self.sprites.primary_sprite_position
empty_val = psp(empty_cell) # Ensure the empty cell is 0, 0, 0
for i, key in enumerate(sptext):
f = i * 9
spmap[f:f + 3] = empty_val if key is empty_cell else psp(key)
self.sprites.set_sprite_map(spmap)
def render_cells(self, sg, sprite_layout):
with self.program:
ul = self.program.uniform_location

View File

@@ -76,24 +76,24 @@ update_ansi_color_table(ColorProfile *self, PyObject *val) {
if (!PyList_Check(val)) { PyErr_SetString(PyExc_TypeError, "color table must be a list"); return NULL; }
#define to_color \
#define TO_COLOR \
t = PyList_GET_ITEM(val, i); \
self->ansi_color_table[i] = PyLong_AsUnsignedLong(t);
for(i = 30; i < 38; i++) {
to_color;
TO_COLOR;
}
i = 39; to_color;
i = 39; TO_COLOR;
for(i = 90; i < 98; i++) {
to_color;
TO_COLOR;
}
i = 99; to_color;
i = 99; TO_COLOR;
for(i = 40; i < 48; i++) {
to_color;
TO_COLOR;
}
i = 49; to_color;
i = 49; TO_COLOR;
for(i = 100; i < 108; i++) {
to_color;
TO_COLOR;
}
Py_RETURN_NONE;
}
@@ -151,6 +151,22 @@ as_color(ColorProfile *self, PyObject *val) {
return ans;
}
uint32_t to_color(ColorProfile *self, uint32_t entry, uint32_t defval) {
unsigned int t = entry & 0xFF, r;
switch(t) {
case 1:
r = (entry >> 8) & 0xff;
return self->ansi_color_table[r];
case 2:
r = (entry >> 8) & 0xff;
return self->color_table_256[r];
case 3:
return entry >> 8;
default:
return defval;
}
}
// Boilerplate {{{
@@ -163,7 +179,7 @@ static PyMethodDef methods[] = {
};
static PyTypeObject ColorProfile_Type = {
PyTypeObject ColorProfile_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
.tp_name = "fast_data_types.ColorProfile",
.tp_basicsize = sizeof(ColorProfile),

View File

@@ -171,9 +171,10 @@ struct SpritePosition {
typedef struct {
PyObject_HEAD
size_t max_array_len, max_texture_size, xnum, ynum, max_y;
unsigned int x, y, z;
size_t max_array_len, max_texture_size, max_y;
unsigned int x, y, z, xnum, ynum;
SpritePosition cache[1024];
bool dirty;
} SpriteMap;

View File

@@ -13,7 +13,8 @@ from kitty.fast_data_types import (
glViewport, enable_automatic_opengl_error_checking, glClearColor,
glUniform2f, glUniform4f, glUniform2ui, glUniform1i, glewInit, glGetString,
GL_VERSION as GL_VERSION_C, GL_VENDOR, GL_SHADING_LANGUAGE_VERSION, GL_RENDERER,
glClear, GL_COLOR_BUFFER_BIT, GL_TRIANGLE_FAN, glDrawArraysInstanced
glClear, GL_COLOR_BUFFER_BIT, GL_TRIANGLE_FAN, glDrawArraysInstanced,
Cursor, LineBuf, ColorProfile
)
@@ -32,11 +33,12 @@ class Renderer:
def __init__(self, w, h):
self.w, self.h = w, h
self.color_pairs = [
((255, 255, 255), (0, 0, 0)),
((0, 0, 0), (255, 255, 255)),
((255, 255, 0), (0, 0, 255)),
]
self.color_pairs = (
(0xffffff, 0),
(0, 0xffffff),
(0xffff00, 0x0000ff)
)
self.color_profile = ColorProfile()
self.program = ShaderProgram(*cell_shader)
self.sprites = Sprites()
self.sprites.initialize()
@@ -54,15 +56,23 @@ class Renderer:
self.sprites.ensure_state()
self.screen_geometry = sg = calculate_vertices(cell_width, cell_height, self.w, self.h)
data = (ctypes.c_uint * (sg.xnum * sg.ynum * 9))()
for i in range(0, len(data), 9):
idx = i // 9
c = '%d' % (idx % 10)
data[i:i+3] = self.sprites.primary_sprite_position((c, 0))
fg, bg = self.color_pairs[idx % 3]
data[i+3:i+9] = fg + bg
lb = LineBuf(sg.ynum, sg.xnum)
i = -1
for y in range(sg.ynum):
line = lb.line(y)
for x in range(sg.xnum):
i += 1
c = Cursor()
fg, bg = self.color_pairs[i % 3]
c.fg = (fg << 8) | 3
c.bg = (bg << 8) | 3
c.x = x
line.set_text('%d' % (i % 10), 0, 1, c)
self.sprites.update_cell_data(line, 0, sg.xnum - 1, self.color_profile, 0xffffff, 0, ctypes.addressof(data))
self.sprites.set_sprite_map(data)
def render(self):
self.sprites.render_dirty_cells()
with self.program:
ul = self.program.uniform_location
sg = self.screen_geometry

View File

@@ -25,17 +25,8 @@ dealloc(Line* self) {
Py_TYPE(self)->tp_free((PyObject*)self);
}
static PyObject*
text_at(Line* self, Py_ssize_t xval) {
#define text_at_doc "[x] -> Return the text in the specified cell"
char_type ch;
combining_type cc;
PyObject* line_text_at(char_type ch, combining_type cc) {
PyObject *ans;
if (xval >= self->xnum) { PyErr_SetString(PyExc_IndexError, "Column number out of bounds"); return NULL; }
ch = self->chars[xval] & CHAR_MASK;
cc = self->combining_chars[xval];
if (cc == 0) {
ans = PyUnicode_New(1, ch);
if (ans == NULL) return PyErr_NoMemory();
@@ -53,6 +44,18 @@ text_at(Line* self, Py_ssize_t xval) {
return ans;
}
static PyObject*
text_at(Line* self, Py_ssize_t xval) {
#define text_at_doc "[x] -> Return the text in the specified cell"
char_type ch;
combining_type cc;
if (xval >= self->xnum) { PyErr_SetString(PyExc_IndexError, "Column number out of bounds"); return NULL; }
ch = self->chars[xval] & CHAR_MASK;
cc = self->combining_chars[xval];
return line_text_at(ch, cc);
}
static PyObject *
as_unicode(Line* self) {
Py_ssize_t n = 0;

View File

@@ -924,7 +924,7 @@ class Screen:
# This is somewhat non-standard but is nonetheless
# supported in quite a few terminals. See discussion
# here https://gist.github.com/XVilka/8346728.
r, gr, b = attrs.pop() << 8, attrs.pop() << 16, attrs.pop() << 24
r, gr, b = attrs.pop() << 24, attrs.pop() << 16, attrs.pop() << 8
setattr(c, key, r | gr | b | 3)
except IndexError:
pass

View File

@@ -20,7 +20,7 @@ from .fast_data_types import (
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
glTexStorage3D, glCopyImageSubData, glTexSubImage3D, ITALIC, BOLD, SpriteMap
)
GL_VERSION = (3, 3)
@@ -45,27 +45,47 @@ class Sprites:
self.x = self.y = self.z = 0
self.texture_id = self.buffer_id = self.buffer_texture_id = None
self.last_num_of_layers = 1
self.last_ynum = -1
self.update_cell_data = lambda *a: None
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.backend = SpriteMap(glGetIntegerv(GL_MAX_TEXTURE_SIZE), glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS))
self.update_cell_data = self.backend.update_cell_data
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
self.cell_width, self.cell_height = cell_width, cell_height
self.backend.layout(cell_width or 1, cell_height or 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
return 1 / self.backend.xnum, 1 / self.backend.ynum
def render_cell(self, text, bold, italic, is_second):
first, second = render_cell(text, bold, italic)
if is_second:
return second or first
return first
def render_dirty_cells(self):
self.backend.render_dirty_cells(self.render_cell, self.send_to_gpu)
def send_to_gpu(self, x, y, z, buf):
if self.backend.z >= self.last_num_of_layers:
self.realloc_texture()
else:
if self.backend.z == 0 and self.backend.ynum > self.last_ynum:
self.realloc_texture()
tgt = GL_TEXTURE_2D_ARRAY
glBindTexture(tgt, self.texture_id)
glPixelStorei(GL_UNPACK_ALIGNMENT, 1)
x, y = x * self.cell_width, y * self.cell_height
glTexSubImage3D(tgt, 0, x, y, self.backend.z, self.cell_width, self.cell_height, 1, GL_RED, GL_UNSIGNED_BYTE, addressof(buf))
glBindTexture(tgt, 0)
def realloc_texture(self):
if self.texture_id is None:
@@ -79,17 +99,18 @@ class Sprites:
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
znum = self.backend.z + 1
width, height = self.backend.xnum * self.cell_width, self.backend.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 = self.backend.ynum
if self.backend.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.last_ynum = self.backend.ynum
self.texture_id = tex
glBindTexture(tgt, 0)
@@ -109,58 +130,12 @@ class Sprites:
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)

View File

@@ -6,6 +6,11 @@
*/
#include "data-types.h"
#include <structmember.h>
extern PyTypeObject Line_Type;
extern PyTypeObject ColorProfile_Type;
extern uint32_t to_color(ColorProfile *, uint32_t, uint32_t);
extern PyObject* line_text_at(char_type, combining_type);
static PyObject*
new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
@@ -17,6 +22,7 @@ new(PyTypeObject *type, PyObject *args, PyObject UNUSED *kwds) {
if (self != NULL) {
self->max_array_len = mlen;
self->max_texture_size = msz;
self->dirty = true;
}
return (PyObject*) self;
}
@@ -43,6 +49,7 @@ layout(SpriteMap *self, PyObject *args) {
if (!PyArg_ParseTuple(args, "kk", &cell_width, &cell_height)) return NULL;
self->xnum = MAX(1, self->max_texture_size / cell_width);
self->max_y = MAX(1, self->max_texture_size / cell_height);
self->ynum = 1;
for (size_t i = 0; i < sizeof(self->cache)/sizeof(self->cache[0]); i++) {
SpritePosition *s = &(self->cache[i]);
@@ -95,8 +102,10 @@ sprite_position_for(SpriteMap *self, char_type ch, combining_type cc, bool is_se
s->cc = cc;
s->is_second = is_second;
s->filled = true;
s->rendered = false;
s->x = self->x; s->y = self->y; s->z = self->z;
increment(self, error);
self->dirty = true;
return s;
}
@@ -122,12 +131,99 @@ position_for(SpriteMap *self, PyObject *args) {
if (pos == NULL) {set_sprite_error(error); return NULL; }
return Py_BuildValue("III", pos->x, pos->y, pos->z);
}
static PyObject*
update_cell_data(SpriteMap *self, PyObject *args) {
#define update_cell_data_doc "update_cell_data(line, xstart, xmax, color_profile, default_bg, default_fg, data_pointer) -> Update the range [xstart, xmax] in data_pointer with the data from line"
Line *line;
unsigned int xstart, xlimit;
SpritePosition *sp;
PyObject *dp;
char_type previous_ch=0, ch;
color_type color;
uint32_t bg, fg;
uint8_t previous_width = 0;
ColorProfile *color_profile;
unsigned long default_bg, default_fg;
int err = 0;
if (!PyArg_ParseTuple(args, "O!IIO!kkO!", &Line_Type, &line, &xstart, &xlimit, &ColorProfile_Type, &color_profile, &default_bg, &default_fg, &PyLong_Type, &dp)) return NULL;
unsigned int *data = PyLong_AsVoidPtr(dp);
size_t base = line->ynum * line->xnum * 9;
default_fg &= COL_MASK;
default_bg &= COL_MASK;
for (size_t i = xstart, offset = base + xstart * 9; i <= xlimit; i++, offset += 9) {
ch = line->chars[i];
if (previous_width == 2) sp = sprite_position_for(self, previous_ch, 0, true, &err);
else sp = sprite_position_for(self, ch, line->combining_chars[i], false, &err);
if (sp == NULL) { set_sprite_error(err); return NULL; }
data[offset] = sp->x;
data[offset+1] = sp->y;
data[offset+2] = sp->z;
color = line->colors[i];
fg = to_color(color_profile, color & COL_MASK, default_fg);
bg = to_color(color_profile, color >> COL_SHIFT, default_bg);
previous_ch = ch; previous_width = (ch >> ATTRS_SHIFT) & WIDTH_MASK;
#define PACK_COL(b, col) data[b] = col >> 16; data[b + 1] = (col >> 8) & 0xff; data[b + 2] = col & 0xff;
PACK_COL(offset + 3, fg);
PACK_COL(offset + 6, bg);
}
Py_RETURN_NONE;
}
static PyObject*
render_dirty_cells(SpriteMap *self, PyObject *args) {
#define render_dirty_cells_doc "Render all cells that are marked as dirty"
PyObject *render_cell, *send_to_gpu;
if (!PyArg_ParseTuple(args, "OO", &render_cell, &send_to_gpu)) return NULL;
if (!self->dirty) { Py_RETURN_NONE; }
for (size_t i = 0; i < sizeof(self->cache)/sizeof(self->cache[0]); i++) {
SpritePosition *sp = &(self->cache[i]);
while (sp) {
if (sp->filled && !sp->rendered) {
PyObject *text = line_text_at(sp->ch & CHAR_MASK, sp->cc);
if (text == NULL) return NULL;
char_type attrs = sp->ch >> ATTRS_SHIFT;
bool bold = (attrs >> BOLD_SHIFT) & 1, italic = (attrs >> ITALIC_SHIFT) & 1;
PyObject *rcell = PyObject_CallFunctionObjArgs(render_cell, text, bold ? Py_True : Py_False, italic ? Py_True : Py_False, sp->is_second ? Py_True : Py_False, NULL);
Py_CLEAR(text);
if (rcell == NULL) return NULL;
PyObject *ret = PyObject_CallFunction(send_to_gpu, "IIIN", sp->x, sp->y, sp->z, rcell);
Py_CLEAR(rcell);
if (ret == NULL) return NULL;
Py_CLEAR(ret);
sp->rendered = true;
}
sp = sp->next;
}
}
self->dirty = false;
Py_RETURN_NONE;
}
// Boilerplate {{{
static PyMemberDef members[] = {
{"xnum", T_UINT, offsetof(SpriteMap, xnum), 0, "xnum"},
{"ynum", T_UINT, offsetof(SpriteMap, ynum), 0, "ynum"},
{"x", T_UINT, offsetof(SpriteMap, x), 0, "x"},
{"y", T_UINT, offsetof(SpriteMap, y), 0, "y"},
{"z", T_UINT, offsetof(SpriteMap, z), 0, "z"},
{NULL} /* Sentinel */
};
static PyMethodDef methods[] = {
METHOD(layout, METH_VARARGS)
METHOD(position_for, METH_VARARGS)
METHOD(update_cell_data, METH_VARARGS)
METHOD(render_dirty_cells, METH_VARARGS)
{NULL} /* Sentinel */
};
@@ -140,6 +236,7 @@ static PyTypeObject SpriteMap_Type = {
.tp_flags = Py_TPFLAGS_DEFAULT,
.tp_doc = "SpriteMap",
.tp_methods = methods,
.tp_members = members,
.tp_new = new,
};