Using an offscreen renderer influences completely separate interactive renderer

I have a qt application with an interactive viewport displaying a model.

The user can save a screenshot of the model to a file.
This is done by creating a new renderer, render window, camera and actors and then capturing the window pixels and storing them.

renderer = vtkOpenGLRenderer()

renderer.SetBackground(COLOR_BG1)

window = vtkRenderWindow()
window.SetSharedRenderWindow(None)
window.SetOffScreenRendering(True)
window.AddRenderer(renderer)

....

window.Render()

Problem is: after calling render() on the renderer connected to the offscreen window, the interactive window is completely black and sometimes the program terminates.

So I seem to have two independent pipelines that are somehow still influencing eachother.

I check SetSharedRenderWindow and it was null, just to be sure I explicitly set it to None on both windows.

  • The window has only one renderer which is the correct renderer.
  • The renderer has only one window which is the correct window.

I’m not yet able to produce a minimum reproduceable example. But the problem is reproducable on my pc.

Using:
windows, python 3.11, vtk 9.4 or 9.2 (tested both), numpy < 2.0

Any suggestions on what to look at are welcome.

Complicating factors:

  1. I can only reproduce the issue when using a certain actor which is a mesh with a large amount (32008) of vertices that I create using the numpy2vtk functions. I can not reproduce the error without, so this may also be source of the problem (the changes in numpy 2.0 maybe?)
  2. The offscreen renderer is executed in a separate python thread.

Hmm…how well does VTK like doing rendering from multiple threads? Are you reusing the pipeline and mappers perhaps? I wonder if OpenGL contexts get confused… (just musing out loud here)

Cc: @spyridon97 @jaswantp

I’ve reproduced the issue with an other (large) actor. So it is not mesh specific, it just needs to be large. In this case a 73728 faced closed sphere.

Tested with numpy > 2 as well, does not change anything.

Why not use vtkWindowToImageFilter on the existing render window?

Otherwise, try doing it on main thread and let us know if that works.

We are rendering a pdf report with pictures showing different views of the model or models other than the one in the viewport.
The background thread is used to keep the gui responsive and give the user the opportunity to cancel the report production.

Producing the image without using a background thread works without issues.
It also works without issues (using a background thread) if I use actors with fewer vertices.

Okay. I’ll try to reproduce with a large sphere.

I made some progress in trying to isolate the problem. Will try to get a minimum reproducible example.

What I have now is:

  1. set up a renderer A in an interactive window

  2. set up an offscreen renderer B in a separate thread.

Add a custom overlay to the background renderer B using

render_window.SetRGBACharPixelData(
            x1, y1, x2, y2, self._vtk_pixel_data, front, blend, right
        )
  1. repeat step 2 in the main thread

  2. Now the interactive renderer crashes

I have managed to boil it down to the following minimal example:

  1. create a renderer in a window
  2. start a thread
  3. in the thread, create new renderer and render to an image
  4. in the thread, make a list with a reference to the render window as well to the list itself. This can not be free-ed by the reference counter.
  5. exit the thread
  6. Execute the garbage-collector, this will clean up the list and the renderer window from the other thread. This causes the following error(s):

image

Python to reproduce:

import gc
from threading import Thread
import vtk


def create_interactive_window():
    # Create a cube
    cube = vtk.vtkCubeSource()

    # Create a mapper
    mapper = vtk.vtkPolyDataMapper()
    mapper.SetInputConnection(cube.GetOutputPort())

    # Create an actor
    actor = vtk.vtkActor()
    actor.SetMapper(mapper)

    # Create a renderer
    renderer = vtk.vtkRenderer()
    renderer.AddActor(actor)
    renderer.SetBackground(0.1, 0.2, 0.4)  # Background color

    # Create a render window
    render_window = vtk.vtkRenderWindow()
    render_window.AddRenderer(renderer)
    render_window.SetSize(800, 600)

    # Create a render window interactor
    render_window_interactor = vtk.vtkRenderWindowInteractor()
    render_window_interactor.SetRenderWindow(render_window)

    # Initialize and start the interaction
    render_window.Render()
    render_window_interactor.Initialize()

    return render_window_interactor

def make_offscreen_renderer():
    # Create a cube
    cube = vtk.vtkCubeSource()

    # Create a mapper
    mapper = vtk.vtkPolyDataMapper()
    mapper.SetInputConnection(cube.GetOutputPort())

    # Create an actor
    actor = vtk.vtkActor()
    actor.SetMapper(mapper)

    # Create a renderer
    renderer = vtk.vtkOpenGLRenderer()
    renderer.AddActor(actor)
    renderer.SetBackground(0.1, 0.2, 0.4)  # Background color

    renwin = vtk.vtkRenderWindow()
    renwin.SetOffScreenRendering(True)
    renwin.AddRenderer(renderer)
    renwin.SetSize(800, 600)
    camera = renderer.GetActiveCamera()

    return renwin

if __name__ == '__main__':

    #

    gc.disable()

    interactor = create_interactive_window()
    #
    #
    def produce_offscreen_image():
        renwin = make_offscreen_renderer()
        renwin.Render()

        # Use PIL to display the image (not required, just to check that it works)
        #
        # from PIL import Image
        # from vtk.util.numpy_support import vtk_to_numpy
        # import numpy as np
        # nx, ny = renwin.GetSize()
        #
        # arr = vtk.vtkUnsignedCharArray()
        # renwin.GetRGBACharPixelData(0, 0, nx - 1, ny - 1, 0, arr)
        #
        # narr = vtk_to_numpy(arr).T[:3].T.reshape([ny, nx, 3])
        # narr = np.flip(narr, axis=0)
        # pil_img = Image.fromarray(narr)
        #
        # pil_img.show()

        whoopsie = [renwin]       # <--- create a list with the renwin object
        whoopsie.append(whoopsie) # <--- append the list to itself, to create a circular reference
        #                           This means it will not be released because the reference count will never reach 0

    new_thread = Thread(
        target=lambda: produce_offscreen_image()
    )

    new_thread.start()
    new_thread.join()
    #
    gc.collect(2)  # <--- this crashes the program

    interactor.Start()

Able to reproduce. You will need to clean up the render window on the thread which initialized the opengl context after you’re done rendering and saving a screenshot.

To fix it, call renwin.Finalize() at the end of your produce_offscreen_image() function.
FYI, here’s the human readable error message with a callstack, it appears FormatMessageW is making a garbled string.

COLLECTING #gc.collect(2)
(   1.067s) [main thread     ]vtkWin32OpenGLRenderWin:259    ERR| vtkWin32OpenGLRenderWindow (000001F4B1F3DB70): wglMakeCurrent failed in MakeCurrent(), error: 2004(The requested transformation operation is not supported.)
(   1.390s) [main thread     ]vtkWin32OpenGLRenderWin:260   WARN| vtkWin32OpenGLRenderWindow (000001F4B1F3DB70):  at vtkWin32OpenGLRenderWindow::MakeCurrent
 at vtkWin32OpenGLRenderWindow::Clean
 at vtkWin32OpenGLRenderWindow::DestroyWindow
 at vtkWin32OpenGLRenderWindow::~vtkWin32OpenGLRenderWindow
 at vtkWin32OpenGLRenderWindow::~vtkWin32OpenGLRenderWindow
 at vtkPythonUtil::RemoveObjectFromMap
 at PyVTKObject_Delete
 at Py_GetLocaleEncoding
 at PyUnicode_DecodeUTF32Stateful
 at PyModule_GetNameObject
 at PyModule_GetNameObject
 at PyImport_ImportModuleLevel
 at PyArg_CheckPositional
 at PyEval_EvalFrameDefault
 at PyType_CalculateMetaclass
 at PyEval_EvalCode
 at PyArena_New
 at PyArena_New
 at PyRun_SimpleFileObject
 at PyRun_SimpleFileObject
 at PyRun_AnyFileObject
 at Py_MakePendingCalls
 at Py_MakePendingCalls
 at Py_RunMain
 at Py_RunMain
 at vtkPythonInterpreter::PyMain
 at vtkPythonInterpreter::PyMain
 at vtkPythonInterpreter::PyMain
 at BaseThreadInitThunk
 at RtlUserThreadStart

Complete code that works

import gc
from threading import Thread
import vtk


def create_interactive_window():
    # Create a cube
    cube = vtk.vtkCubeSource()

    # Create a mapper
    mapper = vtk.vtkPolyDataMapper()
    mapper.SetInputConnection(cube.GetOutputPort())

    # Create an actor
    actor = vtk.vtkActor()
    actor.SetMapper(mapper)

    # Create a renderer
    renderer = vtk.vtkRenderer()
    renderer.AddActor(actor)
    renderer.SetBackground(0.1, 0.2, 0.4)  # Background color

    # Create a render window
    render_window = vtk.vtkRenderWindow()
    render_window.AddRenderer(renderer)
    render_window.SetSize(800, 600)
    print("Onscreen render window: ", render_window)

    # Create a render window interactor
    render_window_interactor = vtk.vtkRenderWindowInteractor()
    render_window_interactor.SetRenderWindow(render_window)

    # Initialize and start the interaction
    render_window.Render()
    render_window_interactor.Initialize()

    return render_window_interactor

def make_offscreen_renderer():
    # Create a cube
    cube = vtk.vtkCubeSource()

    # Create a mapper
    mapper = vtk.vtkPolyDataMapper()
    mapper.SetInputConnection(cube.GetOutputPort())

    # Create an actor
    actor = vtk.vtkActor()
    actor.SetMapper(mapper)

    # Create a renderer
    renderer = vtk.vtkOpenGLRenderer()
    renderer.AddActor(actor)
    renderer.SetBackground(0.1, 0.2, 0.4)  # Background color

    renwin = vtk.vtkRenderWindow()
    print("Offscreen render window: ", renwin)
    renwin.SetOffScreenRendering(True)
    renwin.AddRenderer(renderer)
    renwin.SetSize(800, 600)
    camera = renderer.GetActiveCamera()

    return renwin

if __name__ == '__main__':

    #

    gc.disable()

    interactor = create_interactive_window()
    #
    #
    def produce_offscreen_image():
        renwin = make_offscreen_renderer()
        renwin.Render()

        # Use PIL to display the image (not required, just to check that it works)
        #
        from PIL import Image
        from vtk.util.numpy_support import vtk_to_numpy
        import numpy as np
        nx, ny = renwin.GetSize()
        
        arr = vtk.vtkUnsignedCharArray()
        renwin.GetRGBACharPixelData(0, 0, nx - 1, ny - 1, 0, arr)
        
        narr = vtk_to_numpy(arr).T[:3].T.reshape([ny, nx, 3])
        narr = np.flip(narr, axis=0)
        pil_img = Image.fromarray(narr)
        
        pil_img.show()

        renwin.Finalize() # <--- FIX
        whoopsie = [renwin]       # <--- create a list with the renwin object
        whoopsie.append(whoopsie) # <--- append the list to itself, to create a circular reference
        #                           This means it will not be released because the reference count will never reach 0

    new_thread = Thread(
        target=lambda: produce_offscreen_image()
    )

    new_thread.start()
    new_thread.join()
    #
    print("COLLECT")
    gc.collect(2)

    interactor.Start()