st: optimize box-shadow rendering

Currently the box-shadow is rendering is done like this :

The first time we want to render a node that requires a box-shadow, St
creates an cogl offscreen surface of the size of the allocation and
renders the box into this offscreen buffer using modulation on the
alpha channel, this buffer is then blurred according to the CSS
parameters.

The problem with this method is that every time an StWidget is
resized, its box-shadow offscreen buffer has to be resized and
therefore rendered and blurred.

This patches propose an optimization for this use case by rendering
the box-shadow only once but at a size that is independent of the
StWidget's size. Then every time we need to paint this box-shadow, we
just render this offscreen buffer using a 9-slices.

This method only works when the allocation of the widget is bigger
than the minimum shadow size on which we can apply a 9-slices, that is
given my the radius of the corners. If the allocation is smaller than
this minimum size, we then fallback to the fully render/blur the
shadow (like before this patch).

https://bugzilla.gnome.org/show_bug.cgi?id=689858
This commit is contained in:
Lionel Landwerlin 2013-07-03 17:01:00 +01:00
parent 3582ba0c77
commit 318283fc70
3 changed files with 459 additions and 38 deletions

View File

@ -1370,6 +1370,8 @@ st_theme_node_load_background_image (StThemeNode *node)
return node->background_texture != COGL_INVALID_HANDLE; return node->background_texture != COGL_INVALID_HANDLE;
} }
static void st_theme_node_prerender_shadow (StThemeNode *node, StThemeNodePaintState *state);
static void static void
st_theme_node_render_resources (StThemeNode *node, st_theme_node_render_resources (StThemeNode *node,
StThemeNodePaintState *state, StThemeNodePaintState *state,
@ -1474,38 +1476,54 @@ st_theme_node_render_resources (StThemeNode *node,
state->box_shadow_material = _st_create_shadow_material (box_shadow_spec, state->box_shadow_material = _st_create_shadow_material (box_shadow_spec,
state->prerendered_texture); state->prerendered_texture);
else if (node->background_color.alpha > 0 || has_border) else if (node->background_color.alpha > 0 || has_border)
st_theme_node_prerender_shadow (node, state);
}
}
static void
st_theme_node_update_resources (StThemeNode *node, StThemeNodePaintState *state, float width, float height)
{
gboolean had_prerendered_texture = FALSE;
gboolean had_box_shadow = FALSE;
StShadow *box_shadow_spec;
g_return_if_fail (width > 0 && height > 0);
/* Free handles we can't reuse */
if (state->prerendered_texture != COGL_INVALID_HANDLE)
{ {
CoglHandle buffer, offscreen; cogl_handle_unref (state->prerendered_texture);
int texture_width = ceil (width); state->prerendered_texture = COGL_INVALID_HANDLE;
int texture_height = ceil (height); had_prerendered_texture = TRUE;
}
buffer = cogl_texture_new_with_size (texture_width, if (state->prerendered_material != COGL_INVALID_HANDLE)
texture_height,
COGL_TEXTURE_NO_SLICING,
COGL_PIXEL_FORMAT_ANY);
offscreen = cogl_offscreen_new_to_texture (buffer);
if (offscreen != COGL_INVALID_HANDLE)
{ {
ClutterActorBox box = { 0, 0, width, height }; cogl_handle_unref (state->prerendered_material);
CoglColor clear_color; state->prerendered_material = COGL_INVALID_HANDLE;
cogl_push_framebuffer (offscreen); if (node->border_slices_texture == COGL_INVALID_HANDLE &&
cogl_ortho (0, width, height, 0, 0, 1.0); state->box_shadow_material != COGL_INVALID_HANDLE)
{
cogl_handle_unref (state->box_shadow_material);
state->box_shadow_material = COGL_INVALID_HANDLE;
had_box_shadow = TRUE;
}
}
cogl_color_set_from_4ub (&clear_color, 0, 0, 0, 0); state->alloc_width = width;
cogl_clear (&clear_color, COGL_BUFFER_BIT_COLOR); state->alloc_height = height;
st_theme_node_paint_borders (node, state, &box, 0xFF); box_shadow_spec = st_theme_node_get_box_shadow (node);
cogl_pop_framebuffer ();
cogl_handle_unref (offscreen);
if (had_prerendered_texture)
{
state->prerendered_texture = st_theme_node_prerender_background (node, width, height);
state->prerendered_material = _st_create_texture_material (state->prerendered_texture);
}
if (had_box_shadow)
state->box_shadow_material = _st_create_shadow_material (box_shadow_spec, state->box_shadow_material = _st_create_shadow_material (box_shadow_spec,
buffer); state->prerendered_texture);
}
cogl_handle_unref (buffer);
}
}
} }
static void static void
@ -1792,6 +1810,352 @@ st_theme_node_paint_borders (StThemeNode *node,
} }
} }
static void
st_theme_node_paint_sliced_shadow (StThemeNode *node,
StThemeNodePaintState *state,
const ClutterActorBox *box,
guint8 paint_opacity)
{
guint border_radius[4];
CoglColor color;
StShadow *box_shadow_spec;
gfloat xoffset, yoffset;
gfloat width, height;
gfloat shadow_width, shadow_height;
gfloat xend, yend, top, bottom, left, right;
gfloat s_top, s_bottom, s_left, s_right;
gfloat shadow_blur_radius, x_spread_factor, y_spread_factor;
float rectangles[8 * 9];
gint idx;
if (paint_opacity == 0)
return;
st_theme_node_reduce_border_radius (node, box->x2 - box->x1, box->y2 - box->y1, border_radius);
box_shadow_spec = st_theme_node_get_box_shadow (node);
/* Compute input & output areas :
*
* yoffset ----------------------------
* | | | |
* | | | |
* | | | |
* top ----------------------------
* | | | |
* | | | |
* | | | |
* bottom ----------------------------
* | | | |
* | | | |
* | | | |
* yend ----------------------------
* xoffset left right xend
*
* s_top = top in offscreen's coordinates (0.0 - 1.0)
* s_bottom = bottom in offscreen's coordinates (0.0 - 1.0)
* s_left = left in offscreen's coordinates (0.0 - 1.0)
* s_right = right in offscreen's coordinates (0.0 - 1.0)
*/
if (box_shadow_spec->blur == 0)
shadow_blur_radius = 0;
else
shadow_blur_radius = (5 * (box_shadow_spec->blur / 2.0)) / 2;
shadow_width = state->box_shadow_width + 2 * shadow_blur_radius;
shadow_height = state->box_shadow_height + 2 * shadow_blur_radius;
/* Compute input regions parameters */
s_top = shadow_blur_radius + box_shadow_spec->blur +
MAX (node->border_radius[ST_CORNER_TOPLEFT],
node->border_radius[ST_CORNER_TOPRIGHT]);
s_bottom = shadow_blur_radius + box_shadow_spec->blur +
MAX (node->border_radius[ST_CORNER_BOTTOMLEFT],
node->border_radius[ST_CORNER_BOTTOMRIGHT]);
s_left = shadow_blur_radius + box_shadow_spec->blur +
MAX (node->border_radius[ST_CORNER_TOPLEFT],
node->border_radius[ST_CORNER_BOTTOMLEFT]);
s_right = shadow_blur_radius + box_shadow_spec->blur +
MAX (node->border_radius[ST_CORNER_TOPRIGHT],
node->border_radius[ST_CORNER_BOTTOMRIGHT]);
/* Compute output regions parameters */
xoffset = box->x1 + box_shadow_spec->xoffset - shadow_blur_radius - box_shadow_spec->spread;
yoffset = box->y1 + box_shadow_spec->yoffset - shadow_blur_radius - box_shadow_spec->spread;
width = box->x2 - box->x1 + 2 * shadow_blur_radius;
height = box->y2 - box->y1 + 2 * shadow_blur_radius;
x_spread_factor = (width + 2 * box_shadow_spec->spread) / width;
y_spread_factor = (height + 2 * box_shadow_spec->spread) / height;
width += 2 * box_shadow_spec->spread;
height += 2 * box_shadow_spec->spread;
xend = xoffset + width;
yend = yoffset + height;
top = s_top * y_spread_factor;
bottom = s_bottom * y_spread_factor;
left = s_left * x_spread_factor;
right = s_right * x_spread_factor;
bottom = height - bottom;
right = width - right;
/* Final adjustments */
s_top /= shadow_height;
s_bottom /= shadow_height;
s_left /= shadow_width;
s_right /= shadow_width;
s_bottom = 1.0 - s_bottom;
s_right = 1.0 - s_right;
top += yoffset;
bottom += yoffset;
left += xoffset;
right += xoffset;
/* Setup pipeline */
cogl_color_set_from_4ub (&color,
box_shadow_spec->color.red * paint_opacity / 255,
box_shadow_spec->color.green * paint_opacity / 255,
box_shadow_spec->color.blue * paint_opacity / 255,
box_shadow_spec->color.alpha * paint_opacity / 255);
cogl_color_premultiply (&color);
cogl_material_set_layer_combine_constant (state->box_shadow_material, 0, &color);
cogl_set_source (state->box_shadow_material);
idx = 0;
if (top > 0)
{
if (left > 0)
{
/* Top left corner */
rectangles[idx++] = xoffset;
rectangles[idx++] = yoffset;
rectangles[idx++] = left;
rectangles[idx++] = top;
rectangles[idx++] = 0;
rectangles[idx++] = 0;
rectangles[idx++] = s_left;
rectangles[idx++] = s_top;
}
/* Top middle */
rectangles[idx++] = left;
rectangles[idx++] = yoffset;
rectangles[idx++] = right;
rectangles[idx++] = top;
rectangles[idx++] = s_left;
rectangles[idx++] = 0;
rectangles[idx++] = s_right;
rectangles[idx++] = s_top;
if (right > 0)
{
/* Top right corner */
rectangles[idx++] = right;
rectangles[idx++] = yoffset;
rectangles[idx++] = xend;
rectangles[idx++] = top;
rectangles[idx++] = s_right;
rectangles[idx++] = 0;
rectangles[idx++] = 1;
rectangles[idx++] = s_top;
}
}
if (left > 0)
{
/* Left middle */
rectangles[idx++] = xoffset;
rectangles[idx++] = top;
rectangles[idx++] = left;
rectangles[idx++] = bottom;
rectangles[idx++] = 0;
rectangles[idx++] = s_top;
rectangles[idx++] = s_left;
rectangles[idx++] = s_bottom;
}
/* Center middle */
rectangles[idx++] = left;
rectangles[idx++] = top;
rectangles[idx++] = right;
rectangles[idx++] = bottom;
rectangles[idx++] = s_left;
rectangles[idx++] = s_top;
rectangles[idx++] = s_right;
rectangles[idx++] = s_bottom;
if (right > 0)
{
/* Right middle */
rectangles[idx++] = right;
rectangles[idx++] = top;
rectangles[idx++] = xend;
rectangles[idx++] = bottom;
rectangles[idx++] = s_right;
rectangles[idx++] = s_top;
rectangles[idx++] = 1;
rectangles[idx++] = s_bottom;
}
if (bottom > 0)
{
if (left > 0)
{
/* Bottom left corner */
rectangles[idx++] = xoffset;
rectangles[idx++] = bottom;
rectangles[idx++] = left;
rectangles[idx++] = yend;
rectangles[idx++] = 0;
rectangles[idx++] = s_bottom;
rectangles[idx++] = s_left;
rectangles[idx++] = 1;
}
/* Bottom middle */
rectangles[idx++] = left;
rectangles[idx++] = bottom;
rectangles[idx++] = right;
rectangles[idx++] = yend;
rectangles[idx++] = s_left;
rectangles[idx++] = s_bottom;
rectangles[idx++] = s_right;
rectangles[idx++] = 1;
if (right > 0)
{
/* Bottom right corner */
rectangles[idx++] = right;
rectangles[idx++] = bottom;
rectangles[idx++] = xend;
rectangles[idx++] = yend;
rectangles[idx++] = s_right;
rectangles[idx++] = s_bottom;
rectangles[idx++] = 1;
rectangles[idx++] = 1;
}
}
cogl_rectangles_with_texture_coords (rectangles, idx / 8);
#if 0
/* Visual feedback on shadow's 9-slice and orignal offscreen buffer,
for debug purposes */
cogl_rectangle (xend, yoffset, xend + shadow_width, yoffset + shadow_height);
cogl_set_source_color4ub (0xff, 0x0, 0x0, 0xff);
cogl_rectangle (xoffset, top, xend, top + 1);
cogl_rectangle (xoffset, bottom, xend, bottom + 1);
cogl_rectangle (left, yoffset, left + 1, yend);
cogl_rectangle (right, yoffset, right + 1, yend);
cogl_rectangle (xend, yoffset, xend + shadow_width, yoffset + 1);
cogl_rectangle (xend, yoffset + shadow_height, xend + shadow_width, yoffset + shadow_height + 1);
cogl_rectangle (xend, yoffset, xend + 1, yoffset + shadow_height);
cogl_rectangle (xend + shadow_width, yoffset, xend + shadow_width + 1, yoffset + shadow_height);
s_top *= shadow_height;
s_bottom *= shadow_height;
s_left *= shadow_width;
s_right *= shadow_width;
cogl_rectangle (xend, yoffset + s_top, xend + shadow_width, yoffset + s_top + 1);
cogl_rectangle (xend, yoffset + s_bottom, xend + shadow_width, yoffset + s_bottom + 1);
cogl_rectangle (xend + s_left, yoffset, xend + s_left + 1, yoffset + shadow_height);
cogl_rectangle (xend + s_right, yoffset, xend + s_right + 1, yoffset + shadow_height);
#endif
}
static void
st_theme_node_prerender_shadow (StThemeNode *node, StThemeNodePaintState *state)
{
guint border_radius[4];
int max_borders[4];
int center_radius, corner_id;
CoglHandle buffer, offscreen;
/* Get infos from the node */
if (state->alloc_width < node->box_shadow_min_width ||
state->alloc_height < node->box_shadow_min_height)
st_theme_node_reduce_border_radius (node, state->alloc_width, state->alloc_height, border_radius);
else
for (corner_id = 0; corner_id < 4; corner_id++)
border_radius[corner_id] = node->border_radius[corner_id];
/* Compute maximum borders sizes */
max_borders[ST_SIDE_TOP] = MAX (node->border_radius[ST_CORNER_TOPLEFT],
node->border_radius[ST_CORNER_TOPRIGHT]);
max_borders[ST_SIDE_BOTTOM] = MAX (node->border_radius[ST_CORNER_BOTTOMLEFT],
node->border_radius[ST_CORNER_BOTTOMRIGHT]);
max_borders[ST_SIDE_LEFT] = MAX (node->border_radius[ST_CORNER_TOPLEFT],
node->border_radius[ST_CORNER_BOTTOMLEFT]);
max_borders[ST_SIDE_RIGHT] = MAX (node->border_radius[ST_CORNER_TOPRIGHT],
node->border_radius[ST_CORNER_BOTTOMRIGHT]);
center_radius = (node->box_shadow->blur > 0) ? (2 * node->box_shadow->blur + 1) : 1;
node->box_shadow_min_width = max_borders[ST_SIDE_LEFT] + max_borders[ST_SIDE_RIGHT] + center_radius;
node->box_shadow_min_height = max_borders[ST_SIDE_TOP] + max_borders[ST_SIDE_BOTTOM] + center_radius;
if (state->alloc_width < node->box_shadow_min_width ||
state->alloc_height < node->box_shadow_min_height)
{
state->box_shadow_width = state->alloc_width;
state->box_shadow_height = state->alloc_height;
}
else
{
state->box_shadow_width = node->box_shadow_min_width;
state->box_shadow_height = node->box_shadow_min_height;
}
/* Render offscreen */
buffer = cogl_texture_new_with_size (state->box_shadow_width,
state->box_shadow_height,
COGL_TEXTURE_NO_SLICING,
COGL_PIXEL_FORMAT_ANY);
offscreen = cogl_offscreen_new_to_texture (buffer);
if (offscreen != COGL_INVALID_HANDLE)
{
ClutterActorBox box = { 0, 0, state->box_shadow_width, state->box_shadow_height};
CoglColor clear_color;
cogl_push_framebuffer (offscreen);
cogl_ortho (0, state->box_shadow_width, state->box_shadow_height, 0, 0, 1.0);
cogl_color_set_from_4ub (&clear_color, 0, 0, 0, 0);
cogl_clear (&clear_color, COGL_BUFFER_BIT_COLOR);
st_theme_node_paint_borders (node, state, &box, 0xFF);
cogl_pop_framebuffer ();
cogl_handle_unref (offscreen);
state->box_shadow_material = _st_create_shadow_material (st_theme_node_get_box_shadow (node),
buffer);
}
cogl_handle_unref (buffer);
}
static void static void
st_theme_node_paint_sliced_border_image (StThemeNode *node, st_theme_node_paint_sliced_border_image (StThemeNode *node,
float width, float width,
@ -1945,6 +2309,43 @@ st_theme_node_paint_outline (StThemeNode *node,
cogl_rectangles (rects, 4); cogl_rectangles (rects, 4);
} }
static gboolean
st_theme_node_needs_new_box_shadow_for_size (StThemeNode *node,
StThemeNodePaintState *state,
float width,
float height)
{
if (!node->rendered_once)
return TRUE;
/* The allocation hasn't changed, no need to recompute a new
box-shadow. */
if (state->alloc_width == width ||
state->alloc_height == height)
return FALSE;
/* If there is no shadow, no need to recompute a new box-shadow. */
if (node->box_shadow_min_width == 0 ||
node->box_shadow_min_height == 0)
return FALSE;
/* If the new size is inferior to the box-shadow minimum size (we
already know the size has changed), we need to recompute the
box-shadow. */
if (width < node->box_shadow_min_width ||
height < node->box_shadow_min_height)
return TRUE;
/* Now checking whether the size of the node has crossed the minimum
box-shadow size boundary, from below to above the minimum size .
If that's the case, we need to recompute the box-shadow */
if (state->alloc_width < node->box_shadow_min_width ||
state->alloc_height < node->box_shadow_min_height)
return TRUE;
return FALSE;
}
void void
st_theme_node_paint (StThemeNode *node, st_theme_node_paint (StThemeNode *node,
StThemeNodePaintState *state, StThemeNodePaintState *state,
@ -1964,11 +2365,13 @@ st_theme_node_paint (StThemeNode *node,
if (width <= 0 || height <= 0) if (width <= 0 || height <= 0)
return; return;
if (state->alloc_width != width || state->alloc_height != height)
{
state->node = node; state->node = node;
if (st_theme_node_needs_new_box_shadow_for_size (node, state, width, height))
st_theme_node_render_resources (node, state, width, height); st_theme_node_render_resources (node, state, width, height);
} else
st_theme_node_update_resources (node, state, width, height);
node->rendered_once = TRUE;
/* Rough notes about the relationship of borders and backgrounds in CSS3; /* Rough notes about the relationship of borders and backgrounds in CSS3;
* see http://www.w3.org/TR/css3-background/ for more accurate details. * see http://www.w3.org/TR/css3-background/ for more accurate details.
@ -1997,10 +2400,19 @@ st_theme_node_paint (StThemeNode *node,
*/ */
if (state->box_shadow_material) if (state->box_shadow_material)
{
if (state->alloc_width < node->box_shadow_min_width ||
state->alloc_height < node->box_shadow_min_height)
_st_paint_shadow_with_opacity (node->box_shadow, _st_paint_shadow_with_opacity (node->box_shadow,
state->box_shadow_material, state->box_shadow_material,
&allocation, &allocation,
paint_opacity); paint_opacity);
else
st_theme_node_paint_sliced_shadow (node,
state,
&allocation,
paint_opacity);
}
if (state->prerendered_material != COGL_INVALID_HANDLE || if (state->prerendered_material != COGL_INVALID_HANDLE ||
st_theme_node_load_border_image (node)) st_theme_node_load_border_image (node))
@ -2124,6 +2536,8 @@ st_theme_node_paint_state_copy (StThemeNodePaintState *state,
state->alloc_width = other->alloc_width; state->alloc_width = other->alloc_width;
state->alloc_height = other->alloc_height; state->alloc_height = other->alloc_height;
state->box_shadow_width = other->box_shadow_width;
state->box_shadow_height = other->box_shadow_height;
if (other->box_shadow_material) if (other->box_shadow_material)
state->box_shadow_material = cogl_handle_ref (other->box_shadow_material); state->box_shadow_material = cogl_handle_ref (other->box_shadow_material);

View File

@ -99,6 +99,10 @@ struct _StThemeNode {
guint background_image_shadow_computed : 1; guint background_image_shadow_computed : 1;
guint text_shadow_computed : 1; guint text_shadow_computed : 1;
guint link_type : 2; guint link_type : 2;
guint rendered_once : 1;
int box_shadow_min_width;
int box_shadow_min_height;
CoglHandle border_slices_texture; CoglHandle border_slices_texture;
CoglHandle border_slices_material; CoglHandle border_slices_material;

View File

@ -102,6 +102,9 @@ struct _StThemeNodePaintState {
float alloc_width; float alloc_width;
float alloc_height; float alloc_height;
float box_shadow_width;
float box_shadow_height;
CoglHandle box_shadow_material; CoglHandle box_shadow_material;
CoglHandle prerendered_texture; CoglHandle prerendered_texture;
CoglHandle prerendered_material; CoglHandle prerendered_material;