napari: Very slow rendering when using `autoswap=True` in the VisPy Canvas

🐛 Bug

I was really puzzled why the brush was not so responsive as it should have even on low-resolution images. I measured FPS and discovered that it never exceeded 10-15 when you use the brush even on very small images (e.g. examples\add_labels.py).

Initially, I thought that it was because of some very inefficient code in the Label layer, but after long hours with a debugger and profiler I eliminated this hypothesis. There is certainly room for improvement, but there is no such code that can limit FPS to 15 even on 100x100 images.

Finally, I found the culprit of this issue, it was because of using autoswap=True in the VisPy Canvas. Setting autoswap to False in the following code immediately increases the FPS from 13 to 50 in examples\add_labels.py on my laptop: https://github.com/napari/napari/blob/cf55aa005230a89b906be2447ce900be2c1f200a/napari/_qt/qt_viewer.py#L431-L436

Calling Canvas.swap_buffers at each update below blocks the OpenGL engine at least for 50-60 ms on my laptop not depending on the resolution of an image causing a significant drop in the rendering performance: https://github.com/vispy/vispy/blob/5449e3402b35da4d6a8215d8d72e19dcf05917d7/vispy/app/canvas.py#L236-L237 which leads to the execution of the following code: https://github.com/vispy/vispy/blob/5449e3402b35da4d6a8215d8d72e19dcf05917d7/vispy/app/backends/_qt.py#L869

I didn’t dig any deeper into VisPy as disabling autoswap solves the issue without any noticeable bugs so far for me.

Environment

I tested it on a laptop with Windows and integrated graphics:

napari: 0.5.0a2.dev55+g8a793b9f.d20230406
Platform: Windows-10-10.0.22621-SP0
Python: 3.9.1 (default, Dec 11 2020, 09:29:25) [MSC v.1916 64 bit (AMD64)]
Qt: 5.12.9
PyQt5: 5.12.3
NumPy: 1.22.4
SciPy: 1.9.1
Dask: 2023.3.2
VisPy: 0.12.2
magicgui: 0.7.2
superqt: unknown
in-n-out: 0.1.7
app-model: 0.1.2
npe2: 0.6.2

OpenGL:
- GL version: 4.6.13596 Compatibility Profile Context 20.10.22.14 27.20.11022.14001
- MAX_TEXTURE_SIZE: 16384

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Comments: 44 (36 by maintainers)

Commits related to this issue

Most upvoted comments

Looks like you went already deep down the rabbithole 😃 I think this should be brought up over at Vispy for sure. @ksofiyuk since you have a good picture of the problem, do you mind opening an issue at vispy (linking here as well)? Otherwise let me know and I can take care of it.

In the meantime, even heavy stuff with fast camera movements looks exactly the same for me with autoswap=False, so I’m in favor of doing that in napari until we figure it out on the vispy side.

looking on this wiki https://www.khronos.org/opengl/wiki/Swap_Interval it looks like selection of synchronized and non synchronized swap buffer should be possible.

I see few references to glFinish in vispy. Maybe this could be a good way to research?

Did this fix should happen in napari or in vispy?

I think it has almost nothing to do with napari as all the OpenGL stuff is happening inside VisPy. VisPy should just have another thread for OpenGL stuff.

The simplest solution is to put only SwapBuffers in a separate thread, it works at least for me and completely resolves the issue, using something like this (this code is just for illustrative purposes):

    # def __init__
        self._swap_buffers_worker = threading.Thread(target=self.swap_buffers_thread, daemon=True)
        self._swap_buffers_queue = queue.Queue()
        self._swap_buffers_worker.start()
        # ...

    def swap_buffers(self, event=None):
        self._swap_buffers_queue.put(1)

    def swap_buffers_thread(self):
        while True:
            self._swap_buffers_queue.get()
            self._backend._vispy_swap_buffers()

instead of just

    def swap_buffers(self, event=None):
        self._backend._vispy_swap_buffers()

I think it should be implemented somewhere there: https://github.com/vispy/vispy/blob/5449e3402b35da4d6a8215d8d72e19dcf05917d7/vispy/app/canvas.py#L415

which Xubuntu

I remember multiple problems on Xubuntu, and I finally switched to Gnome Shell. As I read the team who manages XFCE is very limited and they do not have enough resources to fix all problems.

It is just like here PyQt5 OpenGL swapBuffers very slow, where the issue was caused by inappropriate use of swapBuffers.

Based on the accepted response to linked StackOverflow. Could you check if #5710 solves your problem?

As @alisterburt described, the swap buffer is to avoid flickering. It is a common practice used in programming GUI applications (even many games using it).

@Czaki @alisterburt I’m pretty sure that swapBuffers is just not used in a proper way in VisPy causing performance issues on some machines. It is just like here PyQt5 OpenGL swapBuffers very slow, where the issue was caused by inappropriate use of swapBuffers.

I could blame my system if all OpenGL applications were laggy, but I don’t have problems with other 3D applications, even with other PyQt apps that use OpenGL.

thanks for reporting back @ksofiyuk !

I ran the same test as you @ksofiyuk and enabling autoswap makes no discernible difference in FPS on my system.

It does not feel like the double buffering makes a big difference during 2D/3D rendering on my system, there is a slight difference when rotating the 3D camera extremely quickly but nothing perceptible when doing things ‘reasonably’. In this context, I think it would be safe to default to autoswap=False on the canvas to solve this problem

@ksofiyuk Probably not directly related to this, but I note you have PyQt5: 5.12.3 Is there a specific reason you’re using this version, which is quite old (it’s napari min requirement)? I ask because it’s increasingly tricky to support pre 5.13 Qt while trying to improve Qt6 support.

@psobolewskiPhD I’ve just updated it all to Qt: 5.15.2 PyQt5: 5.15.9. The performance issue with autoswap=True remains exactly the same.

There is no any specific reason for PyQt5: 5.12.3. I just haven’t updated it for a long time 😄 .

@ksofiyuk yep - just asserting that the callback is indeed called and the example runs as expected on my machine on main

Ok, then it looks like it is indeed a platform specific bug.

@ksofiyuk yep - just asserting that the callback is indeed called and the example runs as expected on my machine on main

Very cool find @ksofiyuk and thanks for the ping @jni

From some Googling it seems “double buffering” is used to prevent flickering and improve the smoothness

An OpenGL canvas maintains two buffers for drawing:

The front buffer is the buffer that is currently visible on the screen. It contains the latest completed frame. The back buffer is the buffer that is not visible on the screen. It is where the application draws the next frame while the front buffer is being displayed.

“swap buffers” swaps the roles of the front and back buffers. The back buffer, which contains the newly drawn frame, becomes the front buffer and is displayed on the screen, while the previous front buffer becomes the back buffer and is now used for drawing the next frame.

It’s possible that turning this off will lead to some ‘tearing’ where partially rendered frames are visible sometimes - I don’t have any experience with this so I vote we run the experiment, get a few people on different systems to test and see what happens!