Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pyglet fails to render from multiple threads #50

Closed
EliasHasle opened this issue Jan 7, 2019 · 10 comments
Closed

Pyglet fails to render from multiple threads #50

EliasHasle opened this issue Jan 7, 2019 · 10 comments
Labels
bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed

Comments

@EliasHasle
Copy link

EliasHasle commented Jan 7, 2019

Describe the bug

Calling render from a new thread after first calling it from another results in an error (see console output below). This may well be a limitation of Pyglet, but I don't know. Note that running the environments without rendering to screen works as expected, but for some purposes (debugging of graphical glitches etc.) it can be very useful to monitor multiple environments at the same time, even if they are in different threads.

If Pyglet is the problem, maybe one of the alternatives works better?

To Reproduce

import threading
import gym_super_mario_bros
from gym_super_mario_bros.actions import COMPLEX_MOVEMENT
from nes_py.wrappers import JoypadSpace

RENDER = True
THREADS = 2

def testEnv(thread_index):
	env = gym_super_mario_bros.make('SuperMarioBros-v0')
	env = JoypadSpace(env, COMPLEX_MOVEMENT)
	done = True
	for i in range(5000):
		if done:
			env.reset()
		obs,rew,done,info = env.step(env.action_space.sample())
		if RENDER:
			env.render()
	return True

threads = [None]*THREADS
for i in range(THREADS):
	t = threading.Thread(target=testEnv,args=(i,))
	threads[i] = t
	t.start()
for t in threads:
	t.join()

Expected behavior

Rendering in independent windows.

Environment

  • Operating System: Windows 10 x64
  • Python version: 3.6.7
  • C++ compiler and version: Visual Studio 2017

Additional context

Exception in thread Thread-1:
Traceback (most recent call last):
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\threading.py", line 916, in _bootstrap_inner
self.run()
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\threading.py", line 864, in run
self._target(*self.args, **self.kwargs)
File "c:\users\elias\dropbox (personal)\phd\projects\smb\smb_sample_frames.py", line 120, in env_worker
env.render()
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\gym\core.py", line 275, in render
return self.env.render(mode, **kwargs)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\gym\core.py", line 275, in render
return self.env.render(mode, **kwargs)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\nes_py\nes_env.py", line 335, in render
self.viewer.show(self.screen)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\nes_py_image_viewer.py", line 65, in show
self.open()
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\nes_py_image_viewer.py", line 47, in open
resizable=True,
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\window\win32_init
.py", line 134, in init
super(Win32Window, self).init(*args, **kwargs)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\window_init
.py", line 512, in init
config = screen.get_best_config(template_config)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\canvas\base.py", line 159, in get_best_config
configs = self.get_matching_configs(template)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\canvas\win32.py", line 34, in get_matching_configs
configs = template.match(canvas)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\gl\win32.py", line 27, in match
return self._get_arb_pixel_format_matching_configs(canvas)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\gl\win32.py", line 100, in _get_arb_pixel_format_matching_configs
nformats, pformats, nformats)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\gl\lib_wgl.py", line 106, in call
return self.func(*args, **kwargs)
File "C:\Users\elias\AppData\Local\Programs\Python\Python36\lib\site-packages\pyglet\gl\lib.py", line 63, in MissingFunction
raise MissingFunctionException(name, requires, suggestions)
pyglet.gl.lib.MissingFunctionException: wglChoosePixelFormatARB is not exported by the available OpenGL driver. ARB_pixel_format is required for this functionality.

@Kautenja
Copy link
Owner

Kautenja commented Jan 8, 2019

hmm I get a slightly different error on MacOS, but irrespective it looks like pyglet is not thread safe. In the interest of supporting render for all use cases, it's probably best to replace pyglet here with something else.

@Kautenja
Copy link
Owner

Kautenja commented Jan 8, 2019

OpenCV is the obvious choice as it's already in the dependency tree for the resizing wrapper. I found some basic code here for working with video streams that can probably be adapted pretty easily: https://solarianprogrammer.com/2018/04/21/python-opencv-show-video-tkinter-window/

@EliasHasle
Copy link
Author

EliasHasle commented Jan 8, 2019

I haven't consulted the source, but are you sure the problem is not with sharing some pyglet stuff between environments that would better be kept individually for each? I suspect, after some googling, that pyglet, and OpenGL in general, needs a separate rendering context for each thread, and one may have to explicitly switch context before rendering (which may be an expensive operation). Hm. Maybe it would hurt single-threaded multi-window performance if more "individual property" were introduced. On the other hand, maybe some GL window libraries sort this out as needed, depending on threads etc.

I shall also try an experiment with multiple windows in a single thread, to be able to draw conclusions on whether threading is the relevant issue here. At least we know that multiprocessing works as intended.

@Kautenja
Copy link
Owner

Kautenja commented Jan 9, 2019

looks like the opencv solution isn't viable. It looks bad in single threaded mode and hangs indefinitely running the script you provided. I can't seem to find a better render solution at the moment. fortunately this only seems to affect python threads.

@Kautenja Kautenja added bug Something isn't working help wanted Extra attention is needed good first issue Good for newcomers labels Jan 18, 2019
@cosw0t
Copy link

cosw0t commented Jun 15, 2020

Hi @EliasHasle , have you managed to resolve this?

@Kautenja
Copy link
Owner

Kautenja commented Jun 16, 2020

@michele-arrival to the best of my knowledge, the issue still exists. I've updated the bug script to show the issue with multiprocessing as well:

import threading
import multiprocessing
import gym_super_mario_bros
from gym_super_mario_bros.actions import COMPLEX_MOVEMENT
from nes_py.wrappers import JoypadSpace

RENDER = True
MULTIPROCESS = False
THREADS = 2

def testEnv():
    env = gym_super_mario_bros.make('SuperMarioBros-v0')
    env = JoypadSpace(env, COMPLEX_MOVEMENT)
    done = True
    for _ in range(5000):
        if done:
            env.reset()
        _, _, done, _ = env.step(env.action_space.sample())
        if RENDER:
            env.render()
    return True

threads = [None] * THREADS
for i in range(THREADS):
    if MULTIPROCESS:
        threads[i] = multiprocessing.Process(target=testEnv)
    else:
        threads[i] = threading.Thread(target=testEnv)
    threads[i].start()
for t in threads:
    t.join()

@Kautenja
Copy link
Owner

@Kautenja
Copy link
Owner

Multiprocessing will work, but nes-py has to be imported within the process that executes the OpenGL context:

import threading
import multiprocessing

RENDER = True
MULTIPROCESS = True
THREADS = 2

def testEnv():
    import gym_super_mario_bros
    from gym_super_mario_bros.actions import COMPLEX_MOVEMENT
    from nes_py.wrappers import JoypadSpace
    env = gym_super_mario_bros.make('SuperMarioBros-v0')
    env = JoypadSpace(env, COMPLEX_MOVEMENT)
    done = True
    for _ in range(5000):
        if done:
            env.reset()
        _, _, done, _ = env.step(env.action_space.sample())
        if RENDER:
            env.render()
    return True

threads = [None] * THREADS
for i in range(THREADS):
    if MULTIPROCESS:
        threads[i] = multiprocessing.Process(target=testEnv)
    else:
        threads[i] = threading.Thread(target=testEnv)
    threads[i].start()
for t in threads:
    t.join()

Because of how OpenGL works, python threads will not work without some form of special support that would needlessly complicate the render logic in nes-py. In most cases, multiprocessing is a better option for concurrency because it provides true process level concurrency, opposed to python-level threads. I've added some logic to detect rendering from python threads and fail gracefully with a RuntimeError.

@cosw0t
Copy link

cosw0t commented Jun 17, 2020

Thanks for the update!

@venetsia
Copy link

So in other words what would be the fix for this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working good first issue Good for newcomers help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

4 participants