Skip to content

FIX: Prevent Cursor lag on move by using draw_idle instead of draw#31426

Open
MohitPal2005 wants to merge 6 commits intomatplotlib:mainfrom
MohitPal2005:fix-cursor-lag-26901
Open

FIX: Prevent Cursor lag on move by using draw_idle instead of draw#31426
MohitPal2005 wants to merge 6 commits intomatplotlib:mainfrom
MohitPal2005:fix-cursor-lag-26901

Conversation

@MohitPal2005
Copy link
Copy Markdown
Contributor

@MohitPal2005 MohitPal2005 commented Mar 31, 2026

PR summary

closes #26901

Why is this change necessary & what problem does it solve?
Currently, widgets.Cursor.onmove calls a blocking self.canvas.draw() when the mouse leaves the axes and the cursor needs to be cleared. For large or complex figures (e.g., high-res images), this forces a full synchronous re-render, causing massive lag/stuttering (a busy icon that lasts 1-2 seconds per movement).

What is the reasoning for this implementation?
This PR optimizes the clear step by:

  1. Checking if useblit is enabled. If it is, it cleanly restores the saved background and blits only the specific bounding box (self.ax.bbox).
  2. Falling back to the non-blocking self.canvas.draw_idle() instead of draw() if blitting is disabled, preventing the heavy synchronous render lock.

AI Disclosure

I used an AI assistant to help review the Git workflow, structure the blitting fallback logic, and format this Pull Request.

PR checklist

  • "closes #0000" is in the body of the PR description to link the related issue
  • new and changed code is tested
  • [N/A] Plotting related features are demonstrated in an example
  • [N/A] New Features and API Changes are noted with a directive and release note
  • [N/A] Documentation complies with general and docstring guidelines

@MohitPal2005
Copy link
Copy Markdown
Contributor Author

Hi, I have updated the PR based on the CI feedback and restored the correct rendering behavior.

Initially, I attempted to use canvas.draw_idle() to reduce blocking redraws, but this caused issues with interactive tests (especially timing-sensitive ones). I have now reverted to canvas.draw() in the non-blitting path to ensure consistent and immediate rendering.

All CI checks are now passing successfully. Kindly review when you have time. Thanks!

Copy link
Copy Markdown
Member

@timhoffm timhoffm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you understand the needclear logic? I don’t, which is why I cannot judge whether using the background is valid here. Or to phrase it differently, the existing implementation uses blit in other places. Are you sure not using blit here was an oversight and we can safely change to it?

@MohitPal2005
Copy link
Copy Markdown
Contributor Author

Hi @timhoffm, thanks for raising this — that’s a very helpful point.

From my understanding, needclear acts as a state flag indicating whether the cursor was previously drawn and needs to be cleared when the mouse leaves the axes. In that branch, the goal is to restore the canvas to its original state by removing the cursor lines.

Regarding the use of blitting, I initially thought it might be possible to reuse the saved background (as is done in the main drawing path) to clear the cursor more efficiently. However, I’m not fully certain that the background is always valid or up-to-date in this specific case, especially since it depends on prior draw events and backend behavior.

So it’s possible that the current use of draw() here is intentional to guarantee correctness, rather than an oversight.

I’d be happy to investigate this further or try a more careful approach if you think there’s a safe way to extend blitting to this case.

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Apr 3, 2026

I don’t know. Just saying we nee to think this through carefully. It may well be that blitting is possible.

@MohitPal2005
Copy link
Copy Markdown
Contributor Author

That makes sense, thanks for the clarification.

I’ll take a closer look at how and when the blit background is created and whether it can be reliably reused in this branch. In particular, I want to verify that the background is always valid when needclear is triggered and that restoring it won’t introduce artifacts or inconsistencies across backends.

I’ll experiment with a small prototype and share findings before proposing any change here.

@MohitPal2005
Copy link
Copy Markdown
Contributor Author

Thanks for the clarification — I dug deeper into how blitting is used across widgets.

From the implementation of _load_blit_background, it’s clear that the background may legitimately be None at any time (e.g., before a draw event or after invalidation). Other widgets (like CheckButtons/RadioButtons) handle this by conditionally using blitting only when a valid background is available, and otherwise falling back to canvas.draw().

Based on this, I agree that we cannot assume blitting is always safe in the needclear branch.

A safer approach would be:

  • If useblit is enabled and a valid background is available → use restore + blit
  • Otherwise → fall back to canvas.draw()

This keeps correctness intact while still improving performance when possible.

Would you be okay with me updating the PR to follow this pattern?

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Apr 4, 2026

On a contextual background, since #29855, we can switch the canvas on existing figures. The blur background is canvas-dependent and therefore now stored on the canvas. This means we must assume, a background can vanish at any point in time and guard against that. See also #30503.

This however does not resolve the question on needclear. From a quick incomplete look, needclear triggers draw(), and the clear() method is bound to the draw_event hook. This means in the end needclear results in clear() being called. That sounds consistent.

Note that the event is done after the draw, wich makes sense: The background is updated after each full draw.

So if needclear is literally necessary (i.e. implying you need a fresh draw) then you cannot replace it with blitting.

In other words, to move forward you must argue why a full draw is not necessary and the background (in it exists) is not invalid.

@MohitPal2005
Copy link
Copy Markdown
Contributor Author

Hi @timhoffm, thanks again for the detailed explanation — this clarified the draw / background lifecycle a lot.

I took a closer look at how needclear, draw_event, and the blitting background interact.

From testing (including resize and zoom), restoring the saved background via blitting appears to correctly clear the cursor without visible artifacts in typical scenarios. In particular, since the cursor only draws animated artists (linev, lineh) and does not modify the underlying plot, restoring the background should be sufficient to remove it.

However, I understand your point that the background is only guaranteed to be valid after a full draw(), and that it may become invalid due to canvas changes (e.g., backend switches or other redraw triggers). Since clear() is tied to draw_event, the current implementation ensures correctness by forcing a full redraw before clearing.

So the key question seems to be whether needclear truly requires a fresh draw, or whether we can safely rely on the last known background in this specific case.

My current thinking is:

  • If the background is still valid, blitting should be sufficient to clear the cursor.
  • If the background is missing or stale, we must fall back to draw() to ensure correctness.

A possible approach could be to use blitting conditionally (when a valid background is available), and otherwise fall back to draw().

Would you be okay with me prototyping this guarded approach to see if it behaves reliably across backends?

@timhoffm
Copy link
Copy Markdown
Member

timhoffm commented Apr 4, 2026

Your answer sounds very AI generated. If that is the case, it violates our AI policy.

@MohitPal2005
Copy link
Copy Markdown
Contributor Author

Hi @timhoffm, thanks for pointing this out.

I wrote this myself, but yeah maybe I made it too formal.

From what I tested (resize/zoom), blitting seems to clear the cursor fine in most cases. But I also get your point that background might not always be valid after some changes.

So I was thinking — maybe we can use blitting when background is valid, and fallback to draw() if not?

Not fully sure if this is the right direction. Should I try prototyping this approach, or you suggest something else?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ENH]: Remove canvas.draw from widgets.Cursor.onmove

2 participants