Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8a07eef

Browse files
committed
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).
1 parent b549d12 commit 8a07eef

File tree

1 file changed

+92
-33
lines changed

1 file changed

+92
-33
lines changed

lib/matplotlib/backends/backend_cairo.py

Lines changed: 92 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,87 @@ def buffer_info(self):
7979
return (self.__data, self.__size)
8080

8181

82+
# Mapping from Matplotlib Path codes to cairo path codes.
83+
_MPL_TO_CAIRO_PATH_TYPE = np.zeros(80, dtype=int) # CLOSEPOLY = 79.
84+
_MPL_TO_CAIRO_PATH_TYPE[Path.MOVETO] = cairo.PATH_MOVE_TO
85+
_MPL_TO_CAIRO_PATH_TYPE[Path.LINETO] = cairo.PATH_LINE_TO
86+
_MPL_TO_CAIRO_PATH_TYPE[Path.CURVE4] = cairo.PATH_CURVE_TO
87+
_MPL_TO_CAIRO_PATH_TYPE[Path.CLOSEPOLY] = cairo.PATH_CLOSE_PATH
88+
# Sizes in cairo_path_data_t of each cairo path element.
89+
_CAIRO_PATH_TYPE_SIZES = np.zeros(4, dtype=int)
90+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_MOVE_TO] = 2
91+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_LINE_TO] = 2
92+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CURVE_TO] = 4
93+
_CAIRO_PATH_TYPE_SIZES[cairo.PATH_CLOSE_PATH] = 1
94+
95+
96+
def _convert_path(ctx, path, transform, clip=None):
97+
if HAS_CAIRO_CFFI:
98+
try:
99+
return _convert_path_fast(ctx, path, transform, clip)
100+
except NotImplementedError:
101+
pass
102+
return _convert_path_slow(ctx, path, transform, clip)
103+
104+
105+
def _convert_path_slow(ctx, path, transform, clip=None):
106+
for points, code in path.iter_segments(transform, clip=clip):
107+
if code == Path.MOVETO:
108+
ctx.move_to(*points)
109+
elif code == Path.CLOSEPOLY:
110+
ctx.close_path()
111+
elif code == Path.LINETO:
112+
ctx.line_to(*points)
113+
elif code == Path.CURVE3:
114+
ctx.curve_to(points[0], points[1],
115+
points[0], points[1],
116+
points[2], points[3])
117+
elif code == Path.CURVE4:
118+
ctx.curve_to(*points)
119+
120+
121+
def _convert_path_fast(ctx, path, transform, clip=None):
122+
ffi = cairo.ffi
123+
cleaned = path.cleaned(transform=transform, clip=clip)
124+
vertices = cleaned.vertices
125+
codes = cleaned.codes
126+
127+
# TODO: Implement Bezier degree elevation formula. Note that the "slow"
128+
# implementation is, in fact, also incorrect...
129+
if np.any(codes == Path.CURVE3):
130+
raise NotImplementedError("Quadratic Bezier curves are not supported")
131+
# Remove unused vertices and convert to cairo codes. Note that unlike
132+
# cairo_close_path, we do not explicitly insert an extraneous MOVE_TO after
133+
# CLOSE_PATH, so our resulting buffer may be smaller.
134+
if codes[-1] == Path.STOP:
135+
codes = codes[:-1]
136+
vertices = vertices[:-1]
137+
vertices = vertices[codes != Path.CLOSEPOLY]
138+
codes = _MPL_TO_CAIRO_PATH_TYPE[codes]
139+
# Where are the headers of each cairo portions?
140+
cairo_type_sizes = _CAIRO_PATH_TYPE_SIZES[codes]
141+
cairo_type_positions = np.insert(np.cumsum(cairo_type_sizes), 0, 0)
142+
cairo_num_data = cairo_type_positions[-1]
143+
cairo_type_positions = cairo_type_positions[:-1]
144+
145+
# Fill the buffer.
146+
buf = np.empty(cairo_num_data * 16, np.uint8)
147+
as_int = np.frombuffer(buf.data, np.int32)
148+
as_float = np.frombuffer(buf.data, np.float64)
149+
mask = np.ones_like(as_float, bool)
150+
as_int[::4][cairo_type_positions] = codes
151+
as_int[1::4][cairo_type_positions] = cairo_type_sizes
152+
mask[::2][cairo_type_positions] = mask[1::2][cairo_type_positions] = False
153+
as_float[mask] = vertices.ravel()
154+
155+
# Construct the cairo_path_t, and pass it to the context.
156+
ptr = ffi.new("cairo_path_t *")
157+
ptr.status = cairo.STATUS_SUCCESS
158+
ptr.data = ffi.cast("cairo_path_data_t *", ffi.from_buffer(buf))
159+
ptr.num_data = cairo_num_data
160+
cairo.cairo.cairo_append_path(ctx._pointer, ptr)
161+
162+
82163
class RendererCairo(RendererBase):
83164
fontweights = {
84165
100 : cairo.FONT_WEIGHT_NORMAL,
@@ -138,38 +219,16 @@ def _fill_and_stroke(self, ctx, fill_c, alpha, alpha_overrides):
138219
ctx.restore()
139220
ctx.stroke()
140221

141-
@staticmethod
142-
def convert_path(ctx, path, transform, clip=None):
143-
for points, code in path.iter_segments(transform, clip=clip):
144-
if code == Path.MOVETO:
145-
ctx.move_to(*points)
146-
elif code == Path.CLOSEPOLY:
147-
ctx.close_path()
148-
elif code == Path.LINETO:
149-
ctx.line_to(*points)
150-
elif code == Path.CURVE3:
151-
ctx.curve_to(points[0], points[1],
152-
points[0], points[1],
153-
points[2], points[3])
154-
elif code == Path.CURVE4:
155-
ctx.curve_to(*points)
156-
157222
def draw_path(self, gc, path, transform, rgbFace=None):
158223
ctx = gc.ctx
159-
160-
# We'll clip the path to the actual rendering extents
161-
# if the path isn't filled.
162-
if rgbFace is None and gc.get_hatch() is None:
163-
clip = ctx.clip_extents()
164-
else:
165-
clip = None
166-
224+
# Clip the path to the actual rendering extents if it isn't filled.
225+
clip = (ctx.clip_extents()
226+
if rgbFace is None and gc.get_hatch() is None
227+
else None)
167228
transform = (transform
168-
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
169-
229+
+ Affine2D().scale(1, -1).translate(0, self.height))
170230
ctx.new_path()
171-
self.convert_path(ctx, path, transform, clip)
172-
231+
_convert_path(ctx, path, transform, clip)
173232
self._fill_and_stroke(
174233
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
175234

@@ -179,8 +238,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
179238

180239
ctx.new_path()
181240
# Create the path for the marker; it needs to be flipped here already!
182-
self.convert_path(
183-
ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
241+
_convert_path(
242+
ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
184243
marker_path = ctx.copy_path_flat()
185244

186245
# Figure out whether the path has a fill
@@ -193,7 +252,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
193252
filled = True
194253

195254
transform = (transform
196-
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
255+
+ Affine2D().scale(1, -1).translate(0, self.height))
197256

198257
ctx.new_path()
199258
for i, (vertices, codes) in enumerate(
@@ -247,7 +306,7 @@ def draw_image(self, gc, x, y, im):
247306

248307
ctx.save()
249308
ctx.set_source_surface(surface, float(x), float(y))
250-
if gc.get_alpha() != 1.0:
309+
if gc.get_alpha() != 1:
251310
ctx.paint_with_alpha(gc.get_alpha())
252311
else:
253312
ctx.paint()
@@ -413,7 +472,7 @@ def set_clip_path(self, path):
413472
ctx.new_path()
414473
affine = (affine
415474
+ Affine2D().scale(1, -1).translate(0, self.renderer.height))
416-
RendererCairo.convert_path(ctx, tpath, affine)
475+
_convert_path(ctx, tpath, affine)
417476
ctx.clip()
418477

419478
def set_dashes(self, offset, dashes):

0 commit comments

Comments
 (0)