636cef1bd6
This adds a 'dump-atlas-image' debug category. When enabled, CoglAtlas will use Cairo to create a png which visualizes the leaf rectangles of the atlas.
597 lines
17 KiB
C
597 lines
17 KiB
C
/*
|
|
* Cogl
|
|
*
|
|
* An object oriented GL/GLES Abstraction/Utility Layer
|
|
*
|
|
* Copyright (C) 2009 Intel Corporation.
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 59 Temple Place - Suite 330,
|
|
* Boston, MA 02111-1307, USA.
|
|
*
|
|
* Authors:
|
|
* Neil Roberts <neil@linux.intel.com>
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <glib.h>
|
|
|
|
#include "cogl-atlas.h"
|
|
#include "cogl-debug.h"
|
|
|
|
/* Implements a data structure which keeps track of unused
|
|
sub-rectangles within a larger rectangle using a binary tree
|
|
structure. The algorithm for this is based on the description here:
|
|
|
|
http://www.blackpawn.com/texts/lightmaps/default.html
|
|
*/
|
|
|
|
#ifdef COGL_ENABLE_DEBUG
|
|
|
|
/* The cairo header is only used for debugging to generate an image of
|
|
the atlas */
|
|
#include <cairo.h>
|
|
|
|
static void cogl_atlas_dump_image (CoglAtlas *atlas);
|
|
|
|
#endif /* COGL_ENABLE_DEBUG */
|
|
|
|
typedef struct _CoglAtlasNode CoglAtlasNode;
|
|
typedef struct _CoglAtlasStackEntry CoglAtlasStackEntry;
|
|
|
|
typedef void (* CoglAtlasInternalForeachCb) (CoglAtlasNode *node,
|
|
gpointer data);
|
|
|
|
typedef enum
|
|
{
|
|
COGL_ATLAS_BRANCH,
|
|
COGL_ATLAS_FILLED_LEAF,
|
|
COGL_ATLAS_EMPTY_LEAF
|
|
} CoglAtlasNodeType;
|
|
|
|
struct _CoglAtlas
|
|
{
|
|
CoglAtlasNode *root;
|
|
|
|
guint space_remaining;
|
|
guint n_rectangles;
|
|
|
|
GDestroyNotify value_destroy_func;
|
|
};
|
|
|
|
struct _CoglAtlasNode
|
|
{
|
|
CoglAtlasNodeType type;
|
|
|
|
CoglAtlasRectangle rectangle;
|
|
|
|
CoglAtlasNode *parent;
|
|
|
|
union
|
|
{
|
|
/* Fields used when this is a branch */
|
|
struct
|
|
{
|
|
CoglAtlasNode *left;
|
|
CoglAtlasNode *right;
|
|
} branch;
|
|
|
|
/* Field used when this is a filled leaf */
|
|
gpointer data;
|
|
} d;
|
|
};
|
|
|
|
struct _CoglAtlasStackEntry
|
|
{
|
|
/* The node to search */
|
|
CoglAtlasNode *node;
|
|
/* Index of next branch of this node to explore. Basically either 0
|
|
to go left or 1 to go right */
|
|
gboolean next_index;
|
|
/* Next entry in the stack */
|
|
CoglAtlasStackEntry *next;
|
|
};
|
|
|
|
static CoglAtlasNode *
|
|
cogl_atlas_node_new (void)
|
|
{
|
|
return g_slice_new (CoglAtlasNode);
|
|
}
|
|
|
|
static void
|
|
cogl_atlas_node_free (CoglAtlasNode *node)
|
|
{
|
|
g_slice_free (CoglAtlasNode, node);
|
|
}
|
|
|
|
CoglAtlas *
|
|
cogl_atlas_new (guint width, guint height,
|
|
GDestroyNotify value_destroy_func)
|
|
{
|
|
CoglAtlas *atlas = g_new (CoglAtlas, 1);
|
|
CoglAtlasNode *root = cogl_atlas_node_new ();
|
|
|
|
root->type = COGL_ATLAS_EMPTY_LEAF;
|
|
root->parent = NULL;
|
|
root->rectangle.x = 0;
|
|
root->rectangle.y = 0;
|
|
root->rectangle.width = width;
|
|
root->rectangle.height = height;
|
|
|
|
atlas->root = root;
|
|
atlas->space_remaining = width * height;
|
|
atlas->n_rectangles = 0;
|
|
atlas->value_destroy_func = value_destroy_func;
|
|
|
|
return atlas;
|
|
}
|
|
|
|
static CoglAtlasStackEntry *
|
|
cogl_atlas_stack_push (CoglAtlasStackEntry *stack,
|
|
CoglAtlasNode *node,
|
|
gboolean next_index)
|
|
{
|
|
CoglAtlasStackEntry *new_entry = g_slice_new (CoglAtlasStackEntry);
|
|
|
|
new_entry->node = node;
|
|
new_entry->next_index = next_index;
|
|
new_entry->next = stack;
|
|
|
|
return new_entry;
|
|
}
|
|
|
|
static CoglAtlasStackEntry *
|
|
cogl_atlas_stack_pop (CoglAtlasStackEntry *stack)
|
|
{
|
|
CoglAtlasStackEntry *next = stack->next;
|
|
|
|
g_slice_free (CoglAtlasStackEntry, stack);
|
|
|
|
return next;
|
|
}
|
|
|
|
static CoglAtlasNode *
|
|
cogl_atlas_node_split_horizontally (CoglAtlasNode *node,
|
|
guint left_width)
|
|
{
|
|
/* Splits the node horizontally (according to emacs' definition, not
|
|
vim) by converting it to a branch and adding two new leaf
|
|
nodes. The leftmost branch will have the width left_width and
|
|
will be returned. If the node is already just the right size it
|
|
won't do anything */
|
|
|
|
CoglAtlasNode *left_node, *right_node;
|
|
|
|
if (node->rectangle.width == left_width)
|
|
return node;
|
|
|
|
left_node = cogl_atlas_node_new ();
|
|
left_node->type = COGL_ATLAS_EMPTY_LEAF;
|
|
left_node->parent = node;
|
|
left_node->rectangle.x = node->rectangle.x;
|
|
left_node->rectangle.y = node->rectangle.y;
|
|
left_node->rectangle.width = left_width;
|
|
left_node->rectangle.height = node->rectangle.height;
|
|
node->d.branch.left = left_node;
|
|
|
|
right_node = cogl_atlas_node_new ();
|
|
right_node->type = COGL_ATLAS_EMPTY_LEAF;
|
|
right_node->parent = node;
|
|
right_node->rectangle.x = node->rectangle.x + left_width;
|
|
right_node->rectangle.y = node->rectangle.y;
|
|
right_node->rectangle.width = node->rectangle.width - left_width;
|
|
right_node->rectangle.height = node->rectangle.height;
|
|
node->d.branch.right = right_node;
|
|
|
|
node->type = COGL_ATLAS_BRANCH;
|
|
|
|
return left_node;
|
|
}
|
|
|
|
static CoglAtlasNode *
|
|
cogl_atlas_node_split_vertically (CoglAtlasNode *node,
|
|
guint top_height)
|
|
{
|
|
/* Splits the node vertically (according to emacs' definition, not
|
|
vim) by converting it to a branch and adding two new leaf
|
|
nodes. The topmost branch will have the height top_height and
|
|
will be returned. If the node is already just the right size it
|
|
won't do anything */
|
|
|
|
CoglAtlasNode *top_node, *bottom_node;
|
|
|
|
if (node->rectangle.height == top_height)
|
|
return node;
|
|
|
|
top_node = cogl_atlas_node_new ();
|
|
top_node->type = COGL_ATLAS_EMPTY_LEAF;
|
|
top_node->parent = node;
|
|
top_node->rectangle.x = node->rectangle.x;
|
|
top_node->rectangle.y = node->rectangle.y;
|
|
top_node->rectangle.width = node->rectangle.width;
|
|
top_node->rectangle.height = top_height;
|
|
node->d.branch.left = top_node;
|
|
|
|
bottom_node = cogl_atlas_node_new ();
|
|
bottom_node->type = COGL_ATLAS_EMPTY_LEAF;
|
|
bottom_node->parent = node;
|
|
bottom_node->rectangle.x = node->rectangle.x;
|
|
bottom_node->rectangle.y = node->rectangle.y + top_height;
|
|
bottom_node->rectangle.width = node->rectangle.width;
|
|
bottom_node->rectangle.height = node->rectangle.height - top_height;
|
|
node->d.branch.right = bottom_node;
|
|
|
|
node->type = COGL_ATLAS_BRANCH;
|
|
|
|
return top_node;
|
|
}
|
|
|
|
gboolean
|
|
cogl_atlas_add_rectangle (CoglAtlas *atlas,
|
|
guint width, guint height,
|
|
gpointer data,
|
|
CoglAtlasRectangle *rectangle)
|
|
{
|
|
/* Stack of nodes to search in */
|
|
CoglAtlasStackEntry *node_stack;
|
|
CoglAtlasNode *found_node = NULL;
|
|
|
|
/* Zero-sized rectangles break the algorithm for removing rectangles
|
|
so we'll disallow them */
|
|
g_return_val_if_fail (width > 0 && height > 0, FALSE);
|
|
|
|
/* Start with the root node */
|
|
node_stack = cogl_atlas_stack_push (NULL, atlas->root, FALSE);
|
|
|
|
/* Depth-first search for an empty node that is big enough */
|
|
while (node_stack)
|
|
{
|
|
/* Pop an entry off the stack */
|
|
CoglAtlasNode *node = node_stack->node;
|
|
int next_index = node_stack->next_index;
|
|
node_stack = cogl_atlas_stack_pop (node_stack);
|
|
|
|
/* Regardless of the type of the node, there's no point
|
|
descending any further if the new rectangle won't fit within
|
|
it */
|
|
if (node->rectangle.width >= width &&
|
|
node->rectangle.height >= height)
|
|
{
|
|
if (node->type == COGL_ATLAS_EMPTY_LEAF)
|
|
{
|
|
/* We've found a node we can use */
|
|
found_node = node;
|
|
break;
|
|
}
|
|
else if (node->type == COGL_ATLAS_BRANCH)
|
|
{
|
|
if (next_index)
|
|
/* Try the right branch */
|
|
node_stack = cogl_atlas_stack_push (node_stack,
|
|
node->d.branch.right,
|
|
0);
|
|
else
|
|
{
|
|
/* Make sure we remember to try the right branch once
|
|
we've finished descending the left branch */
|
|
node_stack = cogl_atlas_stack_push (node_stack,
|
|
node,
|
|
1);
|
|
/* Try the left branch */
|
|
node_stack = cogl_atlas_stack_push (node_stack,
|
|
node->d.branch.left,
|
|
0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Free the stack */
|
|
while (node_stack)
|
|
node_stack = cogl_atlas_stack_pop (node_stack);
|
|
|
|
if (found_node)
|
|
{
|
|
/* Split according to whichever axis will leave us with the
|
|
largest space */
|
|
if (found_node->rectangle.width - width >
|
|
found_node->rectangle.height - height)
|
|
{
|
|
found_node = cogl_atlas_node_split_horizontally (found_node, width);
|
|
found_node = cogl_atlas_node_split_vertically (found_node, height);
|
|
}
|
|
else
|
|
{
|
|
found_node = cogl_atlas_node_split_vertically (found_node, height);
|
|
found_node = cogl_atlas_node_split_horizontally (found_node, width);
|
|
}
|
|
|
|
found_node->type = COGL_ATLAS_FILLED_LEAF;
|
|
found_node->d.data = data;
|
|
if (rectangle)
|
|
*rectangle = found_node->rectangle;
|
|
|
|
/* Record how much empty space is remaining after this rectangle
|
|
is added */
|
|
g_assert (width * height <= atlas->space_remaining);
|
|
atlas->space_remaining -= width * height;
|
|
atlas->n_rectangles++;
|
|
|
|
#ifdef COGL_ENABLE_DEBUG
|
|
if (G_UNLIKELY (cogl_debug_flags & COGL_DEBUG_DUMP_ATLAS_IMAGE))
|
|
cogl_atlas_dump_image (atlas);
|
|
#endif
|
|
|
|
return TRUE;
|
|
}
|
|
else
|
|
return FALSE;
|
|
}
|
|
|
|
void
|
|
cogl_atlas_remove_rectangle (CoglAtlas *atlas,
|
|
const CoglAtlasRectangle *rectangle)
|
|
{
|
|
CoglAtlasNode *node = atlas->root;
|
|
|
|
/* We can do a binary-chop down the search tree to find the rectangle */
|
|
while (node->type == COGL_ATLAS_BRANCH)
|
|
{
|
|
CoglAtlasNode *left_node = node->d.branch.left;
|
|
|
|
/* If and only if the rectangle is in the left node then the x,y
|
|
position of the rectangle will be within the node's
|
|
rectangle */
|
|
if (rectangle->x < left_node->rectangle.x + left_node->rectangle.width &&
|
|
rectangle->y < left_node->rectangle.y + left_node->rectangle.height)
|
|
/* Go left */
|
|
node = left_node;
|
|
else
|
|
/* Go right */
|
|
node = node->d.branch.right;
|
|
}
|
|
|
|
/* Make sure we found the right node */
|
|
if (node->type != COGL_ATLAS_FILLED_LEAF ||
|
|
node->rectangle.x != rectangle->x ||
|
|
node->rectangle.y != rectangle->y ||
|
|
node->rectangle.width != rectangle->width ||
|
|
node->rectangle.height != rectangle->height)
|
|
/* This should only happen if someone tried to remove a rectangle
|
|
that was not in the atlas so something has gone wrong */
|
|
g_return_if_reached ();
|
|
else
|
|
{
|
|
/* Convert the node back to an empty node */
|
|
if (atlas->value_destroy_func)
|
|
atlas->value_destroy_func (node->d.data);
|
|
node->type = COGL_ATLAS_EMPTY_LEAF;
|
|
|
|
/* Walk back up the tree combining branch nodes that have two
|
|
empty leaves back into a single empty leaf */
|
|
for (node = node->parent; node; node = node->parent)
|
|
{
|
|
/* This node is a parent so it should always be a branch */
|
|
g_assert (node->type == COGL_ATLAS_BRANCH);
|
|
|
|
if (node->d.branch.left->type == COGL_ATLAS_EMPTY_LEAF &&
|
|
node->d.branch.right->type == COGL_ATLAS_EMPTY_LEAF)
|
|
{
|
|
cogl_atlas_node_free (node->d.branch.left);
|
|
cogl_atlas_node_free (node->d.branch.right);
|
|
node->type = COGL_ATLAS_EMPTY_LEAF;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
|
|
/* There is now more free space and one less rectangle */
|
|
atlas->space_remaining += rectangle->width * rectangle->height;
|
|
g_assert (atlas->n_rectangles > 0);
|
|
atlas->n_rectangles--;
|
|
}
|
|
|
|
#ifdef COGL_ENABLE_DEBUG
|
|
if (G_UNLIKELY (cogl_debug_flags & COGL_DEBUG_DUMP_ATLAS_IMAGE))
|
|
cogl_atlas_dump_image (atlas);
|
|
#endif
|
|
}
|
|
|
|
guint
|
|
cogl_atlas_get_width (CoglAtlas *atlas)
|
|
{
|
|
return atlas->root->rectangle.width;
|
|
}
|
|
|
|
guint
|
|
cogl_atlas_get_height (CoglAtlas *atlas)
|
|
{
|
|
return atlas->root->rectangle.height;
|
|
}
|
|
|
|
guint
|
|
cogl_atlas_get_remaining_space (CoglAtlas *atlas)
|
|
{
|
|
return atlas->space_remaining;
|
|
}
|
|
|
|
guint
|
|
cogl_atlas_get_n_rectangles (CoglAtlas *atlas)
|
|
{
|
|
return atlas->n_rectangles;
|
|
}
|
|
|
|
static void
|
|
cogl_atlas_internal_foreach (CoglAtlas *atlas,
|
|
CoglAtlasInternalForeachCb callback,
|
|
gpointer data)
|
|
{
|
|
/* Stack of nodes to search in */
|
|
CoglAtlasStackEntry *node_stack;
|
|
|
|
/* Start with the root node */
|
|
node_stack = cogl_atlas_stack_push (NULL, atlas->root, 0);
|
|
|
|
/* Iterate all nodes depth-first */
|
|
while (node_stack)
|
|
{
|
|
CoglAtlasNode *node = node_stack->node;
|
|
|
|
switch (node->type)
|
|
{
|
|
case COGL_ATLAS_BRANCH:
|
|
if (node_stack->next_index == 0)
|
|
{
|
|
/* Next time we come back to this node, go to the right */
|
|
node_stack->next_index = 1;
|
|
|
|
/* Explore the left branch next */
|
|
node_stack = cogl_atlas_stack_push (node_stack,
|
|
node->d.branch.left,
|
|
0);
|
|
}
|
|
else if (node_stack->next_index == 1)
|
|
{
|
|
/* Next time we come back to this node, stop processing it */
|
|
node_stack->next_index = 2;
|
|
|
|
/* Explore the right branch next */
|
|
node_stack = cogl_atlas_stack_push (node_stack,
|
|
node->d.branch.right,
|
|
0);
|
|
}
|
|
else
|
|
{
|
|
/* We're finished with this node so we can call the callback */
|
|
callback (node, data);
|
|
node_stack = cogl_atlas_stack_pop (node_stack);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
/* Some sort of leaf node, just call the callback */
|
|
callback (node, data);
|
|
node_stack = cogl_atlas_stack_pop (node_stack);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/* The stack should now be empty */
|
|
g_assert (node_stack == NULL);
|
|
}
|
|
|
|
typedef struct _CoglAtlasForeachClosure
|
|
{
|
|
CoglAtlasCallback callback;
|
|
gpointer data;
|
|
} CoglAtlasForeachClosure;
|
|
|
|
static void
|
|
cogl_atlas_foreach_cb (CoglAtlasNode *node, gpointer data)
|
|
{
|
|
CoglAtlasForeachClosure *closure = data;
|
|
|
|
if (node->type == COGL_ATLAS_FILLED_LEAF)
|
|
closure->callback (&node->rectangle, node->d.data, closure->data);
|
|
}
|
|
|
|
void
|
|
cogl_atlas_foreach (CoglAtlas *atlas,
|
|
CoglAtlasCallback callback,
|
|
gpointer data)
|
|
{
|
|
CoglAtlasForeachClosure closure;
|
|
|
|
closure.callback = callback;
|
|
closure.data = data;
|
|
|
|
cogl_atlas_internal_foreach (atlas, cogl_atlas_foreach_cb, &closure);
|
|
}
|
|
|
|
static void
|
|
cogl_atlas_free_cb (CoglAtlasNode *node, gpointer data)
|
|
{
|
|
CoglAtlas *atlas = data;
|
|
|
|
if (node->type == COGL_ATLAS_FILLED_LEAF && atlas->value_destroy_func)
|
|
atlas->value_destroy_func (node->d.data);
|
|
|
|
cogl_atlas_node_free (node);
|
|
}
|
|
|
|
void
|
|
cogl_atlas_free (CoglAtlas *atlas)
|
|
{
|
|
cogl_atlas_internal_foreach (atlas, cogl_atlas_free_cb, atlas);
|
|
g_free (atlas);
|
|
}
|
|
|
|
#ifdef COGL_ENABLE_DEBUG
|
|
|
|
static void
|
|
cogl_atlas_dump_image_cb (CoglAtlasNode *node, gpointer data)
|
|
{
|
|
cairo_t *cr = data;
|
|
|
|
if (node->type == COGL_ATLAS_FILLED_LEAF ||
|
|
node->type == COGL_ATLAS_EMPTY_LEAF)
|
|
{
|
|
/* Fill the rectangle using a different colour depending on
|
|
whether the rectangle is used */
|
|
if (node->type == COGL_ATLAS_FILLED_LEAF)
|
|
cairo_set_source_rgb (cr, 0.0, 0.0, 1.0);
|
|
else
|
|
cairo_set_source_rgb (cr, 0.0, 0.0, 0.0);
|
|
|
|
cairo_rectangle (cr,
|
|
node->rectangle.x,
|
|
node->rectangle.y,
|
|
node->rectangle.width,
|
|
node->rectangle.height);
|
|
|
|
cairo_fill_preserve (cr);
|
|
|
|
/* Draw a white outline around the rectangle */
|
|
cairo_set_source_rgb (cr, 1.0, 1.0, 1.0);
|
|
cairo_stroke (cr);
|
|
}
|
|
}
|
|
|
|
static void
|
|
cogl_atlas_dump_image (CoglAtlas *atlas)
|
|
{
|
|
/* This dumps a png to help visualize the atlas. Each leaf rectangle
|
|
is drawn with a white outline. Unused leaves are filled in black
|
|
and used leaves are blue */
|
|
|
|
cairo_surface_t *surface =
|
|
cairo_image_surface_create (CAIRO_FORMAT_RGB24,
|
|
cogl_atlas_get_width (atlas),
|
|
cogl_atlas_get_height (atlas));
|
|
cairo_t *cr = cairo_create (surface);
|
|
|
|
cogl_atlas_internal_foreach (atlas, cogl_atlas_dump_image_cb, cr);
|
|
|
|
cairo_destroy (cr);
|
|
|
|
cairo_surface_write_to_png (surface, "cogl-atlas-dump.png");
|
|
|
|
cairo_surface_destroy (surface);
|
|
}
|
|
|
|
#endif /* COGL_ENABLE_DEBUG */
|