buffer: optimize memory handling

This commit is contained in:
Wim Taymans 2011-03-29 13:51:25 +02:00
parent a6d2490b10
commit d9becdcd27
2 changed files with 141 additions and 69 deletions

View file

@ -138,20 +138,36 @@ struct _GstMetaItem
GstMetaItem *next; GstMetaItem *next;
GstMeta meta; GstMeta meta;
}; };
#define ITEM_SIZE(info) ((info)->size + sizeof (GstMetaItem))
#define GST_BUFFER_MEMORY(b) (((GstBufferImpl *)(b))->memory) #define GST_BUFFER_MEM_MAX 16
#define GST_BUFFER_META(b) (((GstBufferImpl *)(b))->item)
#define GST_BUFFER_MEM_LEN(b) (((GstBufferImpl *)(b))->len)
#define GST_BUFFER_MEM_ARRAY(b) (((GstBufferImpl *)(b))->mem)
#define GST_BUFFER_MEM_PTR(b,i) (((GstBufferImpl *)(b))->mem[i])
#define GST_BUFFER_META(b) (((GstBufferImpl *)(b))->item)
typedef struct typedef struct
{ {
GstBuffer buffer; GstBuffer buffer;
GPtrArray *memory; guint len;
GstMemory *mem[GST_BUFFER_MEM_MAX];
GstMetaItem *item; GstMetaItem *item;
} GstBufferImpl; } GstBufferImpl;
#define ITEM_SIZE(info) ((info)->size + sizeof (GstMetaItem)) static inline void
_memory_add (GstBuffer * buffer, GstMemory * mem)
{
guint len = GST_BUFFER_MEM_LEN (buffer);
/* FIXME, span buffers when we run out of places */
g_return_if_fail (len < GST_BUFFER_MEM_MAX);
GST_BUFFER_MEM_PTR (buffer, len) = mem;
GST_BUFFER_MEM_LEN (buffer) = len + 1;
}
#if 1
/* buffer alignment in bytes /* buffer alignment in bytes
* an alignment of 8 would be the same as malloc() guarantees * an alignment of 8 would be the same as malloc() guarantees
*/ */
@ -166,6 +182,7 @@ static size_t _gst_buffer_data_alignment = BUFFER_ALIGNMENT;
#error "No buffer alignment configured" #error "No buffer alignment configured"
#endif #endif
#endif /* HAVE_POSIX_MEMALIGN */ #endif /* HAVE_POSIX_MEMALIGN */
#endif
void void
_gst_buffer_initialize (void) _gst_buffer_initialize (void)
@ -249,17 +266,15 @@ gst_buffer_copy_into (GstBuffer * dest, GstBuffer * src,
} }
if (flags & GST_BUFFER_COPY_MEMORY) { if (flags & GST_BUFFER_COPY_MEMORY) {
GPtrArray *sarr = GST_BUFFER_MEMORY (src);
GPtrArray *darr = GST_BUFFER_MEMORY (dest);
GstMemory *mem; GstMemory *mem;
gsize left, len, i, bsize; gsize left, len, i, bsize;
len = sarr->len; len = GST_BUFFER_MEM_LEN (src);
left = size; left = size;
/* copy and subbuffer */ /* copy and subbuffer */
for (i = 0; i < len && left > 0; i++) { for (i = 0; i < len && left > 0; i++) {
mem = g_ptr_array_index (sarr, i); mem = GST_BUFFER_MEM_PTR (src, i);
bsize = gst_memory_get_sizes (mem, NULL); bsize = gst_memory_get_sizes (mem, NULL);
if (bsize <= offset) { if (bsize <= offset) {
@ -275,7 +290,7 @@ gst_buffer_copy_into (GstBuffer * dest, GstBuffer * src,
} else { } else {
mem = gst_memory_ref (mem); mem = gst_memory_ref (mem);
} }
g_ptr_array_add (darr, mem); _memory_add (dest, mem);
left -= tocopy; left -= tocopy;
} }
} }
@ -326,6 +341,8 @@ static void
_gst_buffer_free (GstBuffer * buffer) _gst_buffer_free (GstBuffer * buffer)
{ {
GstMetaItem *walk, *next; GstMetaItem *walk, *next;
guint i, len;
gsize msize;
g_return_if_fail (buffer != NULL); g_return_if_fail (buffer != NULL);
@ -347,10 +364,17 @@ _gst_buffer_free (GstBuffer * buffer)
g_slice_free1 (ITEM_SIZE (info), walk); g_slice_free1 (ITEM_SIZE (info), walk);
} }
/* free our data, unrefs the memory too */ /* get the size, when unreffing the memory, we could also unref the buffer
g_ptr_array_free (GST_BUFFER_MEMORY (buffer), TRUE); * itself */
msize = GST_MINI_OBJECT_SIZE (buffer);
g_slice_free1 (GST_MINI_OBJECT_SIZE (buffer), buffer); /* free our memory */
len = GST_BUFFER_MEM_LEN (buffer);
for (i = 0; i < len; i++)
gst_memory_unref (GST_BUFFER_MEM_PTR (buffer, i));
if (msize)
g_slice_free1 (msize, buffer);
} }
static void static void
@ -365,14 +389,15 @@ gst_buffer_init (GstBufferImpl * buffer, gsize size)
buffer->buffer.mini_object.free = buffer->buffer.mini_object.free =
(GstMiniObjectFreeFunction) _gst_buffer_free; (GstMiniObjectFreeFunction) _gst_buffer_free;
GST_BUFFER (buffer)->pool = NULL;
GST_BUFFER_CAPS (buffer) = NULL;
GST_BUFFER_TIMESTAMP (buffer) = GST_CLOCK_TIME_NONE; GST_BUFFER_TIMESTAMP (buffer) = GST_CLOCK_TIME_NONE;
GST_BUFFER_DURATION (buffer) = GST_CLOCK_TIME_NONE; GST_BUFFER_DURATION (buffer) = GST_CLOCK_TIME_NONE;
GST_BUFFER_OFFSET (buffer) = GST_BUFFER_OFFSET_NONE; GST_BUFFER_OFFSET (buffer) = GST_BUFFER_OFFSET_NONE;
GST_BUFFER_OFFSET_END (buffer) = GST_BUFFER_OFFSET_NONE; GST_BUFFER_OFFSET_END (buffer) = GST_BUFFER_OFFSET_NONE;
/* FIXME, do more efficient with array in the buffer memory itself */ GST_BUFFER_MEM_LEN (buffer) = 0;
GST_BUFFER_MEMORY (buffer) = GST_BUFFER_META (buffer) = NULL;
g_ptr_array_new_with_free_func ((GDestroyNotify) gst_memory_unref);
} }
/** /**
@ -389,7 +414,7 @@ gst_buffer_new (void)
{ {
GstBufferImpl *newbuf; GstBufferImpl *newbuf;
newbuf = g_slice_new0 (GstBufferImpl); newbuf = g_slice_new (GstBufferImpl);
GST_CAT_LOG (GST_CAT_BUFFER, "new %p", newbuf); GST_CAT_LOG (GST_CAT_BUFFER, "new %p", newbuf);
gst_buffer_init (newbuf, sizeof (GstBufferImpl)); gst_buffer_init (newbuf, sizeof (GstBufferImpl));
@ -417,7 +442,12 @@ gst_buffer_new_and_alloc (guint size)
{ {
GstBuffer *newbuf; GstBuffer *newbuf;
GstMemory *mem; GstMemory *mem;
#if 0
guint8 *data;
gsize asize;
#endif
#if 1
if (size > 0) { if (size > 0) {
mem = gst_memory_new_alloc (size, _gst_buffer_data_alignment); mem = gst_memory_new_alloc (size, _gst_buffer_data_alignment);
if (G_UNLIKELY (mem == NULL)) if (G_UNLIKELY (mem == NULL))
@ -429,11 +459,54 @@ gst_buffer_new_and_alloc (guint size)
newbuf = gst_buffer_new (); newbuf = gst_buffer_new ();
if (mem != NULL) if (mem != NULL)
gst_buffer_take_memory (newbuf, mem); _memory_add (newbuf, mem);
GST_CAT_LOG (GST_CAT_BUFFER, "new %p of size %d", newbuf, size); GST_CAT_LOG (GST_CAT_BUFFER, "new %p of size %d", newbuf, size);
return newbuf; return newbuf;
#endif
#if 0
asize = sizeof (GstBufferImpl) + size;
data = g_slice_alloc (asize);
if (G_UNLIKELY (data == NULL))
goto no_memory;
newbuf = GST_BUFFER_CAST (data);
gst_buffer_init ((GstBufferImpl *) data, asize);
if (size > 0) {
mem = gst_memory_new_wrapped (0, data + sizeof (GstBufferImpl), NULL,
size, 0, size);
_memory_add (newbuf, mem);
}
return newbuf;
#endif
#if 0
/* allocate memory and buffer */
asize = sizeof (GstBufferImpl) + size;
mem = gst_memory_new_alloc (asize, 0);
if (G_UNLIKELY (mem == NULL))
goto no_memory;
/* map the data part and init the buffer in it, set the buffer size to 0 so
* that a finalize won't free the buffer */
data = gst_memory_map (mem, &asize, NULL, GST_MAP_WRITE);
gst_buffer_init ((GstBufferImpl *) data, 0);
gst_memory_unmap (mem, data, asize);
/* strip off the buffer */
gst_memory_trim (mem, sizeof (GstBufferImpl), size);
newbuf = GST_BUFFER_CAST (data);
if (size > 0)
_memory_add (newbuf, mem);
#endif
return newbuf;
/* ERRORS */ /* ERRORS */
no_memory: no_memory:
@ -456,7 +529,7 @@ gst_buffer_n_memory (GstBuffer * buffer)
{ {
g_return_val_if_fail (GST_IS_BUFFER (buffer), 0); g_return_val_if_fail (GST_IS_BUFFER (buffer), 0);
return GST_BUFFER_MEMORY (buffer)->len; return GST_BUFFER_MEM_LEN (buffer);
} }
/** /**
@ -474,7 +547,7 @@ gst_buffer_take_memory (GstBuffer * buffer, GstMemory * mem)
g_return_if_fail (gst_buffer_is_writable (buffer)); g_return_if_fail (gst_buffer_is_writable (buffer));
g_return_if_fail (mem != NULL); g_return_if_fail (mem != NULL);
g_ptr_array_add (GST_BUFFER_MEMORY (buffer), mem); _memory_add (buffer, mem);
} }
/** /**
@ -492,13 +565,11 @@ GstMemory *
gst_buffer_peek_memory (GstBuffer * buffer, guint idx) gst_buffer_peek_memory (GstBuffer * buffer, guint idx)
{ {
GstMemory *mem; GstMemory *mem;
GPtrArray *arr;
g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL); g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL);
arr = GST_BUFFER_MEMORY (buffer); g_return_val_if_fail (idx < GST_BUFFER_MEM_LEN (buffer), NULL);
g_return_val_if_fail (idx < arr->len, NULL);
mem = g_ptr_array_index (arr, idx); mem = GST_BUFFER_MEM_PTR (buffer, idx);
return mem; return mem;
} }
@ -516,16 +587,26 @@ gst_buffer_peek_memory (GstBuffer * buffer, guint idx)
void void
gst_buffer_remove_memory_range (GstBuffer * buffer, guint idx, guint length) gst_buffer_remove_memory_range (GstBuffer * buffer, guint idx, guint length)
{ {
GPtrArray *arr; guint len, i, end;
g_return_if_fail (GST_IS_BUFFER (buffer)); g_return_if_fail (GST_IS_BUFFER (buffer));
g_return_if_fail (gst_buffer_is_writable (buffer)); g_return_if_fail (gst_buffer_is_writable (buffer));
arr = GST_BUFFER_MEMORY (buffer);
len = GST_BUFFER_MEM_LEN (buffer);
if (length == -1) { if (length == -1) {
g_return_if_fail (idx < arr->len); g_return_if_fail (idx < len);
length = arr->len - idx; length = len - idx;
} }
g_ptr_array_remove_range (arr, idx, length);
end = idx + length;
for (i = idx; i < end; i++)
gst_memory_unref (GST_BUFFER_MEM_PTR (buffer, i));
if (end != len) {
g_memmove (&GST_BUFFER_MEM_PTR (buffer, idx),
&GST_BUFFER_MEM_PTR (buffer, end), (len - end) * sizeof (gpointer));
}
GST_BUFFER_MEM_LEN (buffer) = len - length;
} }
/** /**
@ -539,17 +620,15 @@ gst_buffer_remove_memory_range (GstBuffer * buffer, guint idx, guint length)
gsize gsize
gst_buffer_get_size (GstBuffer * buffer) gst_buffer_get_size (GstBuffer * buffer)
{ {
GPtrArray *arr;
guint i, size, len; guint i, size, len;
g_return_val_if_fail (GST_IS_BUFFER (buffer), 0); g_return_val_if_fail (GST_IS_BUFFER (buffer), 0);
arr = GST_BUFFER_MEMORY (buffer); len = GST_BUFFER_MEM_LEN (buffer);
len = arr->len;
size = 0; size = 0;
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
size += gst_memory_get_sizes (g_ptr_array_index (arr, i), NULL); size += gst_memory_get_sizes (GST_BUFFER_MEM_PTR (buffer, i), NULL);
} }
return size; return size;
} }
@ -565,7 +644,6 @@ gst_buffer_get_size (GstBuffer * buffer)
void void
gst_buffer_trim (GstBuffer * buffer, gsize offset, gsize size) gst_buffer_trim (GstBuffer * buffer, gsize offset, gsize size)
{ {
GPtrArray *arr;
guint len; guint len;
guint si, di; guint si, di;
gsize bsize; gsize bsize;
@ -576,12 +654,11 @@ gst_buffer_trim (GstBuffer * buffer, gsize offset, gsize size)
g_return_if_fail (gst_buffer_is_writable (buffer)); g_return_if_fail (gst_buffer_is_writable (buffer));
arr = GST_BUFFER_MEMORY (buffer); len = GST_BUFFER_MEM_LEN (buffer);
len = arr->len;
/* copy and trim */ /* copy and trim */
for (di = si = 0; si < len && size > 0; si++) { for (di = si = 0; si < len && size > 0; si++) {
mem = g_ptr_array_index (arr, si); mem = GST_BUFFER_MEM_PTR (buffer, si);
bsize = gst_memory_get_sizes (mem, NULL); bsize = gst_memory_get_sizes (mem, NULL);
if (bsize <= offset) { if (bsize <= offset) {
@ -603,11 +680,11 @@ gst_buffer_trim (GstBuffer * buffer, gsize offset, gsize size)
mem = tmp; mem = tmp;
} }
} }
g_ptr_array_index (arr, di++) = mem; GST_BUFFER_MEM_PTR (buffer, di++) = mem;
size -= tocopy; size -= tocopy;
} }
} }
g_ptr_array_set_size (arr, di); GST_BUFFER_MEM_LEN (buffer) = di;
} }
/** /**
@ -638,14 +715,12 @@ gpointer
gst_buffer_map (GstBuffer * buffer, gsize * size, gsize * maxsize, gst_buffer_map (GstBuffer * buffer, gsize * size, gsize * maxsize,
GstMapFlags flags) GstMapFlags flags)
{ {
GPtrArray *arr;
guint len; guint len;
gpointer data; gpointer data;
g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL); g_return_val_if_fail (GST_IS_BUFFER (buffer), NULL);
arr = GST_BUFFER_MEMORY (buffer); len = GST_BUFFER_MEM_LEN (buffer);
len = arr->len;
if (G_UNLIKELY ((flags & GST_MAP_WRITE) && !gst_buffer_is_writable (buffer))) if (G_UNLIKELY ((flags & GST_MAP_WRITE) && !gst_buffer_is_writable (buffer)))
goto not_writable; goto not_writable;
@ -653,7 +728,7 @@ gst_buffer_map (GstBuffer * buffer, gsize * size, gsize * maxsize,
if (G_LIKELY (len == 1)) { if (G_LIKELY (len == 1)) {
GstMemory *mem; GstMemory *mem;
mem = g_ptr_array_index (arr, 0); mem = GST_BUFFER_MEM_PTR (buffer, 0);
if (flags & GST_MAP_WRITE) { if (flags & GST_MAP_WRITE) {
if (G_UNLIKELY (!GST_MEMORY_IS_WRITABLE (mem))) { if (G_UNLIKELY (!GST_MEMORY_IS_WRITABLE (mem))) {
@ -661,7 +736,7 @@ gst_buffer_map (GstBuffer * buffer, gsize * size, gsize * maxsize,
/* replace with a writable copy */ /* replace with a writable copy */
copy = gst_memory_copy (mem, 0, gst_memory_get_sizes (mem, NULL)); copy = gst_memory_copy (mem, 0, gst_memory_get_sizes (mem, NULL));
g_ptr_array_index (arr, 0) = copy; GST_BUFFER_MEM_PTR (buffer, 0) = copy;
gst_memory_unref (mem); gst_memory_unref (mem);
mem = copy; mem = copy;
} }
@ -698,17 +773,15 @@ not_writable:
gboolean gboolean
gst_buffer_unmap (GstBuffer * buffer, gpointer data, gsize size) gst_buffer_unmap (GstBuffer * buffer, gpointer data, gsize size)
{ {
GPtrArray *arr;
gboolean result; gboolean result;
guint len; guint len;
g_return_val_if_fail (GST_IS_BUFFER (buffer), FALSE); g_return_val_if_fail (GST_IS_BUFFER (buffer), FALSE);
arr = GST_BUFFER_MEMORY (buffer); len = GST_BUFFER_MEM_LEN (buffer);
len = arr->len;
if (G_LIKELY (len == 1)) { if (G_LIKELY (len == 1)) {
GstMemory *mem = g_ptr_array_index (arr, 0); GstMemory *mem = GST_BUFFER_MEM_PTR (buffer, 0);
result = gst_memory_unmap (mem, data, size); result = gst_memory_unmap (mem, data, size);
} else { } else {
@ -731,7 +804,6 @@ void
gst_buffer_fill (GstBuffer * buffer, gsize offset, gconstpointer src, gst_buffer_fill (GstBuffer * buffer, gsize offset, gconstpointer src,
gsize size) gsize size)
{ {
GPtrArray *arr;
gsize i, len; gsize i, len;
const guint8 *ptr = src; const guint8 *ptr = src;
@ -739,15 +811,14 @@ gst_buffer_fill (GstBuffer * buffer, gsize offset, gconstpointer src,
g_return_if_fail (gst_buffer_is_writable (buffer)); g_return_if_fail (gst_buffer_is_writable (buffer));
g_return_if_fail (src != NULL); g_return_if_fail (src != NULL);
arr = GST_BUFFER_MEMORY (buffer); len = GST_BUFFER_MEM_LEN (buffer);
len = arr->len;
for (i = 0; i < len && size > 0; i++) { for (i = 0; i < len && size > 0; i++) {
guint8 *data; guint8 *data;
gsize ssize, tocopy; gsize ssize, tocopy;
GstMemory *mem; GstMemory *mem;
mem = g_ptr_array_index (arr, i); mem = GST_BUFFER_MEM_PTR (buffer, i);
data = gst_memory_map (mem, &ssize, NULL, GST_MAP_WRITE); data = gst_memory_map (mem, &ssize, NULL, GST_MAP_WRITE);
if (ssize > offset) { if (ssize > offset) {
@ -777,22 +848,20 @@ gst_buffer_fill (GstBuffer * buffer, gsize offset, gconstpointer src,
void void
gst_buffer_extract (GstBuffer * buffer, gsize offset, gpointer dest, gsize size) gst_buffer_extract (GstBuffer * buffer, gsize offset, gpointer dest, gsize size)
{ {
GPtrArray *arr;
gsize i, len; gsize i, len;
guint8 *ptr = dest; guint8 *ptr = dest;
g_return_if_fail (GST_IS_BUFFER (buffer)); g_return_if_fail (GST_IS_BUFFER (buffer));
g_return_if_fail (dest != NULL); g_return_if_fail (dest != NULL);
arr = GST_BUFFER_MEMORY (buffer); len = GST_BUFFER_MEM_LEN (buffer);
len = arr->len;
for (i = 0; i < len && size > 0; i++) { for (i = 0; i < len && size > 0; i++) {
guint8 *data; guint8 *data;
gsize ssize, tocopy; gsize ssize, tocopy;
GstMemory *mem; GstMemory *mem;
mem = g_ptr_array_index (arr, i); mem = GST_BUFFER_MEM_PTR (buffer, i);
data = gst_memory_map (mem, &ssize, NULL, GST_MAP_READ); data = gst_memory_map (mem, &ssize, NULL, GST_MAP_READ);
if (ssize > offset) { if (ssize > offset) {
@ -925,18 +994,20 @@ gst_buffer_create_sub (GstBuffer * buffer, gsize offset, gsize size)
gboolean gboolean
gst_buffer_is_span_fast (GstBuffer * buf1, GstBuffer * buf2) gst_buffer_is_span_fast (GstBuffer * buf1, GstBuffer * buf2)
{ {
GPtrArray *arr1, *arr2; GstMemory **arr1, **arr2;
gsize len1, len2;
g_return_val_if_fail (GST_IS_BUFFER (buf1), FALSE); g_return_val_if_fail (GST_IS_BUFFER (buf1), FALSE);
g_return_val_if_fail (GST_IS_BUFFER (buf2), FALSE); g_return_val_if_fail (GST_IS_BUFFER (buf2), FALSE);
g_return_val_if_fail (buf1->mini_object.refcount > 0, FALSE); g_return_val_if_fail (buf1->mini_object.refcount > 0, FALSE);
g_return_val_if_fail (buf2->mini_object.refcount > 0, FALSE); g_return_val_if_fail (buf2->mini_object.refcount > 0, FALSE);
arr1 = GST_BUFFER_MEMORY (buf1); arr1 = GST_BUFFER_MEM_ARRAY (buf1);
arr2 = GST_BUFFER_MEMORY (buf2); len1 = GST_BUFFER_MEM_LEN (buf1);
arr2 = GST_BUFFER_MEM_ARRAY (buf2);
len2 = GST_BUFFER_MEM_LEN (buf2);
return gst_memory_is_span ((GstMemory **) arr1->pdata, arr1->len, return gst_memory_is_span (arr1, len1, arr2, len2, NULL, NULL);
(GstMemory **) arr2->pdata, arr2->len, NULL, NULL);
} }
/** /**
@ -966,7 +1037,8 @@ GstBuffer *
gst_buffer_span (GstBuffer * buf1, gsize offset, GstBuffer * buf2, gsize len) gst_buffer_span (GstBuffer * buf1, gsize offset, GstBuffer * buf2, gsize len)
{ {
GstBuffer *newbuf; GstBuffer *newbuf;
GPtrArray *arr1, *arr2; GstMemory **arr1, **arr2;
gsize len1, len2;
GstMemory *mem; GstMemory *mem;
g_return_val_if_fail (GST_IS_BUFFER (buf1), NULL); g_return_val_if_fail (GST_IS_BUFFER (buf1), NULL);
@ -979,12 +1051,13 @@ gst_buffer_span (GstBuffer * buf1, gsize offset, GstBuffer * buf2, gsize len)
newbuf = gst_buffer_new (); newbuf = gst_buffer_new ();
arr1 = GST_BUFFER_MEMORY (buf1); arr1 = GST_BUFFER_MEM_ARRAY (buf1);
arr2 = GST_BUFFER_MEMORY (buf2); len1 = GST_BUFFER_MEM_LEN (buf1);
arr2 = GST_BUFFER_MEM_ARRAY (buf2);
len2 = GST_BUFFER_MEM_LEN (buf2);
mem = gst_memory_span ((GstMemory **) arr1->pdata, arr1->len, offset, mem = gst_memory_span (arr1, len1, offset, arr2, len2, len);
(GstMemory **) arr2->pdata, arr2->len, len); _memory_add (newbuf, mem);
gst_buffer_take_memory (newbuf, mem);
#if 0 #if 0
/* if the offset is 0, the new buffer has the same timestamp as buf1 */ /* if the offset is 0, the new buffer has the same timestamp as buf1 */

View file

@ -265,14 +265,13 @@ struct _GstBuffer {
/*< public >*/ /* with COW */ /*< public >*/ /* with COW */
GstBufferPool *pool; GstBufferPool *pool;
/* the media type of this buffer */
GstCaps *caps;
/* timestamp */ /* timestamp */
GstClockTime timestamp; GstClockTime timestamp;
GstClockTime duration; GstClockTime duration;
/* the media type of this buffer */
GstCaps *caps;
/* media specific offset */ /* media specific offset */
guint64 offset; guint64 offset;
guint64 offset_end; guint64 offset_end;