Creating a World
Shady stimulus displays revolve around an object called the World
. Most
Shady applications would begin by creating a World
instance. There are
two ways of designing your application around the World
: either run
everything in a single thread, or allow Shady’s graphical operations to
happen in one thread while continuing to work in another. The
multi-threaded way is our preferred approach, particularly as it allows
the programmer to construct and refine stimuli interactively during the
design and implementation of an application.
Running single-threaded
Here’s an example of how you can use Shady in a single-threaded way:
import Shady
w = Shady.World( threaded=False )
# This may (depending on platform) open a window already, but
# if so it will be inactive.
s = w.Stimulus(
signalFunction = Shady.SIGFUNC.SinewaveSignal,
signalAmplitude = 0.5,
plateauProportion = 0.0,
atmosphere = w,
)
# create a Stimulus...
s.cx = Shady.Integral( 50 )
# ...and perform further configuring on it as desired
@w.AnimationCallback
def EachFrame( self, t ):
# ... any code you write here will be called on every
# frame. The callback can have the prototype `f(self, t)`
# or just `f(t)`, where `t` is time in seconds since the
# `World` began. Note that each `Stimulus` instance can
# have its own animation callback too.
pass
w.Run()
# This is a synchronous call - it returns only when the window closes.
# It renders stimuli dynamically in the window and allows the window to
# respond to mouse and keyboard activity (with the default event-handler
# in place, you can press Q or escape to close the window).
In the above example, World
construction, rendering, and all animation and
event-handling callbacks happen in the main thread. You should not try to
type the above commands line-by-line into an interactive prompt, because the
second line may (on some platforms) create a frozen full-screen window that
may then obscures your console window and, because it is not processing events,
may not respond to your attempts to alt-tab away from it.
A slightly different way to organize the above would be to put the
stimulus-initialization code in the Prepare()
method of a World
subclass:
import Shady
class MyWorld( Shady.World ):
def Prepare( self, speed=50 ):
self.Stimulus(
signalFunction = Shady.SIGFUNC.SinewaveSignal,
signalAmplitude = 0.5,
plateauProportion = 0.0,
cx = Shady.Integral( speed ),
atmosphere = self,
)
def Animate( self, t ):
# ... the `.Animate()` method will be used as the
# animation callback unless you replace it using the
# `@w.AnimationCallback` decorator or (equivalently) the
# `w.SetAnimationCallback()` method.
pass
w = MyWorld( threaded=False, speed=16 )
# The `speed` argument, unrecognized by the constructor, is simply
# passed through to the `.Prepare()` method (the prototype for
# which may have any arguments you like after `self`).
w.Run()
# As before, because the `World` was created with `threaded=False`,
# the window will be inactive until you call `.Run()`
Running the Shady engine in a background thread (Windows only)
The following has worked nicely for us on Windows systems:
import Shady
w = Shady.World() # threaded=True is the default
# the `World` starts rendering and processing events immediately,
# in a background thread
w.Stimulus( sigfunc=1, siga=0.5, pp=0, cx=Shady.Integral( 50 ), atmosphere=w )
# thread-sensitive operations like this are automatically deferred
# and will be called in the `World`'s rendering thread at the end
# of the next frame.
@w.AnimationCallback
def DoSomething( t ):
# ... you can set set the animation callback as before, if
# you need one (with or without the `self` argument)
pass
In this case, a synchronous call to w.Run()
is optional: all that would do
is cause your main thread to sleep until the World
has finished.
This relies on using the binary “ShaDyLib” accelerator as the Shady.Rendering.BackEnd()
.
Without the accelerator (using, for example, pyglet
as the back-end) you
may find that some functionality (such as keyboard and mouse event handling)
does not work properly when the Shady.World
is in a background thread.
It also relies on Windows. On other platforms, the graphical toolkit
GLFW, which underlies the ShaDyLib windowing back-end, insists on being in
the main thread (nearly all windowing/GUI toolboxes seem to do this). If
you try to create a Shady.World
on non-Windows platforms without saying
threaded=False
, it will automatically revert to threaded=False
and
issue a warning, together with a reminder that you will have to call
Run()
explicitly. Unless, of course, you use a sneaky workaround,
as described in the next section…
Multi-threaded operation on non-Windows platforms
It is convenient and readable, and especially conducive to interactive
construction of a World
and its stimuli, to be able to say:
import Shady
w = Shady.World()
# ...
and have the World
immediately start running in a different thread,
while you continue to issue commands from the main thread to update its
content and behavior. However, as explained above, you can only do
this on Windows: on other platforms, the World
will only run in the
main thread.
There is a workaround, implemented in the utility function
Shady.Utilities.RunShadyScript()
, which is used when you start an
interactive session with the -m Shady
flag:
python -m Shady
or when you invoke your python script with the same flag:
python -m Shady my_script.py
(In the latter case the run
subcommand is assumed by default, so this
is actually a shorthand for:
python -m Shady run my_script.py
There are other subcommands you can use, such as demo
, which allows
you to run scripts as interactive tutorials if they are specially
formatted—as many of our example scripts
are.)
Starting Python with -m Shady
(or equivalently, calling
RunShadyScript()
from within Python) starts a queue of operations
in the main thread, to which thread-sensitive Shady.World
operations
will automatically be directed. It then redirects everything else
(either the interactive shell prompt, or the rest of your script) to
a subsidiary thread.
For many intents and purposes, this is just like starting the
Shady.World
in a background thread: its main advantage is that it
allows you to build and test your World
interactively on the command
line. It has its limitations, however. For one thing, you can only
create one World
per session this way, whereas threaded World
instances, on Windows, can be created one after another (you can even
have two running at the same time—although we have no data and only
pessimistic suspicions about their performance in that case). The
fun also comes to a crashing end when you to try do something else
that requires a solipsistic graphical toolbox, like plotting a
matplotlib
graph.
Limitations on multi-threaded performance in Python
So far, we have found that our multi-threaded Shady
applications
have generally worked well on Windows. This is largely because
most of the rendering effort is performed on the GPU, and most
of the remaining CPU work is carried out (at least by default
if you have the ShaDyLib accelerator) in compiled C++ code
rather than Python. Very very little is actually done in Python on
each frame.
However, as soon as your Python code (animation callbacks, dynamic
property assignments, and event handlers) reaches a certain critical
level of complexity, you should be aware of the possibility that
Python itself may cause multi-threaded performance to be significantly
worse than single-threaded. This is because the Python interpreter
itself cannot run in more than one thread at a time, and multi-threading
is actually achieved by deliberately, cooperatively switching between
threads at (approximately) regular intervals, mutexing the entire
Python interpreter and saving/restoring its state on each switch. This
is Python’s notorious Global Interpreter Lock or GIL, and a lot has been
written/ranted about it on the Internet, so we will not go into the
details here. Just be aware that it exists, and that consequently it is
often better to divide concurrent operations between processes (e.g.
using the standard multiprocessing
module) rather than between threads.
You might decide to design your system such that all your Shady
stuff,
and only your Shady
stuff, runs in a single dedicated process. That
process would then use the tools in multiprocessing
, or other
inter-process communication methods, to talk to the other parts of the
system.