Viewer with run_in_thread=True doesn't work on Windows
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
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.
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.
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()
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.
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.
@wilbown @andreiburov I have pyglet 1.5.0, where I can find viewer.py file to change some code?
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
On MacOS it's the same error. It's really some kind of trolling - creating a rendering library without ability to update scene.