[clutter/wip/stereo] Adds initial support for stereoscopic rendering



commit dff7660d5da96a87e25a34f5a8212e6aaea22d57
Author: Robert Bragg <robert linux intel com>
Date:   Mon Aug 1 01:11:57 2011 +0100

    Adds initial support for stereoscopic rendering
    
    This adds initial support for stereoscopic rendering of Clutter scenes.
    The major change here is that we've introduce the internal concept of a
    ClutterCamera which encapsulates a viewport, projection and view
    transform. In the future the camera will also encapsulate a reference to
    the destination framebuffer, but for now it's assumed that the backend
    framebuffer behind stage->priv->impl is associated with all cameras.
    
    If clutter_stage_set_stereo_enabled (stage, TRUE) is called then Clutter
    will now enable stereographic rendering, using a simple toe-in approach
    of modifying the view transform for each eye to model that there is a
    gap between the eyes and both eyes look towards the center of the z=0
    plane (This is the plane where most 2d content for a clutter application
    goes)
    
    A notable disadvantage to the simple toe-in approach is that 2D content
    on the z=0 plane will look different for each eye so straight lines
    for example wont appear straight to the user. A better approach we will
    implement later is to setup asymetric projections for each eye that
    can model the eye gap but the frustums cross at the z=0 plane such that
    both eyes would have the same view of 2D content.
    
    Currently there are three modes of output for stereoscopic content.
     Â There is anaglyph rendering (for use with filter glasses with a red
       filter for the left eye and cyan filter for the right)
     Â There is a vertical split mode which splits the stage and shows the
       left eye content on the left and the right eye on the right.
     Â There is a horizontal split mode which splits the stage and shows
       the left eye content on the top and right eye on the bottom.
    
    The mode can be selected using clutter_stage_set_stereo_mode().
    
    The mode can also be explicitly overridden using the CLUTTER_STEREO_MODE
    environment variable by setting it to "default", "anaglyph",
    "vertical-split" or "horizontal-split". Setting this environment
    variable also implicitly forces stereo rendering to be enabled.

 clutter/clutter-actor-private.h        |    3 +-
 clutter/clutter-actor.c                |  687 +++++++++++++++++------
 clutter/clutter-backend.c              |    4 +-
 clutter/clutter-enums.h                |   25 +
 clutter/clutter-offscreen-effect.c     |  480 ++++++++++++----
 clutter/clutter-paint-volume-private.h |    7 +-
 clutter/clutter-paint-volume.c         |   55 ++-
 clutter/clutter-private.h              |   26 +
 clutter/clutter-stage-private.h        |   14 +-
 clutter/clutter-stage-window.c         |   39 ++
 clutter/clutter-stage-window.h         |   18 +-
 clutter/clutter-stage.c                |  953 +++++++++++++++++++++++---------
 clutter/clutter-stage.h                |    5 +
 clutter/clutter-texture.c              |   54 ++-
 clutter/cogl/clutter-stage-cogl.c      |  214 +++++---
 15 files changed, 1915 insertions(+), 669 deletions(-)
---
diff --git a/clutter/clutter-actor-private.h b/clutter/clutter-actor-private.h
index 6b91405..4cb3e71 100644
--- a/clutter/clutter-actor-private.h
+++ b/clutter/clutter-actor-private.h
@@ -251,8 +251,7 @@ void _clutter_actor_queue_redraw_full        (ClutterActor              *self,
                                               ClutterEffect             *effect);
 
 ClutterPaintVolume *_clutter_actor_get_queue_redraw_clip (ClutterActor *self);
-void _clutter_actor_set_queue_redraw_clip     (ClutterActor             *self,
-                                               ClutterPaintVolume *clip_volume);
+int _clutter_actor_get_queue_redraw_camera_index (ClutterActor *self);
 void _clutter_actor_finish_queue_redraw       (ClutterActor             *self,
                                                ClutterPaintVolume       *clip);
 
diff --git a/clutter/clutter-actor.c b/clutter/clutter-actor.c
index f84d7cf..92aa1b9 100644
--- a/clutter/clutter-actor.c
+++ b/clutter/clutter-actor.c
@@ -373,6 +373,21 @@ typedef enum {
                               */
 } MapStateChange;
 
+typedef struct
+{
+  const ClutterCamera *camera;
+
+  /* NB: This volume isn't relative to this actor, it is in eye
+   * coordinates so that it can remain valid after the actor changes.
+   */
+  ClutterPaintVolume eye_volume;
+  gboolean eye_volume_valid;
+
+  /* If this doesn't match camera->age then the above paint-volume
+   * is invalid. */
+  int valid_for_age;
+} PerCameraState;
+
 /* 3 entries should be a good compromise, few layout managers
  * will ask for 3 different preferred size in each allocation cycle */
 #define N_CACHED_SIZE_REQUESTS 3
@@ -396,6 +411,12 @@ struct _ClutterActorPrivate
   ClutterActorBox allocation;
   ClutterAllocationFlags allocation_flags;
 
+  /* State we cache that's specific to a camera view. We only currently
+   * consider their may be two cameras for stereo rendering. */
+  PerCameraState *camera_state;
+  int n_cameras;
+  int cameras_age;
+
   /* depth */
   gfloat z;
 
@@ -415,6 +436,7 @@ struct _ClutterActorPrivate
   ClutterEffect *flatten_effect;
 
   /* scene graph */
+  ClutterStage *stage_cache;
   ClutterActor *parent;
   ClutterActor *prev_sibling;
   ClutterActor *next_sibling;
@@ -449,6 +471,13 @@ struct _ClutterActorPrivate
   /* a counter used to toggle the CLUTTER_INTERNAL_CHILD flag */
   gint internal_child;
 
+  /* XXX: These are a workaround for not being able to break the ABI
+   * of the QUEUE_REDRAW signal. They are out-of-band arguments.
+   * See clutter_actor_queue_clipped_redraw() for details.
+   */
+  ClutterPaintVolume *oob_queue_redraw_clip;
+  int oob_queue_redraw_camera_index;
+
   /* meta classes */
   ClutterMetaGroup *actions;
   ClutterMetaGroup *constraints;
@@ -476,11 +505,6 @@ struct _ClutterActorPrivate
 
   ClutterPaintVolume paint_volume;
 
-  /* NB: This volume isn't relative to this actor, it is in eye
-   * coordinates so that it can remain valid after the actor changes.
-   */
-  ClutterPaintVolume last_paint_volume;
-
   ClutterStageQueueRedrawEntry *queue_redraw_entry;
 
   ClutterColor bg_color;
@@ -1096,11 +1120,40 @@ clutter_actor_update_map_state (ClutterActor  *self,
 #endif
 }
 
+static ClutterStage *
+_clutter_actor_get_stage_real (ClutterActor *actor)
+{
+  ClutterActor *self;
+
+  if (G_LIKELY (actor->priv->stage_cache))
+    return actor->priv->stage_cache;
+
+  self = actor;
+
+  /* Check to see if the actor is associated with a stage yet... */
+  while (actor && !CLUTTER_ACTOR_IS_TOPLEVEL (actor))
+    actor = actor->priv->parent;
+
+  /* Note: we never want to have a type check when we cast here
+   * since this is function can be used very heavily. */
+  self->priv->stage_cache = (ClutterStage *)actor;
+  return self->priv->stage_cache;
+}
+
+ClutterActor *
+_clutter_actor_get_stage_internal (ClutterActor *actor)
+{
+  /* Note: we never want to have a type check when we cast here
+   * since this is function can be used very heavily. */
+  return (ClutterActor *)_clutter_actor_get_stage_real (actor);
+}
+
 static void
 clutter_actor_real_map (ClutterActor *self)
 {
+  ClutterStage *stage = _clutter_actor_get_stage_real (self);
   ClutterActorPrivate *priv = self->priv;
-  ClutterActor *stage, *iter;
+  ClutterActor *iter;
 
   g_assert (!CLUTTER_ACTOR_IS_MAPPED (self));
 
@@ -1109,9 +1162,7 @@ clutter_actor_real_map (ClutterActor *self)
 
   CLUTTER_ACTOR_SET_FLAGS (self, CLUTTER_ACTOR_MAPPED);
 
-  stage = _clutter_actor_get_stage_internal (self);
-  priv->pick_id = _clutter_stage_acquire_pick_id (CLUTTER_STAGE (stage), self);
-
+  priv->pick_id = _clutter_stage_acquire_pick_id (stage, self);
   CLUTTER_NOTE (ACTOR, "Pick id '%d' for actor '%s'",
                 priv->pick_id,
                 _clutter_actor_get_debug_name (self));
@@ -1165,6 +1216,7 @@ clutter_actor_real_unmap (ClutterActor *self)
 {
   ClutterActorPrivate *priv = self->priv;
   ClutterActor *iter;
+  int i;
 
   g_assert (CLUTTER_ACTOR_IS_MAPPED (self));
 
@@ -1180,11 +1232,29 @@ clutter_actor_real_unmap (ClutterActor *self)
 
   CLUTTER_ACTOR_UNSET_FLAGS (self, CLUTTER_ACTOR_MAPPED);
 
-  /* clear the contents of the last paint volume, so that hiding + moving +
-   * showing will not result in the wrong area being repainted
+  /* clear the contents of the - per camera - eye coordinate paint
+   * volumes, so that if we later show the actor again we won't
+   * redundantly also redraw the old location of the actor.
+   *
+   * Note: We only do this if the actor doesn't already have redraw
+   * queued for it that may depend on the last eye_volume to clear
+   * its old location. For example if you were to hide, move and
+   * re-show an actor in preparing for a single frame then in that
+   * case we would have queued a redraw for the hide and and do
+   * need to make sure that the actors old location is redrawn.
    */
-  _clutter_paint_volume_init_static (&priv->last_paint_volume, NULL);
-  priv->last_paint_volume_valid = TRUE;
+  if (priv->queue_redraw_entry != NULL)
+    {
+      for (i = 0; i < priv->n_cameras; i++)
+        {
+          PerCameraState *camera_state = &priv->camera_state[i];
+
+          if (!camera_state->eye_volume_valid)
+            clutter_paint_volume_free (&camera_state->eye_volume);
+          _clutter_paint_volume_init_static (&camera_state->eye_volume, NULL);
+          camera_state->eye_volume_valid = TRUE;
+        }
+    }
 
   /* notify on parent mapped after potentially unmapping
    * children, so apps see a bottom-up notification.
@@ -1194,10 +1264,7 @@ clutter_actor_real_unmap (ClutterActor *self)
   /* relinquish keyboard focus if we were unmapped while owning it */
   if (!CLUTTER_ACTOR_IS_TOPLEVEL (self))
     {
-      ClutterStage *stage;
-
-      stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (self));
-
+      ClutterStage *stage = _clutter_actor_get_stage_real (self);
       if (stage != NULL)
         _clutter_stage_release_pick_id (stage, priv->pick_id);
 
@@ -1340,6 +1407,14 @@ clutter_actor_show (ClutterActor *self)
   g_signal_emit (self, actor_signals[SHOW], 0);
   g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_VISIBLE]);
 
+  /* XXX: shouldn't this be:
+   * if (_clutter_actor_get_stage_real (self))
+   *   clutter_actor_queue_redraw (self);
+   *
+   * XXX: actually shouldn't we queue redraws from map/unmap changes
+   * instead since there's no point queueing a redraw for an actor if
+   * one of its ancestors is unmapped.
+   */
   if (priv->parent != NULL)
     clutter_actor_queue_redraw (priv->parent);
 
@@ -1435,6 +1510,14 @@ clutter_actor_hide (ClutterActor *self)
   g_signal_emit (self, actor_signals[HIDE], 0);
   g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_VISIBLE]);
 
+  /* XXX: shouldn't this be:
+   * if (_clutter_actor_get_stage_real (self))
+   *   clutter_actor_queue_redraw (self);
+   *
+   * XXX: actually shouldn't we queue redraws from map/unmap changes
+   * instead since there's no point queueing a redraw for an actor if
+   * one of its ancestors is unmapped.
+   */
   if (priv->parent != NULL)
     clutter_actor_queue_redraw (priv->parent);
 
@@ -2176,9 +2259,8 @@ clutter_actor_real_queue_redraw (ClutterActor *self,
    */
   if (self->priv->propagated_one_redraw)
     {
-      ClutterActor *stage = _clutter_actor_get_stage_internal (self);
-      if (stage != NULL &&
-          _clutter_stage_has_full_redraw_queued (CLUTTER_STAGE (stage)))
+      ClutterStage *stage = _clutter_actor_get_stage_real (self);
+      if (stage != NULL && _clutter_stage_has_full_redraw_queued (stage))
         return;
     }
 
@@ -2256,7 +2338,7 @@ clutter_actor_apply_relative_transform_to_point (ClutterActor        *self,
   w = 1.0;
 
   if (ancestor == NULL)
-    ancestor = _clutter_actor_get_stage_internal (self);
+    ancestor = CLUTTER_ACTOR (_clutter_actor_get_stage_real (self));
 
   if (ancestor == NULL)
     {
@@ -2270,40 +2352,32 @@ clutter_actor_apply_relative_transform_to_point (ClutterActor        *self,
 
 static gboolean
 _clutter_actor_fully_transform_vertices (ClutterActor *self,
+                                         int camera_index,
                                          const ClutterVertex *vertices_in,
                                          ClutterVertex *vertices_out,
                                          int n_vertices)
 {
   ClutterActor *stage;
+  const ClutterCamera *camera;
   CoglMatrix modelview;
-  CoglMatrix projection;
-  float viewport[4];
 
   g_return_val_if_fail (CLUTTER_IS_ACTOR (self), FALSE);
 
-  stage = _clutter_actor_get_stage_internal (self);
-
   /* We really can't do anything meaningful in this case so don't try
    * to do any transform */
+  stage = CLUTTER_ACTOR (_clutter_actor_get_stage_real (self));
   if (stage == NULL)
     return FALSE;
 
-  /* Note: we pass NULL as the ancestor because we don't just want the modelview
-   * that gets us to stage coordinates, we want to go all the way to eye
-   * coordinates */
-  _clutter_actor_apply_relative_transformation_matrix (self, NULL, &modelview);
+  camera = _clutter_stage_get_camera (CLUTTER_STAGE (stage), camera_index);
+  cogl_matrix_init_from_array (&modelview, (float *)&camera->view);
 
-  /* Fetch the projection and viewport */
-  _clutter_stage_get_projection_matrix (CLUTTER_STAGE (stage), &projection);
-  _clutter_stage_get_viewport (CLUTTER_STAGE (stage),
-                               &viewport[0],
-                               &viewport[1],
-                               &viewport[2],
-                               &viewport[3]);
+  _clutter_actor_apply_relative_transformation_matrix (self, stage, &modelview);
 
+  /* Fetch the projection and viewport */
   _clutter_util_fully_transform_vertices (&modelview,
-                                          &projection,
-                                          viewport,
+                                          &camera->projection,
+                                          camera->viewport,
                                           vertices_in,
                                           vertices_out,
                                           n_vertices);
@@ -2321,6 +2395,9 @@ _clutter_actor_fully_transform_vertices (ClutterActor *self,
  * into screen-relative coordinates with the current actor
  * transformation (i.e. scale, rotation, etc)
  *
+ * <note>If clutter is being used for stereo rendering then this will
+ * simply transform the point according the left eye's view</note>
+ *
  * Since: 0.4
  **/
 void
@@ -2330,14 +2407,13 @@ clutter_actor_apply_transform_to_point (ClutterActor        *self,
 {
   g_return_if_fail (point != NULL);
   g_return_if_fail (vertex != NULL);
-  _clutter_actor_fully_transform_vertices (self, point, vertex, 1);
+  _clutter_actor_fully_transform_vertices (self, 0, point, vertex, 1);
 }
 
 /*
  * _clutter_actor_get_relative_transformation_matrix:
  * @self: The actor whose coordinate space you want to transform from.
- * @ancestor: The ancestor actor whose coordinate space you want to transform too
- *            or %NULL if you want to transform all the way to eye coordinates.
+ * @ancestor: The ancestor actor whose coordinate space you want to transform too.
  * @matrix: A #CoglMatrix to store the transformation
  *
  * This gets a transformation @matrix that will transform coordinates from the
@@ -2347,13 +2423,6 @@ clutter_actor_apply_transform_to_point (ClutterActor        *self,
  * coordinates of @self into stage coordinates you would pass the actor's stage
  * pointer as the @ancestor.
  *
- * If you pass %NULL then the transformation will take you all the way through
- * to eye coordinates. This can be useful if you want to extract the entire
- * modelview transform that Clutter applies before applying the projection
- * transformation. If you want to explicitly set a modelview on a CoglFramebuffer
- * using cogl_set_modelview_matrix() for example then you would want a matrix
- * that transforms into eye coordinates.
- *
  * <note><para>This function explicitly initializes the given @matrix. If you just
  * want clutter to multiply a relative transformation with an existing matrix
  * you can use clutter_actor_apply_relative_transformation_matrix()
@@ -2367,6 +2436,8 @@ _clutter_actor_get_relative_transformation_matrix (ClutterActor *self,
                                                    ClutterActor *ancestor,
                                                    CoglMatrix *matrix)
 {
+  g_return_if_fail (ancestor != NULL);
+
   cogl_matrix_init_identity (matrix);
 
   _clutter_actor_apply_relative_transformation_matrix (self, ancestor, matrix);
@@ -2376,6 +2447,7 @@ _clutter_actor_get_relative_transformation_matrix (ClutterActor *self,
  * transformed vertices to @verts[]. */
 static gboolean
 _clutter_actor_transform_and_project_box (ClutterActor          *self,
+                                          int                    camera_index,
 					  const ClutterActorBox *box,
 					  ClutterVertex          verts[])
 {
@@ -2395,7 +2467,8 @@ _clutter_actor_transform_and_project_box (ClutterActor          *self,
   box_vertices[3].z = 0;
 
   return
-    _clutter_actor_fully_transform_vertices (self, box_vertices, verts, 4);
+    _clutter_actor_fully_transform_vertices (self, camera_index,
+                                             box_vertices, verts, 4);
 }
 
 /**
@@ -2432,25 +2505,26 @@ clutter_actor_get_allocation_vertices (ClutterActor  *self,
   ClutterActorBox box;
   ClutterVertex vertices[4];
   CoglMatrix modelview;
+  ClutterStage *stage;
 
   g_return_if_fail (CLUTTER_IS_ACTOR (self));
   g_return_if_fail (ancestor == NULL || CLUTTER_IS_ACTOR (ancestor));
 
+  priv = self->priv;
+  stage = _clutter_actor_get_stage_real (self);
+
   if (ancestor == NULL)
-    ancestor = _clutter_actor_get_stage_internal (self);
+    ancestor = CLUTTER_ACTOR (stage);
 
   /* Fallback to a NOP transform if the actor isn't parented under a
    * stage. */
   if (ancestor == NULL)
     ancestor = self;
 
-  priv = self->priv;
-
   /* if the actor needs to be allocated we force a relayout, so that
    * we will have valid values to use in the transformations */
   if (priv->needs_allocation)
     {
-      ClutterActor *stage = _clutter_actor_get_stage_internal (self);
       if (stage)
         _clutter_stage_maybe_relayout (stage);
       else
@@ -2505,6 +2579,9 @@ clutter_actor_get_allocation_vertices (ClutterActor  *self,
  *   <listitem><para>v[3] contains (x2, y2)</para></listitem>
  * </itemizedlist>
  *
+ * <note>If clutter is being used for stereo rendering then this will
+ * simply return a box according the left eye's view.</note>
+ *
  * Since: 0.4
  */
 void
@@ -2524,7 +2601,7 @@ clutter_actor_get_abs_allocation_vertices (ClutterActor  *self,
    */
   if (priv->needs_allocation)
     {
-      ClutterActor *stage = _clutter_actor_get_stage_internal (self);
+      ClutterStage *stage = _clutter_actor_get_stage_real (self);
       /* There's nothing meaningful we can do now */
       if (!stage)
         return;
@@ -2539,6 +2616,7 @@ clutter_actor_get_abs_allocation_vertices (ClutterActor  *self,
   actor_space_allocation.x2 = priv->allocation.x2 - priv->allocation.x1;
   actor_space_allocation.y2 = priv->allocation.y2 - priv->allocation.y1;
   _clutter_actor_transform_and_project_box (self,
+                                            0,
 					    &actor_space_allocation,
 					    verts);
 }
@@ -2630,8 +2708,7 @@ _clutter_actor_apply_modelview_transform (ClutterActor *self,
 /*
  * clutter_actor_apply_relative_transformation_matrix:
  * @self: The actor whose coordinate space you want to transform from.
- * @ancestor: The ancestor actor whose coordinate space you want to transform too
- *            or %NULL if you want to transform all the way to eye coordinates.
+ * @ancestor: The ancestor actor whose coordinate space you want to transform too.
  * @matrix: A #CoglMatrix to apply the transformation too.
  *
  * This multiplies a transform with @matrix that will transform coordinates
@@ -2641,13 +2718,6 @@ _clutter_actor_apply_modelview_transform (ClutterActor *self,
  * coordinates of @self into stage coordinates you would pass the actor's stage
  * pointer as the @ancestor.
  *
- * If you pass %NULL then the transformation will take you all the way through
- * to eye coordinates. This can be useful if you want to extract the entire
- * modelview transform that Clutter applies before applying the projection
- * transformation. If you want to explicitly set a modelview on a CoglFramebuffer
- * using cogl_set_modelview_matrix() for example then you would want a matrix
- * that transforms into eye coordinates.
- *
  * <note>This function doesn't initialize the given @matrix, it simply
  * multiplies the requested transformation matrix with the existing contents of
  * @matrix. You can use cogl_matrix_init_identity() to initialize the @matrix
@@ -2661,6 +2731,8 @@ _clutter_actor_apply_relative_transformation_matrix (ClutterActor *self,
 {
   ClutterActor *parent;
 
+  g_return_if_fail (ancestor != NULL);
+
   /* Note we terminate before ever calling stage->apply_transform()
    * since that would conceptually be relative to the underlying
    * window OpenGL coordinates so we'd need a special @ancestor
@@ -2754,9 +2826,9 @@ _clutter_actor_draw_paint_volume (ClutterActor *self)
     {
       gfloat width, height;
       ClutterPaintVolume fake_pv;
+      ClutterStage *stage = _clutter_actor_get_stage_real (self);
 
-      ClutterActor *stage = _clutter_actor_get_stage_internal (self);
-      _clutter_paint_volume_init_static (&fake_pv, stage);
+      _clutter_paint_volume_init_static (&fake_pv, CLUTTER_ACTOR (stage));
 
       clutter_actor_get_size (self, &width, &height);
       clutter_paint_volume_set_width (&fake_pv, width);
@@ -2780,6 +2852,7 @@ _clutter_actor_draw_paint_volume (ClutterActor *self)
 
 static void
 _clutter_actor_paint_cull_result (ClutterActor *self,
+                                  const ClutterCamera *camera,
                                   gboolean success,
                                   ClutterCullResult result)
 {
@@ -2851,32 +2924,36 @@ static gboolean
 cull_actor (ClutterActor *self, ClutterCullResult *result_out)
 {
   ClutterActorPrivate *priv = self->priv;
-  ClutterActor *stage;
   const ClutterPlane *stage_clip;
+  const ClutterCamera *camera;
+  PerCameraState *camera_state;
+  ClutterStage *stage = _clutter_actor_get_stage_real (self);
 
-  if (!priv->last_paint_volume_valid)
+  if (G_UNLIKELY (clutter_paint_debug_flags & CLUTTER_DEBUG_DISABLE_CULLING))
+    return FALSE;
+
+  stage_clip = _clutter_stage_get_clip (stage);
+  if (G_UNLIKELY (!stage_clip))
     {
       CLUTTER_NOTE (CLIPPING, "Bail from cull_actor without culling (%s): "
-                    "->last_paint_volume_valid == FALSE",
+                    "No stage clip set",
                     _clutter_actor_get_debug_name (self));
       return FALSE;
     }
 
-  if (G_UNLIKELY (clutter_paint_debug_flags & CLUTTER_DEBUG_DISABLE_CULLING))
-    return FALSE;
+  camera = _clutter_stage_get_current_camera (stage);
+  camera_state = &priv->camera_state[camera->index];
 
-  stage = _clutter_actor_get_stage_internal (self);
-  stage_clip = _clutter_stage_get_clip (CLUTTER_STAGE (stage));
-  if (G_UNLIKELY (!stage_clip))
+  if (!camera_state->eye_volume_valid)
     {
       CLUTTER_NOTE (CLIPPING, "Bail from cull_actor without culling (%s): "
-                    "No stage clip set",
+                    "->paint_volume_valid == FALSE",
                     _clutter_actor_get_debug_name (self));
       return FALSE;
     }
 
   if (cogl_get_draw_framebuffer () !=
-      _clutter_stage_get_active_framebuffer (CLUTTER_STAGE (stage)))
+      _clutter_stage_get_active_framebuffer (stage))
     {
       CLUTTER_NOTE (CLIPPING, "Bail from cull_actor without culling (%s): "
                     "Current framebuffer doesn't correspond to stage",
@@ -2885,37 +2962,107 @@ cull_actor (ClutterActor *self, ClutterCullResult *result_out)
     }
 
   *result_out =
-    _clutter_paint_volume_cull (&priv->last_paint_volume, stage_clip);
+    _clutter_paint_volume_cull (&camera_state->eye_volume, stage_clip);
   return TRUE;
 }
 
 static void
-_clutter_actor_update_last_paint_volume (ClutterActor *self)
+invalidate_per_camera_eye_volume (PerCameraState *camera_state)
+{
+  if (camera_state->eye_volume_valid)
+    {
+      clutter_paint_volume_free (&camera_state->eye_volume);
+      camera_state->eye_volume_valid = FALSE;
+    }
+}
+
+static PerCameraState *
+_clutter_actor_get_per_camera_state (ClutterActor *self,
+                                     int camera_index)
 {
   ClutterActorPrivate *priv = self->priv;
+  ClutterStage *stage = _clutter_actor_get_stage_real (self);
+  int cameras_age = _clutter_stage_get_cameras_age (stage);
+  PerCameraState *camera_state;
+
+  /* Whenever there are additions or removals of cameras associated with the
+   * stage then the stage's 'cameras_age' is bumped and we throw away any
+   * per-actor cached state associated with the old cameras. */
+
+  if (G_UNLIKELY (cameras_age != priv->cameras_age))
+    {
+      int i;
+      int n_cameras;
+
+      for (i = 0; i < priv->n_cameras; i++)
+        invalidate_per_camera_eye_volume (&priv->camera_state[i]);
+
+      if (priv->camera_state)
+        g_slice_free1 (sizeof (PerCameraState) * priv->n_cameras,
+                       priv->camera_state);
+
+      /* NB: We always allocate for the total number of cameras since
+       * we expect that each camera is likely going to be painted each
+       * frame so we should save having to re-allocate later. */
+      n_cameras = _clutter_stage_get_n_cameras (stage);
+      priv->camera_state = g_slice_alloc (sizeof (PerCameraState) * n_cameras);
+
+      for (i = 0; i < n_cameras; i++)
+        {
+          camera_state = &priv->camera_state[i];
+
+          camera_state->camera = _clutter_stage_get_camera (stage, i);
+          camera_state->eye_volume_valid = FALSE;
+          camera_state->valid_for_age = camera_state->camera->age;
+        }
+
+      priv->n_cameras = n_cameras;
+      priv->cameras_age = cameras_age;
+    }
+
+  camera_state = &priv->camera_state[camera_index];
+  if (camera_state->camera->age != camera_state->valid_for_age)
+    {
+      invalidate_per_camera_eye_volume (camera_state);
+      camera_state->valid_for_age = camera_state->camera->age;
+    }
+
+  return camera_state;
+}
+
+/* NB: This updates the eye coordinates paint volume ("eye_volume") for the
+ * current camera and it's assumed that this is only used during painting where
+ * the current camera is meaningful. */
+static void
+_clutter_actor_update_eye_volume (ClutterActor *self)
+{
   const ClutterPaintVolume *pv;
+  ClutterStage *stage = _clutter_actor_get_stage_real (self);
+  const ClutterCamera *camera = _clutter_stage_get_current_camera (stage);
+  PerCameraState *camera_state =
+    _clutter_actor_get_per_camera_state (self, camera->index);
 
-  if (priv->last_paint_volume_valid)
+  if (camera_state->eye_volume_valid)
     {
-      clutter_paint_volume_free (&priv->last_paint_volume);
-      priv->last_paint_volume_valid = FALSE;
+      clutter_paint_volume_free (&camera_state->eye_volume);
+      camera_state->eye_volume_valid = FALSE;
     }
 
   pv = clutter_actor_get_paint_volume (self);
   if (!pv)
     {
-      CLUTTER_NOTE (CLIPPING, "Bail from update_last_paint_volume (%s): "
+      CLUTTER_NOTE (CLIPPING, "Bail from update_paint_volume (%s): "
                     "Actor failed to report a paint volume",
                     _clutter_actor_get_debug_name (self));
       return;
     }
 
-  _clutter_paint_volume_copy_static (pv, &priv->last_paint_volume);
+  _clutter_paint_volume_copy_static (pv, &camera_state->eye_volume);
 
-  _clutter_paint_volume_transform_relative (&priv->last_paint_volume,
-                                            NULL); /* eye coordinates */
+  _clutter_paint_volume_transform_relative_to_camera (&camera_state->eye_volume,
+                                                      camera);
 
-  priv->last_paint_volume_valid = TRUE;
+  camera_state->eye_volume_valid = TRUE;
 }
 
 static inline gboolean
@@ -3095,6 +3242,9 @@ clutter_actor_paint (ClutterActor *self)
   ClutterPickMode pick_mode;
   gboolean clip_set = FALSE;
   gboolean shader_applied = FALSE;
+  ClutterStage *stage;
+  const ClutterCamera *camera;
+  gboolean set_current_camera;
 
   CLUTTER_STATIC_COUNTER (actor_paint_counter,
                           "Actor real-paint counter",
@@ -3137,6 +3287,39 @@ clutter_actor_paint (ClutterActor *self)
   /* mark that we are in the paint process */
   CLUTTER_SET_PRIVATE_FLAGS (self, CLUTTER_IN_PAINT);
 
+  stage = _clutter_actor_get_stage_real (self);
+  camera = _clutter_stage_get_current_camera (stage);
+
+  /* Although not ideal, we have to support toolkits that may
+   * manually paint actors outside of a standard paint-cycle
+   * (such as as MxOffscreen which may paint individual actors
+   * to an offscreen fbo)
+   *
+   * In this situation we won't have setup a current camera and so,
+   * for compatibility, we make the left_eye camera current so code
+   * relying on this capability won't simply crash.
+   *
+   * It should be noted though that code relying on this behaviour
+   * won't work with stereoscopic rendering.
+   *
+   * XXX: This code should stay very near the beginning of
+   * clutter_actor_paint() to ensure that we do have a valid camera for
+   * subsequent code.
+   */
+  if (!camera)
+    {
+      camera = _clutter_stage_get_camera (stage, 0);
+
+      /* XXX: code relying on this really should be encourage to
+       * switch to a solution that works within the paint-cycle not
+       * least because the state of the current camera is basically
+       * un-defined and may change before the next paint. */
+      _clutter_stage_set_current_camera (stage, camera);
+      set_current_camera = TRUE;
+    }
+  else
+    set_current_camera = FALSE;
+
   cogl_push_matrix();
 
   if (priv->enable_model_view_transform)
@@ -3249,7 +3432,9 @@ clutter_actor_paint (ClutterActor *self)
    * paint then the last-paint-volume would likely represent the new
    * actor position not the old.
    */
-  if (!in_clone_paint () && pick_mode == CLUTTER_PICK_NONE)
+  if (!set_current_camera &&
+      !in_clone_paint () &&
+      pick_mode == CLUTTER_PICK_NONE)
     {
       gboolean success;
       /* annoyingly gcc warns if uninitialized even though
@@ -3261,12 +3446,12 @@ clutter_actor_paint (ClutterActor *self)
                       CLUTTER_DEBUG_DISABLE_CLIPPED_REDRAWS)) !=
                     (CLUTTER_DEBUG_DISABLE_CULLING |
                      CLUTTER_DEBUG_DISABLE_CLIPPED_REDRAWS)))
-        _clutter_actor_update_last_paint_volume (self);
+        _clutter_actor_update_eye_volume (self);
 
       success = cull_actor (self, &result);
 
       if (G_UNLIKELY (clutter_paint_debug_flags & CLUTTER_DEBUG_REDRAWS))
-        _clutter_actor_paint_cull_result (self, success, result);
+        _clutter_actor_paint_cull_result (self, camera, success, result);
       else if (result == CLUTTER_CULL_RESULT_OUT && success)
         goto done;
     }
@@ -3301,6 +3486,9 @@ done:
   if (pick_mode == CLUTTER_PICK_NONE)
     priv->is_dirty = FALSE;
 
+  if (set_current_camera)
+    _clutter_stage_set_current_camera (stage, NULL);
+
   if (clip_set)
     cogl_clip_pop();
 
@@ -3439,6 +3627,7 @@ remove_child (ClutterActor *self,
   if (self->priv->last_child == child)
     self->priv->last_child = prev_sibling;
 
+  child->priv->stage_cache = NULL;
   child->priv->parent = NULL;
   child->priv->prev_sibling = NULL;
   child->priv->next_sibling = NULL;
@@ -4611,6 +4800,21 @@ clutter_actor_dispose (GObject *object)
       priv->layout_manager = NULL;
     }
 
+  if (priv->n_cameras > 0)
+    {
+      int i;
+      for (i = 0; i < priv->n_cameras; i++)
+        {
+          PerCameraState *camera_state = &priv->camera_state[i];
+          if (camera_state->eye_volume_valid)
+            clutter_paint_volume_free (&camera_state->eye_volume);
+        }
+      g_slice_free1 (sizeof (PerCameraState) * priv->n_cameras,
+                     priv->camera_state);
+      priv->camera_state = NULL;
+      priv->n_cameras = -1;
+    }
+
   G_OBJECT_CLASS (clutter_actor_parent_class)->dispose (object);
 }
 
@@ -6455,6 +6659,8 @@ clutter_actor_init (ClutterActor *self)
 
   self->priv = priv = CLUTTER_ACTOR_GET_PRIVATE (self);
 
+  priv->stage_cache = NULL;
+
   priv->id = _clutter_context_acquire_id (self);
   priv->pick_id = -1;
 
@@ -6471,9 +6677,16 @@ clutter_actor_init (ClutterActor *self)
   priv->opacity_override = -1;
   priv->enable_model_view_transform = TRUE;
 
-  /* Initialize an empty paint volume to start with */
-  _clutter_paint_volume_init_static (&priv->last_paint_volume, NULL);
-  priv->last_paint_volume_valid = TRUE;
+  priv->camera_state = NULL;
+  priv->n_cameras = 0;
+
+  /* When an actor first gets associated with a stage we make sure to
+   * initialize this to a value not matching the
+   * stage's->priv->cameras_age, but for the stage itself we need to
+   * make sure the age is initialized to a value other than 0 so that
+   * get_per_camera_state() will correctly initialize the per-camera
+   * state. */
+  priv->cameras_age = -1;
 
   priv->transform_valid = FALSE;
 }
@@ -6529,13 +6742,44 @@ clutter_actor_destroy (ClutterActor *self)
   g_object_unref (self);
 }
 
+/* XXX: This is a workaround for not being able to break the ABI of
+ * the QUEUE_REDRAW signal. It is an out-of-band argument.  See
+ * clutter_actor_queue_clipped_redraw() for details.
+ */
+ClutterPaintVolume *
+_clutter_actor_get_queue_redraw_clip (ClutterActor *self)
+{
+  return self->priv->oob_queue_redraw_clip;
+}
+
+static void
+_clutter_actor_set_queue_redraw_clip (ClutterActor *self,
+                                      ClutterPaintVolume *clip)
+{
+  self->priv->oob_queue_redraw_clip = clip;
+}
+
+int
+_clutter_actor_get_queue_redraw_camera_index (ClutterActor *self)
+{
+  return self->priv->oob_queue_redraw_camera_index;
+}
+
+static void
+_clutter_actor_set_queue_redraw_camera_index (ClutterActor *self,
+                                              int camera_index)
+{
+  self->priv->oob_queue_redraw_camera_index = camera_index;
+}
+
 void
 _clutter_actor_finish_queue_redraw (ClutterActor *self,
                                     ClutterPaintVolume *clip)
 {
   ClutterActorPrivate *priv = self->priv;
-  ClutterPaintVolume *pv;
-  gboolean clipped;
+  ClutterStage *stage = _clutter_actor_get_stage_real (self);
+  int n_cameras = _clutter_stage_get_n_cameras (stage);
+  int i;
 
   /* Remove queue entry early in the process, otherwise a new
      queue_redraw() during signal handling could put back this
@@ -6563,46 +6807,65 @@ _clutter_actor_finish_queue_redraw (ClutterActor *self,
   if (clip)
     {
       _clutter_actor_set_queue_redraw_clip (self, clip);
-      clipped = TRUE;
+      for (i = 0; i < n_cameras; i++)
+        {
+          _clutter_actor_set_queue_redraw_camera_index (self, i);
+          _clutter_actor_signal_queue_redraw (self, self);
+        }
     }
-  else if (G_LIKELY (priv->last_paint_volume_valid))
+  else
     {
-      pv = _clutter_actor_get_paint_volume_mutable (self);
-      if (pv)
+      for (i = 0; i < n_cameras; i++)
         {
-          ClutterActor *stage = _clutter_actor_get_stage_internal (self);
+          PerCameraState *camera_state =
+            _clutter_actor_get_per_camera_state (self, i);
+          ClutterPaintVolume *pv;
 
-          /* make sure we redraw the actors old position... */
-          _clutter_actor_set_queue_redraw_clip (stage,
-                                                &priv->last_paint_volume);
-          _clutter_actor_signal_queue_redraw (stage, stage);
-          _clutter_actor_set_queue_redraw_clip (stage, NULL);
+          if (G_LIKELY (camera_state->eye_volume_valid))
+            {
+              pv = _clutter_actor_get_paint_volume_mutable (self);
+              if (pv)
+                {
+                  ClutterActor *stage_actor = CLUTTER_ACTOR (stage);
+
+                  /* make sure we redraw the actors old position... */
+                  _clutter_actor_set_queue_redraw_clip (stage_actor,
+                                                        &camera_state->eye_volume);
+                  _clutter_actor_signal_queue_redraw (stage_actor,
+                                                      stage_actor);
+                  _clutter_actor_set_queue_redraw_clip (stage_actor, NULL);
+                }
+            }
+          else
+            pv = NULL;
 
-          /* XXX: Ideally the redraw signal would take a clip volume
-           * argument, but that would be an ABI break. Until we can
-           * break the ABI we pass the argument out-of-band
+          /* XXX: Ideally the redraw signal would take clip volume and
+           * camera arguments, but that would be an ABI break. Until
+           * we can break the ABI we pass these arguments out-of-band
+           * via actor->priv members...
            */
 
-          /* setup the clip for the actors new position... */
+          /* Setup the clip for the actor's new position.
+           * Note: pv could be NULL here which will result in a full
+           * redraw. */
           _clutter_actor_set_queue_redraw_clip (self, pv);
-          clipped = TRUE;
+          _clutter_actor_set_queue_redraw_camera_index (self, i);
+          _clutter_actor_signal_queue_redraw (self, self);
         }
-      else
-        clipped = FALSE;
     }
-  else
-    clipped = FALSE;
-
-  _clutter_actor_signal_queue_redraw (self, self);
 
   /* Just in case anyone is manually firing redraw signals without
    * using the public queue_redraw() API we are careful to ensure that
-   * our out-of-band clip member is cleared before returning...
+   * our out-of-band clip member is cleared before returning and
+   * the out-of-band camera index reset to zero.
    *
-   * Note: A NULL clip denotes a full-stage, un-clipped redraw
+   * Note: A NULL clip denotes a full-stage, un-clipped redraw and
+   * camera index 0 corresponds to the main stage camera or the
+   * left eye camera while stereoscopic rendering is enabled.
    */
-  if (G_LIKELY (clipped))
-    _clutter_actor_set_queue_redraw_clip (self, NULL);
+  _clutter_actor_set_queue_redraw_clip (self, NULL);
+
+  _clutter_actor_set_queue_redraw_camera_index (self, 0);
 }
 
 static void
@@ -6638,10 +6901,10 @@ _clutter_actor_queue_redraw_full (ClutterActor       *self,
                                   ClutterEffect      *effect)
 {
   ClutterActorPrivate *priv = self->priv;
+  ClutterStage *stage;
   ClutterPaintVolume allocation_pv;
   ClutterPaintVolume *pv;
   gboolean should_free_pv;
-  ClutterActor *stage;
 
   /* Here's an outline of the actor queue redraw mechanism:
    *
@@ -6716,9 +6979,8 @@ _clutter_actor_queue_redraw_full (ClutterActor       *self,
   if (CLUTTER_ACTOR_IN_DESTRUCTION (self))
     return;
 
-  stage = _clutter_actor_get_stage_internal (self);
-
   /* Ignore queueing a redraw for actors not descended from a stage */
+  stage = _clutter_actor_get_stage_real (self);
   if (stage == NULL)
     return;
 
@@ -6726,6 +6988,8 @@ _clutter_actor_queue_redraw_full (ClutterActor       *self,
   if (CLUTTER_ACTOR_IN_DESTRUCTION (stage))
     return;
 
+  /* FIXME: in this case if a clip was explicitly passed we should
+   * intersect the clip with the allocation. */
   if (flags & CLUTTER_REDRAW_CLIPPED_TO_ALLOCATION)
     {
       ClutterActorBox allocation_clip;
@@ -6736,9 +7000,12 @@ _clutter_actor_queue_redraw_full (ClutterActor       *self,
       if (priv->needs_allocation)
         {
           /* NB: NULL denotes an undefined clip which will result in a
-           * full redraw... */
-          _clutter_actor_set_queue_redraw_clip (self, NULL);
-          _clutter_actor_signal_queue_redraw (self, self);
+           * full redraw according to the stage paint-volume. */
+          priv->queue_redraw_entry =
+            _clutter_stage_queue_actor_redraw (stage,
+                                               priv->queue_redraw_entry,
+                                               self,
+                                               NULL);
           return;
         }
 
@@ -6764,8 +7031,8 @@ _clutter_actor_queue_redraw_full (ClutterActor       *self,
       should_free_pv = FALSE;
     }
 
-  self->priv->queue_redraw_entry =
-    _clutter_stage_queue_actor_redraw (CLUTTER_STAGE (stage),
+  priv->queue_redraw_entry =
+    _clutter_stage_queue_actor_redraw (stage,
                                        priv->queue_redraw_entry,
                                        self,
                                        pv);
@@ -7557,8 +7824,12 @@ void
 clutter_actor_get_allocation_box (ClutterActor    *self,
                                   ClutterActorBox *box)
 {
+  ClutterActorPrivate *priv;
+
   g_return_if_fail (CLUTTER_IS_ACTOR (self));
 
+  priv = self->priv;
+
   /* XXX - if needs_allocation=TRUE, we can either 1) g_return_if_fail,
    * which limits calling get_allocation to inside paint() basically; or
    * we can 2) force a layout, which could be expensive if someone calls
@@ -7572,16 +7843,15 @@ clutter_actor_get_allocation_box (ClutterActor    *self,
    */
 
   /* this implements 2) */
-  if (G_UNLIKELY (self->priv->needs_allocation))
+  if (G_UNLIKELY (priv->needs_allocation))
     {
-      ClutterActor *stage = _clutter_actor_get_stage_internal (self);
-
+      ClutterStage *stage = _clutter_actor_get_stage_real (self);
       /* do not queue a relayout on an unparented actor */
       if (stage)
         _clutter_stage_maybe_relayout (stage);
     }
 
-  /* commenting out the code above and just keeping this assigment
+  /* commenting out the code above and just keeping this assignment
    * implements 3)
    */
   *box = self->priv->allocation;
@@ -7780,9 +8050,13 @@ clutter_actor_allocate (ClutterActor           *self,
   ClutterActorBox old_allocation, real_allocation;
   gboolean origin_changed, child_moved, size_changed;
   gboolean stage_allocation_changed;
+  ClutterStage *stage;
 
   g_return_if_fail (CLUTTER_IS_ACTOR (self));
-  if (G_UNLIKELY (_clutter_actor_get_stage_internal (self) == NULL))
+  priv = self->priv;
+  stage = _clutter_actor_get_stage_real (self);
+
+  if (G_UNLIKELY (stage == NULL))
     {
       g_warning ("Spurious clutter_actor_allocate called for actor %p/%s "
                  "which isn't a descendent of the stage!\n",
@@ -7790,8 +8064,6 @@ clutter_actor_allocate (ClutterActor           *self,
       return;
     }
 
-  priv = self->priv;
-
   old_allocation = priv->allocation;
   real_allocation = *box;
 
@@ -8633,6 +8905,9 @@ clutter_actor_get_transformed_position (ClutterActor *self,
  * information, you need to use clutter_actor_get_abs_allocation_vertices()
  * to get the coords of the actual quadrangle.</note>
  *
+ * <note>If clutter is being used for stereo rendering then this will
+ * simply return a size according the left eye's view.</note>
+ *
  * Since: 0.8
  */
 void
@@ -8673,7 +8948,7 @@ clutter_actor_get_transformed_size (ClutterActor *self,
       box.x2 = natural_width;
       box.y2 = natural_height;
 
-      _clutter_actor_transform_and_project_box (self, &box, v);
+      _clutter_actor_transform_and_project_box (self, 0, &box, v);
     }
   else
     clutter_actor_get_abs_allocation_vertices (self, v);
@@ -9892,6 +10167,53 @@ clutter_actor_get_clip (ClutterActor *self,
     *height = priv->clip.height;
 }
 
+typedef struct
+{
+  ClutterStage *stage;
+  int stage_n_cameras;
+  int stage_cameras_age;
+} InitPerCameraStateClosure;
+
+static ClutterActorTraverseVisitFlags
+init_per_camera_state_cb (ClutterActor *self,
+                          int           depth,
+                          gpointer      user_data)
+{
+  ClutterActorPrivate *priv = self->priv;
+  InitPerCameraStateClosure *closure = user_data;
+  int n_cameras = closure->stage_n_cameras;
+  int i;
+
+  /* This is the first point at which this actor has been
+   * associated with a specific stage and now that we have been
+   * associated with a set of cameras we need to initialize the
+   * actor's per-camera paint-volume to be empty so when it first
+   * gets shown we will only redraw the new area of the actor.
+   *
+   * XXX: it could be nice if re-parenting an actor within the
+   * same stage didn't hit this path too.
+   */
+
+  /* Make sure our camera state doesn't have the same age as the
+   * stage's camera state so we can be sure it will be invalidated
+   * during _clutter_actor_get_per_camera_state() */
+  priv->cameras_age = closure->stage_cameras_age - 1;
+
+  /* XXX: note we don't just rely on the initialization of per camera
+   * state by _clutter_actor_get_per_camera_state() since that will
+   * mark the initial paint_volume as invalid.
+   */
+  for (i = 0; i < n_cameras; i++)
+    {
+      PerCameraState *camera_state =
+        _clutter_actor_get_per_camera_state (self, i);
+      _clutter_paint_volume_init_static (&camera_state->eye_volume, NULL);
+      camera_state->eye_volume_valid = TRUE;
+    }
+
+  return CLUTTER_ACTOR_TRAVERSE_VISIT_CONTINUE;
+}
+
 /**
  * clutter_actor_get_children:
  * @self: a #ClutterActor
@@ -10185,6 +10507,7 @@ clutter_actor_add_child_internal (ClutterActor              *self,
   gboolean check_state;
   gboolean notify_first_last;
   ClutterActor *old_first_child, *old_last_child;
+  ClutterStage *stage;
 
   if (child->priv->parent != NULL)
     {
@@ -10289,6 +10612,25 @@ clutter_actor_add_child_internal (ClutterActor              *self,
   if (self->priv->internal_child)
     CLUTTER_SET_PRIVATE_FLAGS (child, CLUTTER_INTERNAL_CHILD);
 
+  /* Check to see if the actor is associated with a stage yet... */
+  stage = _clutter_actor_get_stage_real (self);
+
+  if (stage)
+    {
+      InitPerCameraStateClosure init_per_camera_state_closure;
+
+      init_per_camera_state_closure.stage_n_cameras =
+        _clutter_stage_get_n_cameras (stage);
+      init_per_camera_state_closure.stage_cameras_age =
+        _clutter_stage_get_cameras_age (stage);
+
+      _clutter_actor_traverse (self,
+                               CLUTTER_ACTOR_TRAVERSE_DEPTH_FIRST,
+                               init_per_camera_state_cb,
+                               NULL,
+                               &init_per_camera_state_closure);
+    }
+
   /* clutter_actor_reparent() will emit ::parent-set for us */
   if (emit_parent_set && !CLUTTER_ACTOR_IN_REPARENT (child))
     g_signal_emit (child, actor_signals[PARENT_SET], 0, NULL);
@@ -12628,15 +12970,6 @@ clutter_actor_is_scaled (ClutterActor *self)
   return FALSE;
 }
 
-ClutterActor *
-_clutter_actor_get_stage_internal (ClutterActor *actor)
-{
-  while (actor && !CLUTTER_ACTOR_IS_TOPLEVEL (actor))
-    actor = actor->priv->parent;
-
-  return actor;
-}
-
 /**
  * clutter_actor_get_stage:
  * @actor: a #ClutterActor
@@ -12653,7 +12986,7 @@ clutter_actor_get_stage (ClutterActor *actor)
 {
   g_return_val_if_fail (CLUTTER_IS_ACTOR (actor), NULL);
 
-  return _clutter_actor_get_stage_internal (actor);
+  return CLUTTER_ACTOR (_clutter_actor_get_stage_real (actor));
 }
 
 /**
@@ -12966,13 +13299,13 @@ out:
 void
 clutter_actor_grab_key_focus (ClutterActor *self)
 {
-  ClutterActor *stage;
+  ClutterStage *stage;
 
   g_return_if_fail (CLUTTER_IS_ACTOR (self));
 
-  stage = _clutter_actor_get_stage_internal (self);
+  stage = _clutter_actor_get_stage_real (self);
   if (stage != NULL)
-    clutter_stage_set_key_focus (CLUTTER_STAGE (stage), self);
+    clutter_stage_set_key_focus (stage, self);
 }
 
 /**
@@ -13710,26 +14043,6 @@ clutter_actor_has_pointer (ClutterActor *self)
   return self->priv->has_pointer;
 }
 
-/* XXX: This is a workaround for not being able to break the ABI of
- * the QUEUE_REDRAW signal. It is an out-of-band argument.  See
- * clutter_actor_queue_clipped_redraw() for details.
- */
-ClutterPaintVolume *
-_clutter_actor_get_queue_redraw_clip (ClutterActor *self)
-{
-  return g_object_get_data (G_OBJECT (self),
-                            "-clutter-actor-queue-redraw-clip");
-}
-
-void
-_clutter_actor_set_queue_redraw_clip (ClutterActor       *self,
-                                      ClutterPaintVolume *clip)
-{
-  g_object_set_data (G_OBJECT (self),
-                     "-clutter-actor-queue-redraw-clip",
-                     clip);
-}
-
 /**
  * clutter_actor_has_allocation:
  * @self: a #ClutterActor
@@ -14425,15 +14738,15 @@ clutter_actor_clear_effects (ClutterActor *self)
 gboolean
 clutter_actor_has_key_focus (ClutterActor *self)
 {
-  ClutterActor *stage;
+  ClutterStage *stage;
 
   g_return_val_if_fail (CLUTTER_IS_ACTOR (self), FALSE);
 
-  stage = _clutter_actor_get_stage_internal (self);
+  stage = _clutter_actor_get_stage_real (self);
   if (stage == NULL)
     return FALSE;
 
-  return clutter_stage_get_key_focus (CLUTTER_STAGE (stage)) == self;
+  return clutter_stage_get_key_focus (stage) == self;
 }
 
 static gboolean
@@ -14561,10 +14874,12 @@ _clutter_actor_get_paint_volume_real (ClutterActor *self,
 static ClutterPaintVolume *
 _clutter_actor_get_paint_volume_mutable (ClutterActor *self)
 {
-  ClutterActorPrivate *priv;
-
-  priv = self->priv;
+  ClutterActorPrivate *priv = self->priv;
 
+  /* NB: the paint volume isn't slice/heap allocated; it's actually
+   * just a member of priv, but we still have to call _free incase
+   * cogl-paint-volume.c associates object/memory references with
+   * the volume... */
   if (priv->paint_volume_valid)
     clutter_paint_volume_free (&priv->paint_volume);
 
@@ -14639,29 +14954,30 @@ clutter_actor_get_paint_volume (ClutterActor *self)
  *   not guaranteed to be valid across multiple frames; if you wish to
  *   keep it, you will have to copy it using clutter_paint_volume_copy().
  *
+ * <note>If stereoscopic rendering has been enabled then the paint
+ * volume is only valid for the eye currently being rendered.</note>
+ *
  * Since: 1.6
  */
 const ClutterPaintVolume *
 clutter_actor_get_transformed_paint_volume (ClutterActor *self,
                                             ClutterActor *relative_to_ancestor)
 {
+  ClutterStage *stage = _clutter_actor_get_stage_real (self);
   const ClutterPaintVolume *volume;
-  ClutterActor *stage;
   ClutterPaintVolume *transformed_volume;
 
-  stage = _clutter_actor_get_stage_internal (self);
   if (G_UNLIKELY (stage == NULL))
     return NULL;
 
   if (relative_to_ancestor == NULL)
-    relative_to_ancestor = stage;
+    relative_to_ancestor = CLUTTER_ACTOR (stage);
 
   volume = clutter_actor_get_paint_volume (self);
   if (volume == NULL)
     return NULL;
 
-  transformed_volume =
-    _clutter_stage_paint_volume_stack_allocate (CLUTTER_STAGE (stage));
+  transformed_volume = _clutter_stage_paint_volume_stack_allocate (stage);
 
   _clutter_paint_volume_copy_static (volume, transformed_volume);
 
@@ -14689,30 +15005,39 @@ clutter_actor_get_transformed_paint_volume (ClutterActor *self,
  * because the actor isn't yet parented under a stage or because
  * the actor is unable to determine a paint volume.
  *
+ * This function may only be called during a paint cycle.
+ *
  * Return value: %TRUE if a 2D paint box could be determined, else
  * %FALSE.
  *
+ * <note>If stereoscopic rendering has been enabled then the paint box
+ * returned will only be valid for the current eye being
+ * rendered</note>
+ *
  * Since: 1.6
  */
 gboolean
 clutter_actor_get_paint_box (ClutterActor    *self,
                              ClutterActorBox *box)
 {
-  ClutterActor *stage;
   ClutterPaintVolume *pv;
+  const ClutterCamera *camera;
+  ClutterStage *stage;
 
   g_return_val_if_fail (CLUTTER_IS_ACTOR (self), FALSE);
   g_return_val_if_fail (box != NULL, FALSE);
 
-  stage = _clutter_actor_get_stage_internal (self);
+  stage = _clutter_actor_get_stage_real (self);
   if (G_UNLIKELY (!stage))
     return FALSE;
 
+  camera = _clutter_stage_get_current_camera (stage);
+
   pv = _clutter_actor_get_paint_volume_mutable (self);
   if (G_UNLIKELY (!pv))
     return FALSE;
 
-  _clutter_paint_volume_get_stage_paint_box (pv, CLUTTER_STAGE (stage), box);
+  _clutter_paint_volume_get_camera_paint_box (pv, camera, box);
 
   return TRUE;
 }
diff --git a/clutter/clutter-backend.c b/clutter/clutter-backend.c
index b50154a..b56b56a 100644
--- a/clutter/clutter-backend.c
+++ b/clutter/clutter-backend.c
@@ -804,8 +804,8 @@ _clutter_backend_ensure_context (ClutterBackend *backend,
            * This dirty mechanism will ensure they are asserted before
            * the next paint...
            */
-          _clutter_stage_dirty_viewport (stage);
-          _clutter_stage_dirty_projection (stage);
+          _clutter_stage_dirty_cogl_viewport (stage);
+          _clutter_stage_dirty_cogl_projection (stage);
         }
 
       /* FIXME: With a NULL stage and thus no active context it may make more
diff --git a/clutter/clutter-enums.h b/clutter/clutter-enums.h
index 40bdb6a..644ac68 100644
--- a/clutter/clutter-enums.h
+++ b/clutter/clutter-enums.h
@@ -1094,6 +1094,31 @@ typedef enum {
   CLUTTER_ACTOR_ALIGN_END
 } ClutterActorAlign;
 
+/**
+ * ClutterStereoMode:
+ * @CLUTTER_STEREO_MODE_DEFAULT: Use the platform's default stereoscopic mode
+ * @CLUTTER_STEREO_MODE_ANAGLYPH: Use anaglyph based stereoscopic rendering; assuming
+ *    a red filter for the left eye and a cyan filter for the right eye.
+ * @CLUTTER_STEREO_MODE_VERTICAL_SPLIT: Split the scene vertically and show the left
+ *    eye contents on the left and the right eye contents on the
+ *    right.
+ * @CLUTTER_STEREO_MODE_HORIZONTAL_SPLIT: Split the scene horizontally and show the left
+ *    eye contents on the top and the right eye contents on the bottom.
+ *
+ * Defines the mode of outputing stereoscopic content to the user.
+ * %CLUTTER_STEREO_MODE_DEFAULT should just do the right thing for any platform that
+ * has native support for stereoscopic hardware, but for platforms without native
+ * stereoscopic support there are several fallback methods including anaglpyh for
+ * using filter glasses and split screen, which many 3D TVs can handle.
+ */
+typedef enum
+{
+  CLUTTER_STEREO_MODE_DEFAULT,
+  CLUTTER_STEREO_MODE_ANAGLYPH,
+  CLUTTER_STEREO_MODE_VERTICAL_SPLIT,
+  CLUTTER_STEREO_MODE_HORIZONTAL_SPLIT
+} ClutterStereoMode;
+
 G_END_DECLS
 
 #endif /* __CLUTTER_ENUMS_H__ */
diff --git a/clutter/clutter-offscreen-effect.c b/clutter/clutter-offscreen-effect.c
index 09443dc..99130a0 100644
--- a/clutter/clutter-offscreen-effect.c
+++ b/clutter/clutter-offscreen-effect.c
@@ -76,26 +76,32 @@
 #include "clutter-private.h"
 #include "clutter-stage-private.h"
 
-struct _ClutterOffscreenEffectPrivate
+typedef struct _PerCameraState
 {
+  const ClutterCamera *camera;
+  int valid_for_age;
+
   CoglHandle offscreen;
-  CoglPipeline *target;
-  CoglHandle texture;
 
-  ClutterActor *actor;
-  ClutterActor *stage;
+  /* aka "target" for legacy reasons */
+  CoglPipeline *pipeline;
+  CoglHandle texture;
 
-  gfloat x_offset;
-  gfloat y_offset;
+  gfloat viewport_x_offset;
+  gfloat viewport_y_offset;
 
   /* This is the calculated size of the fbo before being passed
-     through create_texture(). This needs to be tracked separately so
-     that we can detect when a different size is calculated and
-     regenerate the fbo */
-  int fbo_width;
-  int fbo_height;
-
-  gint old_opacity_override;
+   * through update_fbo() and create_texture(). This needs to be
+   * tracked separately from the final fbo_width/height so that we can
+   * detect when a different size is calculated and regenerate the
+   * fbo.
+   *
+   * NB: We can't just compare the fbo_width/height because some
+   * sub-classes may return a texture from create_texture() that has
+   * a different size from the calculated request size.
+   */
+  int request_width;
+  int request_height;
 
   /* The matrix that was current the last time the fbo was updated. We
      need to keep track of this to detect when we can reuse the
@@ -106,6 +112,18 @@ struct _ClutterOffscreenEffectPrivate
      and it won't cause a redraw to be queued on the parent's
      children. */
   CoglMatrix last_matrix_drawn;
+
+} PerCameraState;
+
+struct _ClutterOffscreenEffectPrivate
+{
+  ClutterActor *actor;
+
+  PerCameraState *camera_state;
+  int n_cameras;
+  int cameras_age;
+
+  gint old_opacity_override;
 };
 
 G_DEFINE_ABSTRACT_TYPE (ClutterOffscreenEffect,
@@ -119,15 +137,20 @@ clutter_offscreen_effect_set_actor (ClutterActorMeta *meta,
   ClutterOffscreenEffect *self = CLUTTER_OFFSCREEN_EFFECT (meta);
   ClutterOffscreenEffectPrivate *priv = self->priv;
   ClutterActorMetaClass *meta_class;
+  int i;
 
   meta_class = CLUTTER_ACTOR_META_CLASS (clutter_offscreen_effect_parent_class);
   meta_class->set_actor (meta, actor);
 
   /* clear out the previous state */
-  if (priv->offscreen != NULL)
+  for (i = 0; i < priv->n_cameras; i++)
     {
-      cogl_handle_unref (priv->offscreen);
-      priv->offscreen = NULL;
+      PerCameraState *camera_state = &priv->camera_state[i];
+      if (camera_state->offscreen != NULL)
+        {
+          cogl_object_unref (camera_state->offscreen);
+          camera_state->offscreen = NULL;
+        }
     }
 
   /* we keep a back pointer here, to avoid going through the ActorMeta */
@@ -144,72 +167,139 @@ clutter_offscreen_effect_real_create_texture (ClutterOffscreenEffect *effect,
                                      COGL_PIXEL_FORMAT_RGBA_8888_PRE);
 }
 
-static gboolean
-update_fbo (ClutterEffect *effect, int fbo_width, int fbo_height)
+static void
+invalidate_per_camera_state (PerCameraState *camera_state)
+{
+  if (camera_state->pipeline)
+    {
+      cogl_object_unref (camera_state->pipeline);
+      camera_state->pipeline = NULL;
+    }
+
+  if (camera_state->offscreen)
+    {
+      cogl_object_unref (camera_state->offscreen);
+      camera_state->offscreen = NULL;
+    }
+}
+
+static PerCameraState *
+get_per_camera_state (ClutterOffscreenEffect *self, int camera_index)
 {
-  ClutterOffscreenEffect *self = CLUTTER_OFFSCREEN_EFFECT (effect);
   ClutterOffscreenEffectPrivate *priv = self->priv;
+  ClutterStage *stage =
+    CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  int cameras_age = _clutter_stage_get_cameras_age (stage);
+  PerCameraState *camera_state;
 
-  priv->stage = clutter_actor_get_stage (priv->actor);
-  if (priv->stage == NULL)
+  /* Whenever there are additions or removals of cameras associated
+   * with the stage then the stage's 'cameras_age' is bumped and we
+   * throw away any cached state associated with the old cameras. */
+
+  if (G_UNLIKELY (cameras_age != priv->cameras_age))
     {
-      CLUTTER_NOTE (MISC, "The actor '%s' is not part of a stage",
-                    clutter_actor_get_name (priv->actor) == NULL
-                      ? G_OBJECT_TYPE_NAME (priv->actor)
-                      : clutter_actor_get_name (priv->actor));
-      return FALSE;
+      int i;
+      int n_cameras;
+
+      for (i = 0; i < priv->n_cameras; i++)
+        invalidate_per_camera_state (&priv->camera_state[i]);
+
+      if (priv->camera_state)
+        g_slice_free1 (sizeof (PerCameraState) * priv->n_cameras,
+                       priv->camera_state);
+
+      /* NB: We always allocate for the total number of cameras since
+       * we expect that each camera is likely going to be painted each
+       * frame so we should save having to re-allocate later. */
+      n_cameras = _clutter_stage_get_n_cameras (stage);
+      priv->camera_state = g_slice_alloc (sizeof (PerCameraState) * n_cameras);
+
+      for (i = 0; i < n_cameras; i++)
+        {
+          camera_state = &priv->camera_state[i];
+
+          camera_state->camera = _clutter_stage_get_camera (stage, i);
+          camera_state->pipeline = NULL;
+          camera_state->texture = NULL;
+          camera_state->offscreen = NULL;
+        }
+
+      priv->n_cameras = n_cameras;
+      priv->cameras_age = cameras_age;
     }
 
-  if (priv->fbo_width == fbo_width &&
-      priv->fbo_height == fbo_height &&
-      priv->offscreen != NULL)
+  camera_state = &priv->camera_state[camera_index];
+  if (camera_state->camera->age != camera_state->valid_for_age)
+    {
+      invalidate_per_camera_state (camera_state);
+      camera_state->valid_for_age = camera_state->camera->age;
+    }
+
+  return camera_state;
+}
+
+static gboolean
+update_fbo (ClutterEffect *effect,
+            int            camera_index,
+            int            request_width,
+            int            request_height)
+{
+  ClutterOffscreenEffect *self = CLUTTER_OFFSCREEN_EFFECT (effect);
+  PerCameraState *camera_state;
+
+  camera_state = get_per_camera_state (self, camera_index);
+  if (camera_state->request_width == request_width &&
+      camera_state->request_height == request_height &&
+      camera_state->offscreen != NULL)
     return TRUE;
 
-  if (priv->target == NULL)
+  if (camera_state->pipeline == NULL)
     {
       CoglContext *ctx =
         clutter_backend_get_cogl_context (clutter_get_default_backend ());
 
-      priv->target = cogl_pipeline_new (ctx);
+      camera_state->pipeline = cogl_pipeline_new (ctx);
 
       /* We're always going to render the texture at a 1:1 texel:pixel
          ratio so we can use 'nearest' filtering to decrease the
          effects of rounding errors in the geometry calculation */
-      cogl_pipeline_set_layer_filters (priv->target,
+      cogl_pipeline_set_layer_filters (camera_state->pipeline,
                                        0, /* layer_index */
                                        COGL_PIPELINE_FILTER_NEAREST,
                                        COGL_PIPELINE_FILTER_NEAREST);
     }
 
-  if (priv->texture != NULL)
+  if (camera_state->texture != NULL)
     {
-      cogl_handle_unref (priv->texture);
-      priv->texture = NULL;
+      cogl_object_unref (camera_state->texture);
+      camera_state->texture = NULL;
     }
 
-  priv->texture =
-    clutter_offscreen_effect_create_texture (self, fbo_width, fbo_height);
-  if (priv->texture == NULL)
+  camera_state->texture =
+    clutter_offscreen_effect_create_texture (self,
+                                             request_width,
+                                             request_height);
+  if (camera_state->texture == NULL)
     return FALSE;
 
-  cogl_pipeline_set_layer_texture (priv->target, 0, priv->texture);
+  cogl_pipeline_set_layer_texture (camera_state->pipeline, 0, camera_state->texture);
 
-  priv->fbo_width = fbo_width;
-  priv->fbo_height = fbo_height;
+  camera_state->request_width = request_width;
+  camera_state->request_height = request_height;
 
-  if (priv->offscreen != NULL)
-    cogl_handle_unref (priv->offscreen);
+  if (camera_state->offscreen != NULL)
+    cogl_object_unref (camera_state->offscreen);
 
-  priv->offscreen = cogl_offscreen_new_to_texture (priv->texture);
-  if (priv->offscreen == NULL)
+  camera_state->offscreen = cogl_offscreen_new_to_texture (camera_state->texture);
+  if (camera_state->offscreen == NULL)
     {
       g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC);
 
-      cogl_handle_unref (priv->target);
-      priv->target = NULL;
+      cogl_object_unref (camera_state->pipeline);
+      camera_state->pipeline = NULL;
 
-      priv->fbo_width = 0;
-      priv->fbo_height = 0;
+      camera_state->request_width = 0;
+      camera_state->request_height = 0;
 
       return FALSE;
     }
@@ -225,10 +315,14 @@ clutter_offscreen_effect_pre_paint (ClutterEffect *effect)
   ClutterActorBox box;
   CoglMatrix projection;
   CoglColor transparent;
-  gfloat fbo_width, fbo_height;
-  gfloat width, height;
+  gfloat fbo_request_width, fbo_request_height;
+  gfloat stage_viewport_x, stage_viewport_y;
+  gfloat stage_viewport_width, stage_viewport_height;
   gfloat xexpand, yexpand;
   int texture_width, texture_height;
+  const ClutterCamera *camera;
+  PerCameraState *camera_state;
+  ClutterStage *stage;
 
   if (!clutter_actor_meta_get_enabled (CLUTTER_ACTOR_META (effect)))
     return FALSE;
@@ -236,71 +330,108 @@ clutter_offscreen_effect_pre_paint (ClutterEffect *effect)
   if (priv->actor == NULL)
     return FALSE;
 
+  stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+
+  camera = _clutter_stage_get_current_camera (stage);
+  camera_state = get_per_camera_state (self, camera->index);
+
+  stage_viewport_x = camera->viewport[0];
+  stage_viewport_y = camera->viewport[1];
+  stage_viewport_width = camera->viewport[2];
+  stage_viewport_height = camera->viewport[3];
+
   /* The paint box is the bounding box of the actor's paint volume in
-   * stage coordinates. This will give us the size for the framebuffer
-   * we need to redirect its rendering offscreen and its position will
-   * be used to setup an offset viewport */
+   * screen coordinates (i.e. stage coordinates that have been
+   * projected by the current camera and had the camera's viewport
+   * transform applied). This will give us the size for the
+   * framebuffer we need to redirect its rendering offscreen.
+   *
+   * The position will be used to setup an offset viewport so we need
+   * an offset that is relative to the top-left of the stage's
+   * viewport rectangle not relative to the screen.
+   *
+   * NB: We can't assume the stage viewport has an origin of (0,0) or
+   * that the viewport size matches the geometry of the stage because
+   * for example when we are running in stereoscopic rendering mode
+   * the stage size might map to half the width or height in screen
+   * coordinates if a horizonal or vertical split screen is being
+   * used.
+   */
   if (clutter_actor_get_paint_box (priv->actor, &box))
     {
-      clutter_actor_box_get_size (&box, &fbo_width, &fbo_height);
-      clutter_actor_box_get_origin (&box, &priv->x_offset, &priv->y_offset);
+      clutter_actor_box_get_size (&box,
+                                  &fbo_request_width, &fbo_request_height);
+      clutter_actor_box_get_origin (&box,
+                                    &camera_state->viewport_x_offset,
+                                    &camera_state->viewport_y_offset);
+      camera_state->viewport_x_offset -= stage_viewport_x;
+      camera_state->viewport_y_offset -= stage_viewport_y;
     }
   else
     {
       /* If we can't get a valid paint box then we fallback to
-       * creating a full stage size fbo. */
-      ClutterActor *stage = _clutter_actor_get_stage_internal (priv->actor);
-      clutter_actor_get_size (stage, &fbo_width, &fbo_height);
-      priv->x_offset = 0.0f;
-      priv->y_offset = 0.0f;
+       * creating a full stage size fbo.
+       *
+       * Note: as mentioned above the stage's viewport might not
+       * match the stage's geometry so if we want to know the stages
+       * size in screen coordinates we should look at the viewport
+       * geometry.
+       *
+       * Note: we may need to change how we determine the screen-space
+       * size of the stage if we add support for sliced stages in the
+       * future.
+       */
+      fbo_request_width = stage_viewport_width;
+      fbo_request_height = stage_viewport_height;
+      camera_state->viewport_x_offset = 0;
+      camera_state->viewport_y_offset = 0;
     }
 
   /* First assert that the framebuffer is the right size... */
-  if (!update_fbo (effect, fbo_width, fbo_height))
+  if (!update_fbo (effect, camera->index,
+                   fbo_request_width, fbo_request_height))
     return FALSE;
 
-  texture_width = cogl_texture_get_width (priv->texture);
-  texture_height = cogl_texture_get_height (priv->texture);
+  texture_width = cogl_texture_get_width (camera_state->texture);
+  texture_height = cogl_texture_get_height (camera_state->texture);
 
   /* get the current modelview matrix so that we can copy it to the
    * framebuffer. We also store the matrix that was last used when we
    * updated the FBO so that we can detect when we don't need to
    * update the FBO to paint a second time */
-  cogl_get_modelview_matrix (&priv->last_matrix_drawn);
+  cogl_get_modelview_matrix (&camera_state->last_matrix_drawn);
 
   /* let's draw offscreen */
-  cogl_push_framebuffer (priv->offscreen);
+  cogl_push_framebuffer (camera_state->offscreen);
 
   /* Copy the modelview that would have been used if rendering onscreen */
-  cogl_set_modelview_matrix (&priv->last_matrix_drawn);
-
-  /* Set up the viewport so that it has the same size as the stage,
-   * but offset it so that the actor of interest lands on our
-   * framebuffer. */
-  clutter_actor_get_size (priv->stage, &width, &height);
+  cogl_set_modelview_matrix (&camera_state->last_matrix_drawn);
 
   /* Expand the viewport if the actor is partially off-stage,
    * otherwise the actor will end up clipped to the stage viewport
    */
   xexpand = 0.f;
-  if (priv->x_offset < 0.f)
-    xexpand = -priv->x_offset;
-  if (priv->x_offset + texture_width > width)
-    xexpand = MAX (xexpand, (priv->x_offset + texture_width) - width);
+  if (camera_state->viewport_x_offset < 0.f)
+    xexpand = -camera_state->viewport_x_offset;
+  if (camera_state->viewport_x_offset + texture_width > stage_viewport_width)
+    xexpand = MAX (xexpand, (camera_state->viewport_x_offset +
+                             texture_width) - stage_viewport_width);
 
   yexpand = 0.f;
-  if (priv->y_offset < 0.f)
-    yexpand = -priv->y_offset;
-  if (priv->y_offset + texture_height > height)
-    yexpand = MAX (yexpand, (priv->y_offset + texture_height) - height);
+  if (camera_state->viewport_y_offset < 0.f)
+    yexpand = -camera_state->viewport_y_offset;
+  if (camera_state->viewport_y_offset + texture_height > stage_viewport_height)
+    yexpand = MAX (yexpand, (camera_state->viewport_y_offset +
+                             texture_height) - stage_viewport_height);
 
   /* Set the viewport */
-  cogl_set_viewport (-(priv->x_offset + xexpand), -(priv->y_offset + yexpand),
-                     width + (2 * xexpand), height + (2 * yexpand));
+  cogl_set_viewport (-(camera_state->viewport_x_offset + xexpand),
+                     -(camera_state->viewport_y_offset + yexpand),
+                     stage_viewport_width + (2 * xexpand),
+                     stage_viewport_height + (2 * yexpand));
 
   /* Copy the stage's projection matrix across to the framebuffer */
-  _clutter_stage_get_projection_matrix (CLUTTER_STAGE (priv->stage),
-                                        &projection);
+  _clutter_stage_get_projection_matrix (stage, &projection);
 
   /* If we've expanded the viewport, make sure to scale the projection
    * matrix accordingly (as it's been initialised to work with the
@@ -310,12 +441,12 @@ clutter_offscreen_effect_pre_paint (ClutterEffect *effect)
     {
       gfloat new_width, new_height;
 
-      new_width = width + (2 * xexpand);
-      new_height = height + (2 * yexpand);
+      new_width = stage_viewport_width + (2 * xexpand);
+      new_height = stage_viewport_height + (2 * yexpand);
 
       cogl_matrix_scale (&projection,
-                         width / new_width,
-                         height / new_height,
+                         stage_viewport_width / new_width,
+                         stage_viewport_height / new_height,
                          1);
     }
 
@@ -344,24 +475,32 @@ clutter_offscreen_effect_real_paint_target (ClutterOffscreenEffect *effect)
 {
   ClutterOffscreenEffectPrivate *priv = effect->priv;
   guint8 paint_opacity;
+  ClutterStage *stage =
+    CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  const ClutterCamera *camera = _clutter_stage_get_current_camera (stage);
+  PerCameraState *camera_state = get_per_camera_state (effect, camera->index);
+  int texture_width;
+  int texture_height;
 
   paint_opacity = clutter_actor_get_paint_opacity (priv->actor);
 
-  cogl_pipeline_set_color4ub (priv->target,
+  cogl_pipeline_set_color4ub (camera_state->pipeline,
                               paint_opacity,
                               paint_opacity,
                               paint_opacity,
                               paint_opacity);
-  cogl_set_source (priv->target);
+  cogl_set_source (camera_state->pipeline);
 
   /* At this point we are in stage coordinates translated so if
    * we draw our texture using a textured quad the size of the paint
    * box then we will overlay where the actor would have drawn if it
    * hadn't been redirected offscreen.
    */
+  texture_width = cogl_texture_get_width (camera_state->texture);
+  texture_height = cogl_texture_get_height (camera_state->texture);
   cogl_rectangle_with_texture_coords (0, 0,
-                                      cogl_texture_get_width (priv->texture),
-                                      cogl_texture_get_height (priv->texture),
+                                      texture_width,
+                                      texture_height,
                                       0.0, 0.0,
                                       1.0, 1.0);
 }
@@ -371,16 +510,62 @@ clutter_offscreen_effect_paint_texture (ClutterOffscreenEffect *effect)
 {
   ClutterOffscreenEffectPrivate *priv = effect->priv;
   CoglMatrix modelview;
+  ClutterStage *stage =
+    CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  const ClutterCamera *camera = _clutter_stage_get_current_camera (stage);
+  PerCameraState *camera_state = get_per_camera_state (effect, camera->index);
+  CoglMatrix saved_projection;
+  gfloat scale_x, scale_y;
+  gfloat stage_x, stage_y;
+  gfloat stage_width, stage_height;
+
+  /* Now reset the modelview/projection to put us in orthographic
+   * stage coordinates so we can draw the result of our offscreen render as
+   * a textured quad...
+   *
+   * XXX: Note we don't use _apply_transform (stage, &modelview) to
+   * put is in regular stage coordinates because that might include a
+   * stereoscopic camera view transform and we don't want our
+   * rectangle to be affected by that.
+   *
+   * XXX: since clutter-stage.c should be free to play tricks with the
+   * viewport and projection matrix to support different forms of
+   * stereoscopic rendering it might make sense at some point to add
+   * some internal _clutter_stage api something like
+   * _clutter_stage_push/pop_orthographic() that can handle the
+   * details of giving us an orthographic projection without
+   * clobbering any of the transforms in place for stereo rendering.
+   *
+   * XXX: For now we are assuming that clutter-stage.c only plays with
+   * the viewport for vertical and horizontal split stereo rendering
+   * but at some point if we start using the projection matrix instead
+   * then this code will conflict with that!
+   */
+
+  cogl_get_projection_matrix (&saved_projection);
+  clutter_actor_get_size (CLUTTER_ACTOR (stage), &stage_width, &stage_height);
+  cogl_ortho (0, stage_width, /* left, right */
+              stage_height, 0, /* bottom, top */
+              -1, 100 /* z near, far */);
 
   cogl_push_matrix ();
 
-  /* Now reset the modelview to put us in stage coordinates so
-   * we can drawn the result of our offscreen render as a textured
-   * quad... */
+  /* NB: camera_state->viewport_x/y_offset are in screen coordinates
+   * relative to the stage's viewport rectangle but here we need a
+   * position in stage coordinates.
+   *
+   * Also our texture size was measured in screen coordinates but we
+   * want to paint the texture in actor coordinates.
+   */
+  scale_x = (stage_width / camera->viewport[2]);
+  scale_y = (stage_height / camera->viewport[3]);
+
+  stage_x = camera_state->viewport_x_offset * scale_x;
+  stage_y = camera_state->viewport_y_offset * scale_y;
 
   cogl_matrix_init_identity (&modelview);
-  _clutter_actor_apply_modelview_transform (priv->stage, &modelview);
-  cogl_matrix_translate (&modelview, priv->x_offset, priv->y_offset, 0.0f);
+  cogl_matrix_translate (&modelview, stage_x, stage_y, 0.0f);
+  cogl_matrix_scale (&modelview, scale_x, scale_y, 1);
   cogl_set_modelview_matrix (&modelview);
 
   /* paint the target material; this is virtualized for
@@ -389,6 +574,7 @@ clutter_offscreen_effect_paint_texture (ClutterOffscreenEffect *effect)
   clutter_offscreen_effect_paint_target (effect);
 
   cogl_pop_matrix ();
+  cogl_set_projection_matrix (&saved_projection);
 }
 
 static void
@@ -396,9 +582,13 @@ clutter_offscreen_effect_post_paint (ClutterEffect *effect)
 {
   ClutterOffscreenEffect *self = CLUTTER_OFFSCREEN_EFFECT (effect);
   ClutterOffscreenEffectPrivate *priv = self->priv;
+  ClutterStage *stage =
+    CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  const ClutterCamera *camera = _clutter_stage_get_current_camera (stage);
+  PerCameraState *camera_state = get_per_camera_state (self, camera->index);
 
-  if (priv->offscreen == NULL ||
-      priv->target == NULL ||
+  if (camera_state->offscreen == NULL ||
+      camera_state->pipeline == NULL ||
       priv->actor == NULL)
     return;
 
@@ -418,20 +608,27 @@ clutter_offscreen_effect_paint (ClutterEffect           *effect,
   ClutterOffscreenEffect *self = CLUTTER_OFFSCREEN_EFFECT (effect);
   ClutterOffscreenEffectPrivate *priv = self->priv;
   CoglMatrix matrix;
+  ClutterStage *stage =
+    CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  const ClutterCamera *camera = _clutter_stage_get_current_camera (stage);
+  PerCameraState *camera_state = get_per_camera_state (self, camera->index);
 
   cogl_get_modelview_matrix (&matrix);
 
   /* If we've already got a cached image for the same matrix and the
-     actor hasn't been redrawn then we can just use the cached image
-     in the fbo */
-  if (priv->offscreen == NULL ||
+   * actor hasn't been redrawn then we can just use the cached image
+   * in the fbo
+   */
+  if (camera_state->offscreen == NULL ||
       (flags & CLUTTER_EFFECT_PAINT_ACTOR_DIRTY) ||
-      !cogl_matrix_equal (&matrix, &priv->last_matrix_drawn))
+      !cogl_matrix_equal (&matrix, &camera_state->last_matrix_drawn) ||
+      camera_state->valid_for_age != camera_state->camera->age)
     {
       /* Chain up to the parent paint method which will call the pre and
          post paint functions to update the image */
       CLUTTER_EFFECT_CLASS (clutter_offscreen_effect_parent_class)->
         paint (effect, flags);
+      camera_state->valid_for_age = camera_state->camera->age;
     }
   else
     clutter_offscreen_effect_paint_texture (self);
@@ -442,15 +639,29 @@ clutter_offscreen_effect_finalize (GObject *gobject)
 {
   ClutterOffscreenEffect *self = CLUTTER_OFFSCREEN_EFFECT (gobject);
   ClutterOffscreenEffectPrivate *priv = self->priv;
+  int i;
+
+  for (i = 0; i < priv->n_cameras; i++)
+    {
+      PerCameraState *camera_state = &priv->camera_state[i];
+
+      if (camera_state->offscreen)
+        cogl_object_unref (camera_state->offscreen);
 
-  if (priv->offscreen)
-    cogl_handle_unref (priv->offscreen);
+      if (camera_state->pipeline)
+        cogl_object_unref (camera_state->pipeline);
 
-  if (priv->target)
-    cogl_handle_unref (priv->target);
+      if (camera_state->texture)
+        cogl_object_unref (camera_state->texture);
+    }
 
-  if (priv->texture)
-    cogl_handle_unref (priv->texture);
+  if (priv->camera_state)
+    {
+      g_slice_free1 (sizeof (PerCameraState) * priv->n_cameras,
+                     priv->camera_state);
+      priv->camera_state = NULL;
+      priv->n_cameras = 0;
+    }
 
   G_OBJECT_CLASS (clutter_offscreen_effect_parent_class)->finalize (gobject);
 }
@@ -482,6 +693,8 @@ clutter_offscreen_effect_init (ClutterOffscreenEffect *self)
   self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
                                             CLUTTER_TYPE_OFFSCREEN_EFFECT,
                                             ClutterOffscreenEffectPrivate);
+
+  self->priv->cameras_age = -1;
 }
 
 /**
@@ -507,10 +720,23 @@ clutter_offscreen_effect_init (ClutterOffscreenEffect *self)
 CoglHandle
 clutter_offscreen_effect_get_texture (ClutterOffscreenEffect *effect)
 {
+  ClutterOffscreenEffect *self;
+  ClutterOffscreenEffectPrivate *priv;
+  ClutterStage *stage;
+  const ClutterCamera *camera;
+  PerCameraState *camera_state;
+
   g_return_val_if_fail (CLUTTER_IS_OFFSCREEN_EFFECT (effect),
                         NULL);
 
-  return effect->priv->texture;
+  self = CLUTTER_OFFSCREEN_EFFECT (effect);
+  priv = self->priv;
+
+  stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  camera = _clutter_stage_get_current_camera (stage);
+  camera_state = get_per_camera_state (self, camera->index);
+
+  return camera_state->texture;
 }
 
 /**
@@ -532,10 +758,23 @@ clutter_offscreen_effect_get_texture (ClutterOffscreenEffect *effect)
 CoglMaterial *
 clutter_offscreen_effect_get_target (ClutterOffscreenEffect *effect)
 {
+  ClutterOffscreenEffect *self;
+  ClutterOffscreenEffectPrivate *priv;
+  ClutterStage *stage;
+  const ClutterCamera *camera;
+  PerCameraState *camera_state;
+
   g_return_val_if_fail (CLUTTER_IS_OFFSCREEN_EFFECT (effect),
                         NULL);
 
-  return (CoglMaterial *)effect->priv->target;
+  self = CLUTTER_OFFSCREEN_EFFECT (effect);
+  priv = self->priv;
+
+  stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  camera = _clutter_stage_get_current_camera (stage);
+  camera_state = get_per_camera_state (self, camera->index);
+
+  return (CoglMaterial *)camera_state->pipeline;
 }
 
 /**
@@ -594,6 +833,10 @@ clutter_offscreen_effect_create_texture (ClutterOffscreenEffect *effect,
  * implementations, from within the <function>paint_target()</function>
  * virtual function.
  *
+ * <note>If stereoscopic rendering has been enabled then this function
+ * returns the size according to the eye currently being
+ * rendered.</note>
+ *
  * Return value: %TRUE if the offscreen buffer has a valid size,
  *   and %FALSE otherwise
  *
@@ -605,19 +848,26 @@ clutter_offscreen_effect_get_target_size (ClutterOffscreenEffect *effect,
                                           gfloat                 *height)
 {
   ClutterOffscreenEffectPrivate *priv;
+  ClutterStage *stage;
+  const ClutterCamera *camera;
+  PerCameraState *camera_state;
 
   g_return_val_if_fail (CLUTTER_IS_OFFSCREEN_EFFECT (effect), FALSE);
 
   priv = effect->priv;
 
-  if (priv->texture == NULL)
+  stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->actor));
+  camera = _clutter_stage_get_current_camera (stage);
+  camera_state = get_per_camera_state (effect, camera->index);
+
+  if (camera_state->texture == NULL)
     return FALSE;
 
   if (width)
-    *width = cogl_texture_get_width (priv->texture);
+    *width = cogl_texture_get_width (camera_state->texture);
 
   if (height)
-    *height = cogl_texture_get_height (priv->texture);
+    *height = cogl_texture_get_height (camera_state->texture);
 
   return TRUE;
 }
diff --git a/clutter/clutter-paint-volume-private.h b/clutter/clutter-paint-volume-private.h
index 72bc7ae..e2c1b78 100644
--- a/clutter/clutter-paint-volume-private.h
+++ b/clutter/clutter-paint-volume-private.h
@@ -126,13 +126,16 @@ void                _clutter_paint_volume_set_reference_actor  (ClutterPaintVolu
 ClutterCullResult   _clutter_paint_volume_cull                 (ClutterPaintVolume *pv,
                                                                 const ClutterPlane       *planes);
 
-void                _clutter_paint_volume_get_stage_paint_box  (ClutterPaintVolume *pv,
-                                                                ClutterStage *stage,
+void                _clutter_paint_volume_get_camera_paint_box (ClutterPaintVolume *pv,
+                                                                const ClutterCamera *camera,
                                                                 ClutterActorBox *box);
 
 void                _clutter_paint_volume_transform_relative   (ClutterPaintVolume *pv,
                                                                 ClutterActor *relative_to_ancestor);
 
+void                _clutter_paint_volume_transform_relative_to_camera (ClutterPaintVolume *pv,
+                                                                        const ClutterCamera *camera);
+
 G_END_DECLS
 
 #endif /* __CLUTTER_PAINT_VOLUME_PRIVATE_H__ */
diff --git a/clutter/clutter-paint-volume.c b/clutter/clutter-paint-volume.c
index 725be79..7103aed 100644
--- a/clutter/clutter-paint-volume.c
+++ b/clutter/clutter-paint-volume.c
@@ -1128,14 +1128,12 @@ _clutter_paint_volume_cull (ClutterPaintVolume *pv,
 }
 
 void
-_clutter_paint_volume_get_stage_paint_box (ClutterPaintVolume *pv,
-                                           ClutterStage *stage,
-                                           ClutterActorBox *box)
+_clutter_paint_volume_get_camera_paint_box (ClutterPaintVolume *pv,
+                                            const ClutterCamera *camera,
+                                            ClutterActorBox *box)
 {
   ClutterPaintVolume projected_pv;
   CoglMatrix modelview;
-  CoglMatrix projection;
-  float viewport[4];
   float width;
   float height;
 
@@ -1145,20 +1143,18 @@ _clutter_paint_volume_get_stage_paint_box (ClutterPaintVolume *pv,
 
   /* If the paint volume isn't already in eye coordinates... */
   if (pv->actor)
-    _clutter_actor_apply_relative_transformation_matrix (pv->actor, NULL,
-                                                         &modelview);
-
-  _clutter_stage_get_projection_matrix (stage, &projection);
-  _clutter_stage_get_viewport (stage,
-                               &viewport[0],
-                               &viewport[1],
-                               &viewport[2],
-                               &viewport[3]);
+    {
+      ClutterActor *stage =
+        CLUTTER_ACTOR (_clutter_actor_get_stage_internal (pv->actor));
+      cogl_matrix_init_from_array (&modelview, (float *)&camera->view);
+      _clutter_actor_apply_relative_transformation_matrix (pv->actor, stage,
+                                                           &modelview);
+    }
 
   _clutter_paint_volume_project (&projected_pv,
                                  &modelview,
-                                 &projection,
-                                 viewport);
+                                 &camera->projection,
+                                 camera->viewport);
 
   _clutter_paint_volume_get_bounding_box (&projected_pv, box);
 
@@ -1220,13 +1216,38 @@ _clutter_paint_volume_transform_relative (ClutterPaintVolume *pv,
   actor = pv->actor;
 
   g_return_if_fail (actor != NULL);
+  g_return_if_fail (relative_to_ancestor != NULL);
 
   _clutter_paint_volume_set_reference_actor (pv, relative_to_ancestor);
 
   cogl_matrix_init_identity (&matrix);
   _clutter_actor_apply_relative_transformation_matrix (actor,
                                                        relative_to_ancestor,
-                                                      &matrix);
+                                                       &matrix);
 
   _clutter_paint_volume_transform (pv, &matrix);
 }
+
+void
+_clutter_paint_volume_transform_relative_to_camera (ClutterPaintVolume *pv,
+                                                    const ClutterCamera *camera)
+{
+  CoglMatrix modelview;
+  ClutterActor *stage;
+  ClutterActor *actor;
+
+  actor = pv->actor;
+
+  g_return_if_fail (actor != NULL);
+
+  stage = CLUTTER_ACTOR (_clutter_actor_get_stage_internal (actor));
+
+  _clutter_paint_volume_set_reference_actor (pv, NULL);
+
+  cogl_matrix_init_from_array (&modelview, (float *)&camera->view);
+  _clutter_actor_apply_relative_transformation_matrix (actor,
+                                                       CLUTTER_ACTOR (stage),
+                                                       &modelview);
+
+  _clutter_paint_volume_transform (pv, &modelview);
+}
diff --git a/clutter/clutter-private.h b/clutter/clutter-private.h
index df17d6a..9aa6b84 100644
--- a/clutter/clutter-private.h
+++ b/clutter/clutter-private.h
@@ -265,6 +265,32 @@ typedef enum _ClutterCullResult
   CLUTTER_CULL_RESULT_PARTIAL
 } ClutterCullResult;
 
+typedef struct _ClutterCamera
+{
+  /* Cameras are always maintained in a packed array associated with the stage
+   * and this is the index into that array. */
+  int       index;
+
+  /* TODO: It should be possible to associate cameras with CoglOffscreen
+   * framebuffers. Currently cameras are only used for stereoscopic rendering
+   * and it's assumed that all cameras are associated with the stage's
+   * ClutterStageWindow. */
+
+  CoglMatrix projection;
+  CoglMatrix inverse_projection;
+  float      viewport[4];
+  CoglMatrix view;
+
+  /* NB: This age is bumped when the viewport, view or projection change.
+   *
+   * For example clutter-actor.c can use this age to know if something cached
+   * in eye coordinates (which depends on a view transform) is still valid.
+   *
+   * NB: only do == or != comparisons with the age so wrapping should never be
+   * a problem. */
+  int        age;
+} ClutterCamera;
+
 G_END_DECLS
 
 #endif /* __CLUTTER_PRIVATE_H__ */
diff --git a/clutter/clutter-stage-private.h b/clutter/clutter-stage-private.h
index d2c7f78..81da8a7 100644
--- a/clutter/clutter-stage-private.h
+++ b/clutter/clutter-stage-private.h
@@ -44,7 +44,7 @@ void                _clutter_stage_set_window            (ClutterStage
 ClutterStageWindow *_clutter_stage_get_window            (ClutterStage          *stage);
 void                _clutter_stage_get_projection_matrix (ClutterStage          *stage,
                                                           CoglMatrix            *projection);
-void                _clutter_stage_dirty_projection      (ClutterStage          *stage);
+void                _clutter_stage_dirty_cogl_projection (ClutterStage          *stage);
 void                _clutter_stage_set_viewport          (ClutterStage          *stage,
                                                           float                  x,
                                                           float                  y,
@@ -55,9 +55,9 @@ void                _clutter_stage_get_viewport          (ClutterStage
                                                           float                 *y,
                                                           float                 *width,
                                                           float                 *height);
-void                _clutter_stage_dirty_viewport        (ClutterStage          *stage);
+void                _clutter_stage_dirty_cogl_viewport   (ClutterStage          *stage);
 void                _clutter_stage_maybe_setup_viewport  (ClutterStage          *stage);
-void                _clutter_stage_maybe_relayout        (ClutterActor          *stage);
+void                _clutter_stage_maybe_relayout        (ClutterStage          *stage);
 gboolean            _clutter_stage_needs_update          (ClutterStage          *stage);
 gboolean            _clutter_stage_do_update             (ClutterStage          *stage);
 
@@ -116,6 +116,14 @@ gboolean                _clutter_stage_update_state     (ClutterStage      *stag
                                                          ClutterStageState  unset_state,
                                                          ClutterStageState  set_state);
 
+const ClutterCamera  *_clutter_stage_get_camera         (ClutterStage        *stage,
+                                                         int                  camera_index);
+const ClutterCamera  *_clutter_stage_get_current_camera (ClutterStage        *stage);
+void                  _clutter_stage_set_current_camera (ClutterStage        *stage,
+                                                         const ClutterCamera *camera);
+int                   _clutter_stage_get_n_cameras      (ClutterStage        *stage);
+int                   _clutter_stage_get_cameras_age    (ClutterStage        *stage);
+
 G_END_DECLS
 
 #endif /* __CLUTTER_STAGE_PRIVATE_H__ */
diff --git a/clutter/clutter-stage-window.c b/clutter/clutter-stage-window.c
index 3ba3c99..b6383a0 100644
--- a/clutter/clutter-stage-window.c
+++ b/clutter/clutter-stage-window.c
@@ -38,6 +38,21 @@ clutter_stage_window_default_init (ClutterStageWindowInterface *iface)
   g_object_interface_install_property (iface, pspec);
 }
 
+gboolean
+_clutter_stage_window_has_feature (ClutterStageWindow       *window,
+                                   ClutterStageWindowFeature feature)
+{
+  ClutterStageWindowIface *iface;
+
+  g_return_val_if_fail (CLUTTER_IS_STAGE_WINDOW (window), FALSE);
+
+  iface = CLUTTER_STAGE_WINDOW_GET_IFACE (window);
+  if (iface->redraw_without_swap)
+    return iface->has_feature (window, feature);
+  else
+    return FALSE;
+}
+
 ClutterActor *
 _clutter_stage_window_get_wrapper (ClutterStageWindow *window)
 {
@@ -236,6 +251,30 @@ _clutter_stage_window_redraw (ClutterStageWindow *window)
     iface->redraw (window);
 }
 
+void
+_clutter_stage_window_redraw_without_swap (ClutterStageWindow *window)
+{
+  ClutterStageWindowIface *iface;
+
+  g_return_if_fail (CLUTTER_IS_STAGE_WINDOW (window));
+
+  iface = CLUTTER_STAGE_WINDOW_GET_IFACE (window);
+  if (iface->redraw_without_swap)
+    iface->redraw_without_swap (window);
+}
+
+void
+_clutter_stage_window_swap_buffers (ClutterStageWindow *window)
+{
+  ClutterStageWindowIface *iface;
+
+  g_return_if_fail (CLUTTER_IS_STAGE_WINDOW (window));
+
+  iface = CLUTTER_STAGE_WINDOW_GET_IFACE (window);
+  if (iface->swap_buffers)
+    iface->swap_buffers (window);
+}
+
 /* NB: The presumption shouldn't be that a stage can't be comprised of
  * multiple internal framebuffers, so instead of simply naming this
  * function _clutter_stage_window_get_framebuffer(), the "active"
diff --git a/clutter/clutter-stage-window.h b/clutter/clutter-stage-window.h
index 041a255..6f938d2 100644
--- a/clutter/clutter-stage-window.h
+++ b/clutter/clutter-stage-window.h
@@ -22,7 +22,13 @@ G_BEGIN_DECLS
 typedef struct _ClutterStageWindow      ClutterStageWindow; /* dummy */
 typedef struct _ClutterStageWindowIface ClutterStageWindowIface;
 
-/*
+typedef enum
+{
+  /*< private >*/
+  CLUTTER_STAGE_WINDOW_FEATURE_SWAP_BUFFERS
+} ClutterStageWindowFeature;
+
+/**
  * ClutterStageWindowIface: (skip)
  *
  * The interface implemented by backends for stage windows
@@ -34,6 +40,9 @@ struct _ClutterStageWindowIface
   /*< private >*/
   GTypeInterface parent_iface;
 
+  gboolean          (* has_feature)             (ClutterStageWindow       *stage_window,
+                                                 ClutterStageWindowFeature feature);
+
   ClutterActor     *(* get_wrapper)             (ClutterStageWindow *stage_window);
 
   void              (* set_title)               (ClutterStageWindow *stage_window,
@@ -72,6 +81,8 @@ struct _ClutterStageWindowIface
                                                  gboolean            accept_focus);
 
   void              (* redraw)                  (ClutterStageWindow *stage_window);
+  void              (* redraw_without_swap)     (ClutterStageWindow *stage_window);
+  void              (* swap_buffers)            (ClutterStageWindow *stage_window);
 
   CoglFramebuffer  *(* get_active_framebuffer)  (ClutterStageWindow *stage_window);
 
@@ -80,6 +91,9 @@ struct _ClutterStageWindowIface
 
 GType _clutter_stage_window_get_type (void) G_GNUC_CONST;
 
+gboolean          _clutter_stage_window_has_feature        (ClutterStageWindow       *window,
+                                                            ClutterStageWindowFeature feature);
+
 ClutterActor *    _clutter_stage_window_get_wrapper        (ClutterStageWindow *window);
 
 void              _clutter_stage_window_set_title          (ClutterStageWindow *window,
@@ -116,6 +130,8 @@ void              _clutter_stage_window_set_accept_focus        (ClutterStageWin
                                                                  gboolean            accept_focus);
 
 void              _clutter_stage_window_redraw                  (ClutterStageWindow *window);
+void              _clutter_stage_window_redraw_without_swap     (ClutterStageWindow *window);
+void              _clutter_stage_window_swap_buffers            (ClutterStageWindow *window);
 
 CoglFramebuffer  *_clutter_stage_window_get_active_framebuffer  (ClutterStageWindow *window);
 
diff --git a/clutter/clutter-stage.c b/clutter/clutter-stage.c
index 123caeb..bf2d0ca 100644
--- a/clutter/clutter-stage.c
+++ b/clutter/clutter-stage.c
@@ -50,6 +50,7 @@
 
 #include <math.h>
 #include <cairo.h>
+#include <stdlib.h>
 
 #define CLUTTER_DISABLE_DEPRECATION_WARNINGS
 
@@ -121,11 +122,27 @@ struct _ClutterStagePrivate
   /* the stage implementation */
   ClutterStageWindow *impl;
 
-  ClutterPerspective perspective;
-  CoglMatrix projection;
-  CoglMatrix inverse_projection;
-  CoglMatrix view;
   float viewport[4];
+  ClutterPerspective perspective;
+
+  /* NB: When we start adding support for more cameras we'll need to ensure
+   * that we continue to maintain all valid cameras as a contiguous range of
+   * indices from 0 since we have various bits of code that expect to iterate
+   * through indices 0 to (n_cameras - 1) */
+  ClutterCamera cameras[2];
+  int n_cameras;
+  /* NB: The age is bumped whenever cameras are added or removed. It
+   * isn't bumped when cameras are modified. To determine if an
+   * individual camera has been modified there is a per camera age.
+   *
+   * NB: only do == or != comparisons with the age so wrapping should never be
+   * a problem. */
+  int cameras_age;
+
+  const ClutterCamera *current_camera;
+  const ClutterCamera *last_flushed_camera;
+
+  ClutterStereoMode stereo_mode;
 
   ClutterFog fog;
 
@@ -171,11 +188,15 @@ struct _ClutterStagePrivate
   guint use_alpha              : 1;
   guint min_size_changed       : 1;
   guint dirty_viewport         : 1;
+  guint dirty_cogl_viewport    : 1;
   guint dirty_projection       : 1;
+  guint dirty_cogl_projection  : 1;
+  guint dirty_view             : 1;
   guint have_valid_pick_buffer : 1;
   guint accept_focus           : 1;
   guint motion_events_enabled  : 1;
   guint has_custom_perspective : 1;
+  guint stereo_enabled         : 1;
 };
 
 enum
@@ -495,7 +516,7 @@ typedef struct _Vector4
 static void
 _cogl_util_get_eye_planes_for_screen_poly (float *polygon,
                                            int n_vertices,
-                                           float *viewport,
+                                           const float *viewport,
                                            const CoglMatrix *projection,
                                            const CoglMatrix *inverse_project,
                                            ClutterPlane *planes)
@@ -657,9 +678,9 @@ _clutter_stage_do_paint (ClutterStage                *stage,
 
   _cogl_util_get_eye_planes_for_screen_poly (clip_poly,
                                              4,
-                                             priv->viewport,
-                                             &priv->projection,
-                                             &priv->inverse_projection,
+                                             priv->current_camera->viewport,
+                                             &priv->current_camera->projection,
+                                             &priv->current_camera->inverse_projection,
                                              priv->current_clip_planes);
 
   _clutter_stage_paint_volume_stack_free_all (stage);
@@ -753,6 +774,7 @@ clutter_stage_realize (ClutterActor *self)
    */
   priv->dirty_viewport = TRUE;
   priv->dirty_projection = TRUE;
+  priv->dirty_view = TRUE;
 
   g_assert (priv->impl != NULL);
   is_realized = _clutter_stage_window_realize (priv->impl);
@@ -814,7 +836,7 @@ clutter_stage_show (ClutterActor *self)
 
   /* Possibly do an allocation run so that the stage will have the
      right size before we map it */
-  _clutter_stage_maybe_relayout (self);
+  _clutter_stage_maybe_relayout (CLUTTER_STAGE (self));
 
   g_assert (priv->impl != NULL);
   _clutter_stage_window_show (priv->impl, TRUE);
@@ -1046,9 +1068,8 @@ _clutter_stage_needs_update (ClutterStage *stage)
 }
 
 void
-_clutter_stage_maybe_relayout (ClutterActor *actor)
+_clutter_stage_maybe_relayout (ClutterStage *stage)
 {
-  ClutterStage *stage = CLUTTER_STAGE (actor);
   ClutterStagePrivate *priv = stage->priv;
   gfloat natural_width, natural_height;
   ClutterActorBox box = { 0, };
@@ -1115,12 +1136,376 @@ _clutter_stage_set_pick_buffer_valid (ClutterStage   *stage,
   stage->priv->pick_buffer_mode = mode;
 }
 
+void
+_clutter_stage_set_current_camera (ClutterStage *stage,
+                                   const ClutterCamera *camera)
+{
+  ClutterStagePrivate *priv = stage->priv;
+
+  if (priv->current_camera == camera)
+    return;
+
+  priv->current_camera = camera;
+}
+
+/* This calculates a distance into the view frustum to position the
+ * stage so there is a decent amount of space to position geometry
+ * between the stage and the near clipping plane.
+ *
+ * Some awkward issues with this problem are:
+ * - It's not possible to have a gap as large as the stage size with
+ *   a fov > 53Â which is basically always the case since the default
+ *   fov is 60Â.
+ *    - This can be deduced if you consider that this requires a
+ *      triangle as wide as it is deep to fit in the frustum in front
+ *      of the z_near plane. That triangle will always have an angle
+ *      of 53.13Â at the point sitting on the z_near plane, but if the
+ *      frustum has a wider fov angle the left/right clipping planes
+ *      can never converge with the two corners of our triangle no
+ *      matter what size the triangle has.
+ * - With a fov > 53Â there is a trade off between maximizing the gap
+ *   size relative to the stage size but not loosing depth precision.
+ * - Perhaps ideally we wouldn't just consider the fov on the y-axis
+ *   that is usually used to define a perspective, we would consider
+ *   the fov of the axis with the largest stage size so the gap would
+ *   accommodate that size best.
+ *
+ * After going around in circles a few times with how to handle these
+ * issues, we decided in the end to go for the simplest solution to
+ * start with instead of an elaborate function that handles arbitrary
+ * fov angles that we currently have no use-case for.
+ *
+ * The solution assumes a fovy of 60Â and for that case gives a gap
+ * that's 85% of the stage height. We can consider more elaborate
+ * functions if necessary later.
+ *
+ * One guide we had to steer the gap size we support is the
+ * interactive test, test-texture-quality which expects to animate an
+ * actor to +400 on the z axis with a stage size of 640x480. A gap
+ * that's 85% of the stage height gives a gap of 408 in that case.
+ */
+static float
+calculate_z_translation (float z_near)
+{
+  /* This solution uses fairly basic trigonometry, but is seems worth
+   * clarifying the particular geometry we are looking at in-case
+   * anyone wants to develop this further later. Not sure how well an
+   * ascii diagram is going to work :-)
+   *
+   *    |--- stage_height ---|
+   *    |     stage line     |
+   *   âââââââââââââââââââââââ------------
+   *    â.  (2)   â        .â       |   |
+   *   C â .      â      . â     gap|   |
+   * =0.5Ââ  . a  â    .  â         |   |
+   *      bâ(1). Dâ  .   â          |   |
+   *        â   B.â.    ânear plane |   |
+   *      A= âââââââââââ-------------   |
+   *     120Â â c â   â  |            z_2d
+   *           â  â  â  z_near          |
+   *       left â â â    |              |
+   *       clip  60Âfovy |              |
+   *       plane  â----------------------
+   *              |
+   *              |
+   *         origin line
+   *
+   * The area of interest is the triangle labeled (1) at the top left
+   * marked with the ... line (a) from where the origin line crosses
+   * the near plane to the top left where the stage line cross the
+   * left clip plane.
+   *
+   * The sides of the triangle are a, b and c and the corresponding
+   * angles opposite those sides are A, B and C.
+   *
+   * The angle of C is what trades off the gap size we have relative
+   * to the stage size vs the depth precision we have.
+   *
+   * As mentioned above we arove at the angle for C is by working
+   * backwards from how much space we want for test-texture-quality.
+   * With a stage_height of 480 we want a gap > 400, ideally we also
+   * wanted a somewhat round number as a percentage of the height for
+   * documentation purposes. ~87% or a gap of ~416 is the limit
+   * because that's where we approach a C angle of 0Â and effectively
+   * loose all depth precision.
+   *
+   * So for our test app with a stage_height of 480 if we aim for a
+   * gap of 408 (85% of 480) we can get the angle D as
+   * atan (stage_height/2/408) = 30.5Â.
+   *
+   * That gives us the angle for B as 90Â - 30.5Â = 59.5Â
+   *
+   * We can already determine that A has an angle of (fovy/2 + 90Â) =
+   * 120Â
+   *
+   * Therefore C = 180 - A - B = 0.5Â
+   *
+   * The length of c = z_near * tan (30Â)
+   *
+   * Now we can use the rule a/SinA = c/SinC to calculate the
+   * length of a. After some rearranging that gives us:
+   *
+   *      a              c
+   *  ----------  =  ----------
+   *  sin (120Â)     sin (0.5Â)
+   *
+   *      c * sin (120Â)
+   *  a = --------------
+   *        sin (0.5Â)
+   *
+   * And with that we can determine z_2d = cos (D) * a =
+   * cos (30.5Â) * a + z_near:
+   *
+   *         c * sin (120Â) * cos (30.5Â)
+   *  z_2d = --------------------------- + z_near
+   *                 sin (0.5Â)
+   */
+#define _DEG_TO_RAD (G_PI / 180.0)
+  return z_near * tanf (30.0f * _DEG_TO_RAD) *
+         sinf (120.0f * _DEG_TO_RAD) * cosf (30.5f * _DEG_TO_RAD) /
+         sinf (0.5f * _DEG_TO_RAD) +
+         z_near;
+#undef _DEG_TO_RAD
+   /* We expect the compiler should boil this down to z_near * CONSTANT */
+}
+
+static void
+clutter_stage_set_perspective_internal (ClutterStage       *stage,
+                                        ClutterPerspective *perspective)
+{
+  ClutterStagePrivate *priv = stage->priv;
+
+  if (priv->perspective.fovy == perspective->fovy &&
+      priv->perspective.aspect == perspective->aspect &&
+      priv->perspective.z_near == perspective->z_near &&
+      priv->perspective.z_far == perspective->z_far)
+    return;
+
+  priv->perspective = *perspective;
+
+  priv->dirty_projection = TRUE;
+  clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
+}
+
+static void
+_clutter_stage_update_modelview_projection (ClutterStage *stage)
+{
+  ClutterStagePrivate *priv = stage->priv;
+
+  if (priv->dirty_viewport)
+    {
+      /* The Cogl viewport is implicitly dirty... */
+      priv->dirty_cogl_viewport = TRUE;
+
+      /* The view transform is based on the viewport width/height */
+      priv->dirty_view = TRUE;
+
+      if (priv->stereo_enabled)
+        {
+          priv->cameras[0].viewport[0] = priv->viewport[0];
+          priv->cameras[0].viewport[1] = priv->viewport[1];
+
+          if (priv->stereo_mode == CLUTTER_STEREO_MODE_VERTICAL_SPLIT)
+            {
+              priv->cameras[0].viewport[2] = priv->viewport[2] / 2;
+              priv->cameras[0].viewport[3] = priv->viewport[3];
+              priv->cameras[0].age++;
+
+              priv->cameras[1].viewport[0] =
+                (priv->viewport[0] / 2) + (priv->viewport[2] / 2);
+              priv->cameras[1].viewport[1] = priv->viewport[1];
+              priv->cameras[1].viewport[2] = priv->viewport[2] / 2;
+              priv->cameras[1].viewport[3] = priv->viewport[3];
+              priv->cameras[1].age++;
+            }
+          else if (priv->stereo_mode == CLUTTER_STEREO_MODE_HORIZONTAL_SPLIT)
+            {
+              priv->cameras[0].viewport[2] = priv->viewport[2];
+              priv->cameras[0].viewport[3] = priv->viewport[3] / 2;
+              priv->cameras[0].age++;
+
+              priv->cameras[1].viewport[0] = priv->viewport[0];
+              priv->cameras[1].viewport[1] =
+                (priv->viewport[1] / 2)  + (priv->viewport[3] / 2);
+              priv->cameras[1].viewport[2] = priv->viewport[2];
+              priv->cameras[1].viewport[3] = priv->viewport[3] / 2;
+              priv->cameras[1].age++;
+            }
+          else
+            {
+              memcpy (priv->cameras[0].viewport, priv->viewport,
+                      sizeof (float) * 4);
+              memcpy (priv->cameras[1].viewport, priv->viewport,
+                      sizeof (float) * 4);
+            }
+        }
+      else
+        {
+          memcpy (priv->cameras[0].viewport, priv->viewport,
+                  sizeof (float) * 4);
+          memcpy (priv->cameras[1].viewport, priv->viewport,
+                  sizeof (float) * 4);
+        }
+
+      priv->dirty_viewport = FALSE;
+    }
+
+  if (priv->dirty_view)
+    {
+      ClutterPerspective perspective;
+      float z_2d;
+
+      perspective = priv->perspective;
+
+      /* Ideally we want to regenerate the perspective matrix whenever
+       * the size changes but if the user has provided a custom matrix
+       * then we don't want to override it */
+      if (!priv->has_custom_perspective)
+        {
+          perspective.aspect = priv->viewport[2] / priv->viewport[3];
+          z_2d = calculate_z_translation (perspective.z_near);
+
+#define _DEG_TO_RAD (G_PI / 180.0)
+          /* NB: z_2d is only enough room for 85% of the stage_height between
+           * the stage and the z_near plane. For behind the stage plane we
+           * want a more consistent gap of 10 times the stage_height before
+           * hitting the far plane so we calculate that relative to the final
+           * height of the stage plane at the z_2d_distance we got... */
+          perspective.z_far = z_2d +
+            tanf ((perspective.fovy / 2.0f) * _DEG_TO_RAD) * z_2d * 20.0f;
+#undef _DEG_TO_RAD
+
+          clutter_stage_set_perspective_internal (stage, &perspective);
+        }
+      else
+        z_2d = calculate_z_translation (perspective.z_near);
+
+      cogl_matrix_init_identity (&priv->cameras[0].view);
+
+      /* For now we use the simple "toe-in" method for stereoscopic
+       * rendering whereby we simply offset two cameras to the left
+       * and to the right of the origin, both pointing diagonally into
+       * the center of our z_2d plane. A disadvantage of this approach
+       * is that straight lines on the z_2d plane will not be
+       * perceived as straight since the planes intersecting each
+       * camera frustum at the z_2d distance aren't equal. */
+      if (priv->stereo_enabled)
+        {
+          cogl_matrix_init_identity (&priv->cameras[1].view);
+
+#define EYE_OFFSET 0.1
+          /* FIXME: the 0.5 offsets are just hacky constants for now! */
+          cogl_matrix_look_at (&priv->cameras[0].view,
+                               -EYE_OFFSET, 0, 0,
+                               0, 0, -z_2d,
+                               0, 1, 0);
+          cogl_matrix_look_at (&priv->cameras[1].view,
+                               EYE_OFFSET, 0, 0,
+                               0, 0, -z_2d,
+                               0, 1, 0);
+        }
+
+      cogl_matrix_view_2d_in_perspective (&priv->cameras[0].view,
+                                          perspective.fovy,
+                                          perspective.aspect,
+                                          perspective.z_near,
+                                          z_2d,
+                                          priv->viewport[2],
+                                          priv->viewport[3]);
+      priv->cameras[0].age++;
+
+      if (priv->stereo_enabled)
+        {
+          cogl_matrix_view_2d_in_perspective (&priv->cameras[1].view,
+                                              perspective.fovy,
+                                              perspective.aspect,
+                                              perspective.z_near,
+                                              z_2d,
+                                              priv->viewport[2],
+                                              priv->viewport[3]);
+          priv->cameras[1].age++;
+        }
+
+      priv->dirty_view = FALSE;
+    }
+
+  /* XXX: This must be done after checking for a dirty view since updates to
+   * the view might also result in a corresponding update to the projection */
+  if (priv->dirty_projection)
+    {
+      /* The Cogl projection is implicitly dirty... */
+      priv->dirty_cogl_projection = TRUE;
+
+      cogl_matrix_init_identity (&priv->cameras[0].projection);
+      cogl_matrix_perspective (&priv->cameras[0].projection,
+                               priv->perspective.fovy,
+                               priv->perspective.aspect,
+                               priv->perspective.z_near,
+                               priv->perspective.z_far);
+      cogl_matrix_get_inverse (&priv->cameras[0].projection,
+                               &priv->cameras[0].inverse_projection);
+      priv->cameras[0].age++;
+
+      /* NB: For now, we simply use the toe in approach for handling
+       * stereoscopic rendering and so both eyes always have the same
+       * projection matrix. */
+      if (priv->stereo_enabled)
+        {
+          cogl_matrix_init_from_array (&priv->cameras[1].projection,
+                                       (float *)&priv->cameras[0].projection);
+          cogl_matrix_init_from_array (&priv->cameras[1].inverse_projection,
+                                       (float *)
+                                       &priv->cameras[0].inverse_projection);
+          priv->cameras[1].age++;
+        }
+
+      priv->dirty_projection = FALSE;
+    }
+}
+
+static void
+_clutter_stage_flush_modelview_projection (ClutterStage *stage)
+{
+  ClutterStagePrivate *priv = stage->priv;
+
+  g_return_if_fail (priv->current_camera != NULL);
+
+  if (priv->last_flushed_camera != priv->current_camera)
+    {
+      priv->dirty_cogl_viewport = TRUE;
+      priv->dirty_cogl_projection = TRUE;
+    }
+  priv->last_flushed_camera = priv->current_camera;
+
+  if (priv->dirty_cogl_viewport)
+    {
+      CLUTTER_NOTE (PAINT,
+                    "Setting up the viewport { w:%f, h:%f }",
+                    priv->current_camera->viewport[2], priv->current_camera->viewport[3]);
+      cogl_set_viewport (priv->current_camera->viewport[0],
+                         priv->current_camera->viewport[1],
+                         priv->current_camera->viewport[2],
+                         priv->current_camera->viewport[3]);
+
+      priv->dirty_cogl_viewport = FALSE;
+    }
+
+  if (priv->dirty_cogl_projection)
+    {
+      cogl_set_projection_matrix ((CoglMatrix *)
+                                  &priv->current_camera->projection);
+
+      priv->dirty_cogl_projection = FALSE;
+    }
+}
+
 static void
 clutter_stage_do_redraw (ClutterStage *stage)
 {
   ClutterBackend *backend = clutter_get_default_backend ();
   ClutterActor *actor = CLUTTER_ACTOR (stage);
   ClutterStagePrivate *priv = stage->priv;
+  CoglFramebuffer *fb;
 
   CLUTTER_STATIC_COUNTER (redraw_counter,
                           "clutter_stage_do_redraw counter",
@@ -1153,14 +1538,68 @@ clutter_stage_do_redraw (ClutterStage *stage)
         priv->fps_timer = g_timer_new ();
     }
 
-  _clutter_stage_maybe_setup_viewport (stage);
+  _clutter_stage_update_modelview_projection (stage);
+
+  if (priv->stereo_enabled)
+    {
+      gboolean anaglyph =
+        (priv->stereo_mode == CLUTTER_STEREO_MODE_DEFAULT) ||
+        (priv->stereo_mode == CLUTTER_STEREO_MODE_ANAGLYPH);
+      float viewport[4];
 
-  CLUTTER_COUNTER_INC (_clutter_uprof_context, redraw_counter);
-  CLUTTER_TIMER_START (_clutter_uprof_context, redraw_timer);
+      _clutter_stage_set_current_camera (stage, &priv->cameras[0]);
+      _clutter_stage_flush_modelview_projection (stage);
 
-  _clutter_stage_window_redraw (priv->impl);
+      fb = cogl_get_draw_framebuffer ();
 
-  CLUTTER_TIMER_STOP (_clutter_uprof_context, redraw_timer);
+      if (anaglyph)
+        cogl_framebuffer_set_color_mask (fb, COGL_COLOR_MASK_RED);
+
+      _clutter_stage_window_redraw_without_swap (priv->impl);
+
+      _clutter_stage_set_current_camera (stage, &priv->cameras[1]);
+      _clutter_stage_flush_modelview_projection (stage);
+      if (anaglyph)
+        cogl_framebuffer_set_color_mask (fb,
+                                         COGL_COLOR_MASK_GREEN |
+                                         COGL_COLOR_MASK_BLUE);
+      else
+        {
+          /* XXX: For now we scissor the right-eye render so that the stage
+           * clear doesn't clobber the rendering of the left-eye, but later it
+           * might be better to instead add a mechanism for painting the stage
+           * except without clearing the color buffer. */
+          cogl_framebuffer_get_viewport4fv (fb, viewport);
+          cogl_clip_push_window_rectangle (viewport[0], viewport[1],
+                                           viewport[2], viewport[3]);
+        }
+
+      CLUTTER_COUNTER_INC (_clutter_uprof_context, redraw_counter);
+      CLUTTER_TIMER_START (_clutter_uprof_context, redraw_timer);
+
+      _clutter_stage_window_redraw (priv->impl);
+
+      CLUTTER_TIMER_STOP (_clutter_uprof_context, redraw_timer);
+
+      if (anaglyph)
+        cogl_framebuffer_set_color_mask (fb, COGL_COLOR_MASK_ALL);
+      else
+        cogl_clip_pop ();
+    }
+  else
+    {
+      _clutter_stage_set_current_camera (stage, &priv->cameras[0]);
+      _clutter_stage_flush_modelview_projection (stage);
+
+      CLUTTER_COUNTER_INC (_clutter_uprof_context, redraw_counter);
+      CLUTTER_TIMER_START (_clutter_uprof_context, redraw_timer);
+
+      _clutter_stage_window_redraw (priv->impl);
+
+      CLUTTER_TIMER_STOP (_clutter_uprof_context, redraw_timer);
+    }
+
+  _clutter_stage_set_current_camera (stage, NULL);
 
   if (_clutter_context_get_show_fps ())
     {
@@ -1209,7 +1648,7 @@ _clutter_stage_do_update (ClutterStage *stage)
    * check or clear the pending redraws flag since a relayout may
    * queue a redraw.
    */
-  _clutter_stage_maybe_relayout (CLUTTER_ACTOR (stage));
+  _clutter_stage_maybe_relayout (stage);
 
   if (!priv->redraw_pending)
     return FALSE;
@@ -1255,6 +1694,8 @@ clutter_stage_real_queue_redraw (ClutterActor *actor,
   ClutterStage *stage = CLUTTER_STAGE (actor);
   ClutterStageWindow *stage_window;
   ClutterPaintVolume *redraw_clip;
+  int camera_index;
+  ClutterCamera *camera;
   ClutterActorBox bounding_box;
   ClutterActorBox intersection_box;
   cairo_rectangle_int_t geom, stage_clip;
@@ -1285,9 +1726,11 @@ clutter_stage_real_queue_redraw (ClutterActor *actor,
       return;
     }
 
-  _clutter_paint_volume_get_stage_paint_box (redraw_clip,
-                                             stage,
-                                             &bounding_box);
+  camera_index = _clutter_actor_get_queue_redraw_camera_index (leaf);
+  camera = &stage->priv->cameras[camera_index];
+
+  _clutter_paint_volume_get_camera_paint_box (redraw_clip,
+                                              camera, &bounding_box);
 
   _clutter_stage_window_get_geometry (stage_window, &geom);
 
@@ -1305,9 +1748,14 @@ clutter_stage_real_queue_redraw (ClutterActor *actor,
    * clip rectangle outwards... */
   stage_clip.x = intersection_box.x1;
   stage_clip.y = intersection_box.y1;
+  /* XXX: This doesn't seem right - it seems like this will
+   * potentially round down to be smaller than it needs to beâ */
   stage_clip.width = intersection_box.x2 - stage_clip.x;
   stage_clip.height = intersection_box.y2 - stage_clip.y;
 
+  /* XXX: When we add support for cameras associated with
+   * CoglOffscreen framebuffers then we shouldn't assume that the
+   * backend needs to be notified of redraw clips. */
   _clutter_stage_window_add_redraw_clip (stage_window, &stage_clip);
 }
 
@@ -1473,8 +1921,13 @@ _clutter_stage_do_pick (ClutterStage   *stage,
 
   _clutter_backend_ensure_context (context->backend, stage);
 
+  /* Note: If stereoscopic rendering is enabled then we will currently
+   * just perform picking according to the left eye... */
+  _clutter_stage_set_current_camera (stage, &priv->cameras[0]);
+
   /* needed for when a context switch happens */
-  _clutter_stage_maybe_setup_viewport (stage);
+  _clutter_stage_update_modelview_projection (stage);
+  _clutter_stage_flush_modelview_projection (stage);
 
   /* If we are seeing multiple picks per frame that means the scene is static
    * so we promote to doing a non-scissored pick render so that all subsequent
@@ -1514,6 +1967,8 @@ _clutter_stage_do_pick (ClutterStage   *stage,
   context->pick_mode = CLUTTER_PICK_NONE;
   CLUTTER_TIMER_STOP (_clutter_uprof_context, pick_paint);
 
+  _clutter_stage_set_current_camera (stage, NULL);
+
   if (is_clipped)
     {
       if (G_LIKELY (!(clutter_pick_debug_flags &
@@ -1601,7 +2056,7 @@ clutter_stage_real_apply_transform (ClutterActor *stage,
   /* FIXME: we probably shouldn't be explicitly reseting the matrix
    * here... */
   cogl_matrix_init_identity (matrix);
-  cogl_matrix_multiply (matrix, matrix, &priv->view);
+  cogl_matrix_multiply (matrix, matrix, &priv->current_camera->view);
 }
 
 static void
@@ -2203,6 +2658,17 @@ clutter_stage_init (ClutterStage *self)
   ClutterStageWindow *impl;
   ClutterBackend *backend;
   GError *error;
+  char *stereo_mode;
+  struct {
+      const char *name;
+      ClutterStereoMode mode;
+  } stereo_modes[] = {
+        { "default", CLUTTER_STEREO_MODE_DEFAULT },
+        { "anaglyph", CLUTTER_STEREO_MODE_ANAGLYPH },
+        { "vertical-split", CLUTTER_STEREO_MODE_VERTICAL_SPLIT },
+        { "horizontal-split", CLUTTER_STEREO_MODE_HORIZONTAL_SPLIT },
+        { 0 }
+  };
 
   /* a stage is a top-level object */
   CLUTTER_SET_PRIVATE_FLAGS (self, CLUTTER_IS_TOPLEVEL);
@@ -2232,6 +2698,32 @@ clutter_stage_init (ClutterStage *self)
         g_critical ("Unable to create a new stage implementation.");
     }
 
+  stereo_mode = getenv ("CLUTTER_STEREO_MODE");
+  if (stereo_mode)
+    {
+      int i;
+      priv->stereo_enabled = TRUE;
+      priv->stereo_mode = CLUTTER_STEREO_MODE_DEFAULT;
+      for (i = 0; stereo_modes[i].name; i++)
+        {
+          if (strcmp (stereo_modes[i].name, stereo_mode) == 0)
+            priv->stereo_mode = stereo_modes[i].mode;
+        }
+    }
+  else
+    {
+      priv->stereo_enabled = FALSE;
+      priv->stereo_mode = CLUTTER_STEREO_MODE_DEFAULT;
+    }
+
+  priv->n_cameras = priv->stereo_enabled ? 2 : 1;
+  priv->cameras_age = 0;
+
+  priv->cameras[0].index = 0;
+  priv->cameras[1].index = 1;
+
+  priv->current_camera = NULL;
+
   priv->event_queue = g_queue_new ();
 
   priv->is_fullscreen = FALSE;
@@ -2257,16 +2749,22 @@ clutter_stage_init (ClutterStage *self)
   priv->perspective.z_near = 0.1;
   priv->perspective.z_far  = 100.0;
 
-  cogl_matrix_init_identity (&priv->projection);
-  cogl_matrix_perspective (&priv->projection,
+  /* Ideally we would set the cameras up lazily when we first paint,
+   * but we have to consider for example that using
+   * clutter_texture_new_from_actor results in clutter trying to
+   * calculate the paint-box of the source-actor outside of the paint
+   * cycle and so it gets upset if we don't initialize the left-eye
+   * camera to something reasonable early... */
+  cogl_matrix_init_identity (&priv->cameras[0].projection);
+  cogl_matrix_perspective (&priv->cameras[0].projection,
                            priv->perspective.fovy,
                            priv->perspective.aspect,
                            priv->perspective.z_near,
                            priv->perspective.z_far);
-  cogl_matrix_get_inverse (&priv->projection,
-                           &priv->inverse_projection);
-  cogl_matrix_init_identity (&priv->view);
-  cogl_matrix_view_2d_in_perspective (&priv->view,
+  cogl_matrix_get_inverse (&priv->cameras[0].projection,
+                           &priv->cameras[0].inverse_projection);
+  cogl_matrix_init_identity (&priv->cameras[0].view);
+  cogl_matrix_view_2d_in_perspective (&priv->cameras[0].view,
                                       priv->perspective.fovy,
                                       priv->perspective.aspect,
                                       priv->perspective.z_near,
@@ -2274,6 +2772,9 @@ clutter_stage_init (ClutterStage *self)
                                       geom.width,
                                       geom.height);
 
+  priv->dirty_viewport = TRUE;
+  priv->dirty_projection = TRUE;
+  priv->dirty_view = TRUE;
 
   /* FIXME - remove for 2.0 */
   priv->fog.z_near = 1.0;
@@ -2371,47 +2872,20 @@ clutter_stage_set_color (ClutterStage       *stage,
   g_object_notify (G_OBJECT (stage), "color");
 }
 
-/**
- * clutter_stage_get_color:
- * @stage: A #ClutterStage
- * @color: (out caller-allocates): return location for a #ClutterColor
- *
- * Retrieves the stage color.
- *
- * Deprecated: 1.10: Use clutter_actor_get_background_color() instead.
- */
-void
-clutter_stage_get_color (ClutterStage *stage,
-			 ClutterColor *color)
-{
-  clutter_actor_get_background_color (CLUTTER_ACTOR (stage), color);
-}
-
-static void
-clutter_stage_set_perspective_internal (ClutterStage       *stage,
-                                        ClutterPerspective *perspective)
-{
-  ClutterStagePrivate *priv = stage->priv;
-
-  if (priv->perspective.fovy == perspective->fovy &&
-      priv->perspective.aspect == perspective->aspect &&
-      priv->perspective.z_near == perspective->z_near &&
-      priv->perspective.z_far == perspective->z_far)
-    return;
-
-  priv->perspective = *perspective;
-
-  cogl_matrix_init_identity (&priv->projection);
-  cogl_matrix_perspective (&priv->projection,
-                           priv->perspective.fovy,
-                           priv->perspective.aspect,
-                           priv->perspective.z_near,
-                           priv->perspective.z_far);
-  cogl_matrix_get_inverse (&priv->projection,
-                           &priv->inverse_projection);
-
-  priv->dirty_projection = TRUE;
-  clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
+/**
+ * clutter_stage_get_color:
+ * @stage: A #ClutterStage
+ * @color: (out caller-allocates): return location for a #ClutterColor
+ *
+ * Retrieves the stage color.
+ *
+ * Deprecated: 1.10: Use clutter_actor_get_background_color() instead.
+ */
+void
+clutter_stage_get_color (ClutterStage *stage,
+			 ClutterColor *color)
+{
+  clutter_actor_get_background_color (CLUTTER_ACTOR (stage), color);
 }
 
 /**
@@ -2471,7 +2945,10 @@ clutter_stage_get_perspective (ClutterStage       *stage,
  * Retrieves the @stage's projection matrix. This is derived from the
  * current perspective set using clutter_stage_set_perspective().
  *
- * Since: 1.6
+ * XXX: it's not clear a.t.m what the right way to make something like
+ * this public would be, considering that the stage may be associated
+ * with multiple cameras. The limited internal use-cases we have
+ * currently will work if we just return the current camera.
  */
 void
 _clutter_stage_get_projection_matrix (ClutterStage *stage,
@@ -2480,7 +2957,7 @@ _clutter_stage_get_projection_matrix (ClutterStage *stage,
   g_return_if_fail (CLUTTER_IS_STAGE (stage));
   g_return_if_fail (projection != NULL);
 
-  *projection = stage->priv->projection;
+  *projection = stage->priv->current_camera->projection;
 }
 
 /* This simply provides a simple mechanism for us to ensure that
@@ -2488,9 +2965,9 @@ _clutter_stage_get_projection_matrix (ClutterStage *stage,
  *
  * This is used when switching between multiple stages */
 void
-_clutter_stage_dirty_projection (ClutterStage *stage)
+_clutter_stage_dirty_cogl_projection (ClutterStage *stage)
 {
-  stage->priv->dirty_projection = TRUE;
+  stage->priv->dirty_cogl_projection = TRUE;
 }
 
 /*
@@ -2543,7 +3020,9 @@ _clutter_stage_set_viewport (ClutterStage *stage,
 
   priv = stage->priv;
 
-
+  /* NB: in stereo mode we don't have to worry about checking against
+   * the second camera too since we can assume both cameras have the
+   * same viewport */
   if (x == priv->viewport[0] &&
       y == priv->viewport[1] &&
       width == priv->viewport[2] &&
@@ -2555,6 +3034,8 @@ _clutter_stage_set_viewport (ClutterStage *stage,
   priv->viewport[2] = width;
   priv->viewport[3] = height;
 
+  /* NB: we update the actual camera viewports lazily during
+   * _clutter_stage_update_modelview_projection () */
   priv->dirty_viewport = TRUE;
 
   queue_full_redraw (stage);
@@ -2565,9 +3046,9 @@ _clutter_stage_set_viewport (ClutterStage *stage,
  *
  * This is used when switching between multiple stages */
 void
-_clutter_stage_dirty_viewport (ClutterStage *stage)
+_clutter_stage_dirty_cogl_viewport (ClutterStage *stage)
 {
-  stage->priv->dirty_viewport = TRUE;
+  stage->priv->dirty_cogl_viewport = TRUE;
 }
 
 /*
@@ -2829,15 +3310,42 @@ clutter_stage_read_pixels (ClutterStage *stage,
                            gint          width,
                            gint          height)
 {
+  ClutterStagePrivate *priv;
+  gboolean set_camera = FALSE;
   ClutterGeometry geom;
   guchar *pixels;
 
   g_return_val_if_fail (CLUTTER_IS_STAGE (stage), NULL);
 
+  priv = stage->priv;
+
   /* Force a redraw of the stage before reading back pixels */
   clutter_stage_ensure_current (stage);
+
+  /* XXX: Ideally we'd just assert that priv->current_camera is NULL
+   * but I think there is already code in the wild that expects to be
+   * able to issue a read_pixels request mid-scene, so even though
+   * that's quite likely to break anyway we try not to make it
+   * any more likely than before.
+   *
+   * If a read_pixels request is made mid-scene then we will already
+   * have a current_camera which we don't want to disrupt but
+   * otherwise we make the left_eye camera current before issuing
+   * the stage paint.
+   */
+  if (!priv->current_camera)
+    {
+      _clutter_stage_update_modelview_projection (stage);
+      _clutter_stage_set_current_camera (stage, &priv->cameras[0]);
+      _clutter_stage_flush_modelview_projection (stage);
+      set_camera = TRUE;
+    }
+
   clutter_actor_paint (CLUTTER_ACTOR (stage));
 
+  if (set_camera)
+    _clutter_stage_set_current_camera (stage, NULL);
+
   clutter_actor_get_allocation_geometry (CLUTTER_ACTOR (stage), &geom);
 
   if (width < 0)
@@ -3338,194 +3846,17 @@ clutter_stage_ensure_viewport (ClutterStage *stage)
 {
   g_return_if_fail (CLUTTER_IS_STAGE (stage));
 
-  _clutter_stage_dirty_viewport (stage);
+  _clutter_stage_dirty_cogl_viewport (stage);
 
   clutter_actor_queue_redraw (CLUTTER_ACTOR (stage));
 }
 
-/* This calculates a distance into the view frustum to position the
- * stage so there is a decent amount of space to position geometry
- * between the stage and the near clipping plane.
- *
- * Some awkward issues with this problem are:
- * - It's not possible to have a gap as large as the stage size with
- *   a fov > 53Â which is basically always the case since the default
- *   fov is 60Â.
- *    - This can be deduced if you consider that this requires a
- *      triangle as wide as it is deep to fit in the frustum in front
- *      of the z_near plane. That triangle will always have an angle
- *      of 53.13Â at the point sitting on the z_near plane, but if the
- *      frustum has a wider fov angle the left/right clipping planes
- *      can never converge with the two corners of our triangle no
- *      matter what size the triangle has.
- * - With a fov > 53Â there is a trade off between maximizing the gap
- *   size relative to the stage size but not loosing depth precision.
- * - Perhaps ideally we wouldn't just consider the fov on the y-axis
- *   that is usually used to define a perspective, we would consider
- *   the fov of the axis with the largest stage size so the gap would
- *   accommodate that size best.
- *
- * After going around in circles a few times with how to handle these
- * issues, we decided in the end to go for the simplest solution to
- * start with instead of an elaborate function that handles arbitrary
- * fov angles that we currently have no use-case for.
- *
- * The solution assumes a fovy of 60Â and for that case gives a gap
- * that's 85% of the stage height. We can consider more elaborate
- * functions if necessary later.
- *
- * One guide we had to steer the gap size we support is the
- * interactive test, test-texture-quality which expects to animate an
- * actor to +400 on the z axis with a stage size of 640x480. A gap
- * that's 85% of the stage height gives a gap of 408 in that case.
- */
-static float
-calculate_z_translation (float z_near)
-{
-  /* This solution uses fairly basic trigonometry, but is seems worth
-   * clarifying the particular geometry we are looking at in-case
-   * anyone wants to develop this further later. Not sure how well an
-   * ascii diagram is going to work :-)
-   *
-   *    |--- stage_height ---|
-   *    |     stage line     |
-   *   âââââââââââââââââââââââ------------
-   *    â.  (2)   â        .â       |   |
-   *   C â .      â      . â     gap|   |
-   * =0.5Ââ  . a  â    .  â         |   |
-   *      bâ(1). Dâ  .   â          |   |
-   *        â   B.â.    ânear plane |   |
-   *      A= âââââââââââ-------------   |
-   *     120Â â c â   â  |            z_2d
-   *           â  â  â  z_near          |
-   *       left â â â    |              |
-   *       clip  60Âfovy |              |
-   *       plane  â----------------------
-   *              |
-   *              |
-   *         origin line
-   *
-   * The area of interest is the triangle labeled (1) at the top left
-   * marked with the ... line (a) from where the origin line crosses
-   * the near plane to the top left where the stage line cross the
-   * left clip plane.
-   *
-   * The sides of the triangle are a, b and c and the corresponding
-   * angles opposite those sides are A, B and C.
-   *
-   * The angle of C is what trades off the gap size we have relative
-   * to the stage size vs the depth precision we have.
-   *
-   * As mentioned above we arove at the angle for C is by working
-   * backwards from how much space we want for test-texture-quality.
-   * With a stage_height of 480 we want a gap > 400, ideally we also
-   * wanted a somewhat round number as a percentage of the height for
-   * documentation purposes. ~87% or a gap of ~416 is the limit
-   * because that's where we approach a C angle of 0Â and effectively
-   * loose all depth precision.
-   *
-   * So for our test app with a stage_height of 480 if we aim for a
-   * gap of 408 (85% of 480) we can get the angle D as
-   * atan (stage_height/2/408) = 30.5Â.
-   *
-   * That gives us the angle for B as 90Â - 30.5Â = 59.5Â
-   *
-   * We can already determine that A has an angle of (fovy/2 + 90Â) =
-   * 120Â
-   *
-   * Therefore C = 180 - A - B = 0.5Â
-   *
-   * The length of c = z_near * tan (30Â)
-   *
-   * Now we can use the rule a/SinA = c/SinC to calculate the
-   * length of a. After some rearranging that gives us:
-   *
-   *      a              c
-   *  ----------  =  ----------
-   *  sin (120Â)     sin (0.5Â)
-   *
-   *      c * sin (120Â)
-   *  a = --------------
-   *        sin (0.5Â)
-   *
-   * And with that we can determine z_2d = cos (D) * a =
-   * cos (30.5Â) * a + z_near:
-   *
-   *         c * sin (120Â) * cos (30.5Â)
-   *  z_2d = --------------------------- + z_near
-   *                 sin (0.5Â)
-   */
-#define _DEG_TO_RAD (G_PI / 180.0)
-  return z_near * tanf (30.0f * _DEG_TO_RAD) *
-         sinf (120.0f * _DEG_TO_RAD) * cosf (30.5f * _DEG_TO_RAD) /
-         sinf (0.5f * _DEG_TO_RAD) +
-         z_near;
-#undef _DEG_TO_RAD
-   /* We expect the compiler should boil this down to z_near * CONSTANT */
-}
-
+/* TODO: remove this badly named API */
 void
 _clutter_stage_maybe_setup_viewport (ClutterStage *stage)
 {
-  ClutterStagePrivate *priv = stage->priv;
-
-  if (priv->dirty_viewport)
-    {
-      ClutterPerspective perspective;
-      float z_2d;
-
-      CLUTTER_NOTE (PAINT,
-                    "Setting up the viewport { w:%f, h:%f }",
-                    priv->viewport[2], priv->viewport[3]);
-
-      cogl_set_viewport (priv->viewport[0],
-                         priv->viewport[1],
-                         priv->viewport[2],
-                         priv->viewport[3]);
-
-      perspective = priv->perspective;
-
-      /* Ideally we want to regenerate the perspective matrix whenever
-       * the size changes but if the user has provided a custom matrix
-       * then we don't want to override it */
-      if (!priv->has_custom_perspective)
-        {
-          perspective.aspect = priv->viewport[2] / priv->viewport[3];
-          z_2d = calculate_z_translation (perspective.z_near);
-
-#define _DEG_TO_RAD (G_PI / 180.0)
-          /* NB: z_2d is only enough room for 85% of the stage_height between
-           * the stage and the z_near plane. For behind the stage plane we
-           * want a more consistent gap of 10 times the stage_height before
-           * hitting the far plane so we calculate that relative to the final
-           * height of the stage plane at the z_2d_distance we got... */
-          perspective.z_far = z_2d +
-            tanf ((perspective.fovy / 2.0f) * _DEG_TO_RAD) * z_2d * 20.0f;
-#undef _DEG_TO_RAD
-
-          clutter_stage_set_perspective_internal (stage, &perspective);
-        }
-      else
-        z_2d = calculate_z_translation (perspective.z_near);
-
-      cogl_matrix_init_identity (&priv->view);
-      cogl_matrix_view_2d_in_perspective (&priv->view,
-                                          perspective.fovy,
-                                          perspective.aspect,
-                                          perspective.z_near,
-                                          z_2d,
-                                          priv->viewport[2],
-                                          priv->viewport[3]);
-
-      priv->dirty_viewport = FALSE;
-    }
-
-  if (priv->dirty_projection)
-    {
-      cogl_set_projection_matrix (&priv->projection);
-
-      priv->dirty_projection = FALSE;
-    }
+  _clutter_stage_update_modelview_projection (stage);
+  _clutter_stage_flush_modelview_projection (stage);
 }
 
 /**
@@ -4424,3 +4755,111 @@ _clutter_stage_update_state (ClutterStage      *stage,
 
   return TRUE;
 }
+
+/**
+ * clutter_stage_set_stereo_enabled:
+ * @stage: A #ClutterStage
+ * @enabled: Whether stereoscopic rendering should be enabled or not
+ *
+ * Enables stereoscopic rendering of the stage if @enabled = %TRUE or
+ * disables stereoscopic rendering if enabled = %FALSE;
+ *
+ * Since: 1.8
+ */
+void
+clutter_stage_set_stereo_enabled (ClutterStage *stage,
+                                  gboolean enabled)
+{
+  ClutterStagePrivate *priv;
+
+  g_return_if_fail (CLUTTER_IS_STAGE (stage));
+
+  priv = stage->priv;
+
+  if (priv->stereo_enabled == enabled)
+    return;
+
+  /* Currently we only support anaglyph based stereoscopic rendering
+   * and for that we compose each frame by asking the backend to
+   * redraw the scene from two eye positions per frame but that
+   * also means we don't want the backend to automatically present
+   * the frame until we have drawn for both eyes so we need
+   * the swap-buffers feature to enable stereo rendering... */
+  if (enabled &&
+      !_clutter_stage_window_has_feature (stage->priv->impl,
+                                          CLUTTER_STAGE_WINDOW_FEATURE_SWAP_BUFFERS))
+    return;
+
+  priv->stereo_enabled = enabled;
+
+  priv->n_cameras = priv->stereo_enabled ? 2 : 1;
+
+  priv->dirty_viewport = TRUE;
+  priv->dirty_projection = TRUE;
+  priv->dirty_view = TRUE;
+  priv->cameras_age++;
+}
+
+/**
+ * clutter_stage_set_stereo_mode:
+ * @stage: A #ClutterStage
+ * @mode: A #ClutterStereoMode selecting the mode of stereoscopic
+ *        output.
+ *
+ * Changes the mode of outputing stereoscopic content. The default
+ * mode will take advantage of any platform specific support for
+ * stereoscopic output but there are also some platform independent
+ * modes including anaglyph rendering for use with filter glasses with
+ * a red filter for the left eye and a cyan filter for the right, it's
+ * also possible to split the stage horizontally or vertically showing
+ * the left and right eye content on opposite sides of the stage. This
+ * can be used with a lot of 3D TVs.
+ *
+ * Since: 1.8
+ */
+void
+clutter_stage_set_stereo_mode (ClutterStage     *stage,
+                               ClutterStereoMode mode)
+{
+  ClutterStagePrivate *priv;
+
+  g_return_if_fail (CLUTTER_IS_STAGE (stage));
+
+  priv = stage->priv;
+
+  if (priv->stereo_mode == mode)
+    return;
+
+  priv->stereo_mode = mode;
+
+  if (priv->stereo_enabled)
+    {
+      priv->dirty_viewport = TRUE;
+      priv->dirty_projection = TRUE;
+      priv->dirty_view = TRUE;
+    }
+}
+
+const ClutterCamera *
+_clutter_stage_get_current_camera (ClutterStage *stage)
+{
+  return stage->priv->current_camera;
+}
+
+const ClutterCamera *
+_clutter_stage_get_camera (ClutterStage *stage, int camera_index)
+{
+  return &stage->priv->cameras[camera_index];
+}
+
+int
+_clutter_stage_get_n_cameras (ClutterStage *stage)
+{
+  return stage->priv->n_cameras;
+}
+
+int
+_clutter_stage_get_cameras_age (ClutterStage *stage)
+{
+  return stage->priv->cameras_age;
+}
diff --git a/clutter/clutter-stage.h b/clutter/clutter-stage.h
index e3c1d7e..7e91a39 100644
--- a/clutter/clutter-stage.h
+++ b/clutter/clutter-stage.h
@@ -202,6 +202,11 @@ void            clutter_stage_ensure_current                    (ClutterStage
 void            clutter_stage_ensure_viewport                   (ClutterStage          *stage);
 void            clutter_stage_ensure_redraw                     (ClutterStage          *stage);
 
+void                  clutter_stage_set_stereo_enabled        (ClutterStage     *stage,
+                                                               gboolean          enabled);
+void                  clutter_stage_set_stereo_mode           (ClutterStage     *stage,
+                                                               ClutterStereoMode  mode);
+
 G_END_DECLS
 
 #endif /* __CLUTTER_STAGE_H__ */
diff --git a/clutter/clutter-texture.c b/clutter/clutter-texture.c
index c521c80..c5257bf 100644
--- a/clutter/clutter-texture.c
+++ b/clutter/clutter-texture.c
@@ -541,9 +541,11 @@ update_fbo (ClutterActor *self)
       if ((source_parent = clutter_actor_get_parent (priv->fbo_source)))
         {
           CoglMatrix modelview;
-          cogl_matrix_init_identity (&modelview);
+          const ClutterCamera *camera =
+            _clutter_stage_get_camera (CLUTTER_STAGE (stage), 0);
+          cogl_matrix_init_from_array (&modelview, (float *)&camera->view);
           _clutter_actor_apply_relative_transformation_matrix (source_parent,
-                                                               NULL,
+                                                               stage,
                                                                &modelview);
           cogl_set_modelview_matrix (&modelview);
         }
@@ -2363,14 +2365,30 @@ on_fbo_source_size_change (GObject          *object,
                            GParamSpec       *param_spec,
                            ClutterTexture   *texture)
 {
+  ClutterStage *stage;
   ClutterTexturePrivate *priv = texture->priv;
   gfloat w, h;
   ClutterActorBox box;
   gboolean status;
 
-  status = clutter_actor_get_paint_box (priv->fbo_source, &box);
-  if (status)
-    clutter_actor_box_get_size (&box, &w, &h);
+  stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (priv->fbo_source));
+  if (stage)
+    {
+      const ClutterCamera *left_eye;
+
+      g_return_if_fail (_clutter_stage_get_current_camera (stage) == NULL);
+
+      left_eye = _clutter_stage_get_camera (stage, 0);
+      _clutter_stage_set_current_camera (stage, left_eye);
+
+      status = clutter_actor_get_paint_box (priv->fbo_source, &box);
+      if (status)
+        clutter_actor_box_get_size (&box, &w, &h);
+
+      _clutter_stage_set_current_camera (stage, NULL);
+    }
+  else
+    status = FALSE;
 
   /* In the end we will size the framebuffer according to the paint
    * box, but for code that does:
@@ -2532,6 +2550,10 @@ fbo_source_queue_relayout_cb (ClutterActor *source,
  *   </listitem>
  * </itemizedlist>
  *
+ * <note>If clutter is being used for stereo rendering then the
+ * texture represent the actor as it would be seen from the left
+ * eye.</note>
+ *
  * Return value: A newly created #ClutterTexture object, or %NULL on failure.
  *
  * Deprecated: 1.8: Use the #ClutterOffscreenEffect and #ClutterShaderEffect
@@ -2543,6 +2565,7 @@ fbo_source_queue_relayout_cb (ClutterActor *source,
 ClutterActor *
 clutter_texture_new_from_actor (ClutterActor *actor)
 {
+  ClutterStage *stage;
   ClutterTexture        *texture;
   ClutterTexturePrivate *priv;
   gfloat w, h;
@@ -2562,9 +2585,24 @@ clutter_texture_new_from_actor (ClutterActor *actor)
 	return NULL;
     }
 
-  status = clutter_actor_get_paint_box (actor, &box);
-  if (status)
-    clutter_actor_box_get_size (&box, &w, &h);
+  stage = CLUTTER_STAGE (_clutter_actor_get_stage_internal (actor));
+  if (stage)
+    {
+      const ClutterCamera *left_eye;
+
+      g_return_val_if_fail (_clutter_stage_get_current_camera (stage) == NULL, NULL);
+
+      left_eye = _clutter_stage_get_camera (stage, 0);
+      _clutter_stage_set_current_camera (stage, left_eye);
+
+      status = clutter_actor_get_paint_box (actor, &box);
+      if (status)
+        clutter_actor_box_get_size (&box, &w, &h);
+
+      _clutter_stage_set_current_camera (stage, NULL);
+    }
+  else
+    status = FALSE;
 
   /* In the end we will size the framebuffer according to the paint
    * box, but for code that does:
diff --git a/clutter/cogl/clutter-stage-cogl.c b/clutter/cogl/clutter-stage-cogl.c
index 618723a..2b17bae 100644
--- a/clutter/cogl/clutter-stage-cogl.c
+++ b/clutter/cogl/clutter-stage-cogl.c
@@ -63,6 +63,16 @@ enum {
   PROP_LAST
 };
 
+static gboolean
+clutter_stage_cogl_has_feature (ClutterStageWindow       *window,
+                                ClutterStageWindowFeature feature)
+{
+  if (feature == CLUTTER_STAGE_WINDOW_FEATURE_SWAP_BUFFERS)
+    return TRUE;
+  else
+    return FALSE;
+}
+
 static void
 clutter_stage_cogl_unrealize (ClutterStageWindow *stage_window)
 {
@@ -312,9 +322,42 @@ clutter_stage_cogl_get_redraw_clip_bounds (ClutterStageWindow    *stage_window,
   return FALSE;
 }
 
-/* XXX: This is basically identical to clutter_stage_glx_redraw */
 static void
-clutter_stage_cogl_redraw (ClutterStageWindow *stage_window)
+get_clipped_redraw_status (ClutterStageWindow *stage_window,
+                           gboolean *may_use_clipped_redraw,
+                           gboolean *use_clipped_redraw)
+{
+  ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_window);
+  gboolean can_blit_sub_buffer =
+    cogl_clutter_winsys_has_feature (COGL_WINSYS_FEATURE_SWAP_REGION);
+
+  if (G_LIKELY (can_blit_sub_buffer) &&
+      /* NB: a zero width redraw clip == full stage redraw */
+      stage_cogl->bounding_redraw_clip.width != 0 &&
+      /* some drivers struggle to get going and produce some junk
+       * frames when starting up... */
+      G_LIKELY (stage_cogl->frame_count > 3)
+      /* While resizing a window clipped redraws are disabled to avoid
+       * artefacts. See clutter-event-x11.c:event_translate for a
+       * detailed explanation */
+      && _clutter_stage_window_can_clip_redraws (stage_window)
+      )
+    {
+      *may_use_clipped_redraw = TRUE;
+    }
+  else
+    *may_use_clipped_redraw = FALSE;
+
+  if (*may_use_clipped_redraw &&
+      G_LIKELY (!(clutter_paint_debug_flags &
+                  CLUTTER_DEBUG_DISABLE_CLIPPED_REDRAWS)))
+    *use_clipped_redraw = TRUE;
+  else
+    *use_clipped_redraw = FALSE;
+}
+
+static void
+clutter_stage_cogl_swap_buffers (ClutterStageWindow *stage_window)
 {
   ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_window);
   gboolean may_use_clipped_redraw;
@@ -322,11 +365,6 @@ clutter_stage_cogl_redraw (ClutterStageWindow *stage_window)
   gboolean can_blit_sub_buffer;
   ClutterActor *wrapper;
 
-  CLUTTER_STATIC_TIMER (painting_timer,
-                        "Redrawing", /* parent */
-                        "Painting actors",
-                        "The time spent painting actors",
-                        0 /* no application private data */);
   CLUTTER_STATIC_TIMER (swapbuffers_timer,
                         "Redrawing", /* parent */
                         "SwapBuffers",
@@ -338,34 +376,93 @@ clutter_stage_cogl_redraw (ClutterStageWindow *stage_window)
                         "The time spent in blit_sub_buffer",
                         0 /* no application private data */);
 
-  wrapper = CLUTTER_ACTOR (stage_cogl->wrapper);
-
   if (!stage_cogl->onscreen)
     return;
 
-  CLUTTER_TIMER_START (_clutter_uprof_context, painting_timer);
+  get_clipped_redraw_status (stage_window,
+                             &may_use_clipped_redraw,
+                             &use_clipped_redraw);
 
-  can_blit_sub_buffer =
-    cogl_clutter_winsys_has_feature (COGL_WINSYS_FEATURE_SWAP_REGION);
+  /* push on the screen */
+  if (use_clipped_redraw)
+    {
+      cairo_rectangle_int_t *clip = &stage_cogl->bounding_redraw_clip;
+      int copy_area[4];
 
-  may_use_clipped_redraw = FALSE;
-  if (_clutter_stage_window_can_clip_redraws (stage_window) &&
-      can_blit_sub_buffer &&
-      /* NB: a zero width redraw clip == full stage redraw */
-      stage_cogl->bounding_redraw_clip.width != 0 &&
-      /* some drivers struggle to get going and produce some junk
-       * frames when starting up... */
-      stage_cogl->frame_count > 3)
+      /* XXX: It seems there will be a race here in that the stage
+       * window may be resized before the cogl_onscreen_swap_region
+       * is handled and so we may copy the wrong region. I can't
+       * really see how we can handle this with the current state of X
+       * but at least in this case a full redraw should be queued by
+       * the resize anyway so it should only exhibit temporary
+       * artefacts.
+       */
+
+      copy_area[0] = clip->x;
+      copy_area[1] = clip->y;
+      copy_area[2] = clip->width;
+      copy_area[3] = clip->height;
+
+      CLUTTER_NOTE (BACKEND,
+                    "cogl_onscreen_swap_region (onscreen: %p, "
+                                                "x: %d, y: %d, "
+                                                "width: %d, height: %d)",
+                    stage_cogl->onscreen,
+                    copy_area[0], copy_area[1], copy_area[2], copy_area[3]);
+
+
+      CLUTTER_TIMER_START (_clutter_uprof_context, blit_sub_buffer_timer);
+
+      cogl_onscreen_swap_region (stage_cogl->onscreen, copy_area, 1);
+
+      CLUTTER_TIMER_STOP (_clutter_uprof_context, blit_sub_buffer_timer);
+    }
+  else
     {
-      may_use_clipped_redraw = TRUE;
+      CLUTTER_NOTE (BACKEND, "cogl_onscreen_swap_buffers (onscreen: %p)",
+                    stage_cogl->onscreen);
+
+      /* If we have swap buffer events then
+       * cogl_onscreen_swap_buffers will return immediately and we
+       * need to track that there is a swap in progress... */
+      if (clutter_feature_available (CLUTTER_FEATURE_SWAP_EVENTS))
+        stage_cogl->pending_swaps++;
+
+      CLUTTER_TIMER_START (_clutter_uprof_context, swapbuffers_timer);
+      cogl_onscreen_swap_buffers (stage_cogl->onscreen);
+      CLUTTER_TIMER_STOP (_clutter_uprof_context, swapbuffers_timer);
     }
 
-  if (may_use_clipped_redraw &&
-      G_LIKELY (!(clutter_paint_debug_flags &
-                  CLUTTER_DEBUG_DISABLE_CLIPPED_REDRAWS)))
-    use_clipped_redraw = TRUE;
-  else
-    use_clipped_redraw = FALSE;
+  /* reset the redraw clipping for the next paint... */
+  stage_cogl->initialized_redraw_clip = FALSE;
+
+  stage_cogl->frame_count++;
+}
+
+static void
+clutter_stage_cogl_redraw_without_swap (ClutterStageWindow *stage_window)
+{
+  ClutterStageCogl *stage_cogl = CLUTTER_STAGE_COGL (stage_window);
+  ClutterActor *wrapper;
+  gboolean may_use_clipped_redraw;
+  gboolean use_clipped_redraw;
+
+  CLUTTER_STATIC_TIMER (painting_timer,
+                        "Redrawing", /* parent */
+                        "Painting actors",
+                        "The time spent painting actors",
+                        0 /* no application private data */);
+
+  wrapper = CLUTTER_ACTOR (stage_cogl->wrapper);
+
+  if (!stage_cogl->onscreen)
+    return;
+
+  CLUTTER_TIMER_START (_clutter_uprof_context, painting_timer);
+
+  get_clipped_redraw_status (stage_window,
+                             &may_use_clipped_redraw,
+                             &use_clipped_redraw);
 
   if (use_clipped_redraw)
     {
@@ -454,61 +551,13 @@ clutter_stage_cogl_redraw (ClutterStageWindow *stage_window)
     }
 
   CLUTTER_TIMER_STOP (_clutter_uprof_context, painting_timer);
+}
 
-  /* push on the screen */
-  if (use_clipped_redraw)
-    {
-      cairo_rectangle_int_t *clip = &stage_cogl->bounding_redraw_clip;
-      int copy_area[4];
-
-      /* XXX: It seems there will be a race here in that the stage
-       * window may be resized before the cogl_onscreen_swap_region
-       * is handled and so we may copy the wrong region. I can't
-       * really see how we can handle this with the current state of X
-       * but at least in this case a full redraw should be queued by
-       * the resize anyway so it should only exhibit temporary
-       * artefacts.
-       */
-
-      copy_area[0] = clip->x;
-      copy_area[1] = clip->y;
-      copy_area[2] = clip->width;
-      copy_area[3] = clip->height;
-
-      CLUTTER_NOTE (BACKEND,
-                    "cogl_onscreen_swap_region (onscreen: %p, "
-                                                "x: %d, y: %d, "
-                                                "width: %d, height: %d)",
-                    stage_cogl->onscreen,
-                    copy_area[0], copy_area[1], copy_area[2], copy_area[3]);
-
-
-      CLUTTER_TIMER_START (_clutter_uprof_context, blit_sub_buffer_timer);
-
-      cogl_onscreen_swap_region (stage_cogl->onscreen, copy_area, 1);
-
-      CLUTTER_TIMER_STOP (_clutter_uprof_context, blit_sub_buffer_timer);
-    }
-  else
-    {
-      CLUTTER_NOTE (BACKEND, "cogl_onscreen_swap_buffers (onscreen: %p)",
-                    stage_cogl->onscreen);
-
-      /* If we have swap buffer events then cogl_onscreen_swap_buffers
-       * will return immediately and we need to track that there is a
-       * swap in progress... */
-      if (clutter_feature_available (CLUTTER_FEATURE_SWAP_EVENTS))
-        stage_cogl->pending_swaps++;
-
-      CLUTTER_TIMER_START (_clutter_uprof_context, swapbuffers_timer);
-      cogl_onscreen_swap_buffers (stage_cogl->onscreen);
-      CLUTTER_TIMER_STOP (_clutter_uprof_context, swapbuffers_timer);
-    }
-
-  /* reset the redraw clipping for the next paint... */
-  stage_cogl->initialized_redraw_clip = FALSE;
-
-  stage_cogl->frame_count++;
+static void
+clutter_stage_cogl_redraw (ClutterStageWindow *stage_window)
+{
+  clutter_stage_cogl_redraw_without_swap (stage_window);
+  clutter_stage_cogl_swap_buffers (stage_window);
 }
 
 static CoglFramebuffer *
@@ -522,6 +571,7 @@ clutter_stage_cogl_get_active_framebuffer (ClutterStageWindow *stage_window)
 static void
 clutter_stage_window_iface_init (ClutterStageWindowIface *iface)
 {
+  iface->has_feature = clutter_stage_cogl_has_feature;
   iface->realize = clutter_stage_cogl_realize;
   iface->unrealize = clutter_stage_cogl_unrealize;
   iface->get_wrapper = clutter_stage_cogl_get_wrapper;
@@ -535,6 +585,8 @@ clutter_stage_window_iface_init (ClutterStageWindowIface *iface)
   iface->ignoring_redraw_clips = clutter_stage_cogl_ignoring_redraw_clips;
   iface->get_redraw_clip_bounds = clutter_stage_cogl_get_redraw_clip_bounds;
   iface->redraw = clutter_stage_cogl_redraw;
+  iface->redraw_without_swap = clutter_stage_cogl_redraw_without_swap;
+  iface->swap_buffers = clutter_stage_cogl_swap_buffers;
   iface->get_active_framebuffer = clutter_stage_cogl_get_active_framebuffer;
 }
 



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]