Hi, 2009/7/10 Martin Renold <[email protected]>: > When this is fixed it can go into main repository. I did not give it much > thought yet, but I'm sure there is a practical solution for this.
Done. The patch follows. It contains a couple of other minor changes too. > There is now a reasonable performance testing infrastructure in git. > The runner script is in tests/test_performance.py. Very nice. Here are my results before and after this patch: memory_zoomed_out_5x 175274.000 ==> 194414.000 paint_rotated 2.013 ==> 2.077 scroll_nozoom 1.735 ==> 1.818 startup 1.376 ==> 1.422 paint 1.148 ==> 1.213 saveload 13.528 ==> 13.625 layerpaint_nozoom 2.216 ==> 2.301 paint_zoomed_out_5x 14.142 ==> 13.416 layerpaint_zoomed_out_5x 17.545 ==> 14.035 scroll_zoomed_out_5x 23.202 ==> 0.704 Regards, Álinson.
From 012ed270824218149e415873c508fa9fe5d275c5 Mon Sep 17 00:00:00 2001 From: Alinson Santos <[email protected]> Date: Mon, 6 Jul 2009 11:28:26 -0300 Subject: [PATCH] Mipmapping, faster zoom out --- gui/drawwindow.py | 2 +- gui/tileddrawwidget.py | 9 ++++- lib/backgroundsurface.py | 17 +++++++++-- lib/document.py | 6 ++-- lib/pixops.hpp | 70 ++++++++++++++++++++++++++++++++++++++++++++++ lib/tiledsurface.hpp | 1 + lib/tiledsurface.py | 49 +++++++++++++++++++++++++++---- 7 files changed, 138 insertions(+), 16 deletions(-) diff --git a/gui/drawwindow.py b/gui/drawwindow.py index 6ced06a..3a221dc 100644 --- a/gui/drawwindow.py +++ b/gui/drawwindow.py @@ -57,7 +57,7 @@ class Window(gtk.Window): pixbuf = gdk.pixbuf_new_from_file(filename) self.tdw.neutral_background_pixbuf = backgroundsurface.Background(helpers.gdkpixbuf2numpy(pixbuf)) - self.zoomlevel_values = [2.0/11, 0.25, 1.0/3, 0.50, 2.0/3, 1.0, 1.5, 2.0, 3.0, 4.0, 5.5, 8.0] + self.zoomlevel_values = [1.0/8, 2.0/11, 0.25, 1.0/3, 0.50, 2.0/3, 1.0, 1.5, 2.0, 3.0, 4.0, 5.5, 8.0] self.zoomlevel = self.zoomlevel_values.index(1.0) self.tdw.zoom_min = min(self.zoomlevel_values) self.tdw.zoom_max = max(self.zoomlevel_values) diff --git a/gui/tileddrawwidget.py b/gui/tileddrawwidget.py index ce63eea..925a3c4 100644 --- a/gui/tileddrawwidget.py +++ b/gui/tileddrawwidget.py @@ -8,7 +8,7 @@ import gtk, cairo, random gdk = gtk.gdk -from math import floor, ceil, pi +from math import floor, ceil, pi, log from lib import helpers, tiledsurface, pixbufsurface import cursor @@ -234,6 +234,11 @@ class TiledDrawWidget(gtk.DrawingArea): # bye bye device coordinates self.get_model_coordinates_cairo_context(cr) + # choose best mipmap + mipmap_level = max(0, int(ceil(log(1/self.scale,2)))) + mipmap_level = min(mipmap_level, tiledsurface.MAX_MIPMAP_LEVEL) + cr.scale(2**mipmap_level, 2**mipmap_level) + translation_only = self.is_translation_only() # calculate the final model bbox with all the clipping above @@ -298,7 +303,7 @@ class TiledDrawWidget(gtk.DrawingArea): dst = surface.get_tile_memory(tx, ty) - self.doc.blit_tile_into(dst, tx, ty, layers, background) + self.doc.blit_tile_into(dst, tx, ty, mipmap_level, layers, background) if translation_only: # not sure why, but using gdk directly is notably faster than the same via cairo diff --git a/lib/backgroundsurface.py b/lib/backgroundsurface.py index 7edb240..6563ecb 100644 --- a/lib/backgroundsurface.py +++ b/lib/backgroundsurface.py @@ -9,10 +9,10 @@ import numpy import mypaintlib, helpers -from tiledsurface import N +from tiledsurface import N, MAX_MIPMAP_LEVEL class Background: - def __init__(self, obj): + def __init__(self, obj, mipmap_level=0): try: obj = helpers.gdkpixbuf2numpy(obj) except: @@ -39,7 +39,18 @@ class Background: tile[:,:,:] = obj[N*ty:N*(ty+1), N*tx:N*(tx+1), :] self.tiles[tx, ty] = tile - def blit_tile_into(self, dst, tx, ty): + # generate mipmap + self.mipmap_level = mipmap_level + if mipmap_level < MAX_MIPMAP_LEVEL: + mipmap_obj = numpy.zeros((self.th*N, self.tw*N, 3), dtype='uint8') + for ty in range(self.th): + for tx in range(self.tw): + mypaintlib.tile_downscale_rgb8(self.tiles[tx, ty], mipmap_obj, tx*N/2, ty*N/2, True) + self.mipmap = Background(mipmap_obj, mipmap_level+1) + + def blit_tile_into(self, dst, tx, ty, mipmap_level=0): + if self.mipmap_level < mipmap_level: + return self.mipmap.blit_tile_into(dst, tx, ty, mipmap_level) rgb = self.tiles[tx%self.tw, ty%self.th] # render solid or tiled background #dst[:] = rgb # 13 times slower than below, with some bursts having the same speed as below (huh?) diff --git a/lib/document.py b/lib/document.py index d2106cd..dab16b2 100644 --- a/lib/document.py +++ b/lib/document.py @@ -168,17 +168,17 @@ class Document(): res.expandToIncludeRect(bbox) return res - def blit_tile_into(self, dst, tx, ty, layers=None, background=None): + def blit_tile_into(self, dst, tx, ty, mipmap=1, layers=None, background=None): if layers is None: layers = self.layers if background is None: background = self.background - background.blit_tile_into(dst, tx, ty) + background.blit_tile_into(dst, tx, ty, mipmap) for layer in layers: surface = layer.surface - surface.composite_tile_over(dst, tx, ty, layer.opacity) + surface.composite_tile_over(dst, tx, ty, mipmap, layer.opacity) def add_layer(self, insert_idx): self.do(command.AddLayer(self, insert_idx)) diff --git a/lib/pixops.hpp b/lib/pixops.hpp index 83c9863..a0085ab 100644 --- a/lib/pixops.hpp +++ b/lib/pixops.hpp @@ -7,6 +7,76 @@ * (at your option) any later version. */ +// downscale a tile to half its size using bilinear interpolation +// used mainly for generating background mipmaps +void tile_downscale_rgb8(PyObject *src, PyObject *dst, int dst_x, int dst_y, bool repeat) { + /* disabled as optimization + assert(PyArray_DIM(src, 0) == TILE_SIZE); + assert(PyArray_DIM(src, 1) == TILE_SIZE); + assert(PyArray_TYPE(src) == NPY_UINT8); + assert(PyArray_ISCARRAY(src)); + + assert(PyArray_TYPE(dst) == NPY_UINT8); + assert(PyArray_ISCARRAY(dst)); + */ + + PyArrayObject* src_arr = ((PyArrayObject*)src); + PyArrayObject* dst_arr = ((PyArrayObject*)dst); + + for (int y=0; y<TILE_SIZE/2; y++) { + uint8_t * src_p = (uint8_t*)(src_arr->data + (2*y)*src_arr->strides[0]); + uint8_t * dst_p = (uint8_t*)(dst_arr->data + (y+dst_y)*dst_arr->strides[0]); + dst_p += 3*dst_x; + for(int x=0; x<TILE_SIZE/2; x++) { + dst_p[0] = src_p[0]/4 + (src_p+3)[0]/4 + (src_p+3*TILE_SIZE)[0]/4 + (src_p+3*TILE_SIZE+3)[0]/4; + dst_p[1] = src_p[1]/4 + (src_p+3)[1]/4 + (src_p+3*TILE_SIZE)[1]/4 + (src_p+3*TILE_SIZE+3)[1]/4; + dst_p[2] = src_p[2]/4 + (src_p+3)[2]/4 + (src_p+3*TILE_SIZE)[2]/4 + (src_p+3*TILE_SIZE+3)[2]/4; + src_p += 6; + dst_p += 3; + } + if(repeat) { + uint8_t *p1 = (uint8_t*)(dst_arr->data + (y+dst_y)*dst_arr->strides[0] + 3*dst_x); + uint8_t *p2 = p1 + 3 * dst_arr->dimensions[1] / 2; + uint8_t *p3 = p1 + dst_arr->strides[0] * dst_arr->dimensions[0] / 2; + uint8_t *p4 = p3 + 3 * dst_arr->dimensions[1] / 2; + memcpy(p2, p1, 3*TILE_SIZE/2); + memcpy(p3, p1, 3*TILE_SIZE/2); + memcpy(p4, p1, 3*TILE_SIZE/2); + } + } +} +// downscale a tile to half its size using bilinear interpolation +// used mainly for generating tiledsurface mipmaps +void tile_downscale_rgba16(PyObject *src, PyObject *dst, int dst_x, int dst_y) { + /* disabled as optimization + assert(PyArray_DIM(src, 0) == TILE_SIZE); + assert(PyArray_DIM(src, 1) == TILE_SIZE); + assert(PyArray_TYPE(src) == NPY_UINT16); + assert(PyArray_ISCARRAY(src)); + + assert(PyArray_DIM(dst, 0) == TILE_SIZE); + assert(PyArray_DIM(dst, 1) == TILE_SIZE); + assert(PyArray_TYPE(dst) == NPY_UINT16); + assert(PyArray_ISCARRAY(dst)); + */ + + PyArrayObject* src_arr = ((PyArrayObject*)src); + PyArrayObject* dst_arr = ((PyArrayObject*)dst); + + for (int y=0; y<TILE_SIZE/2; y++) { + uint16_t * src_p = (uint16_t*)(src_arr->data + (2*y)*src_arr->strides[0]); + uint16_t * dst_p = (uint16_t*)(dst_arr->data + (y+dst_y)*dst_arr->strides[0]); + dst_p += 4*dst_x; + for(int x=0; x<TILE_SIZE/2; x++) { + dst_p[0] = src_p[0]/4 + (src_p+4)[0]/4 + (src_p+4*TILE_SIZE)[0]/4 + (src_p+4*TILE_SIZE+4)[0]/4; + dst_p[1] = src_p[1]/4 + (src_p+4)[1]/4 + (src_p+4*TILE_SIZE)[1]/4 + (src_p+4*TILE_SIZE+4)[1]/4; + dst_p[2] = src_p[2]/4 + (src_p+4)[2]/4 + (src_p+4*TILE_SIZE)[2]/4 + (src_p+4*TILE_SIZE+4)[2]/4; + dst_p[3] = src_p[3]/4 + (src_p+4)[3]/4 + (src_p+4*TILE_SIZE)[3]/4 + (src_p+4*TILE_SIZE+4)[3]/4; + src_p += 8; + dst_p += 4; + } + } +} void tile_composite_rgba16_over_rgb8(PyObject * src, PyObject * dst, float alpha) { /* disabled as optimization diff --git a/lib/tiledsurface.hpp b/lib/tiledsurface.hpp index 1a9aa60..afc5a72 100644 --- a/lib/tiledsurface.hpp +++ b/lib/tiledsurface.hpp @@ -8,6 +8,7 @@ */ #define TILE_SIZE 64 +#define MAX_MIPMAP_LEVEL 3 class TiledSurface : public Surface { // the Python half of this class is in tiledsurface.py diff --git a/lib/tiledsurface.py b/lib/tiledsurface.py index b7b0192..f220a4d 100644 --- a/lib/tiledsurface.py +++ b/lib/tiledsurface.py @@ -13,10 +13,10 @@ import time import mypaintlib, helpers tilesize = N = mypaintlib.TILE_SIZE +MAX_MIPMAP_LEVEL = mypaintlib.MAX_MIPMAP_LEVEL import pixbufsurface - class Tile: def __init__(self, copy_from=None): # note: pixels are stored with premultiplied alpha @@ -27,6 +27,7 @@ class Tile: else: self.rgba = copy_from.rgba.copy() self.readonly = False + self.mipmap_dirty = False def copy(self): return Tile(copy_from=self) @@ -44,11 +45,19 @@ class SurfaceSnapshot: class Surface(mypaintlib.TiledSurface): # the C++ half of this class is in tiledsurface.hpp - def __init__(self): + def __init__(self, mipmap_level=0): mypaintlib.TiledSurface.__init__(self, self) self.tiledict = {} self.observers = [] + self.mipmap_level = mipmap_level + self.mipmap = None + self.parent = None + + if mipmap_level < MAX_MIPMAP_LEVEL: + self.mipmap = Surface(mipmap_level+1) + self.mipmap.parent = self + def notify_observers(self, *args): for f in self.observers: f(*args) @@ -57,6 +66,7 @@ class Surface(mypaintlib.TiledSurface): tiles = self.tiledict.keys() self.tiledict = {} self.notify_observers(*get_tiles_bbox(tiles)) + if self.mipmap: self.mipmap.clear() def get_tile_memory(self, tx, ty, readonly): # copy-on-write for readonly tiles @@ -71,34 +81,57 @@ class Surface(mypaintlib.TiledSurface): else: t = Tile() self.tiledict[(tx, ty)] = t + if t.mipmap_dirty: + # regenerate mipmap + if self.mipmap_level > 0: + for x in xrange(2): + for y in xrange(2): + src = self.parent.get_tile_memory(tx*2 + x, ty*2 + y, True) + mypaintlib.tile_downscale_rgba16(src, t.rgba, x*N/2, y*N/2) + t.mipmap_dirty = False if t.readonly and not readonly: # OPTIMIZE: we could do the copying in save_snapshot() instead, this might reduce the latency while drawing # (eg. tile.valid_copy = some_other_tile_instance; and valid_copy = None here) # before doing this, measure the worst-case time of the call below; same thing with new tiles t = t.copy() self.tiledict[(tx, ty)] = t + if not readonly: + self.mark_mipmap_dirty(tx, ty, t) return t.rgba + def mark_mipmap_dirty(self, tx, ty, t=None): + if t is None: + t = self.tiledict.get((tx,ty)) + if t is None: + t = Tile() + self.tiledict[(tx, ty)] = t + if not t.mipmap_dirty: + t.mipmap_dirty = True + if self.mipmap: + self.mipmap.mark_mipmap_dirty(tx/2, ty/2) + def blit_tile_into(self, dst, tx, ty): # used mainly for saving (transparent PNG) assert dst.shape[2] == 4 tmp = self.get_tile_memory(tx, ty, readonly=True) return mypaintlib.tile_convert_rgba16_to_rgba8(tmp, dst) - def composite_tile_over(self, dst, tx, ty, opac): + def composite_tile_over(self, dst, tx, ty, mipmap_level=0, opac=1.0): """ composite one tile of this surface over the array dst, modifying only dst """ - tile = self.tiledict.get((tx, ty)) - if tile is None: + + if self.mipmap_level < mipmap_level: + return self.mipmap.composite_tile_over(dst, tx, ty, mipmap_level, opac) + if not (tx,ty) in self.tiledict: return + src = self.get_tile_memory(tx, ty, True) if dst.shape[2] == 3 and dst.dtype == 'uint8': - mypaintlib.tile_composite_rgba16_over_rgb8(tile.rgba, dst, opac) + mypaintlib.tile_composite_rgba16_over_rgb8(src, dst, opac) elif dst.shape[2] == 4 and dst.dtype == 'uint16': # rarely used (only for merging layers) # src (premultiplied) OVER dst (premultiplied) # dstColor = srcColor + (1.0 - srcAlpha) * dstColor - src = tile.rgba one_minus_srcAlpha = (1<<15) - (opac * src[:,:,3:4]).astype('uint32') dst[:,:,:] = opac * src[:,:,:] + ((one_minus_srcAlpha * dst[:,:,:]) >> 15).astype('uint16') @@ -114,6 +147,8 @@ class Surface(mypaintlib.TiledSurface): self.tiledict = sshot.tiledict.copy() new = set(self.tiledict.items()) dirty = old.symmetric_difference(new) + for pos, tile in dirty: + self.mark_mipmap_dirty(*pos) bbox = get_tiles_bbox([pos for (pos, tile) in dirty]) if not bbox.empty(): self.notify_observers(*bbox) -- 1.6.3.3
_______________________________________________ Mypaint-discuss mailing list [email protected] https://mail.gna.org/listinfo/mypaint-discuss
