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

Skip to content

Commit a8d1dcb

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 77af0f7 commit a8d1dcb

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
@@ -78,6 +78,87 @@ def buffer_info(self):
7878
return (self.__data, self.__size)
7979

8080

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

144-
@staticmethod
145-
def convert_path(ctx, path, transform, clip=None):
146-
for points, code in path.iter_segments(transform, clip=clip):
147-
if code == Path.MOVETO:
148-
ctx.move_to(*points)
149-
elif code == Path.CLOSEPOLY:
150-
ctx.close_path()
151-
elif code == Path.LINETO:
152-
ctx.line_to(*points)
153-
elif code == Path.CURVE3:
154-
ctx.curve_to(points[0], points[1],
155-
points[0], points[1],
156-
points[2], points[3])
157-
elif code == Path.CURVE4:
158-
ctx.curve_to(*points)
159-
160-
161225
def draw_path(self, gc, path, transform, rgbFace=None):
162226
ctx = gc.ctx
163-
164-
# We'll clip the path to the actual rendering extents
165-
# if the path isn't filled.
166-
if rgbFace is None and gc.get_hatch() is None:
167-
clip = ctx.clip_extents()
168-
else:
169-
clip = None
170-
171-
transform = transform + \
172-
Affine2D().scale(1.0, -1.0).translate(0, self.height)
173-
227+
# Clip the path to the actual rendering extents if it isn't filled.
228+
clip = (ctx.clip_extents()
229+
if rgbFace is None and gc.get_hatch() is None
230+
else None)
231+
transform = (transform
232+
+ Affine2D().scale(1.0, -1.0).translate(0, self.height))
174233
ctx.new_path()
175-
self.convert_path(ctx, path, transform, clip)
176-
177-
self._fill_and_stroke(ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
234+
_convert_path(ctx, path, transform, clip)
235+
self._fill_and_stroke(
236+
ctx, rgbFace, gc.get_alpha(), gc.get_forced_alpha())
178237

179238
def draw_markers(self, gc, marker_path, marker_trans, path, transform, rgbFace=None):
180239
ctx = gc.ctx
181240

182241
ctx.new_path()
183242
# Create the path for the marker; it needs to be flipped here already!
184-
self.convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
243+
_convert_path(ctx, marker_path, marker_trans + Affine2D().scale(1.0, -1.0))
185244
marker_path = ctx.copy_path_flat()
186245

187246
# Figure out whether the path has a fill
@@ -430,7 +489,7 @@ def set_clip_path(self, path):
430489
ctx = self.ctx
431490
ctx.new_path()
432491
affine = affine + Affine2D().scale(1.0, -1.0).translate(0.0, self.renderer.height)
433-
RendererCairo.convert_path(ctx, tpath, affine)
492+
_convert_path(ctx, tpath, affine)
434493
ctx.clip()
435494

436495
def set_dashes(self, offset, dashes):

0 commit comments

Comments
 (0)