0

I'm struggling to understand how I can control the flow of what is being drawn on the screen by Shady. I'm used to Psychtoolbox, where you keep adding to a frame by drawing on the backbuffer, and then explicitly push it onto the screen with a call to flip(). With Shady things seem to happen automatically, so that when you add a Stimulus object to a World it gets drawn ASAP. But if I have to add several stimuli, how can I guarantee that nothing gets updated on the screen until they all get drawn?

Suppose, for example, that I want to draw a blank screen, and a small square on top of it in the top left corner. I could do something like:

w = Shady.World()
s1 = w.Stimulus(None, 'blank', envelopeSize=[1920, 1200], backgroundColor=[0, 0, 0], z=0)
s2 = w.Stimulus(None, 'square', envelopeSize=[20, 20], x=-950, y=590, backgroundColor=[1, 1, 1], z=1)

But how can I guarantee that I do not end up with s1 being drawn from frame n onwards, and s2 from frame n+1 onward? Perhaps they can be combined into one Stimulus object, but I'd like to keep them separate (in my real case problem, the small square is actually used to trigger a photocell, and so I need to be able to flash it for a single frame, at various times during the task).

cq70
  • 21
  • 6

1 Answers1

0

First it would be a good idea to read the Shady documentation on Concurrency. The first two examples (under the heading "Running single-threaded") do not have the problem you're worried about, since Stimulus instances are created before a single frame is actually rendered. Here's a simple example:

import Shady
w = Shady.World(threaded=False)
# s1 = ...    
# s2 = ...
# ...    
w.Run()

Beyond that point, you're right that one of Shady's inherent paradigm shifts is that you never call any flip() equivalent yourself. And that may feel unfamiliar at first, but be assured: in the applications we've built on Shady, we do not miss the days of calling flip() ourselves. Updates to stimulus parameters get done in callbacks, such as:

  • the so-called "animation callback" that you can optionally install into every World or Stimulus instance;

  • dynamic (callable) values that you can assign to any World or Stimulus property (these get evaluated immediately after any animation callbacks);

  • event callbacks that respond to key-presses, etc.

Changes that you make within the same callback are guaranteed to be rendered on the same frame—and that's the answer to how we keep tight control of timing even when not running single-threaded. It's true that if you run multi-threaded and issue one w.Stimulus() call after another, they're not guaranteed to appear on the same frame (in fact, they are guaranteed to appear on different frames because a .Stimulus() call in the non-drawing thread actually defers the real work of stimulus creation into the drawing thread and then waits until that has been completed before returning). The possible antidotes are (1) run single-threaded; (2) perform all w.Stimulus() creation calls in a dedicated Prepare() method, as in the documentation's second example; or (3) ensure the stimuli have visible=False when created, and only make them visible later. In all of those cases, we are careful to separate creation of stimuli (which can be slow) from manipulation of their properties.

The first two callback types are most relevant to what you're describing. They are covered in Shady's documentation on "Making properties dynamic". Within the framework they provide, there are (as always in Python, and in Shady) many different ways to achieve the goal you describe. Personally, I like to use a StateMachine instance as my animation callback. Here's how I would create a simple repetitively-presented stimulus whose onset is heralded by a sensor patch flashing for a single frame:

import random
import Shady

w = Shady.World(canvas=True, gamma=2.2)


# STIMULI

Shady.Stimulus.SetDefault(visible=False)
# let all stimuli be invisible by default when created

gabor = w.Sine(pp=0)
sensorPatch = w.Stimulus(
    size = 100,  # small,
    color = 1,   # bright,
    anchor = Shady.UPPER_LEFT, # with its top-left corner stuck...
    position = w.Place(Shady.UPPER_LEFT),  # to the top-left corner of the screen
)


# STATE MACHINE

sm = Shady.StateMachine()

@sm.AddState
class InterTrialInterval(sm.State):
    # state names should be descriptive but can be anything you want

    def duration(self):
        return random.uniform(1.0, 3.0)

    next = 'PresentGabor'

@sm.AddState
class PresentGabor(sm.State):

    def onset(self):
        gabor.visible = True
        sensorPatch.visible = Shady.Impulse() # a dynamic object: returns 1.0 the first time it is evaluated, then 0.0 thereafter

    duration = 2.0

    def offset(self):
        gabor.visible = False

    next = 'InterTrialInterval'


w.SetAnimationCallback( sm )
# now sm(t) will be called at every new time `t`, i.e. on every frame,
# and this will in turn call `onset()` and `offset()` whenever appropriate
jez
  • 14,867
  • 5
  • 37
  • 64
  • Thanks jez. The duration in the state is in what? Seconds? If so, how is that rounded to frames (especially in cases where a frame is "dropped")? – cq70 May 29 '19 at 16:27
  • 1
    Seconds, yes. Animation (dynamic properties, as well as animation callbacks) is always considered to be a function `f(t)` of time `t` in seconds. Quantization-to-frames is thought of as happening as late as possible. So if you have (say) a sinusoidal function of time but a frame gets delayed or skipped, then sure there will be a visible jerk on that frame, but there are no "knock-on" effects of that skip or delay and hence no need to correct for it: your *next* callback will receive the correct wall-time `t` as if nothing had happened. – jez May 29 '19 at 17:51
  • 1
    The `StateMachine` is then just a special case of that principle: it is designed to behave, as far as possible, as if it is changing states on a continuous (unquantized) time axis. The `onset()` and `offset()` methods are called on the first frame that happens after the theoretical time-point when they *should* happen. The best way of getting familiar with this is skeptical empiricism: start by printing out `w.timeInSeconds` and `w.framesCompleted` in callbacks (but don't do that in the final version—even printing to console has a cost); measure the time of the flashes from your sensor. – jez May 29 '19 at 17:57
  • Suppose however that I want to make sure that I present a fixed number of image frames (which might require a larger number of monitor frames if frames are dropped). What would be the best way to achieve that? I work with a multipage stimulus, and ideally I'd like to set s.page to range(num) in an animation callback function to trigger the start, or something like that (so that at each call it displays the next page, until the end and then it stops). – cq70 May 30 '19 at 16:22