From dff7660d5da96a87e25a34f5a8212e6aaea22d57 Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Mon, 1 Aug 2011 01:11:57 +0100 Subject: Adds initial support for stereoscopic rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 6b91405c1..4cb3e71b8 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 f84d7cf9d..92aa1b907 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) * + * If clutter is being used for stereo rendering then this will + * simply transform the point according the left eye's view + * * 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. - * * 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, * v[3] contains (x2, y2) * * + * If clutter is being used for stereo rendering then this will + * simply return a box according the left eye's view. + * * 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. - * * 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. * + * If clutter is being used for stereo rendering then this will + * simply return a size according the left eye's view. + * * 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(). * + * If stereoscopic rendering has been enabled then the paint + * volume is only valid for the eye currently being rendered. + * * 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. * + * If stereoscopic rendering has been enabled then the paint box + * returned will only be valid for the current eye being + * rendered + * * 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 b50154a57..b56b56a47 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 40bdb6aac..644ac6865 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 09443dc35..99130a0ed 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 paint_target() * virtual function. * + * If stereoscopic rendering has been enabled then this function + * returns the size according to the eye currently being + * rendered. + * * 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 72bc7aee3..e2c1b7827 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 725be7917..7103aedec 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 df17d6a00..9aa6b844e 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 d2c7f7845..81da8a781 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 3ba3c9983..b6383a00c 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 041a2557c..6f938d2a5 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 123caeb09..bf2d0cafe 100644 --- a/clutter/clutter-stage.c +++ b/clutter/clutter-stage.c @@ -50,6 +50,7 @@ #include #include +#include #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 e3c1d7e9c..7e91a392d 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 c521c807f..c5257bfb6 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, * * * + * If clutter is being used for stereo rendering then the + * texture represent the actor as it would be seen from the left + * eye. + * * 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 618723ac1..2b17baecb 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; } -- cgit v1.2.1