Skip to content

Speed up Tkagg blit with Tk_PhotoPutBlock #20840

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Aug 20, 2021
13 changes: 13 additions & 0 deletions doc/api/next_api_changes/development/20840-RJS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Increase to minimum supported optional dependencies
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

For Matplotlib 3.5, the :ref:`minimum supported versions of optional dependencies
<optional_dependencies>` are being bumped:

+------------+-----------------+---------------+
| Dependency | min in mpl3.4 | min in mpl3.5 |
+============+=================+===============+
| Tk | 8.3 | 8.4 |
+------------+-----------------+---------------+

This is consistent with our :ref:`min_deps_policy`
2 changes: 1 addition & 1 deletion doc/devel/dependencies.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Matplotlib figures can be rendered to various user interfaces. See
:ref:`what-is-a-backend` for more details on the optional Matplotlib backends
and the capabilities they provide.

* Tk_ (>= 8.3, != 8.6.0 or 8.6.1) [#]_: for the Tk-based backends.
* Tk_ (>= 8.4, != 8.6.0 or 8.6.1) [#]_: for the Tk-based backends.
* PyQt6_ (>= 6.1), PySide6_, PyQt5_, or PySide2_: for the Qt-based backends.
* PyGObject_: for the GTK3-based backends [#]_.
* wxPython_ (>= 4) [#]_: for the wx-based backends.
Expand Down
22 changes: 10 additions & 12 deletions lib/matplotlib/backends/_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,22 +53,20 @@ def _restore_foreground_window_at_end():
# Initialize to a non-empty string that is not a Tcl command
_blit_tcl_name = "mpl_blit_" + uuid.uuid4().hex

TK_PHOTO_COMPOSITE_OVERLAY = 0 # apply transparency rules pixel-wise
TK_PHOTO_COMPOSITE_SET = 1 # set image buffer directly


def _blit(argsid):
"""
Thin wrapper to blit called via tkapp.call.

*argsid* is a unique string identifier to fetch the correct arguments from
the ``_blit_args`` dict, since arguments cannot be passed directly.

photoimage blanking must occur in the same event and thread as blitting
to avoid flickering.
"""
photoimage, dataptr, offsets, bboxptr, blank = _blit_args.pop(argsid)
if blank:
photoimage.blank()
_tkagg.blit(
photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr)
photoimage, dataptr, offsets, bboxptr, comp_rule = _blit_args.pop(argsid)
_tkagg.blit(photoimage.tk.interpaddr(), str(photoimage), dataptr,
comp_rule, offsets, bboxptr)


def blit(photoimage, aggimage, offsets, bbox=None):
Expand All @@ -81,7 +79,7 @@ def blit(photoimage, aggimage, offsets, bbox=None):
for big-endian ARGB32 (i.e. ARGB8888) data.

If *bbox* is passed, it defines the region that gets blitted. That region
will NOT be blanked before blitting.
will be composed with the previous data according to the alpha channel.

Tcl events must be dispatched to trigger a blit from a non-Tcl thread.
"""
Expand All @@ -95,10 +93,10 @@ def blit(photoimage, aggimage, offsets, bbox=None):
y1 = max(math.floor(y1), 0)
y2 = min(math.ceil(y2), height)
bboxptr = (x1, x2, y1, y2)
blank = False
comp_rule = TK_PHOTO_COMPOSITE_OVERLAY
else:
bboxptr = (0, width, 0, height)
blank = True
comp_rule = TK_PHOTO_COMPOSITE_SET

# NOTE: _tkagg.blit is thread unsafe and will crash the process if called
# from a thread (GH#13293). Instead of blanking and blitting here,
Expand All @@ -107,7 +105,7 @@ def blit(photoimage, aggimage, offsets, bbox=None):

# tkapp.call coerces all arguments to strings, so to avoid string parsing
# within _blit, pack up the arguments into a global data structure.
args = photoimage, dataptr, offsets, bboxptr, blank
args = photoimage, dataptr, offsets, bboxptr, comp_rule
# Need a unique key to avoid thread races.
# Again, make the key a string to avoid string parsing in _blit.
argsid = str(id(args))
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_backend_tk.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def test_blit(): # pragma: no cover
for bad_box in bad_boxes:
try:
_tkagg.blit(
photoimage.tk.interpaddr(), str(photoimage), dataptr,
photoimage.tk.interpaddr(), str(photoimage), dataptr, 0,
(0, 1, 2, 3), bad_box)
except ValueError:
print("success")
Expand Down
29 changes: 21 additions & 8 deletions src/_tkagg.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ static int convert_voidptr(PyObject *obj, void *p)
// Global vars for Tk functions. We load these symbols from the tkinter
// extension module or loaded Tk libraries at run-time.
static Tk_FindPhoto_t TK_FIND_PHOTO;
static Tk_PhotoPutBlock_NoComposite_t TK_PHOTO_PUT_BLOCK_NO_COMPOSITE;
static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK;
#ifdef WIN32_DLL
// Global vars for Tcl functions. We load these symbols from the tkinter
// extension module or loaded Tcl libraries at run-time.
Expand All @@ -63,13 +63,16 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args)
char const *photo_name;
int height, width;
unsigned char *data_ptr;
int comp_rule;
int put_retval;
int o0, o1, o2, o3;
int x1, x2, y1, y2;
Tk_PhotoHandle photo;
Tk_PhotoImageBlock block;
if (!PyArg_ParseTuple(args, "O&s(iiO&)(iiii)(iiii):blit",
if (!PyArg_ParseTuple(args, "O&s(iiO&)i(iiii)(iiii):blit",
convert_voidptr, &interp, &photo_name,
&height, &width, convert_voidptr, &data_ptr,
&comp_rule,
&o0, &o1, &o2, &o3,
&x1, &x2, &y1, &y2)) {
goto exit;
Expand All @@ -82,7 +85,12 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args)
PyErr_SetString(PyExc_ValueError, "Attempting to draw out of bounds");
goto exit;
}
if (comp_rule != TK_PHOTO_COMPOSITE_OVERLAY && comp_rule != TK_PHOTO_COMPOSITE_SET) {
PyErr_SetString(PyExc_ValueError, "Invalid comp_rule argument");
goto exit;
}

Py_BEGIN_ALLOW_THREADS
block.pixelPtr = data_ptr + 4 * ((height - y2) * width + x1);
block.width = x2 - x1;
block.height = y2 - y1;
Expand All @@ -92,8 +100,13 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args)
block.offset[1] = o1;
block.offset[2] = o2;
block.offset[3] = o3;
TK_PHOTO_PUT_BLOCK_NO_COMPOSITE(
photo, &block, x1, height - y2, x2 - x1, y2 - y1);
put_retval = TK_PHOTO_PUT_BLOCK(
interp, photo, &block, x1, height - y2, x2 - x1, y2 - y1, comp_rule);
Py_END_ALLOW_THREADS
if (put_retval == TCL_ERROR) {
return PyErr_NoMemory();
}

exit:
if (PyErr_Occurred()) {
return NULL;
Expand Down Expand Up @@ -219,8 +232,8 @@ int load_tk(T lib)
return
!!(TK_FIND_PHOTO =
(Tk_FindPhoto_t)dlsym(lib, "Tk_FindPhoto")) +
!!(TK_PHOTO_PUT_BLOCK_NO_COMPOSITE =
(Tk_PhotoPutBlock_NoComposite_t)dlsym(lib, "Tk_PhotoPutBlock_NoComposite"));
!!(TK_PHOTO_PUT_BLOCK =
(Tk_PhotoPutBlock_t)dlsym(lib, "Tk_PhotoPutBlock"));
}

#ifdef WIN32_DLL
Expand Down Expand Up @@ -341,8 +354,8 @@ PyMODINIT_FUNC PyInit__tkagg(void)
} else if (!TK_FIND_PHOTO) {
PyErr_SetString(PyExc_RuntimeError, "Failed to load Tk_FindPhoto");
return NULL;
} else if (!TK_PHOTO_PUT_BLOCK_NO_COMPOSITE) {
PyErr_SetString(PyExc_RuntimeError, "Failed to load Tk_PhotoPutBlock_NoComposite");
} else if (!TK_PHOTO_PUT_BLOCK) {
PyErr_SetString(PyExc_RuntimeError, "Failed to load Tk_PhotoPutBlock");
return NULL;
}
return PyModule_Create(&_tkagg_module);
Expand Down
11 changes: 8 additions & 3 deletions src/_tkmini.h
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,19 @@ typedef struct Tk_PhotoImageBlock
int offset[4];
} Tk_PhotoImageBlock;

#define TK_PHOTO_COMPOSITE_OVERLAY 0 // apply transparency rules pixel-wise
#define TK_PHOTO_COMPOSITE_SET 1 // set image buffer directly
#define TCL_OK 0
#define TCL_ERROR 1

/* Typedefs derived from function signatures in Tk header */
/* Tk_FindPhoto typedef */
typedef Tk_PhotoHandle (*Tk_FindPhoto_t) (Tcl_Interp *interp, const char
*imageName);
/* Tk_PhotoPutBLock_NoComposite typedef */
typedef void (*Tk_PhotoPutBlock_NoComposite_t) (Tk_PhotoHandle handle,
/* Tk_PhotoPutBLock typedef */
typedef int (*Tk_PhotoPutBlock_t) (Tcl_Interp *interp, Tk_PhotoHandle handle,
Tk_PhotoImageBlock *blockPtr, int x, int y,
int width, int height);
int width, int height, int compRule);

#ifdef WIN32_DLL
/* Typedefs derived from function signatures in Tcl header */
Expand Down