Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 8a07eef

Browse filesBrowse 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
Copy full SHA for 8a07eef

File tree

Expand file treeCollapse file tree

1 file changed

+92
-33
lines changed
Filter options
Expand file treeCollapse file tree

1 file changed

+92
-33
lines changed

‎lib/matplotlib/backends/backend_cairo.py

Copy file name to clipboardExpand all lines: lib/matplotlib/backends/backend_cairo.py
+92-33Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.