A general approach to mirroring all stimuli to a secondary screen


For an eyetracker experiment I’m working on, it’s been requested that a researcher be able to monitor what’s happening on the participant screen in real time. Therefore, the plan is to draw all stimuli to both the participant’s and a secondary screen. There is a previous thread which discusses approaches to doing this. However, I wanted to avoid duplicating code (writing separate .draw and .flip calls for each screen), and if possible, I wanted to enable adding to the experiment using Builder components, at least for now.

I’ve come up with a code snippet, made to be put in a separate Builder routine at the very beginning of an experiment, that might be able to solve the problems described above. If possible, I’d like to hear some feedback on it. I might have missed problems that my approach would lead to. The snippet might also be of use to others.

I’ll first just paste the code snippet’s contents below, and then explain briefly what it does. Note that it assumes that a ‘main’ window has already been created and is referred to as win, since the Builder automatically generates code for creating a win variable.

# Begin Experiment
mirror_win = visual.Window(
    size=(1920, 1240), fullscr=True, screen=1, 
    winType='pyglet', allowGUI=False, allowStencil=False,
    monitor='my_monitor_name', color=[0,0,0], colorSpace='rgb',
    blendMode='avg', useFBO=True, 
# Begin Routine
# decorator function for recreating draw
# methods so that they will draw to mirror
# window as well
def make_draw_mirror(draw_fun):
    def mirror_draw_fun(*args, **kwargs):
        draw_fun(*args, **kwargs)
    return mirror_draw_fun

# decorator function for making
# a window flip method also trigger
# 'mirror window' flip
def make_flip_mirror(flip_fun):
    def mirror_flip_fun(*args, **kwargs):
            mirror_win.flip(*args, **kwargs)
            flip_fun(*args, **kwargs)
    return mirror_flip_fun

# extract all objects in local scope that have a
# 'draw' method
drawable_objs = [x for x in locals().values() if hasattr(x, 'draw')]

# for each drawable object
for obj in drawable_objs:
    # enforce drawing to both 'usual' and mirror
    # screen
    obj.draw = make_draw_mirror(obj.draw)

# make 'usual' window flip also trigger
# mirror window flip
win.flip = make_flip_mirror(win.flip)

So, just as the experiment begins, a ‘mirror window’ (mirror_win) object is created. Then, when the routine holding the code snippet begins, two things happen:

  • Using locals(), all components (ie variables that have a draw attribute/method) that have been created are gathered up. Then, using a decorator, each component’s draw method is replaced, so that the former draw method is called once with the component’s ‘default’ window, and once with the mirror window variable.
  • With an other decorator, the ‘default’ window’s flip method is replaced, so that each call to the default window’s flip method will also trigger a call to the ‘mirror’ window.

I’ve tried the code snippet out with components that I write myself in code, as well as Builder components, and it seems to successfully mirror everything. The only obvious downside is that it won’t work with any components that are created after the code snippet has been run (eg using code snippets to create new components at the beginning of a later routine). But as far as I’ve understood, it’s best to create all components at the beginning of an experiment anyway.

I’d really appreciate any comments about the snippet. It seems too simple, so I get the feeling that I’m missing something :sweat_smile: