From 32384c160aad455a21716d31f248a3cdcd180dfe Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 21:01:55 -0700 Subject: [PATCH 1/6] cairo: Don't copy the ravelled image. flatten() always makes a copy, whereas ravel() does not. --- lib/matplotlib/backends/backend_cairo.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index c41c7471121..3c03abda4a8 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -231,14 +231,14 @@ def draw_image(self, gc, x, y, im): # on ctypes to get a pointer to the numpy array. This works # correctly on a numpy array in python3 but not 2.7. We replicate # the array.array functionality here to get cross version support. - imbuffer = ArrayWrapper(im.flatten()) + imbuffer = ArrayWrapper(im.ravel()) else: # py2cairo uses PyObject_AsWriteBuffer # to get a pointer to the numpy array this works correctly # on a regular numpy array but not on a memory view. # At the time of writing the latest release version of # py3cairo still does not support create_for_data - imbuffer = im.flatten() + imbuffer = im.ravel() surface = cairo.ImageSurface.create_for_data(imbuffer, cairo.FORMAT_ARGB32, im.shape[1], From f923cc6b7803c99a83f38353b22c5e9511469ec0 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 22:22:07 -0700 Subject: [PATCH 2/6] cairo: save a pair of ctx.save/ctx.restore. The removed pair of ctx.save and ctx.restore was clearly unnecessary (the outer one is still there, and the font is reset at each loop iteration). --- lib/matplotlib/backends/backend_cairo.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 3c03abda4a8..d6d37d624ef 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -301,7 +301,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.move_to(ox, oy) fontProp = ttfFontProperty(font) - ctx.save() ctx.select_font_face (fontProp.name, self.fontangles [fontProp.style], self.fontweights[fontProp.weight]) @@ -311,7 +310,6 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): if not six.PY3 and isinstance(s, six.text_type): s = s.encode("utf-8") ctx.show_text(s) - ctx.restore() for ox, oy, w, h in rects: ctx.new_path() From 91fd305b5557cf0ee72c2140ab7be7f7d11201ff Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 14:32:42 -0700 Subject: [PATCH 3/6] Faster path drawing with cairocffi. Improves the performance of mplot3d/wire3d_animation on the gtk3cairo backend from ~8.3fps to ~10.5fps (as a comparison, gtk3agg is at ~16.2fps). --- lib/matplotlib/backends/backend_cairo.py | 125 +++++++++++++++++++++++-------- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index d6d37d624ef..d19a024e4f5 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -78,6 +78,87 @@ def buffer_info(self): return (self.__data, self.__size) +# Mapping from Matplotlib Path codes to cairo path codes. +_MPL_TO_CAIRO_PATH_TYPE = np.zeros(80, dtype=int) # CLOSEPOLY = 79. +_MPL_TO_CAIRO_PATH_TYPE[Path.MOVETO] = cairo.PATH_MOVE_TO +_MPL_TO_CAIRO_PATH_TYPE[Path.LINETO] = cairo.PATH_LINE_TO +_MPL_TO_CAIRO_PATH_TYPE[Path.CURVE4] = cairo.PATH_CURVE_TO +_MPL_TO_CAIRO_PATH_TYPE[Path.CLOSEPOLY] = cairo.PATH_CLOSE_PATH +# Sizes in cairo_path_data_t of each cairo path element. +_CAIRO_PATH_TYPE_SIZES = np.zeros(4, dtype=int) +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_MOVE_TO] = 2 +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_LINE_TO] = 2 +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CURVE_TO] = 4 +_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1 + + +def _convert_path(ctx, path, transform, clip=None): + if HAS_CAIRO_CFFI: + try: + return _convert_path_fast(ctx, path, transform, clip) + except NotImplementedError: + pass + return _convert_path_slow(ctx, path, transform, clip) + + +def _convert_path_slow(ctx, path, transform, clip=None): + for points, code in path.iter_segments(transform, clip=clip): + if code == Path.MOVETO: + ctx.move_to(*points) + elif code == Path.CLOSEPOLY: + ctx.close_path() + elif code == Path.LINETO: + ctx.line_to(*points) + elif code == Path.CURVE3: + ctx.curve_to(points[0], points[1], + points[0], points[1], + points[2], points[3]) + elif code == Path.CURVE4: + ctx.curve_to(*points) + + +def _convert_path_fast(ctx, path, transform, clip=None): + ffi = cairo.ffi + cleaned = path.cleaned(transform=transform, clip=clip) + vertices = cleaned.vertices + codes = cleaned.codes + + # TODO: Implement Bezier degree elevation formula. Note that the "slow" + # implementation is, in fact, also incorrect... + if np.any(codes == Path.CURVE3): + raise NotImplementedError("Quadratic Bezier curves are not supported") + # Remove unused vertices and convert to cairo codes. Note that unlike + # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after + # CLOSE_PATH, so our resulting buffer may be smaller. + if codes[-1] == Path.STOP: + codes = codes[:-1] + vertices = vertices[:-1] + vertices = vertices[codes != Path.CLOSEPOLY] + codes = _MPL_TO_CAIRO_PATH_TYPE[codes] + # Where are the headers of each cairo portions? + cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes] + cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0) + cairo_num_data = cairo_type_positions[-1] + cairo_type_positions = cairo_type_positions[:-1] + + # Fill the buffer. + buf = np.empty(cairo_num_data * 16, np.uint8) + as_int = np.frombuffer(buf.data, np.int32) + as_float = np.frombuffer(buf.data, np.float64) + mask = np.ones_like(as_float, bool) + as_int[::4][cairo_type_positions] = codes + as_int[1::4][cairo_type_positions] = cairo_type_sizes + mask[::2][cairo_type_positions] = mask[1::2][cairo_type_positions] = False + as_float[mask] = vertices.ravel() + + # Construct the cairo_path_t, and pass it to the context. + ptr = ffi.new("cairo_path_t *") + ptr.status = cairo.STATUS_SUCCESS + ptr.data = ffi.cast("cairo_path_data_t *", ffi.from_buffer(buf)) + ptr.num_data = cairo_num_data + cairo.cairo.cairo_append_path(ctx._pointer, ptr) + + class RendererCairo(RendererBase): fontweights = { 100 : cairo.FONT_WEIGHT_NORMAL, @@ -141,47 +222,25 @@ def _fill_and_stroke (self, ctx, fill_c, alpha, alpha_overrides): ctx.restore() ctx.stroke() - @staticmethod - def convert_path(ctx, path, transform, clip=None): - for points, code in path.iter_segments(transform, clip=clip): - if code == Path.MOVETO: - ctx.move_to(*points) - elif code == Path.CLOSEPOLY: - ctx.close_path() - elif code == Path.LINETO: - ctx.line_to(*points) - elif code == Path.CURVE3: - ctx.curve_to(points[0], points[1], - points[0], points[1], - points[2], points[3]) - elif code == Path.CURVE4: - ctx.curve_to(*points) - - def draw_path(self, gc, path, transform, rgbFace=None): ctx = gc.ctx - - # We'll clip the path to the actual rendering extents - # if the path isn't filled. - if rgbFace is None and gc.get_hatch() is None: - clip = ctx.clip_extents() - else: - clip = None - - transform = transform + \ - Affine2D().scale(1.0, -1.0).translate(0, self.height) - + # Clip the path to the actual rendering extents if it isn't filled. + clip = (ctx.clip_extents() + if rgbFace is None and gc.get_hatch() is None + else None) + transform = (transform + + Affine2D().scale(1.0, -1.0).translate(0, self.height)) ctx.new_path() - self.convert_path(ctx, path, transform, clip) - - self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) + _convert_path(ctx, path, transform, clip) + self._fill_and_stroke( + ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None): ctx = gc.ctx ctx.new_path() # Create the path for the marker; it needs to be flipped here already! - self.convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) + _convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0)) marker_path = ctx.copy_path_flat() # Figure out whether the path has a fill @@ -430,7 +489,7 @@ def set_clip_path(self, path): ctx = self.ctx ctx.new_path() affine = affine + Affine2D().scale(1.0, -1.0).translate(0.0, self.renderer.height) - RendererCairo.convert_path(ctx, tpath, affine) + _convert_path(ctx, tpath, affine) ctx.clip() def set_dashes(self, offset, dashes): From dbe1577ab739dea3514deafd99ea60e432696afd Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 15:48:16 -0700 Subject: [PATCH 4/6] Implement draw_path_collection for cairo. Further increase the performance of mplot3d/wire3d_animation on the gtk3cairo backend from ~10.5fps to ~11.6fps (as a comparison, gtk3agg is at ~16.2fps). --- lib/matplotlib/backends/backend_cairo.py | 109 +++++++++++++++++++++++-------- 1 file changed, 81 insertions(+), 28 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index d19a024e4f5..1f24a1572c0 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -23,6 +23,7 @@ import six +import copy import os, sys, warnings, gzip import numpy as np @@ -93,35 +94,41 @@ def buffer_info(self): def _convert_path(ctx, path, transform, clip=None): + return _convert_paths(ctx, [path], [transform], clip) + + +def _convert_paths(ctx, paths, transforms, clip=None): if HAS_CAIRO_CFFI: try: - return _convert_path_fast(ctx, path, transform, clip) + return _convert_paths_fast(ctx, paths, transforms, clip) except NotImplementedError: pass - return _convert_path_slow(ctx, path, transform, clip) - - -def _convert_path_slow(ctx, path, transform, clip=None): - for points, code in path.iter_segments(transform, clip=clip): - if code == Path.MOVETO: - ctx.move_to(*points) - elif code == Path.CLOSEPOLY: - ctx.close_path() - elif code == Path.LINETO: - ctx.line_to(*points) - elif code == Path.CURVE3: - ctx.curve_to(points[0], points[1], - points[0], points[1], - points[2], points[3]) - elif code == Path.CURVE4: - ctx.curve_to(*points) - - -def _convert_path_fast(ctx, path, transform, clip=None): + return _convert_paths_slow(ctx, paths, transforms, clip) + + +def _convert_paths_slow(ctx, paths, transforms, clip=None): + for path, transform in zip(paths, transforms): + for points, code in path.iter_segments(transform, clip=clip): + if code == Path.MOVETO: + ctx.move_to(*points) + elif code == Path.CLOSEPOLY: + ctx.close_path() + elif code == Path.LINETO: + ctx.line_to(*points) + elif code == Path.CURVE3: + ctx.curve_to(points[0], points[1], + points[0], points[1], + points[2], points[3]) + elif code == Path.CURVE4: + ctx.curve_to(*points) + + +def _convert_paths_fast(ctx, paths, transforms, clip=None): ffi = cairo.ffi - cleaned = path.cleaned(transform=transform, clip=clip) - vertices = cleaned.vertices - codes = cleaned.codes + cleaneds = [path.cleaned(transform=transform, clip=clip) + for path, transform in zip(paths, transforms)] + vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds]) + codes = np.concatenate([cleaned.codes for cleaned in cleaneds]) # TODO: Implement Bezier degree elevation formula. Note that the "slow" # implementation is, in fact, also incorrect... @@ -130,10 +137,8 @@ def _convert_path_fast(ctx, path, transform, clip=None): # Remove unused vertices and convert to cairo codes. Note that unlike # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after # CLOSE_PATH, so our resulting buffer may be smaller. - if codes[-1] == Path.STOP: - codes = codes[:-1] - vertices = vertices[:-1] - vertices = vertices[codes != Path.CLOSEPOLY] + vertices = vertices[(codes != Path.STOP) & (codes != Path.CLOSEPOLY)] + codes = codes[codes != Path.STOP] codes = _MPL_TO_CAIRO_PATH_TYPE[codes] # Where are the headers of each cairo portions? cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes] @@ -278,6 +283,54 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=N if not filled: self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha()) + def draw_path_collection( + self, gc, master_transform, paths, all_transforms, offsets, + offsetTrans, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position): + + path_ids = [] + for path, transform in self._iter_collection_raw_paths( + master_transform, paths, all_transforms): + path_ids.append((path, Affine2D(transform))) + + reuse_key = None + grouped_draw = [] + + def _draw_paths(): + if not grouped_draw: + return + gc_vars, rgb_fc = reuse_key + gc = copy.copy(gc0) + vars(gc).update(gc_vars) + for k, v in gc_vars.items(): + try: + getattr(gc, "set" + k)(v) + except (AttributeError, TypeError): + pass + gc.ctx.new_path() + paths, transforms = zip(*grouped_draw) + grouped_draw.clear() + _convert_paths(gc.ctx, paths, transforms) + self._fill_and_stroke( + gc.ctx, rgb_fc, gc.get_alpha(), gc.get_forced_alpha()) + + for xo, yo, path_id, gc0, rgb_fc in self._iter_collection( + gc, master_transform, all_transforms, path_ids, offsets, + offsetTrans, facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position): + path, transform = path_id + transform = (Affine2D(transform.get_matrix()).translate(xo, yo) + + Affine2D().scale(1, -1).translate(0, self.height)) + # rgb_fc could be a ndarray, for which equality is elementwise. + new_key = vars(gc0), tuple(rgb_fc) if rgb_fc is not None else None + if new_key == reuse_key: + grouped_draw.append((path, transform)) + else: + _draw_paths() + grouped_draw.append((path, transform)) + reuse_key = new_key + _draw_paths() + def draw_image(self, gc, x, y, im): # bbox - not currently used if sys.byteorder == 'little': From 86738e8234c317cf66885afc3612d1497aaab2a3 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Wed, 21 Jun 2017 23:39:55 -0700 Subject: [PATCH 5/6] Document a bit the cairo fast path. --- lib/matplotlib/backends/backend_cairo.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 1f24a1572c0..34285d02273 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -124,22 +124,31 @@ def _convert_paths_slow(ctx, paths, transforms, clip=None): def _convert_paths_fast(ctx, paths, transforms, clip=None): + # We directly convert to the internal representation used by cairo, for + # which ABI compatibility is guaranteed. The layout is for each item is + # --CODE(4)-- -LENGTH(4)- ---------PAD(8)--------- + # ----------X(8)---------- ----------Y(8)---------- + # with the size in bytes in parentheses, and (X, Y) repeated as many times + # as there are points for the current code. ffi = cairo.ffi cleaneds = [path.cleaned(transform=transform, clip=clip) for path, transform in zip(paths, transforms)] vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds]) codes = np.concatenate([cleaned.codes for cleaned in cleaneds]) - # TODO: Implement Bezier degree elevation formula. Note that the "slow" - # implementation is, in fact, also incorrect... + # TODO: Implement Bezier degree elevation formula. For now, fall back to + # the "slow" implementation, though note that that implementation is, in + # fact, also incorrect... if np.any(codes == Path.CURVE3): raise NotImplementedError("Quadratic Bezier curves are not supported") + # Remove unused vertices and convert to cairo codes. Note that unlike # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after # CLOSE_PATH, so our resulting buffer may be smaller. vertices = vertices[(codes != Path.STOP) & (codes != Path.CLOSEPOLY)] codes = codes[codes != Path.STOP] codes = _MPL_TO_CAIRO_PATH_TYPE[codes] + # Where are the headers of each cairo portions? cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes] cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0) @@ -301,6 +310,7 @@ def _draw_paths(): return gc_vars, rgb_fc = reuse_key gc = copy.copy(gc0) + # We actually need to call the setters to reset the internal state. vars(gc).update(gc_vars) for k, v in gc_vars.items(): try: From abce70293d062aff9436bb0b4a1e0e023d195a43 Mon Sep 17 00:00:00 2001 From: Antony Lee Date: Fri, 23 Jun 2017 14:06:45 -0700 Subject: [PATCH 6/6] Actually support quadratic Beziers. For the slow code path, implement the degree elevation formula. For the fast code path, the path cleaner was already handling this for us, converting everything to lines. --- lib/matplotlib/backends/backend_cairo.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 34285d02273..0f2bfa19ea0 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -98,12 +98,8 @@ def _convert_path(ctx, path, transform, clip=None): def _convert_paths(ctx, paths, transforms, clip=None): - if HAS_CAIRO_CFFI: - try: - return _convert_paths_fast(ctx, paths, transforms, clip) - except NotImplementedError: - pass - return _convert_paths_slow(ctx, paths, transforms, clip) + return (_convert_paths_fast if HAS_CAIRO_CFFI else _convert_paths_slow)( + ctx, paths, transforms, clip) def _convert_paths_slow(ctx, paths, transforms, clip=None): @@ -116,9 +112,10 @@ def _convert_paths_slow(ctx, paths, transforms, clip=None): elif code == Path.LINETO: ctx.line_to(*points) elif code == Path.CURVE3: - ctx.curve_to(points[0], points[1], - points[0], points[1], - points[2], points[3]) + cur = ctx.get_current_point() + ctx.curve_to( + *np.concatenate([cur / 3 + points[:2] * 2 / 3, + points[:2] * 2 / 3 + points[-2:] / 3])) elif code == Path.CURVE4: ctx.curve_to(*points) @@ -131,17 +128,15 @@ def _convert_paths_fast(ctx, paths, transforms, clip=None): # with the size in bytes in parentheses, and (X, Y) repeated as many times # as there are points for the current code. ffi = cairo.ffi - cleaneds = [path.cleaned(transform=transform, clip=clip) + + # Convert curves to segment, so that 1. we don't have to handle + # variable-sized CURVE-n codes, and 2. we don't have to implement degree + # elevation for quadratic Beziers. + cleaneds = [path.cleaned(transform=transform, clip=clip, curves=False) for path, transform in zip(paths, transforms)] vertices = np.concatenate([cleaned.vertices for cleaned in cleaneds]) codes = np.concatenate([cleaned.codes for cleaned in cleaneds]) - # TODO: Implement Bezier degree elevation formula. For now, fall back to - # the "slow" implementation, though note that that implementation is, in - # fact, also incorrect... - if np.any(codes == Path.CURVE3): - raise NotImplementedError("Quadratic Bezier curves are not supported") - # Remove unused vertices and convert to cairo codes. Note that unlike # cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after # CLOSE_PATH, so our resulting buffer may be smaller.