OSDN Git Service

Mipmapping, faster zoom out
authorAlinson Santos <isoron@gmail.com>
Mon, 6 Jul 2009 14:28:26 +0000 (11:28 -0300)
committerMartin Renold <martinxyz@gmx.ch>
Sun, 12 Jul 2009 20:49:02 +0000 (22:49 +0200)
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

gui/drawwindow.py
gui/tileddrawwidget.py
lib/backgroundsurface.py
lib/document.py
lib/pixops.hpp
lib/tiledsurface.hpp
lib/tiledsurface.py

index 6ced06a..3a221dc 100644 (file)
@@ -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)
index ce63eea..925a3c4 100644 (file)
@@ -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
index 7edb240..6563ecb 100644 (file)
@@ -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?)
index d2106cd..dab16b2 100644 (file)
@@ -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))
index 83c9863..a0085ab 100644 (file)
@@ -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
index 1a9aa60..afc5a72 100644 (file)
@@ -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
index b7b0192..f220a4d 100644 (file)
@@ -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)