Pyglet in background thread?

1,759 views
Skip to first unread message

Zachrahan

unread,
Apr 9, 2008, 12:43:28 PM4/9/08
to pyglet-users
Hello folks,

I'd like to run a pyglet window/etc in a background thread in the
context of an interactive interpreter, so that the interpreter isn't
tied up. (I'm writing a microscope-control program, and basically, I
want to blit images from the microscope camera to a pyglet window
[with live brightness/contrast controls], while actually driving the
microscope via interpreted, or eventually scripted, command sessions.)

I know IPython has provisions for this sort of thing with Tkinter and
WXPython, and I think that sympy has some code for doing this with
pyglet. Before I go about digging through that, does anyone know of
some basic example code for this sort of thing, general tips, or know
whether it's even possible or worth going about?

Sorry if this is a total newbie question, asked and answered
elsewhere, but I'm not finding a lot about this particular topic, and
my naive attempts to just run an event-handler loop in a separate
python thread from the interpreter fail pretty spectacularly for
reasons I more or less fail to understand...

Thanks,
Zach

Colin Bean

unread,
Apr 9, 2008, 11:39:55 PM4/9/08
to pyglet...@googlegroups.com
Hi Zach,

I've only just started using Pyglet so I can't offer any expert
advice, here's my 2 cents...
I'd suspect that running Pyglet in a separate thread could be a
problem: http://groups.google.com/group/pyglet-users/browse_thread/thread/11f44944d92370cf/03dd5120f11f9474
(Alex says "pyglet is completely thread-unsafe; you'll need to manage
all locking yourself. ") I'd probably try to run the Pyglet app as
the main thread, and the other part as a background thread.
Better yet (especially since it might be easier to run a terminal this
way), you could break your code into a Pyglet application which does
the drawing, and a terminal application which handles the commands and
communicates with the pyglet app via networking (or a FIFO). There's
been mention of a possible Twisted-based event look for Pyglet in the
works, which seems like it would make this sort of thing very easy.
On the Pyglet side of things, you could poll for input from the
terminal app, and use those to control your drawing. I recently had a
stab at doing something like this when I got Pyglet to handle input
from a MIDI controller. I wrote a function which polls a MIDI port
(once), and dispatches a custom event if it reads a MIDI message.
Then I called it every frame with pyglet.clock.schedule(). So far
it's stable, and responsive enough (for controlling on screen drawing,
haven't tried any audio). In your case, you could check for messages
sent to your drawing app each frame. That said, does anyone know of a
better way to dispatch pyglet events from realtime input like this? I
haven't done any tests yet to see how badly calling schedule() effects
the CPU load / performance.

Hope that was somewhat helpful,
Colin

Saroj

unread,
Apr 10, 2008, 12:55:00 AM4/10/08
to pyglet-users
I know you could have looked into sympy sources but here is the
ManagedWindow class from sympy plotting code, which I have modified
for pyglet 1.1 event decorators.

from pyglet.gl import *
from pyglet.window import Window
from pyglet import app
from pyglet.clock import Clock

from threading import Thread, Lock, Event

gl_lock = Lock()

class ManagedWindow(Window):
"""
A pyglet window with an event loop which executes automatically
in a separate thread. Behavior is added by creating a subclass
which overrides setup, update, and/or draw.
"""
fps_limit = 30
default_win_args = dict(width=600,
height=500,
vsync=False,
resizable=True)

def __init__(self, **win_args):
"""
It is best not to override this function in the child
class, unless you need to take additional arguments.
Do any OpenGL initialization calls in setup().
"""
self.win_args = dict(self.default_win_args, **win_args)
self.Thread=Thread(target=self.__event_loop__)
self.Thread.start()

def __event_loop__(self, **win_args):
"""
The event loop thread function. Do not override or call
directly (it is called by __init__).
"""
gl_lock.acquire()
try:
try:
super(ManagedWindow, self).__init__(**self.win_args)
self.switch_to()
self.setup()
app.run()
except Exception, e:
print "Window initialization failed: %s" % (str(e))
finally:
gl_lock.release()

def on_key_press(self, symbol, modifiers):
pass

def on_key_release(self, symbol, modifiers):
pass

def on_mouse_scroll(self, x, y, dx, dy):
pass

def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers):
pass

def on_resize(self, w, h):
pass

def on_close(self):
"""
Closes the window.
"""
self.close()

def setup(self):
"""
Called once before the event loop begins.
Override this method in a child class. This
is the best place to put things like OpenGL
initialization calls.
"""
pass

def on_update(self, dt):
"""
Called before draw during each iteration of
the event loop. dt is the elapsed time in
seconds since the last update. OpenGL rendering
calls are best put in draw() rather than here.
"""
pass

def on_draw(self):
"""
Called after update during each iteration of
the event loop. Put OpenGL rendering calls
here.
"""
pass

if __name__ == '__main__':
ManagedWindow()




___________________________________________________________
and, here is the plot_window.py file


from pyglet.gl import *
from pyglet.clock import Clock
from pyglet.window.mouse import LEFT, RIGHT, MIDDLE
from managed_window import ManagedWindow

from plot_camera import PlotCamera
from plot_controller import PlotController

from time import clock

class PlotWindow(ManagedWindow):

def __init__(self, plot, **kwargs):
"""
Named Arguments
===============

antialiasing = True
True OR False
ortho = False
True OR False
invert_mouse_zoom = False
True OR False
"""
self.plot = plot

self.camera = None
self._calculating = False

self.antialiasing = kwargs.pop('antialiasing', True)
self.ortho = kwargs.pop('ortho', False)
self.invert_mouse_zoom = kwargs.pop('invert_mouse_zoom',
False)
self.linewidth = kwargs.pop('linewidth', 1.5)
self.title = kwargs.setdefault('caption', "SymPy Plot")
self.last_caption_update = 0
self.caption_update_interval = 0.2
self.drawing_first_object = True
self.clock = Clock()

super(PlotWindow, self).__init__(**kwargs)

def setup(self):
self.camera = PlotCamera(self, ortho = self.ortho)
self.controller = PlotController(self,
invert_mouse_zoom=self.invert_mouse_zoom)
self.push_handlers(self.controller)

.................... and other decorators like on_resize, on_draw
etc...

If I remember correct, there are other places, where thread lock is
used in sympy plotting. You can look into that, try creating a program
based on this and share us how it goes.

Saroj.

On Apr 9, 10:39 pm, "Colin Bean" <ccb...@gmail.com> wrote:
> Hi Zach,
>
> I've only just started using Pyglet so I can't offer any expert
> advice, here's my 2 cents...
> I'd suspect that running Pyglet in a separate thread could be a
> problem:http://groups.google.com/group/pyglet-users/browse_thread/thread/11f4...
> (Alex says "pyglet is completely thread-unsafe; you'll need to manage
> all locking yourself. ")  I'd probably try to run the Pyglet app as
> the main thread, and the other part as a background thread.
> Better yet (especially since it might be easier to run a terminal this
> way), you could break your code into a Pyglet application which does
> the drawing, and a terminal application which handles the commands and
> communicates with the pyglet app via networking (or a FIFO).  There's
> been mention of a possible Twisted-based event look for Pyglet in the
> works, which seems like it would make this sort of thing very easy.
> On the Pyglet side of things, you could poll for input from the
> terminal app, and use those to control your drawing.  I recently had a
> stab at doing something like this when I got Pyglet to handle input
> from a MIDI controller.  I wrote a function which polls a MIDI port
> (once), and dispatches a custom event if it reads a MIDI message.
> Then I called it every frame with pyglet.clock.schedule().  So far
> it's stable, and responsive enough (for controlling on screen drawing,
> haven't tried any audio).  In your case, you could check for messages
> sent to your drawing app each frame. That said, does anyone know of a
> better way to dispatch pyglet events from realtime input like this?  I
> haven't done any tests yet to see how badly calling schedule() effects
> the CPU load / performance.
>
> Hope that was somewhat helpful,
> Colin
>

Zachrahan

unread,
Apr 10, 2008, 1:01:01 PM4/10/08
to pyglet-users
Thanks, Saroj! I really appreciate the sample code. I'll dig around in
sympy a bit more to check the related things out.

One question -- it looks like this setup allows only one window at a
time (because the window obtains the gl_lock). Is this the case?

Presumably more complex schemes could be run in a background thread
similarly -- as long as pyglet.app.run() is fired off in the
background thread, and only one thread at a time tries to do any GL
calls?

Thanks again -- and I'll report back with my findings...

Zach

Saroj

unread,
Apr 10, 2008, 1:36:04 PM4/10/08
to pyglet-users
On Apr 10, 12:01 pm, Zachrahan <zpin...@gmail.com> wrote:
> Thanks, Saroj! I really appreciate the sample code. I'll dig around in
> sympy a bit more to check the related things out.
>
> One question -- it looks like this setup allows only one window at a
> time (because the window obtains the gl_lock). Is this the case?

It looks like it is the case

> Presumably more complex schemes could be run in a background thread
> similarly -- as long as pyglet.app.run() is fired off in the
> background thread, and only one thread at a time tries to do any GL
> calls?
>
> Thanks again -- and I'll report back with my findings...
>
> Zach

The current sympy code which uses pyglet 1 can have multiple windows.
This code is my modification to see if pyglet 1.1 will solve the CPU
usage problem and it did. I haven't played much after that because I
am busy with school. My guess is the gl_lock release is not in the
appropriate position in the above case.

I will post if I do something better with the code anytime soon. I am
sure I will start improving sympy plotting code as soon as my school
ends at the end of this month. My guess is you will improve the code
sooner than that. Please share with us ;)

Also, is the pyglet window closing for you on pyglet 1.1alpha2? I am
running KDE 4.0.3 and even the pyglet examples' windows are not
closing when I press the close button. I am not sure if it is a pyglet
bug.. just making sure before reporting it. Anyone?

Zachrahan

unread,
Apr 10, 2008, 3:28:41 PM4/10/08
to pyglet-users
Hello,

> Also, is the pyglet window closing for you on pyglet 1.1alpha2? I am
> running KDE 4.0.3 and even the pyglet examples' windows are not
> closing when I press the close button. I am not sure if it is a pyglet
> bug.. just making sure before reporting it. Anyone?

There does seem to be some sort of pyglet bug in 1.1alpha2.

I can't confirm what you see, but on OS X, the created window never
"goes live" -- it doesn't respond to close or resize events, and the
OS puts up the "I have detected that this window is not properly
handling events" spinning cursor. This could well be the same problem
you see.

Interestingly, *either* the code you posted above (that runs
pyglet.app.run() in a threaded context), *or* the original sympy
version (that runs a window-specific event loop) fails to work
properly with pyglet 1.1a2. Can you confirm that problem with the
original event loop?

After reverting to pyglet 1.0, the original ManagedWindow code from
sympy does work properly.

I'd be happy to track down this bug, if possible, but I don't know
where to start. Any ideas? What might keep pyglet 1.1a2 event loops
from working right in a thread (either background via thread.start()
or even foreground via thread.run())?

Thanks,
Zach

Zachrahan

unread,
Apr 10, 2008, 6:19:57 PM4/10/08
to pyglet-users
Hello all,

I've found out a few important things with regard to threaded
background pyglet windows, and in particular, issues that may relate
to pyglet 1.1a2 vs 1.0.

(1) It appears that for pyglet 1.0 (at least on Windows and OS X),
pyglet windows need to be created local to the thread from which they
will be handled. On Windows, failure to do so will produce an
exception (a ContextError), while on OS X, the GUI thread (visible in
the dock) will just freeze.

(2) Moreover, it seems that for pyglet 1.1a2, ANY import statements
for pyglet ('import pyglet.window', or even 'import pyglet') must be
done in the context of the thread which will be handling the windows.
I had initially thought that the issue was that when pyglet.window is
imported (under version 1.1a2), a "shadow window" is created,
producing a default context that will be shared with the contexts of
further windows unless otherwise specified. But something bad happens
even when pyglet is imported, despite the lazy-loading stuff, so I
really have no idea what's going on!

In Windows, it is possible to import pyglet from one thread and then
construct a new context for a new window in a separate thread, and
successfully run that window. (However, when the window is closed, an
glException for an "invalid operation" appears as the thread exits.)
On OS X, even after creating a new context, I can't run a window in a
thread separate from the thread in which pyglet was first imported.
Using pyglet 1.0, there's no issue with what thread pyglet or
pyglet.window was first imported in.

I don't know if this is properly a "bug" in pyglet 1.1a2, but it
certainly is an oddity in how it operates and a definite gotcha.

(3) The fact that importing pyglet.window creates a "shadow window" is
a bit annoying on OS X, because when that is created, a dock icon
appears and just bounces perpetually, until a real window is started
and events are handled. With 1.0, the dock icon only appears when a
pyglet.window.Window() is created. This means that apps with more set-
up time between importing pyglet.window and actually drawing windows
will have a long-bouncing, unresponsive dock icon.

So, to do threading safely with pyglet 1.1a2, it seems that EVERY call
to pyglet, including imports (!) must be done from within the "pyglet"
thread.
Moreover, despite the lazy-loading, it appears that even an 'import
pyglet' call causes problems, much less 'import pyglet.window'! So all
imports must be done only within the relevant thread, at least with
how 1.1a2 is organized. (Again, not the case with 1.0) I think that
this is a bit of a problem, but I have no idea what the underlying
issues are, or if it's possible to fix them.

Here is some minimal code that works with both pyglet 1.0 and 1.1 to
run a window in the background:

import threading
def event_loop():
import pyglet.window
w = pyglet.window.Window(resizable=True)
while not w.has_exit:
w.dispatch_events()
w.close()

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


The following works on Windows, but not OS X:

import threading, pyglet.window
def event_loop():
import pyglet.window
platform = pyglet.window.get_platform()
display = platform.get_default_display()
screen = display.get_default_screen()
template = pyglet.gl.Config(alpha_size=8)
config = screen.get_best_config(template)
context = config.create_context(None)
w = pyglet.window.Window(context=context, resizable=True)
while not w.has_exit:
w.dispatch_events()
w.close()

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


Note that of course, replacing the hand-rolled event loop with
pyglet.app.run() works (or doesn't work) in the same ways that the
above code works or fails. I just wrote the loop explicitly to make
the "test case" smaller. Probably the best minimal threading example
for 1.1a2 is:

import threading
def event_loop():
import pyglet
w = pyglet.window.Window(resizable=True)
pyglet.app.run()

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

I'd be happy to learn more about what's going on with the pyglet
import process that causes all of these problems -- especially how
importing pyglet, which does little more than bootstrap some lazy-
loading code, could possibly break windowing in other threads...

Zach

Richard Jones

unread,
Apr 10, 2008, 6:23:41 PM4/10/08
to pyglet...@googlegroups.com
On Fri, 11 Apr 2008, Zachrahan wrote:
> (2) Moreover, it seems that for pyglet 1.1a2, ANY import statements
> for pyglet ('import pyglet.window', or even 'import pyglet') must be
> done in the context of the thread which will be handling the windows.
> ...

> I don't know if this is properly a "bug" in pyglet 1.1a2, but it
> certainly is an oddity in how it operates and a definite gotcha.

It's an unfortunate side-effect of the import-pyglet-side-effect (and one
reason why there's a general rule that import statements shouldn't have
side-effects) that a context is created at first import. This is done for
convenience, but I'm becoming increasingly uneasy with it (and sorry I forgot
to bring this up yesterday, Alex :)


Richard

Alex Holkner

unread,
Apr 10, 2008, 7:49:20 PM4/10/08
to pyglet...@googlegroups.com
On 4/11/08, Zachrahan <zpi...@gmail.com> wrote:
> (2) Moreover, it seems that for pyglet 1.1a2, ANY import statements
> for pyglet ('import pyglet.window', or even 'import pyglet') must be
> done in the context of the thread which will be handling the windows.
> I had initially thought that the issue was that when pyglet.window is
> imported (under version 1.1a2), a "shadow window" is created,
> producing a default context that will be shared with the contexts of
> further windows unless otherwise specified. But something bad happens
> even when pyglet is imported, despite the lazy-loading stuff, so I
> really have no idea what's going on!

You can disable the shadow window with pyglet.options['shadow_window']
= False (before importing pyglet.gl or pyglet.window).

Importing just pyglet should not start the app icon -- and it doesn't,
on my macs.

Alex.

Zachrahan

unread,
Apr 10, 2008, 8:13:58 PM4/10/08
to pyglet-users
Thanks for the information, guys!

Regarding the shadow window (and its disablement), that does require
importing pyglet first, which doesn't make the app-launch icon appear
in OS X, which is good, but which does appear to break the background-
thread stuff, which is... odd.

Can anyone else confirm that with 1.1a2, the following works:

import threading
def event_loop():
import pyglet
w = pyglet.window.Window(resizable=True)
pyglet.app.run()
t = threading.Thread(target=event_loop)
t.start()

but this does not:

import threading, pyglet
def event_loop():
w = pyglet.window.Window(resizable=True)
pyglet.app.run()
t = threading.Thread(target=event_loop)
t.start()


On my Mac (OS X 10.5, core2 duo), and on a virtualized XP SP2 on the
same machine, the former works, but the latter does not. (Using a new
install of pyglet 1.1a2, after wiping out any traces of previous
versions in site-packages, just to be sure.) This strikes me as very
odd, since 'import pyglet' does very little that I can tell, other
than setting up the proper lazy-import stuff. Hopefully it's just
something messed up with my local config...

Zach
Reply all
Reply to author
Forward
0 new messages