From 470d88a9504c37c2bee6969173729ce02f593cd1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 10 Nov 2016 23:20:42 +0530 Subject: [PATCH] Speed up the update_cell_data inner loop --- kitty/char_grid.py | 66 +++++++++--------------------- kitty/colors.c | 34 +++++++++++----- kitty/data-types.h | 5 ++- kitty/develop_gl.py | 34 ++++++++++------ kitty/line.c | 23 ++++++----- kitty/screen.py | 2 +- kitty/shaders.py | 95 ++++++++++++++++---------------------------- kitty/sprites.c | 97 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 215 insertions(+), 141 deletions(-) diff --git a/kitty/char_grid.py b/kitty/char_grid.py index f98a350b6..c7f8998d9 100644 --- a/kitty/char_grid.py +++ b/kitty/char_grid.py @@ -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 diff --git a/kitty/colors.c b/kitty/colors.c index 09a1dd3b9..e4b8ea09d 100644 --- a/kitty/colors.c +++ b/kitty/colors.c @@ -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), diff --git a/kitty/data-types.h b/kitty/data-types.h index ea266d001..2aea8c2d5 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -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; diff --git a/kitty/develop_gl.py b/kitty/develop_gl.py index 7e7b951c5..181a495ec 100644 --- a/kitty/develop_gl.py +++ b/kitty/develop_gl.py @@ -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 diff --git a/kitty/line.c b/kitty/line.c index d0dc7daa7..65db254f5 100644 --- a/kitty/line.c +++ b/kitty/line.c @@ -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; diff --git a/kitty/screen.py b/kitty/screen.py index 2a644f3df..38e3de34a 100644 --- a/kitty/screen.py +++ b/kitty/screen.py @@ -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 diff --git a/kitty/shaders.py b/kitty/shaders.py index 8cc1c2f8d..5b5add8d8 100644 --- a/kitty/shaders.py +++ b/kitty/shaders.py @@ -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) diff --git a/kitty/sprites.c b/kitty/sprites.c index c36d20b9c..b72390d20 100644 --- a/kitty/sprites.c +++ b/kitty/sprites.c @@ -6,6 +6,11 @@ */ #include "data-types.h" +#include +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, };