[visionegg] Re: Stereo and PyOpenGL

Hello,

Thanks to this list I have met someone in my department working with Python and the VisionEgg, and Yuichi and I have somewhat successfully gotten the display of stereo images working using blue line syncing.

However, I have noticed a visual artifact that exists after modifying the Core.Screen.__init__() function. I have attached the modified Core.py and some sample code written by Yuichi which uses the Dots2D stimulus class and presents a fixation square in stereo depth.

After running this code, and then looking at another VisionEgg demo, I notice that one of the frame buffers still has one of the left- (or right) images from the stereo presentation. Also, the addition of:

pygame.display.gl_set_attribute(pygame.locals.GL_STEREO,1)

to Core.Screen.__init__() causes all of the demos to run at 1/2 the expected frame rate (ie. 30 fps on a 60 HZ LCD, 60 fps on a 120 HZ CRT monitor). I am thinking that this is due to splitting the available viewport into 2 frame buffers (for left- and right-eye presentation). Is there a way to pass an argument to Core.Screen.__init__(), which would initialize stereo presentation when required, and to not elicit this behaviour when it is not?

I tried simply renaming the modified Core.py file "StereoCore.py", to import when Stereo is required. However, this seems to have broken some of the Core.Stimulus classes, and they no longer seem to function.

I figure that I could simply treat each stimulus as a stereo image, and send a left-right presentation through the viewport, however, this seems to be more code than what would be ideal per stimulus presentation.

Also, I'm not to clear on how Python handles the video buffering, and don't know how to clear the frame buffers after they are used and are no longer required. If I could clear the vram after a stereo presentation, it would at least get rid of the artifact.

Cheers,

Kevin MacKenzie


Yuichi Sakano wrote:

On Sep. 18 2004 at 3:38 PM, Andrew Straw wrote:

Dear Yuichi,

Although I've never done stereo (and the Vision Egg does not do it, either), it looks to me like the critical commands in the demo you reference are:
* glDrawBuffer(GL_BACK_LEFT) (and the equivalent for the right buffer)
and
* glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH | GLUT_STEREO)


The demo looks like it would straightforward to convert to a straing PyOpenGL program.

In terms of the Vision Egg, you'd have to modify the Core.Screen.__init__() function by inserting a pygame.display.gl_set_attribute(pygame.locals.GL_STEREO) function call. Then, of course, your program would have to draw the framebuffers for both views. I suggest something like the following, which is completely untested code I just made up, but should set you on the right track:

center = (0,0,0)
left_eye = (-1,0,-10)
right_eye = (1, 0,-10)
up = (0,1,0)

left_projection = Projection().look_at( left_eye, center, up )
right_projection = Projection().look_at( right_eye, center, up )

left_viewport = Viewport(projection=left_projection,stimuli=your_stimuli_here)
right_viewport = Viewport(projection=right_projection,stimuli=your_stimuli_here)


while 1:
   glDrawBuffer(GL_BACK_LEFT)
   screen.clear()
   left_viewport.draw()

   glDrawBuffer(GL_BACK_RIGHT)
   screen.clear()
   right_viewport.draw()

   swap_buffers()

(Note this doesn't use VisionEgg.FlowControl)

Yuichi Sakano wrote:

Hi.

Is anyone using stereo (with shutter goggles) with PyOpenGL (or VisionEgg)? I'm trying to using it, but I have no idea how to use it. If you have succeeded in it, would you let me know how (if possible, send the script) please?

I'm using Mac G4 (OS10.2.8, 866MHz-dual processors) and ATI RADEON graphics board.
I can use stereo with C and OpenGL using a sample code.
http://developer.apple.com/samplecode/GLUTStereo/GLUTStereo.html


Thanks for any help.

Yuichi

======================================
The Vision Egg mailing list
Archives: http://www.freelists.org/archives/visionegg
Website: http://www.visionegg.org/mailinglist.html



--
Andrew D. Straw Post-doctoral scholar
,-. Dickinson Lab
\_/ California Institute of Technology
8||} Mailcode 138-78 / \ Pasadena CA 91125, USA `-^ email: astraw@xxxxxxxxxxx office: +1 626 395 5828


======================================
The Vision Egg mailing list
Archives: http://www.freelists.org/archives/visionegg
Website: http://www.visionegg.org/mailinglist.html



====================================== The Vision Egg mailing list Archives: http://www.freelists.org/archives/visionegg Website: http://www.visionegg.org/mailinglist.html


"""Sticking out FixPoint (stereo) & Random dots."""

# The first Stereo program that can be seen stereoscopically!!!
# The stimulus disappears after any key-pressing.

# written by Yuichi Sakano (2004.9.30)

import VisionEgg
VisionEgg.start_default_logging(); VisionEgg.watch_exceptions()

from VisionEgg.StereoCore import *
import pygame
from pygame.locals import *
from VisionEgg.Text import *
from VisionEgg.Dots import *
from OpenGL.GL.GL__init__ import *

screen = get_default_screen()
screen.parameters.bgcolor = (0.0,0.0,0.0) # black (RGB)
# the simple dots2d script.  need to get this for display of a cylinder
dots = DotArea2D( position                = ( screen.size[0]/2.0, 
screen.size[1]/2.0 ),
                  size                    = ( 300.0 , 300.0 ),
                  signal_fraction         = 0.1,
                  signal_direction_deg    = 180.0,
                  velocity_pixels_per_sec = 10.0,
                  dot_lifespan_sec        = 5.0,
                  dot_size                = 3.0,
                  num_dots                = 300)

# this is where the disparity is introduced (I am assuming).  Fixation spot 
'pop' out.
fixation_spotR = FixationSpot(position=(screen.size[0]/2-10,screen.size[1]/2),
                                                         anchor='center',
                                                         color=(255,0,0,0),
                                                         size=(14,14))
fixation_spotL = FixationSpot(position=(screen.size[0]/2+10,screen.size[1]/2),
                                                         anchor='center',
                                                         color=(255,0,0,0),
                                                         size=(14,14))

blue_lineR = FixationSpot(position=(0,0),# (x,y)(lowerleft:(0,0))
                                                         anchor='lowerleft',
                                                         color=(0,0,255,0),
                                                         
size=(screen.size[0]*0.80, 0.5))
blue_lineL = FixationSpot(position=(0,0),
                                                         anchor='lowerleft',
                                                         color=(0,0,255,0),
                                                         
size=(screen.size[0]*0.30, 0.5))

center = (0,0,0)
left_eye = (-1,0,-10)
right_eye = (1, 0,-10)
up = (0,1,0)

left_projection = Projection().look_at( left_eye, center, up )
right_projection = Projection().look_at( right_eye, center, up )

left_viewport = Viewport(screen=screen,
                                projection=left_projection,
                                stimuli=[dots, fixation_spotL, blue_lineL])
right_viewport = Viewport(screen=screen,
                                projection=right_projection,
                                stimuli=[dots, fixation_spotR, blue_lineR])


### For goggle synchronizing ###

glPushAttrib(GL_ALL_ATTRIB_BITS)

glDisable(GL_ALPHA_TEST)
glDisable(GL_BLEND)

for i in range(6):
    glDisable(GL_CLIP_PLANE0+i)

glDisable(GL_COLOR_LOGIC_OP)
glDisable(GL_COLOR_MATERIAL)
glDisable(GL_DEPTH_TEST)
glDisable(GL_DITHER)
glDisable(GL_FOG)
glDisable(GL_LIGHTING)
glDisable(GL_LINE_SMOOTH)
glDisable(GL_LINE_STIPPLE)
glDisable(GL_SCISSOR_TEST)
#glDisable(GL_SHARED_TEXTURE_PALETTE_EXT)#This arg is not defined.
glDisable(GL_STENCIL_TEST)
glDisable(GL_TEXTURE_1D)
glDisable(GL_TEXTURE_2D)
#glDisable(GL_TEXTURE_3D)#This arg is not defined.
#glDisable(GL_TEXTURE_CUBE_MAP)#This arg is not defined.
#glDisable(GL_TEXTURE_RECTANGLE_EXT)#This arg is not defined.
#glDisable(GL_VERTEX_PROGRAM_ARB)#This arg is not defined.

######################

frame_timer = FrameTimer()
quit_now = 0
while not quit_now:
    for event in pygame.event.get():
        if event.type in (QUIT,KEYDOWN,MOUSEBUTTONDOWN):
            quit_now = 1

    Buffers = [GL_BACK_LEFT, GL_BACK_RIGHT]
    for buffer in Buffers:
        glDrawBuffer(buffer)

        glColor3d(0.0, 0.0, 0.0)
        glBegin(GL_LINES)# Draw a background line
        glVertex3f(0.0, screen.size[1] - 0.5, 0.0)
        glVertex3f(screen.size[0], screen.size[1] - 0.5, 0.0)
        glEnd()
        glColor3d(0.0, 0.0, 1.0)
        glBegin(GL_LINES)# Draw a line of the correct length 
        glVertex3f(0.0, screen.size[1] - 0.5, 0.0)
        if buffer == GL_BACK_LEFT:
            glVertex3f(screen.size[0] * 0.30, screen.size[1] - 0.5, 0.0)#Left
        else:
            glVertex3f(screen.size[0] * 0.80, screen.size[1] - 0.5, 0.0)#Right
        glEnd()

        screen.clear()
        if buffer == GL_BACK_LEFT:
            left_viewport.draw()
        else:
            right_viewport.draw()

    swap_buffers()
    frame_timer.tick()
frame_timer.log_histogram()
# The Vision Egg: Core
#
# Copyright (C) 2001-2003 Andrew Straw.
# Author: Andrew Straw <astraw@xxxxxxxxxxxxxxxxxxxxx>
# URL: <http://www.visionegg.org/>
#
# Distributed under the terms of the GNU Lesser General Public License
# (LGPL). See LICENSE.TXT that came with this file.
#
# $Id: Core.py,v 1.151 2003/12/01 23:42:57 astraw Exp $

"""
Core Vision Egg functionality.

This module contains the architectural foundations of the Vision Egg.

"""

####################################################################
#
#        Import all the necessary packages
#
####################################################################

import sys, types, math, time, os               # standard Python modules
import StringIO

try:
    import logging                              # available in Python 2.3
except ImportError:
    import VisionEgg.py_logging as logging      # use local copy otherwise

import VisionEgg                                # Vision Egg base module 
(__init__.py)
import VisionEgg.PlatformDependent              # platform dependent Vision Egg 
C code
import VisionEgg.ParameterTypes as ve_types     # Vision Egg type checking
import VisionEgg.GLTrace                        # Allows tracing of all OpenGL 
calls
import VisionEgg.ThreeDeeMath                   # OpenGL math simulation

import pygame                                   # pygame handles OpenGL window 
setup
import pygame.locals
import pygame.display

import VisionEgg.GL as gl # get all OpenGL stuff in one namespace

import Numeric                                  # Numeric Python package

__version__ = VisionEgg.release_name
__cvs__ = '$Revision: 1.151 $'.split()[1]
__date__ = ' '.join('$Date: 2003/12/01 23:42:57 $'.split()[1:3])
__author__ = 'Andrew Straw <astraw@xxxxxxxxxxxxxxxxxxxxx>'

# Use Python's bool constants if available, make aliases if not
try:
    True
except NameError:
    True = 1==1
    False = 1==0

# Define "sum" if it's not available as Python function
try:
    sum
except NameError:
    import operator
    def sum( values ):
        return reduce(operator.add, values )

def swap_buffers():
    VisionEgg.config._FRAMECOUNT_ABSOLUTE += 1
    return pygame.display.flip()

####################################################################
#
#        Screen
#
####################################################################

class Screen(VisionEgg.ClassWithParameters):
    """An OpenGL window, possibly displayed across multiple displays.

    A Screen instance is an OpenGL window for the Vision Egg to draw
    in.  For an instance of Screen to do anything useful, it must
    contain one or more instances of the Viewport class and one or
    more instances of the Stimulus class.

    Currently, only one OpenGL window is supported by the library with
    which the Vision Egg initializes graphics (pygame/SDL).  However,
    this need not limit display to a single physical display device.
    Many video drivers, for example, allow applications to treat two
    separate monitors as one large array of contiguous pixels.  By
    sizing a window such that it occupies both monitors and creating
    separate viewports for the portion of the window on each monitor,
    a multiple screen effect can be created.

    Public variables
    ================
    size -- Tuple of 2 integers specifying width and height
    red_bits -- Integer (or None if not supported) specifying framebuffer depth
    green_bits -- Integer (or None if not supported) specifying framebuffer 
depth
    blue_bits -- Integer (or None if not supported) specifying framebuffer depth
    alpha_bits -- Integer (or None if not supported) specifying framebuffer 
depth

    Parameters
    ==========
    bgcolor -- background color (AnyOf(Sequence3 of Real or Sequence4 of Real))
               Default: (0.5, 0.5, 0.5, 0.0)

    Constant Parameters
    ===================
    frameless     -- remove standard window frame? Can be set with 
VISIONEGG_FRAMELESS_WINDOW (Boolean)
                     Default: (determined at runtime)
    fullscreen    -- use full screen? Can be set with VISIONEGG_FULLSCREEN 
(Boolean)
                     Default: (determined at runtime)
    hide_mouse    -- hide the mouse cursor? Can be set with 
VISIONEGG_HIDE_MOUSE (Boolean)
                     Default: (determined at runtime)
    maxpriority   -- raise priority? (platform dependent) Can be set with 
VISIONEGG_MAXPRIORITY (Boolean)
                     Default: (determined at runtime)
    preferred_bpp -- preferred bits per pixel (bit depth) Can be set with 
VISIONEGG_PREFERRED_BPP (UnsignedInteger)
                     Default: (determined at runtime)
    size          -- size (units: pixels) Can be set with VISIONEGG_SCREEN_W 
and VISIONEGG_SCREEN_H (Sequence2 of Real)
                     Default: (determined at runtime)
    sync_swap     -- synchronize buffer swaps to vertical sync? Can be set with 
VISIONEGG_SYNC_SWAP (Boolean)
                     Default: (determined at runtime)

    Altered code in SCREEN by kjm to establish GL_STEREO, 2004.10.05
    """

    parameters_and_defaults = VisionEgg.ParameterDefinition({
        'bgcolor':((0.5,0.5,0.5,0.0),
                   ve_types.AnyOf(ve_types.Sequence3(ve_types.Real),
                                  ve_types.Sequence4(ve_types.Real)),
                   'background color',),
        })
    
    constant_parameters_and_defaults = VisionEgg.ParameterDefinition({
        'size':(None,
                ve_types.Sequence2(ve_types.Real),
                'size (units: pixels) Can be set with VISIONEGG_SCREEN_W and 
VISIONEGG_SCREEN_H'),
        'fullscreen':(None,
                      ve_types.Boolean,
                      'use full screen? Can be set with VISIONEGG_FULLSCREEN'),
        'preferred_bpp':(None,
                         ve_types.UnsignedInteger,
                         'preferred bits per pixel (bit depth) Can be set with 
VISIONEGG_PREFERRED_BPP'),
        'maxpriority':(None,
                       ve_types.Boolean,
                       'raise priority? (platform dependent) Can be set with 
VISIONEGG_MAXPRIORITY'),
        'hide_mouse':(None,
                      ve_types.Boolean,
                      'hide the mouse cursor? Can be set with 
VISIONEGG_HIDE_MOUSE'),
        'frameless':(None,
                     ve_types.Boolean,
                     'remove standard window frame? Can be set with 
VISIONEGG_FRAMELESS_WINDOW'),
        'sync_swap':(None,
                     ve_types.Boolean,
                     'synchronize buffer swaps to vertical sync? Can be set 
with VISIONEGG_SYNC_SWAP'),
        })
    
    __slots__ = (
        'red_bits',
        'green_bits',
        'blue_bits',
        'alpha_bits',
        '__cursor_visible_func__',
        '__pygame_quit__',
        '_put_pixels_texture_stimulus',
        '_pixel_coord_projection',
        )
    
    def __init__(self,**kw):
        logger = logging.getLogger('VisionEgg.Core')
        
        VisionEgg.ClassWithParameters.__init__(self,**kw)

        cp = self.constant_parameters # shorthand
        if cp.size is None:
            cp.size = (VisionEgg.config.VISIONEGG_SCREEN_W,
                       VisionEgg.config.VISIONEGG_SCREEN_H)
        if cp.fullscreen is None:
            cp.fullscreen = VisionEgg.config.VISIONEGG_FULLSCREEN
        if cp.preferred_bpp is None:
            cp.preferred_bpp = VisionEgg.config.VISIONEGG_PREFERRED_BPP
        if cp.maxpriority is None:
            cp.maxpriority = VisionEgg.config.VISIONEGG_MAXPRIORITY
        if cp.hide_mouse is None:
            cp.hide_mouse = VisionEgg.config.VISIONEGG_HIDE_MOUSE
        if cp.frameless is None:
            cp.frameless = VisionEgg.config.VISIONEGG_FRAMELESS_WINDOW
        if cp.sync_swap is None:
            cp.sync_swap = VisionEgg.config.VISIONEGG_SYNC_SWAP
            
        if VisionEgg.config.SYNCLYNC_PRESENT:
            global synclync # import into global namespace
            import synclync
            try:
                VisionEgg.config._SYNCLYNC_CONNECTION = 
synclync.SyncLyncConnection()
            except synclync.SyncLyncError, x:
                logger.warning( "Could not connect to SyncLync device 
(SyncLyncError: %s)."%str(x))
                VisionEgg.config._SYNCLYNC_CONNECTION = None
            else:
                logger.info( "Connected to SyncLync device" )
        else:
            VisionEgg.config._SYNCLYNC_CONNECTION = None

        # Attempt to synchronize buffer swapping with vertical sync
        if self.constant_parameters.sync_swap:
            sync_success = 
VisionEgg.PlatformDependent.sync_swap_with_vbl_pre_gl_init()

        # Initialize pygame stuff
        if sys.platform == "darwin": # bug in Mac OS X version of pygame
            pygame.init()
        pygame.display.init()

        # Request framebuffer depths
        r = VisionEgg.config.VISIONEGG_REQUEST_RED_BITS
        g = VisionEgg.config.VISIONEGG_REQUEST_GREEN_BITS
        b = VisionEgg.config.VISIONEGG_REQUEST_BLUE_BITS
        a = VisionEgg.config.VISIONEGG_REQUEST_ALPHA_BITS
        # code added kjm, 2004.10.05 for STEREO_GL
        if hasattr(pygame.display,'gl_set_attribute'):
            pygame.display.gl_set_attribute(pygame.locals.GL_STEREO,1)
        else:
            logger.debug("Could not set opengl left- and right- buffers.")

        if hasattr(pygame.display,"gl_set_attribute"):
            pygame.display.gl_set_attribute(pygame.locals.GL_RED_SIZE,r)
            pygame.display.gl_set_attribute(pygame.locals.GL_GREEN_SIZE,g)
            pygame.display.gl_set_attribute(pygame.locals.GL_BLUE_SIZE,b)
            pygame.display.gl_set_attribute(pygame.locals.GL_ALPHA_SIZE,a)
        else:
            logger.debug("Could not request or query exact bit depths "
                         "or alpha in framebuffer because you need "
                         "pygame release 1.4.9 or greater. This is "
                         "only of concern if you use a stimulus that "
                         "needs this. In that case, the stimulus "
                         "should check for the desired feature(s).")
            
        if not hasattr(pygame.display,"set_gamma_ramp"):
            logger.debug("set_gamma_ramp function not available "
                         "because you need pygame release 1.5 or "
                         "greater. This is only of concern if you "
                         "need this feature.")
        pygame.display.set_caption("Vision Egg")
        
        flags = pygame.locals.OPENGL | pygame.locals.DOUBLEBUF
        if self.constant_parameters.fullscreen:
            flags = flags | pygame.locals.FULLSCREEN

        if self.constant_parameters.frameless:
            flags = flags | pygame.locals.NOFRAME

        try_bpp = self.constant_parameters.preferred_bpp

        append_str = ""
        if self.constant_parameters.fullscreen:
            screen_mode = "fullscreen"
        else:
            screen_mode = "window"
        if hasattr(pygame.display,"gl_set_attribute"):
            append_str = " (%d %d %d %d RGBA)."%(r,g,b,a)
            
        logger.info("Requesting %s %d x %d %d bpp%s"%
                    (screen_mode,self.size[0],self.size[1],
                     try_bpp,append_str))

        pygame.display.set_mode(self.size, flags, try_bpp )
        # set a global variable so we know workaround avoid pygame bug        
        VisionEgg.config._pygame_started = 1

        try:
            if sys.platform != 'darwin':
                
pygame.display.set_icon(pygame.transform.scale(pygame.image.load(
                    os.path.join(VisionEgg.config.VISIONEGG_SYSTEM_DIR,
                                 'data','visionegg.bmp')).convert(),(32,32)))
            else:
                import AppKit # requires PyObjC, which is required by pygame osx
                im = AppKit.NSImage.alloc()
                im.initWithContentsOfFile_(
                    os.path.join(VisionEgg.config.VISIONEGG_SYSTEM_DIR,
                                 'data','visionegg.tif'))
                AppKit.NSApplication.setApplicationIconImage_(AppKit.NSApp(),im)
                
        except Exception,x:
            logger.info("Error while trying to set_icon: %s: %s"%
                        (str(x.__class__),str(x)))

        global gl_vendor, gl_renderer, gl_version
        gl_vendor = gl.glGetString(gl.GL_VENDOR)
        gl_renderer = gl.glGetString(gl.GL_RENDERER)
        gl_version = gl.glGetString(gl.GL_VERSION)

        logger.info("OpenGL %s, %s, %s"%
                    (gl_version, gl_renderer, gl_vendor))

        if gl_renderer == "GDI Generic" and gl_vendor == "Microsoft 
Corporation":
            logger.warning("Using default Microsoft Windows OpenGL "
                           "drivers.  Please (re-)install the latest "
                           "video drivers from your video card "
                           "manufacturer to get hardware accelerated "
                           "performance.")
        if gl_renderer == "Mesa GLX Indirect" and gl_vendor == "VA Linux 
Systems, Inc.":
            logger.warning("Using default Mesa GLX drivers. Please "
                           "(re-)install the latest video drivers from "
                           "your video card manufacturer or DRI "
                           "project to get hardware accelarated "
                           "performance.")
        self.red_bits = None
        self.green_bits = None
        self.blue_bits = None
        self.alpha_bits = None
        got_bpp = pygame.display.Info().bitsize
        append_str = ""
        if hasattr(pygame.display,"gl_get_attribute"):
            self.red_bits = 
pygame.display.gl_get_attribute(pygame.locals.GL_RED_SIZE)
            self.green_bits = 
pygame.display.gl_get_attribute(pygame.locals.GL_GREEN_SIZE)
            self.blue_bits = 
pygame.display.gl_get_attribute(pygame.locals.GL_BLUE_SIZE)
            self.alpha_bits = 
pygame.display.gl_get_attribute(pygame.locals.GL_ALPHA_SIZE)
            append_str = " (%d %d %d %d 
RGBA)"%(self.red_bits,self.green_bits,self.blue_bits,self.alpha_bits)
        logger.info("Video system reports %d bpp%s."%(got_bpp,append_str))
        if got_bpp < try_bpp:
            logger.warning("Video system reports %d bits per pixel, "
                           "while your program requested %d. Can you "
                           "adjust your video drivers?"%(got_bpp,
                           try_bpp))
        # Save the address of these functions so they can be called
        # when closing the screen.
        self.__cursor_visible_func__ = pygame.mouse.set_visible
        self.__pygame_quit__ = pygame.quit

        # Attempt to synchronize buffer swapping with vertical sync again
        if self.constant_parameters.sync_swap:
            if not sync_success:
                if not 
VisionEgg.PlatformDependent.sync_swap_with_vbl_post_gl_init():
                    self.constant_parameters.sync_swap = False
                    logger.warning("Unable to detect or automatically "
                                   "synchronize buffer swapping with "
                                   "vertical retrace. May be possible "
                                   "by manually adjusting video "
                                   "drivers. (Look for 'Enable "
                                   "Vertical Sync' or similar.) If "
                                   "buffer swapping is not "
                                   "synchronized, frame by frame "
                                   "control will not be possible. "
                                   "Because of this, you will probably "
                                   "get a warning about calculated "
                                   "frames per second different than "
                                   "specified.")
        # Check previously made OpenGL assumptions now that we have OpenGL 
window
        post_gl_init()
        
        if self.constant_parameters.hide_mouse:
            self.__cursor_visible_func__(0)

        # Attempt to set maximum priority (This may not be the best
        # place in the code to do it because it's an application-level
        # thing, not a screen-level thing, but it fits reasonably well
        # here for now.)
        if self.constant_parameters.maxpriority:
            VisionEgg.PlatformDependent.set_priority() # defaults to max 
priority
            
        if hasattr(VisionEgg.config,'_open_screens'):
            VisionEgg.config._open_screens.append(self)
        else:
            VisionEgg.config._open_screens = [self]

    # Use Python descriptors (introduced in Python 2.2) to link size
    # attribute to constant_parameters.size.
    def get_size(self): return self.constant_parameters.size
    def set_size(self, value): raise RuntimeError("Attempting to set read-only 
value")
    size = property(get_size,set_size)

    def get_framebuffer_as_image(self,
                                 buffer='back',
                                 format=gl.GL_RGB,
                                 position=(0,0),
                                 anchor='lowerleft',
                                 size=None, # if None, use full screen
                                 ):
        """get pixel values from framebuffer to PIL image"""
        import Image # Could import this at the beginning of the file, but it 
breaks sometimes.
        
        fb_array = self.get_framebuffer_as_array(buffer=buffer,
                                                 format=format,
                                                 position=position,
                                                 anchor=anchor,
                                                 size=size,
                                                 )
        size = fb_array.shape[1], fb_array.shape[0]
        if format == gl.GL_RGB:
            pil_mode = 'RGB'
        elif format == gl.GL_RGBA:
            pil_mode = 'RGBA'
        fb_image = Image.fromstring(pil_mode,size,fb_array.tostring())
        fb_image = fb_image.transpose( Image.FLIP_TOP_BOTTOM )
        return fb_image

    def get_framebuffer_as_array(self,
                                 buffer='back',
                                 format=gl.GL_RGB,
                                 position=(0,0),
                                 anchor='lowerleft',
                                 size=None, # if None, use full screen
                                 ):
        """get pixel values from framebuffer to Numeric array"""# (SLOW)"""
        if size is None:
            size = self.size
        lowerleft = VisionEgg._get_lowerleft(position,anchor,size)
        if buffer == 'front':
            gl.glReadBuffer( gl.GL_FRONT )
        elif buffer == 'back':
            gl.glReadBuffer( gl.GL_BACK )
        else:
            raise ValueError('No support for "%s" framebuffer'%buffer)

        # according to Apple's glGrab demo, this should force DMA transfers:
        gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 4)
        gl.glPixelStorei(gl.GL_PACK_ROW_LENGTH, 0)
        gl.glPixelStorei(gl.GL_PACK_SKIP_ROWS, 0)
        gl.glPixelStorei(gl.GL_PACK_SKIP_PIXELS, 0)
        if gl_version >= '1.2' and hasattr(gl,'GL_BGRA'):
            framebuffer_pixels = gl.glReadPixels(lowerleft[0],lowerleft[1],
                                                 size[0],size[1],
                                                 gl.GL_BGRA,
                                                 gl.GL_UNSIGNED_INT_8_8_8_8_REV)
            raw_format = 'BGRA'
        else:
            framebuffer_pixels = gl.glReadPixels(lowerleft[0],lowerleft[1],
                                                 size[0],size[1],
                                                 gl.GL_RGBA,
                                                 gl.GL_UNSIGNED_BYTE)
            raw_format = 'RGBA'
        fb_array = Numeric.fromstring(framebuffer_pixels,Numeric.UnsignedInt8)
        fb_array = Numeric.reshape(fb_array,(size[1],size[0],4))
        # These work, but I don't know why.  There must be something I
        # don't understand about byte ordering!
        if format == gl.GL_RGB:
            if raw_format == 'BGRA':
                fb_array = fb_array[:,:,1:]
            elif raw_format == 'RGBA':
                fb_array = fb_array[:,:,:3]
        elif format == gl.GL_RGBA:
            if raw_format == 'BGRA':
                alpha = fb_array[:,:,0,Numeric.NewAxis]
                fb_array = fb_array[:,:,1:]
                fb_array = Numeric.concatenate( (fb_array,alpha), axis=2)
            elif raw_format == 'RGBA':
                pass
        else:
            raise NotImplementedError("Only RGB and RGBA formats currently 
supported")
        return fb_array

    def put_pixels(self,
                   pixels=None,
                   position=(0,0),
                   anchor='lowerleft',
                   scale_x=1.0, # "zoom" the pixels
                   scale_y=1.0, # "zoom" the pixels
                   texture_min_filter=gl.GL_NEAREST, # only used if scale < 1.0
                   texture_mag_filter=gl.GL_NEAREST, # only used if scale > 1.0
                   internal_format=gl.GL_RGB, # pixel data converted to this 
format in texture (gl.GL_RGBA also useful)
                   ):
        """Put pixel values to screen.

        Pixel values become texture data using the VisionEgg.Textures
        module.  Any source of texture data accepted by that module is
        accepted here.

        This function could be sped up by allocating a fixed OpenGL texture 
object.
        
        """
        
        import VisionEgg.Textures # import here to avoid import loop
        make_new_texture_object = 0
        if not hasattr(self, "_put_pixels_texture_stimulus"): 
            make_new_texture_object = 1
        else:
            if internal_format != 
self._put_pixels_texture_stimulus.constant_parameters.internal_format:
                make_new_texture_object = 1
        if make_new_texture_object:
            # For speed, don't do this on anything other than 1st run
            texture = VisionEgg.Textures.Texture(pixels)
            on_screen_size = (texture.size[0]*scale_x, texture.size[1]*scale_y)
            t = VisionEgg.Textures.TextureStimulus(texture=texture,
                                                   position=position,
                                                   anchor=anchor,
                                                   size=on_screen_size,
                                                   mipmaps_enabled=0,
                                                   
texture_min_filter=texture_min_filter,
                                                   
texture_mag_filter=texture_mag_filter,
                                                   internal_format = 
internal_format,
                                                   )
            self._put_pixels_texture_stimulus = t # rename
            self._pixel_coord_projection = OrthographicProjection(left=0,
                                                                  
right=self.size[0],
                                                                  bottom=0,
                                                                  
top=self.size[1],
                                                                  
z_clip_near=0.0,
                                                                  
z_clip_far=1.0)
        else:
            # We've run once before and therefore already have a
            # texture stimulus. (XXX In the future, make use of
            # already assigned texture object and use put_sub_image
            # for speed.)
            self._put_pixels_texture_stimulus.parameters.texture = 
VisionEgg.Textures.Texture(pixels)

        self._pixel_coord_projection.push_and_set_gl_projection() # Save 
projection
        self._put_pixels_texture_stimulus.draw() # Draw pixels as texture
        
        gl.glMatrixMode(gl.GL_PROJECTION) # Restore projection
        gl.glPopMatrix()
                
    def query_refresh_rate(self):
        return VisionEgg.PlatformDependent.query_refresh_rate(self)

    def measure_refresh_rate(self,average_over_seconds=0.1):
        """Measure the refresh rate. Assumes swap buffers synced."""
        start_time = VisionEgg.time_func()
        duration_sec = 0.0
        num_frames = 0
        while duration_sec < average_over_seconds:
            swap_buffers()
            now = VisionEgg.time_func()
            num_frames += 1
            duration_sec = now - start_time
        if duration_sec > 0.0:
            fps = num_frames / duration_sec
        else:
            fps = 0.0
        return fps

    def set_gamma_ramp(self, red, green, blue):
        return pygame.display.set_gamma_ramp(red,green,blue)

    def clear(self):
        """Called by Presentation instance. Clear the screen."""

        c = self.parameters.bgcolor # Shorthand
        if len(c) == 4:
            gl.glClearColor(*c)
        else:
            gl.glClearColor(c[0],c[1],c[2],0.0) # set alpha to 0.0 unless 
specified
        gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)

    def make_current(self):
        """Called by Viewport instance. Makes screen active for drawing.

        Can not be implemented until multiple screens are possible."""
        pass

    def set_gamma_ramp(self,*args,**kw):
        """Set the gamma_ramp, if supported.

        Call pygame.display.set_gamma_ramp, if available.

        Returns True on success, False otherwise."""
        if not hasattr(pygame.display,"set_gamma_ramp"):
            logger = logging.getLogger('VisionEgg.Core')
            logger.error("Need pygame 1.5 or greater for set_gamma_ramp 
function")
            return False
        if pygame.display.set_gamma_ramp(*args,**kw):
            return True
        else:
            return False

    def close(self):
        """Close the screen.

        You can call this to close the screen.  Not necessary during
        normal operation because it gets automatically deleted."""
        # Close pygame if possible
        if hasattr(VisionEgg.config,'_open_screens'):
            if self in VisionEgg.config._open_screens:
                VisionEgg.config._open_screens.remove(self)
            if len(VisionEgg.config._open_screens) == 0:
                # no more open screens
                if hasattr(self,"__cursor_visible_func__"):
                    self.__cursor_visible_func__(1)
                pygame.quit()
        # No access to the cursor visible function anymore
        if hasattr(self,"__cursor_visible_func__"):
            del self.__cursor_visible_func__
            
    def __del__(self):
        # Make sure mouse is visible after screen closed.
        if hasattr(self,"__cursor_visible_func__"):
            try:
                self.__cursor_visible_func__(1)
                self.__pygame_quit__()
            except pygame.error, x:
                if str(x) != 'video system not initialized':
                    raise

    def create_default():
        """Alternative constructor using configuration variables.

        Most of the time you can create and instance of Screen using
        this method.  If your script needs explicit control of the
        Screen parameters, initialize with the normal constructor.

        Uses VisionEgg.config.VISIONEGG_GUI_INIT to determine how the
        default screen parameters should are determined.  If this
        value is 0, the values from VisionEgg.cfg are used.  If this
        value is 1, a GUI panel is opened and allows manual settings
        of the screen parameters.  """

        global VisionEgg # Allow "VisionEgg.config" instead of just "config"
        if VisionEgg.config.VISIONEGG_GUI_INIT:
            import VisionEgg.GUI # Could import in beginning, but no need if 
not using GUI
            window = VisionEgg.GUI.GraphicsConfigurationWindow()
            window.mainloop() # All this does is adjust VisionEgg.config
            if not window.clicked_ok:
                sys.exit() # User wants to quit
        screen = None
        try:
            screen = Screen(size=(VisionEgg.config.VISIONEGG_SCREEN_W,
                                  VisionEgg.config.VISIONEGG_SCREEN_H),
                            fullscreen=VisionEgg.config.VISIONEGG_FULLSCREEN,
                            
preferred_bpp=VisionEgg.config.VISIONEGG_PREFERRED_BPP,
                            bgcolor=(0.5,0.5,0.5,0.0),
                            maxpriority=VisionEgg.config.VISIONEGG_MAXPRIORITY,
                            
frameless=VisionEgg.config.VISIONEGG_FRAMELESS_WINDOW,
                            hide_mouse=VisionEgg.config.VISIONEGG_HIDE_MOUSE)
        finally:
            if screen is None:
                # Opening screen failed.  Let's do any cleanup that 
Screen.__init__ missed.
                try:
                    pygame.mouse.set_visible(1) # make sure mouse is visible
                    pygame.quit() # close screen
                except pygame.error, x:
                    if str(x) != 'video system not initialized':
                        raise

        if screen is None:
            raise RuntimeError("Screen open failed. Check your error log for a 
traceback.")

        gamma_source = VisionEgg.config.VISIONEGG_GAMMA_SOURCE.lower()
        if gamma_source != 'none':
            if gamma_source == 'invert':
                native_red = VisionEgg.config.VISIONEGG_GAMMA_INVERT_RED
                native_green = VisionEgg.config.VISIONEGG_GAMMA_INVERT_GREEN
                native_blue = VisionEgg.config.VISIONEGG_GAMMA_INVERT_BLUE
                red = screen._create_inverted_gamma_ramp( native_red )
                green = screen._create_inverted_gamma_ramp( native_green )
                blue = screen._create_inverted_gamma_ramp( native_blue )
                gamma_set_string = "linearized gamma lookup tables to correct 
"+\
                                   "monitor with native gammas (%f, %f, %f) 
RGB"%(
                    native_red,
                    native_green,
                    native_blue)
            elif gamma_source == 'file':
                filename = VisionEgg.config.VISIONEGG_GAMMA_FILE
                red, green, blue = screen._open_gamma_file(filename)
                gamma_set_string = "set gamma lookup tables from data in file 
%s"%os.path.abspath(filename)
            else:
                raise ValueError("Unknown gamma source: '%s'"%gamma_source)
            logger = logging.getLogger('VisionEgg.Core')
            if not screen.set_gamma_ramp(red,green,blue):
                logger.warning( "Setting gamma ramps failed." )
            else:
                logger.info( "Gamma set sucessfully: %s"%gamma_set_string )
        return screen
    create_default = staticmethod(create_default)

    def _create_inverted_gamma_ramp(self, gamma):
        # c is a constant scale factor.  It is always 1.0 when
        # luminance is normalized to range [0.0,1.0] and input units
        # in range [0.0,1.0], as is OpenGL standard.
        c = 1.0
        inc = 1.0/255
        target_luminances = Numeric.arange(0.0,1.0+inc,inc)
        output_ramp = Numeric.zeros(target_luminances.shape,Numeric.Int)
        for i in range(len(target_luminances)):
            L = target_luminances[i]
            if L == 0.0:
                v_88fp = 0
            else:
                v = math.exp( (math.log(L) - math.log(c)) /gamma)
                v_88fp = int(round((v*255) * 256)) # convert to from [0.0,1.0] 
floating point to [0.0,255.0] 8.8 fixed point
            output_ramp[i] = v_88fp # 8.8 fixed point format
        return list(output_ramp) # convert to Python list
        
    def _open_gamma_file(self, filename):
        fd = open(filename,"r")
        gamma_values = []
        for line in fd.readlines():
            line = line.strip() # remove leading/trailing whitespace
            if line.startswith("#"): # comment, ignore
                continue
            gamma_values.append( map(int, line.split() ) )
            if len(gamma_values[-1]) != 3:
                raise FileError("expected 3 values per gamma entry")
        if len(gamma_values) != 256:
            raise FileError("expected 256 gamma entries")
        red, green, blue = zip(*gamma_values)
        return red,green,blue
            
def get_default_screen():
    """Make an instance of Screen using a GUI window or from config file."""
    return Screen.create_default()

####################################################################
#
#        Projection and derived classes
#
####################################################################

class Projection(VisionEgg.ClassWithParameters):
    """Converts stimulus coordinates to viewport coordinates.

    This is an abstract base class which should be subclassed for
    actual use.

    This class is largely convenience for using OpenGL's
    PROJECTION_MATRIX.


    Parameters
    ==========
    matrix -- matrix specifying projection (Sequence4x4 of Real)
              Default: [[1 0 0 0]
                        [0 1 0 0]
                        [0 0 1 0]
                        [0 0 0 1]]
    """
    parameters_and_defaults = VisionEgg.ParameterDefinition({
        'matrix':( Numeric.identity(4), # 4x4 identity matrix
                   ve_types.Sequence4x4(ve_types.Real),
                   'matrix specifying projection'),
        })
    
    def __init__(self,**kw):
        VisionEgg.ClassWithParameters.__init__(self,**kw)

    def set_gl_projection(self):
        """Set the OpenGL projection matrix."""
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glLoadMatrixf(self.parameters.matrix)

    def push_and_set_gl_projection(self):
        """Set the OpenGL projection matrix, pushing current projection matrix 
to stack."""
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glPushMatrix()
        gl.glLoadMatrixf(self.parameters.matrix)

    def translate(self,x,y,z):
        """Compose a translation and set the OpenGL projection matrix."""
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glLoadMatrixf(self.parameters.matrix)
        gl.glTranslatef(x,y,z)
        self.parameters.matrix = gl.glGetFloatv(gl.GL_PROJECTION_MATRIX)

    def stateless_translate(self,x,y,z):
        """Compose a translation without changing OpenGL state."""
        M = VisionEgg.ThreeDeeMath.TransformMatrix(self.parameters.matrix)
        M.translate(x,y,z)
        self.parameters.matrix = M.get_matrix()

    def rotate(self,angle_degrees,x,y,z):
        """Compose a rotation and set the OpenGL projection matrix."""
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glLoadMatrixf(self.parameters.matrix)
        gl.glRotatef(angle_degrees,x,y,z)
        self.parameters.matrix = gl.glGetFloatv(gl.GL_PROJECTION_MATRIX)

    def stateless_rotate(self,angle_degrees,x,y,z):
        """Compose a rotation without changing OpenGL state."""
        M = VisionEgg.ThreeDeeMath.TransformMatrix(self.parameters.matrix)
        M.rotate(angle_degrees,x,y,z)
        self.parameters.matrix = M.get_matrix()

    def scale(self,x,y,z):
        """Compose a rotation and set the OpenGL projection matrix."""
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glLoadMatrixf(self.parameters.matrix)
        gl.glScalef(x,y,z)
        self.parameters.matrix = gl.glGetFloatv(gl.GL_PROJECTION_MATRIX)

    def stateless_scale(self,x,y,z):
        """Compose a rotation without changing OpenGL state."""
        M = VisionEgg.ThreeDeeMath.TransformMatrix(self.parameters.matrix)
        M.scale(x,y,z)
        self.parameters.matrix = M.get_matrix()

    def get_matrix(self):
        return self.parameters.matrix

    def look_at(self, eye, center, up ):
        # Basically the same as gluLookAt
        def normalize(vec):
            numpy_vec = Numeric.array(vec)
            mag = math.sqrt(Numeric.sum(numpy_vec*numpy_vec))
            return numpy_vec / mag
        def cross(vec1,vec2):
            return ( vec1[1]*vec2[2] - vec1[2]*vec2[1],
                     vec1[2]*vec2[0] - vec1[0]*vec2[2],
                     vec1[0]*vec2[1] - vec1[1]*vec2[0] )
        forward = Numeric.array(( center[0] - eye[0],
                                  center[1] - eye[1],
                                  center[2] - eye[2]),'f')
        forward = normalize(forward)
        side = cross(forward,up)
        side = normalize(side)
        new_up = cross(side,forward) # recompute up
        # XXX I might have to transpose this matrix
        m = Numeric.array([[side[0], new_up[0], -forward[0], 0.0],
                           [side[1], new_up[1], -forward[1], 0.0],
                           [side[2], new_up[2], -forward[2], 0.0],
                           [    0.0,       0.0,         0.0, 1.0]])
        # XXX This should get optimized -- don't do it in OpenGL
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glLoadMatrixf(self.parameters.matrix)
        gl.glMultMatrixf(m)
        gl.glTranslatef(-eye[0],-eye[1],-eye[2])
        self.parameters.matrix = gl.glGetFloatv(gl.GL_PROJECTION_MATRIX)
    def eye_2_clip(self,eye_coords_vertex):
        """Transform eye coordinates to clip coordinates"""
        m = Numeric.array(self.parameters.matrix)
        v = Numeric.array(eye_coords_vertex)
        homog = VisionEgg.ThreeDeeMath.make_homogeneous_coord_rows(v)
        r = Numeric.matrixmultiply(homog,m)
        if len(homog.shape) > len(v.shape):
            r = Numeric.reshape(r,(4,))
        return r
    def clip_2_norm_device(self,clip_coords_vertex):
        """Transform clip coordinates to normalized device coordinates"""
        v = Numeric.array(clip_coords_vertex)
        homog = VisionEgg.ThreeDeeMath.make_homogeneous_coord_rows(v)
        r = (homog/homog[:,3,Numeric.NewAxis])[:,:3]
        if len(homog.shape) > len(v.shape):
            r = Numeric.reshape(r,(3,))
        return r
    def eye_2_norm_device(self,eye_coords_vertex):
        """Transform eye coordinates to normalized device coordinates"""
        return self.clip_2_norm_device(self.eye_2_clip(eye_coords_vertex))
    
class OrthographicProjection(Projection):
    """An orthographic projection.

    Parameters
    ==========
    matrix -- matrix specifying projection (Sequence4x4 of Real)
              Default: [[1 0 0 0]
                        [0 1 0 0]
                        [0 0 1 0]
                        [0 0 0 1]]
    """

    def 
__init__(self,left=0.0,right=640.0,bottom=0.0,top=480.0,z_clip_near=0.0,z_clip_far=1.0):
        """Create an orthographic projection.

        Defaults to map x eye coordinates in the range [0,640], y eye
        coordinates [0,480] and clip coordinates [0,1] to [0,1].
        Therefore, if the viewport is 640 x 480, eye coordinates
        correspond 1:1 with window (pixel) coordinates.  Only points
        between these clipping planes will be displayed.
        """

        # using Numeric (from the OpenGL spec):
        matrix = Numeric.array([[ 2./(right-left), 0.,              0.,         
                  -(right+left)/(right-left)],
                                [ 0.,              2./(top-bottom), 0.,         
                  -(top+bottom)/(top-bottom)],
                                [ 0.,              0.,              
-2./(z_clip_far-z_clip_near), 
-(z_clip_far+z_clip_near)/(z_clip_far-z_clip_near)],
                                [ 0.,              0.,              0.,         
                  1.0]])
        matrix = Numeric.transpose(matrix) # convert to OpenGL format

        ## same as above, but use OpenGL
        #gl.glMatrixMode(gl.GL_PROJECTION)
        #gl.glPushMatrix() # save current matrix
        #gl.glLoadIdentity()
        #gl.glOrtho(left,right,bottom,top,z_clip_near,z_clip_far)
        #matrix = gl.glGetFloatv(gl.GL_PROJECTION_MATRIX)
        #gl.glPopMatrix() # restore original matrix
            
        Projection.__init__(self,**{'matrix':matrix})

class OrthographicProjectionNoZClip(Projection):
    """An orthographic projection without Z clipping.

    Parameters
    ==========
    matrix -- matrix specifying projection (Sequence4x4 of Real)
              Default: [[1 0 0 0]
                        [0 1 0 0]
                        [0 0 1 0]
                        [0 0 0 1]]
    """
    
    def __init__(self,left=0.0,right=640.0,bottom=0.0,top=480.0):
        """Create an orthographic projection without Z clipping.

        Defaults to map x eye coordinates in the range [0,640] and y
        eye coordinates [0,480] -> [0,1].  Therefore, if the viewport
        is 640 x 480, eye coordinates correspond 1:1 with window
        (pixel) coordinates.
        """

        # using Numeric (from the OpenGL spec):
        matrix = Numeric.array([[ 2./(right-left), 0,               0, 
-(right+left)/(right-left)],
                                [ 0,               2./(top-bottom), 0, 
-(top+bottom)/(top-bottom)],
                                [ 0,               0,              -1, -1.],
                                [ 0,               0,               0,  1]])
        matrix = Numeric.transpose(matrix) # convert to OpenGL format

        Projection.__init__(self,**{'matrix':matrix})

class SimplePerspectiveProjection(Projection):
    """A simplified perspective projection.

    Parameters
    ==========
    matrix -- matrix specifying projection (Sequence4x4 of Real)
              Default: [[1 0 0 0]
                        [0 1 0 0]
                        [0 0 1 0]
                        [0 0 0 1]]
    """
    
    def __init__(self,fov_x=45.0,z_clip_near = 
0.1,z_clip_far=10000.0,aspect_ratio=4.0/3.0):
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        matrix = self._compute_matrix(fov_x,z_clip_near,z_clip_far,aspect_ratio)
        Projection.__init__(self,**{'matrix':matrix})

    def _compute_matrix(self,fov_x=45.0,z_clip_near = 
0.1,z_clip_far=10000.0,aspect_ratio=4.0/3.0):
        """Compute a 4x4 projection matrix that performs a perspective 
distortion."""
        fov_y = fov_x / aspect_ratio
        # This is a translation of what gluPerspective does:
        #glu.gluPerspective(fov_y,aspect_ratio,z_clip_near,z_clip_far)
        radians = fov_y / 2.0 * math.pi / 180.0
        delta_z = z_clip_far - z_clip_near
        sine = math.sin(radians)
        if (delta_z == 0.0) or (sine == 0.0) or (aspect_ratio == 0.0):
            raise ValueError("Invalid parameters passed to 
SimpleProjection.__init__()")
        cotangent = math.cos(radians) / sine
        matrix = Numeric.zeros((4,4),'f')
        matrix[0][0] = cotangent/aspect_ratio
        matrix[1][1] = cotangent
        matrix[2][2] = -(z_clip_far + z_clip_near) / delta_z
        matrix[2][3] = -1.0 # XXX this
        matrix[3][2] = -2.0 * z_clip_near * z_clip_far / delta_z # XXX and this 
might cause the matrix to need to be transposed
        matrix[3][3] = 0.0
        return matrix
                                                  
class PerspectiveProjection(Projection):
    """A perspective projection.

    Parameters
    ==========
    matrix -- matrix specifying projection (Sequence4x4 of Real)
              Default: [[1 0 0 0]
                        [0 1 0 0]
                        [0 0 1 0]
                        [0 0 0 1]]
    """
    
    def __init__(self,left,right,bottom,top,near,far):
        gl.glMatrixMode(gl.GL_PROJECTION) # Set OpenGL matrix state to modify 
the projection matrix
        gl.glLoadIdentity() # Clear the projection matrix
        gl.glFrustum(left,right,bottom,top,near,far) # Let GL create a matrix 
and compose it
        matrix = gl.glGetFloatv(gl.GL_PROJECTION_MATRIX)
        if matrix is None:
            # OpenGL wasn't started
            raise RuntimeError("OpenGL matrix operations can only take place 
once OpenGL context started.")
        if type(matrix) != Numeric.ArrayType:
            matrix = Numeric.array(matrix) # Convert to Numeric array
        Projection.__init__(self,**{'matrix':matrix})

####################################################################
#
#        Stimulus - Base class
#
####################################################################

class Stimulus(VisionEgg.ClassWithParameters):
    """Base class for a stimulus.

    Any stimulus element should be a subclass of this Stimulus class.
    The draw() method contains the code executed before every buffer
    swap in order to render the stimulus to the frame buffer.  It
    should execute as quickly as possible.  The init_gl() method must
    be called before the first call to draw() so that any internal
    data, OpenGL display lists, and OpenGL:texture objects can be
    established.

    To illustrate the concept of the Stimulus class, here is a
    description of several methods of drawing two spots.  If your
    experiment displays two spots simultaneously, you could create two
    instances of (a single subclass of) Stimulus, varying parameters
    so each draws at a different location.  Another possibility is to
    create one instance of a subclass that draws two spots.  Another,
    somewhat obscure, possibility is to create a single instance and
    add it to two different viewports.  (Something that will not work
    would be adding the same instance two times to the same viewport.
    It would also get drawn twice, although at exactly the same
    location.)

    OpenGL is a 'state machine', meaning that it has internal
    parameters whose values vary and affect how it operates.  Because
    of this inherent uncertainty, there are only limited assumptions
    about the state of OpenGL that an instance of Stimulus should
    expect when its draw() method is called.  Because the Vision Egg
    loops through stimuli this also imposes some important behaviors:

    First, the framebuffer will contain the results of any drawing
    operations performed since the last buffer swap by other instances
    of (subclasses of) Stimulus. Therefore, the order in which stimuli
    are present in the stimuli list of an instance of Viewport may be
    important.  Additionally, if there are overlapping viewports, the
    order in which viewports are added to an instance of Screen is
    important.

    Second, previously established OpenGL display lists and OpenGL
    texture objects will be available.  The __init__() method should
    establish these things.

    Third, there are several OpenGL state variables which are
    commonly set by subclasses of Stimulus, and which cannot be
    assumed to have any particular value at the time draw() is called.
    These state variables are: blending mode and function, texture
    state and environment, the matrix mode (modelview or projection),
    the modelview matrix, depth mode and settings. Therefore, if the
    draw() method depends on specific values for any of these states,
    it must specify its own values to OpenGL.

    Finally, a well-behaved Stimulus subclass resets any OpenGL state
    values other than those listed above to their initial state before
    draw() and init_gl() were called.  In other words, before your
    stimulus changes the state of an OpenGL variable, use
    glGetBoolean, glGetInteger, glGetFloat, or a similar function to
    query its value and restore it later.  For example, upon calling
    the draw() method, the projection matrix will be that which was
    set by the viewport. If the draw() method alters the projection
    matrix, it must be restored. The glPushMatrix() and glPopMatrix()
    commands provide an easy way to do this.

    The default projection of Viewport maps eye coordinates in a 1:1
    fashion to window coordinates (in other words, it sets eye
    coordinates to use pixel units from the lower left corner of the
    viewport). Therefore the default parameters for a stimulus should
    specify pixel coordinates if possible (such as for a 2D
    stimulus). Assuming a window size of 640 by 480 for the default
    parameters is a pretty safe way to do things.

    Also, be sure to check for any assumptions made about the system
    in the __init__ method.  For example, if your stimulus needs alpha
    in the framebuffer, check the value of
    glGetIntegerv(GL_ALPHA_BITS) and raise an exception if it is not
    available.
    """

    def __init__(self,**kw):
        """Instantiate and get ready to draw.

        Set parameter values and create anything needed to draw the
        stimulus including OpenGL state variables such display lists
        and texture objects.

        """
        VisionEgg.ClassWithParameters.__init__(self,**kw)
        
    def draw(self):
        """Draw the stimulus. (Called by Viewport instance.)
        
        This method is called every frame.  This method actually
        performs the OpenGL calls to draw the stimulus.
        
        """
        pass

####################################################################
#
#        Viewport
#
####################################################################

class Viewport(VisionEgg.ClassWithParameters):
    """Connects stimuli to a screen.

    A viewport defines a (possibly clipped region) of the screen on
    which stimuli are drawn.

    A screen may have multiple viewports.  The viewports may be
    overlapping.

    A viewport may have multiple stimuli.

    A single stimulus may be drawn simultaneously by several
    viewports, although this is typically useful only for 3D stimuli
    to represent different views of the same object.

    The coordinates of the stimulus are converted to screen
    coordinates via several steps, the most important of which is the
    projection, which is defined by an instance of the Projection
    class.

    By default, a viewport has a projection which maps eye coordinates
    to viewport coordinates in 1:1 manner.  In other words, eye
    coordinates specify pixel location in the viewport.

    For cases where pixel units are not natural to describe
    coordinates of a stimulus, the application should specify the a
    projection other than the default.  This is usually the case for
    3D stimuli.

    For details of the projection and clipping process, see the
    section 'Coordinate Transformations' in the book/online document
    'The OpenGL Graphics System: A Specification'

    Parameters
    ==========
    anchor      -- How position parameter is interpreted (String)
                   Default: lowerleft
    depth_range -- depth range (in object units) for rendering (Sequence2 of 
Real)
                   Default: (0, 1)
    position    -- Position (in pixel units) within the screen (Sequence2 of 
Real)
                   Default: (0, 0)
    projection  -- projection for coordinate transforms (Instance of <class 
'VisionEgg.Core.Projection'>)
                   Default: (determined at runtime)
    screen      -- The screen in which this viewport is drawn (Instance of 
<class 'VisionEgg.Core.Screen'>)
                   Default: (determined at runtime)
    size        -- Size (in pixel units) (Sequence2 of Real)
                   Default: (determined at runtime)
    stimuli     -- sequence of stimuli to draw in screen (Sequence of Instance 
of <class 'VisionEgg.Core.Stimulus'>)
                   Default: (determined at runtime)
    """

    parameters_and_defaults = VisionEgg.ParameterDefinition({
        'screen':(None,
                  ve_types.Instance(Screen),
                  'The screen in which this viewport is drawn'),
        'position':((0,0),
                    ve_types.Sequence2(ve_types.Real),
                    'Position (in pixel units) within the screen'), 
        'anchor':('lowerleft',
                  ve_types.String,
                  'How position parameter is interpreted'),
        'depth_range':((0,1),
                       ve_types.Sequence2(ve_types.Real),
                       'depth range (in object units) for rendering'), 
        'size':(None, # will use screen.size if not specified
                ve_types.Sequence2(ve_types.Real),
                'Size (in pixel units)'),
        'projection':(None, # instance of VisionEgg.Core.Projection
                      ve_types.Instance(Projection),
                      'projection for coordinate transforms'),
        'stimuli':(None,
                   ve_types.Sequence(ve_types.Instance(Stimulus)),
                   'sequence of stimuli to draw in screen'),
        'lowerleft':(None,  # DEPRECATED -- don't use
                     ve_types.Sequence2(ve_types.Real),
                     'position (in pixel units) of lower-left viewport corner',
                     VisionEgg.ParameterDefinition.DEPRECATED), 
        })

    __slots__ = (
        '_is_drawing',
        )

    def __init__(self,**kw):
        """Create a new instance.

        Required arguments:

        screen

        Optional arguments (specify parameter value other than default):

        position -- defaults to (0,0), position relative to screen by anchor 
(see below)
        anchor -- defaults to 'lowerleft'
        size -- defaults to screen.size
        projection -- defaults to self.make_new_pixel_coord_projection()
        stimuli -- defaults to empty list
        """
        VisionEgg.ClassWithParameters.__init__(self,**kw)

        if self.parameters.screen is None:
            raise ValueError("Must specify screen when creating an instance of 
Viewport.")
        
        p = self.parameters # shorthand
        if p.size is None:
            p.size = p.screen.constant_parameters.size
        if p.projection is None:
            # Default projection maps eye coordinates 1:1 on window (pixel) 
coordinates
            p.projection = self.make_new_pixel_coord_projection()
        if p.stimuli is None:
            p.stimuli = []
        self._is_drawing = False

    def make_new_pixel_coord_projection(self):
        """Create instance of Projection mapping eye coordinates 1:1 with pixel 
coordinates."""
        return 
OrthographicProjectionNoZClip(left=0,right=self.parameters.size[0],
                                             
bottom=0,top=self.parameters.size[1])

    def make_current(self):
        p = self.parameters # shorthand
        p.screen.make_current()
        
        if p.lowerleft != None:
            if not hasattr(Viewport,"_gave_lowerleft_warning"):
                logger = logging.getLogger('VisionEgg.Core')
                logger.warning("lowerleft parameter of Viewport class "
                               "will stop being supported. Use "
                               "'position' instead with anchor set to "
                               "'lowerleft'.")
                Viewport._gave_lowerleft_warning = True
            p.anchor = 'lowerleft'
            p.position = p.lowerleft[0], p.lowerleft[1] # copy values (don't 
copy ref to tuple)
            
        lowerleft = VisionEgg._get_lowerleft(p.position,p.anchor,p.size)
        
        gl.glViewport(lowerleft[0],
                      lowerleft[1],
                      p.size[0],
                      p.size[1])
        gl.glDepthRange(p.depth_range[0],p.depth_range[1])

        p.projection.set_gl_projection()

    def draw(self):
        """Set the viewport and draw stimuli."""
        self.make_current()
        self._is_drawing = True
        for stimulus in self.parameters.stimuli:
            stimulus.draw()
        self._is_drawing = False
            
    def norm_device_2_window(self,norm_device_vertex):
        """Transform normalized device coordinates to window coordinates"""
        v = Numeric.asarray(norm_device_vertex)
        homog = VisionEgg.ThreeDeeMath.make_homogeneous_coord_rows(v)
        xd = homog[:,0,Numeric.NewAxis]
        yd = homog[:,1,Numeric.NewAxis]
        zd = homog[:,2,Numeric.NewAxis]

        p = self.parameters # shorthand
        lowerleft = VisionEgg._get_lowerleft(p.position,p.anchor,p.size)
        x,y = lowerleft
        w,h = p.size
        n,f = p.depth_range
        
        # clamp n and f
        n = max(1.0,min(0.0,n))
        f = max(1.0,min(0.0,f))

        ox = x + w/2.0
        oy = y + h/2.0
        px = w
        py = h
        xw = (px/2.0)*xd + ox
        yw = (py/2.0)*yd + oy
        zw = ((f-n)/2.0)*zd + (n+f)/2.0
        # XXX I think zw (or zd) is clamped in OpenGL, but I can't
        # find it in any spec!
        #zw = Numeric.clip(zw,0.0,1.0) # clamp
        r = Numeric.concatenate((xw,yw,zw),axis=1)
        if len(homog.shape) > len(v.shape):
            r = Numeric.reshape(r,(3,))
        return r
    def clip_2_window(self,eye_coords_vertex):
        """Transform clip coordinates to window coordinates"""
        my_proj = self.parameters.projection
        return self.norm_device_2_window( my_proj.clip_2_norm_device( 
eye_coords_vertex ) )
    def eye_2_window(self,eye_coords_vertex):
        """Transform eye coordinates to window coordinates"""
        my_proj = self.parameters.projection
        return self.norm_device_2_window( my_proj.eye_2_norm_device( 
eye_coords_vertex ) )
        
####################################################################
#
#        FixationSpot
#
####################################################################

class FixationSpot(Stimulus):
    """A rectangle stimulus, typically used as a fixation spot.

    Parameters
    ==========
    anchor   -- how position parameter is used (String)
                Default: center
    color    -- color (AnyOf(Sequence3 of Real or Sequence4 of Real))
                Default: (1.0, 1.0, 1.0)
    on       -- draw? (Boolean)
                Default: True
    position -- position in eye coordinates (AnyOf(Sequence2 of Real or 
Sequence3 of Real or Sequence4 of Real))
                Default: (320.0, 240.0)
    size     -- size in eye coordinates (Sequence2 of Real)
                Default: (4.0, 4.0)
    """

    parameters_and_defaults = VisionEgg.ParameterDefinition({
        'on':(True,
              ve_types.Boolean,
              'draw?'),
        'color':((1.0,1.0,1.0),
                 ve_types.AnyOf(ve_types.Sequence3(ve_types.Real),
                                ve_types.Sequence4(ve_types.Real)),
                 'color'),
        'position' : ( ( 320.0, 240.0 ), # in eye coordinates
                       ve_types.AnyOf(ve_types.Sequence2(ve_types.Real),
                                      ve_types.Sequence3(ve_types.Real),
                                      ve_types.Sequence4(ve_types.Real)),
                       'position in eye coordinates'),
        'anchor' : ('center',
                    ve_types.String,
                    'how position parameter is used'),
        'size':((4.0,4.0), # horiz and vertical size
                ve_types.Sequence2(ve_types.Real),
                'size in eye coordinates'),
        'center' : (None,  # DEPRECATED -- don't use
                    ve_types.Sequence2(ve_types.Real),
                    'position in eye coordinates',
                    VisionEgg.ParameterDefinition.DEPRECATED),
        })
    
    def __init__(self,**kw):
        Stimulus.__init__(self,**kw)

    def draw(self):
        p = self.parameters # shorthand
        if p.center is not None:
            if not hasattr(VisionEgg.config,"_GAVE_CENTER_DEPRECATION"):
                logger = logging.getLogger('VisionEgg.Core')
                logger.warning("Specifying FixationSpot by deprecated "
                               "'center' parameter deprecated.  Use "
                               "'position' parameter instead.  (Allows "
                               "use of 'anchor' parameter to set to "
                               "other values.)")
                VisionEgg.config._GAVE_CENTER_DEPRECATION = 1
            p.anchor = 'center'
            p.position = p.center[0], p.center[1] # copy values (don't copy ref 
to tuple)
        if p.on:
            # calculate center
            center = VisionEgg._get_center(p.position,p.anchor,p.size)
            gl.glDisable(gl.GL_DEPTH_TEST)
            gl.glDisable(gl.GL_TEXTURE_2D)
            gl.glDisable(gl.GL_BLEND)

            gl.glMatrixMode(gl.GL_MODELVIEW)
            gl.glLoadIdentity()

            gl.glColorf(*p.color)

            # This could go in a display list to speed it up, but then
            # size wouldn't be dynamically adjustable this way.  Could
            # still use one of the matrices to make it change size.
            x_size = self.parameters.size[0]/2.0
            y_size = self.parameters.size[1]/2.0
            x,y = center[0],center[1]
            x1 = x-x_size; x2 = x+x_size
            y1 = y-y_size; y2 = y+y_size
            gl.glBegin(gl.GL_QUADS)
            gl.glVertex2f(x1,y1)
            gl.glVertex2f(x2,y1)
            gl.glVertex2f(x2,y2)
            gl.glVertex2f(x1,y2)
            gl.glEnd() # GL_QUADS

####################################################################
#
#        Frame timing information
#
####################################################################

class FrameTimer:
    """Time inter frame intervals and compute frames per second."""
    def __init__(self, bin_start_msec=2, bin_stop_msec=28, bin_width_msec=2, 
running_average_num_frames=0):
        """Create instance of FrameTimer."""
        self.bins = Numeric.arange( bin_start_msec, bin_stop_msec, 
bin_width_msec )
        self.bin_width_msec = float(bin_width_msec)
        self.timing_histogram = Numeric.zeros( self.bins.shape, Numeric.Float ) 
# make float to avoid (early) overflow errors
        self._true_time_last_frame = None # no frames yet
        self.longest_frame_draw_time_sec = None
        self.first_tick_sec = None
        self.total_frames = 0
        self.running_average_num_frames = running_average_num_frames
        if self.running_average_num_frames:
            self.last_n_frame_times_sec = [None]*self.running_average_num_frames
        
    def tick(self):
        """Declare a frame has just been drawn."""
        true_time_now = VisionEgg.true_time_func()
        if self._true_time_last_frame != None:
            this_frame_draw_time_sec = true_time_now - 
self._true_time_last_frame
            index = 
int(math.ceil(this_frame_draw_time_sec*1000.0/self.bin_width_msec))-1
            if index > (len(self.timing_histogram)-1):
                index = -1
            self.timing_histogram[index] += 1
            self.longest_frame_draw_time_sec = 
max(self.longest_frame_draw_time_sec,this_frame_draw_time_sec)
            if self.running_average_num_frames:
                self.last_n_frame_times_sec.append(true_time_now)
                self.last_n_frame_times_sec.pop(0)
        else:
            self.first_tick_sec = true_time_now
        self._true_time_last_frame = true_time_now # set for next frame

    def get_longest_frame_duration_sec(self):
        return self.longest_frame_draw_time_sec
        
    def get_running_average_ifi_sec(self):
        if self.running_average_num_frames:
            frame_times = []
            for frame_time in self.last_n_frame_times_sec:
                if frame_time is not None:
                    frame_times.append( frame_time )
            if len(frame_times) >= 2:
                return (frame_times[-1] - frame_times[0]) / len(frame_times)
        else:
            raise RuntimeError("running_average_num_frames not set when 
creating FrameTimer instance")
    
    def get_average_ifi_sec(self):
        if self._true_time_last_frame is None:
            raise RuntimeError("No frames were drawn, can't calculate average 
IFI")
        return (self._true_time_last_frame - self.first_tick_sec) / sum( 
self.timing_histogram )

    def print_histogram(self):
        logger = logging.getLogger('VisionEgg.Core')
        logger.warning("print_histogram() method of FrameTimer is "
                       "deprecated will stop being supported. Use "
                       "log_histogram() instead.")
        self.log_histogram()
    
    def log_histogram(self):
        """Send histogram to logger."""
        buffer = StringIO.StringIO()

        n_frames = sum( self.timing_histogram )+1
        if n_frames < 2:
            print >> buffer, '%d frames were drawn.'%n_frames
            return
        average_ifi_sec = self.get_average_ifi_sec()
        print >> buffer, '%d frames were drawn.'%int(n_frames)
        print >> buffer, 'Mean frame was %.2f msec (%.2f fps), longest frame 
was %.2f msec.'%(
            
average_ifi_sec*1000.0,1.0/average_ifi_sec,self.longest_frame_draw_time_sec*1000.0)
        
        h = hist = self.timing_histogram # shorthand
        maxhist = float(max(h))
        if maxhist == 0:
            print >> buffer, "No frames were drawn."
            return
        lines = min(10,int(math.ceil(maxhist)))
        hist = hist/maxhist*float(lines) # normalize to number of lines
        print >> buffer, "histogram:"
        for line in range(lines):
            val = float(lines)-1.0-float(line)
            timing_string = "%6d   "%(round(maxhist*val/lines),)
            q = Numeric.greater(hist,val)
            for qi in q:
                s = ' '
                if qi:
                    s = '*'
                timing_string += "%4s "%(s,)
            print >> buffer, timing_string
        timing_string = " Time: "
        timing_string += "%4d "%(0,)
        for bin in self.bins[:-1]:
            timing_string += "%4d "%(bin,)
        timing_string += "+(msec)\n"
        timing_string += "Total:    "
        for hi in h:
            if hi <= 999:
                num_str = str(int(hi)).center(5)
            else:
                num_str = " +++ "
            timing_string += num_str
        print >> buffer, timing_string
        
        buffer.seek(0)
        logger = logging.getLogger('VisionEgg.Core')
        logger.info(buffer.read())
        
####################################################################
#
#        Error handling and assumption checking
#
####################################################################

import VisionEgg.Deprecated
Message = VisionEgg.Deprecated.Message

message = VisionEgg.Deprecated.Message() # create instance of Message class for 
everything to use
    
gl_assumptions = []

def add_gl_assumption(gl_variable,required_value,failure_callback):
    """Save assumptions for later checking once OpenGL context created."""
    if type(failure_callback) != types.FunctionType:
        raise ValueError("failure_callback must be a function!")
    gl_assumptions.append((gl_variable,required_value,failure_callback))

def init_gl_extension(prefix,name):
    global gl # interpreter knows when we're up to something funny with GLTrace
    logger = logging.getLogger('VisionEgg.Core')

    if gl is VisionEgg.GLTrace:
        watched = True
        gl = VisionEgg.GLTrace.gl # manipulate original module for now
    else:
        watched = False
    
    module_name = "OpenGL.GL.%(prefix)s.%(name)s"%locals()
    try:
        exec "import "+module_name
    except ImportError:
        logger.warning("Could not import %s -- some features will be "
                       "missing."%(module_name,))
        return False
    module = eval(module_name)
    init_function_name = "glInit"+name.title().replace('_','')+prefix
    init_function = getattr(module,init_function_name)
    if not init_function():
        logger.warning("Could not initialize %s -- some features will "
                       "be missing."%(module_name,))
        return False
    for attr_name in dir(module):
        # put attributes from module into "gl" module dictionary
        # (Namespace overlap as you'd get OpenGL apps written in C)
        attr = getattr(module,attr_name)
        # reject unwanted attributes
        if attr_name.startswith('__'):
            continue
        elif attr_name == init_function_name:
            continue
        elif attr_name == 'gl':
            continue
        elif type(attr) == type(VisionEgg): # module type
            continue
        
        gl_attr_name = attr_name
        setattr(gl,gl_attr_name,attr)
        
    if watched:
        VisionEgg.GLTrace.attach() # (re)scan namespace
        gl = VisionEgg.GLTrace # reinstall GLTrace
    return True # success!
            
def post_gl_init():
    """Called by Screen instance. Requires OpenGL context to be created."""
    global gl_vendor, gl_renderer, gl_version # set above
    logger = logging.getLogger('VisionEgg.Core')
    
    if gl_version < '1.3':
        if not init_gl_extension('ARB','multitexture'):
            logger.warning("multitexturing not available.  Some features "
                           "will not be available")
    else:
        if not hasattr(gl,'glActiveTexture'):
            logger.debug("PyOpenGL bug: OpenGL multitexturing not available "
                         "even though OpenGL is 1.3 or greater. "
                         "Attempting ctypes-based workaround.")
            VisionEgg.PlatformDependent.attempt_to_load_multitexturing()
        if hasattr(gl,'glActiveTexture'): # the above worked or PyOpenGL fixed
            # OpenGL 1.3 has this extension built-in,
            # but doing this allows use of ARB names.
            gl.glActiveTextureARB = gl.glActiveTexture
            gl.glMultiTexCoord2fARB = gl.glMultiTexCoord2f
            gl.GL_TEXTURE0_ARB = gl.GL_TEXTURE0
            gl.GL_TEXTURE1_ARB = gl.GL_TEXTURE1
        
    if gl_version < '1.2':
        if init_gl_extension('EXT','bgra'):
            # make sure gl.GL_BRGA is defined
            gl.GL_BGRA = gl.GL_BGRA_EXT

    for gl_variable,required_value,failure_callback in gl_assumptions:
        # Code required for each variable to be checked
        if gl_variable == "__SPECIAL__":
            if required_value == "linux_nvidia_or_new_ATI":
                ok = 0
                # Test for nVidia
                if "nvidia" == gl_vendor.split()[0].lower():
                    ok = 1 # yes it is
                if gl_renderer.startswith('Mesa DRI Radeon'):
                    date = gl_renderer.split()[3]
                    if date > "20021216": # not sure about exact date
                        ok=1
                if not ok:
                    failure_callback()
            else:
                raise RuntimeError, "Unknown gl_assumption: %s == 
%s"%(gl_variable,required_value)
            
        elif gl_variable.upper() == "GL_VERSION":
            value_str = gl_version.split()[0]
            value_ints = map(int,value_str.split('.'))
            value = float( str(value_ints[0]) + "." + 
''.join(map(str,value_ints[1:])))
            if value < required_value:
                failure_callback()
        else:
            raise RuntimeError, "Unknown gl_assumption"

    # Do we have gl.GL_CLAMP_TO_EDGE ?
    try:
        gl.GL_CLAMP_TO_EDGE
    except AttributeError:
        if gl_version >= '1.2':
            # If OpenGL version >= 1.2, this should be defined
            # It seems to be a PyOpenGL bug that it's not.
            logger.debug("GL_CLAMP_TO_EDGE is not defined. "
                         "Because you have OpenGL version 1.2 or "
                         "greater, this is probably a bug in "
                         "PyOpenGL.  Assigning GL_CLAMP_TO_EDGE to "
                         "the value that is usually used.")
            gl.GL_CLAMP_TO_EDGE = 0x812F
        else:
            try:
                init_gl_extension('SGIS','texture_edge_clamp')
                gl.GL_CLAMP_TO_EDGE = gl.GL_CLAMP_TO_EDGE_SGIS
            except:
                
                logger.warning("GL_CLAMP_TO_EDGE is not "
                               "available.  OpenGL version is "
                               "less than 1.2, and the "
                               "texture_edge_clamp_SGIS extension "
                               "failed to load. It may be impossible to "
                               "get exact 1:1 reproduction of "
                               "textures.  Using GL_CLAMP instead of "
                               "GL_CLAMP_TO_EDGE.")
                gl.GL_CLAMP_TO_EDGE = gl.GL_CLAMP

#########################################################################
#
#       Moved to FlowControl.py -- here only for backwards compatibility
#
#########################################################################

import VisionEgg.FlowControl
Presentation = VisionEgg.FlowControl.Presentation
Controller = VisionEgg.FlowControl.Controller
ConstantController = VisionEgg.FlowControl.ConstantController
EvalStringController = VisionEgg.FlowControl.EvalStringController
ExecStringController = VisionEgg.FlowControl.ExecStringController
FunctionController = VisionEgg.FlowControl.FunctionController
EncapsulatedController = VisionEgg.FlowControl.EncapsulatedController

Other related posts: