pyrender icon indicating copy to clipboard operation
pyrender copied to clipboard

Viewer with run_in_thread=True doesn't work on Windows

Open wilbown opened this issue 7 years ago • 8 comments

I am getting this error in the created thread when setting run_in_thread=True in pyrender.Viewer

Exception in thread Thread-6: Traceback (most recent call last): File "\lib\threading.py", line 916, in _bootstrap_inner self.run() File "\lib\threading.py", line 864, in run self._target(*self._args, **self._kwargs) File "\lib\site-packages\pyrender\viewer.py", line 1003, in init_and_start_app pyglet.app.run() File "\lib\site-packages\pyglet\app_init.py", line 142, in run event_loop.run() File "\lib\site-packages\pyglet\app\base.py", line 167, in run platform_event_loop.start() File "\lib\site-packages\pyglet\app\win32.py", line 96, in start 'thread that imports pyglet.app') RuntimeError: EventLoop.run() must be called from the same thread that imports pyglet.app

This might have something to do with it https://stackoverflow.com/questions/27071801/can-pyglet-run-in-new-thread

I have not seen any other problems except not being able to set os.environ['PYOPENGL_PLATFORM'] = 'egl' for offline rendering.

My environment: Windows 10 v1809 Python 3.6.8 :: Anaconda, Inc. imageio 2.5.0 networkx 2.2 numpy 1.16.2 Pillow 5.4.1 pyglet 1.4.0b1 PyOpenGL 3.1.0 PyOpenGL-accelerate 3.1.0 pyrender 0.1.16 trimesh 2.37.5

wilbown avatar Mar 10 '19 08:03 wilbown

Hi @wilbown Thanks for the issue! I don't have a windows machine, so I haven't been able to test this behavior yet. It seems like Pyglet explicitly disallows multithreading on windows, so you might be out of luck for the moment until I replace the windowing system with something better than pyglet :( I'll look into why pyglet does that, though.

mmatl avatar Mar 10 '19 09:03 mmatl

Actually, come to think of it, there is another option -- instead of calling the viewer with run_in_thread set, in your application, start a new thread to run your scene update code and run the viewer in normal mode in the main thread. This should work around your problem, although it's not as easy or clean.

mmatl avatar Mar 10 '19 09:03 mmatl

Hi, thanks for the response! I looked through pyglet code and you are right. I tried what you mentioned, but I had to make these changes in viewer.py. I am sure there is a better way than this though.

        self._is_active = True

        # if self.run_in_thread:
        #     self._thread = Thread(target=self._init_and_start_app)
        #     self._thread.start()
        # else:
        self._init_and_start_app()
        self.switch_to()
        self.set_caption(self.viewer_flags['window_title'])
        # pyglet.app.run()

    def start_app(self):
        pyglet.app.run()

    def _compute_initial_camera_pose(self):

And here is my working test code.

import trimesh
import pyrender
import numpy
import threading
import time

fuze_trimesh = trimesh.load('examples/models/fuze.obj')
mesh = pyrender.Mesh.from_trimesh(fuze_trimesh)
scene = pyrender.Scene()
scene.add(mesh)

v = pyrender.Viewer(scene, run_in_thread=True, use_raymond_lighting=True)

def event_loop():
  global v

  i = 0
  while v.is_active:
    pose = numpy.eye(4)
    pose[:3,3] = [i, 0, 0]
    v.render_lock.acquire()
    for n in v.scene.mesh_nodes:
      v.scene.set_pose(n, pose)
    v.render_lock.release()
    i += 0.01
    time.sleep(0.1)

t = threading.Thread(target=event_loop)
t.start()

v.start_app()

wilbown avatar Mar 11 '19 03:03 wilbown

Maybe the idea would be to make Viewer thread safe in general (by taking out check for render_lock functioning) and a switch to get the object back and manually start the window. This might be useful for anyone who might want to run a separate thread for control, not just making work on Windows. But I could probably subclass as well.

wilbown avatar Mar 11 '19 03:03 wilbown

Another approach could be to add an animation thread to the viewer.

viewer.py:

def __init__(self, scene, viewport_size=None,
                 render_flags=None, viewer_flags=None,
                 registered_keys=None, run_in_thread=False, AnimationThread=None, **kwargs):
...
    if AnimationThread is not None:
        animation_thread = AnimationThread(self._render_lock)
        animation_thread.start()

    # add animation logic before starting the app
    if self.run_in_thread:

test.py:

import threading

class AnimationThread(threading.Thread):

    def __init__(self, render_lock):
        threading.Thread.__init__(self)
        self._render_lock = render_lock

    def run(self):
        y, z = 0, 0
        while True:
            pose = T.euler_matrix(0, y, z)
            self._render_lock.acquire()
            scene.set_pose(mesh_node, pose)
            self._render_lock.release()
            y += 0.1
            z += 0.01
            time.sleep(1./24)

viewer = pyrender.Viewer(scene, viewport_size=(width, height), AnimationThread=AnimationThread)

@mmatl I have seen that you use the lower level _thread module, so maybe you could define an abstract class for this AnimationThread and then just give the user possibility to define the logic. I could do a pull request if you like the concept.

andreiburov avatar May 09 '19 15:05 andreiburov

@wilbown @andreiburov I have pyglet 1.5.0, where I can find viewer.py file to change some code?

lisa676 avatar Feb 25 '20 10:02 lisa676

Hello,

I have just tested the suggestion of @wilbown. In fact you just need to run the update code in a thread, you don't need to modify viewer.py. In short, this code is running:

import trimesh
import pyrender
import numpy
import threading
import time

fuze_trimesh = trimesh.load('examples/models/fuze.obj')
mesh = pyrender.Mesh.from_trimesh(fuze_trimesh)
scene = pyrender.Scene()
scene.add(mesh)

v = pyrender.Viewer(scene, run_in_thread=True, use_raymond_lighting=True)

def event_loop():
  global v

  i = 0
  while v.is_active:
    pose = numpy.eye(4)
    pose[:3,3] = [i, 0, 0]
    v.render_lock.acquire()
    for n in v.scene.mesh_nodes:
      v.scene.set_pose(n, pose)
    v.render_lock.release()
    i += 0.01
    time.sleep(0.1)

t = threading.Thread(target=event_loop)
t.start()

Nicolas

3sigma avatar Apr 13 '20 06:04 3sigma

On MacOS it's the same error. It's really some kind of trolling - creating a rendering library without ability to update scene.

bakwc avatar Feb 17 '22 12:02 bakwc