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

Skip to content

Commit c78badd

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 14d7893 commit c78badd

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
@@ -62,6 +62,87 @@ def buffer_info(self):
6262
return (self.__data, self.__size)
6363

6464

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

124-
@staticmethod
125-
def convert_path(ctx, path, transform, clip=None):
126-
for points, code in path.iter_segments(transform, clip=clip):
127-
if code == Path.MOVETO:
128-
ctx.move_to(*points)
129-
elif code == Path.CLOSEPOLY:
130-
ctx.close_path()
131-
elif code == Path.LINETO:
132-
ctx.line_to(*points)
133-
elif code == Path.CURVE3:
134-
ctx.curve_to(points[0], points[1],
135-
points[0], points[1],
136-
points[2], points[3])
137-
elif code == Path.CURVE4:
138-
ctx.curve_to(*points)
139-
140205
def draw_path(self, gc, path, transform, rgbFace=None):
141206
ctx = gc.ctx
142-
143-
# We'll clip the path to the actual rendering extents
144-
# if the path isn't filled.
145-
if rgbFace is None and gc.get_hatch() is None:
146-
clip = ctx.clip_extents()
147-
else:
148-
clip = None
149-
207+
# Clip the path to the actual rendering extents if it isn't filled.
208+
clip = (ctx.clip_extents()
209+
if rgbFace is None and gc.get_hatch() is None
210+
else None)
150211
transform = (transform
151-
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
152-
212+
+ Affine2D().scale(1, -1).translate(0, self.height))
153213
ctx.new_path()
154-
self.convert_path(ctx, path, transform, clip)
155-
214+
_convert_path(ctx, path, transform, clip)
156215
self._fill_and_stroke(
157216
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
158217

@@ -162,8 +221,8 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
162221

163222
ctx.new_path()
164223
# Create the path for the marker; it needs to be flipped here already!
165-
self.convert_path(
166-
ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
224+
_convert_path(
225+
ctx, marker_path, marker_trans + Affine2D().scale(1, -1))
167226
marker_path = ctx.copy_path_flat()
168227

169228
# Figure out whether the path has a fill
@@ -176,7 +235,7 @@ def draw_markers(self, gc, marker_path, marker_trans, path, transform,
176235
filled = True
177236

178237
transform = (transform
179-
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
238+
+ Affine2D().scale(1, -1).translate(0, self.height))
180239

181240
ctx.new_path()
182241
for i, (vertices, codes) in enumerate(
@@ -230,7 +289,7 @@ def draw_image(self, gc, x, y, im):
230289

231290
ctx.save()
232291
ctx.set_source_surface(surface, float(x), float(y))
233-
if gc.get_alpha() != 1.0:
292+
if gc.get_alpha() != 1:
234293
ctx.paint_with_alpha(gc.get_alpha())
235294
else:
236295
ctx.paint()
@@ -396,7 +455,7 @@ def set_clip_path(self, path):
396455
ctx.new_path()
397456
affine = (affine
398457
+ Affine2D().scale(1, -1).translate(0, self.renderer.height))
399-
RendererCairo.convert_path(ctx, tpath, affine)
458+
_convert_path(ctx, tpath, affine)
400459
ctx.clip()
401460

402461
def set_dashes(self, offset, dashes):

0 commit comments

Comments
 (0)