Xavier Claessens 096bd3c4a2 gstbuffer: Add parent meta when a copy shares memory with parent
When copying a buffer, for example with gst_buffer_make_writable(), the
new buffer might reference the same GstMemory as the src buffer,
making those memories not writable. If the src buffer gets disposed
first it should return to its buffer pool, but since some of its
memories are not writable it gets discarded and new buffer/memory gets
allocated.

Solves this by making the new buffer keep a reference to the src buffer,
that ensures that by the time the src buffer gets disposed no other
buffer are referencing its memories and it can thus return safely to its
pool.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/4176>
2023-03-16 00:03:11 +00:00

473 lines
14 KiB
C

/* GStreamer
* Copyright (C) 2014 Stefan Sauer <ensonic@users.sf.net>
*
* gstbufferpool.c: Unit test for GstBufferPool
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, write to the
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <gst/check/gstcheck.h>
static GstBufferPool *
create_pool (guint size, guint min_buf, guint max_buf)
{
GstBufferPool *pool = gst_buffer_pool_new ();
GstStructure *conf = gst_buffer_pool_get_config (pool);
GstCaps *caps = gst_caps_new_empty_simple ("test/data");
gst_buffer_pool_config_set_params (conf, caps, size, min_buf, max_buf);
gst_buffer_pool_set_config (pool, conf);
gst_caps_unref (caps);
return pool;
}
static void
buffer_destroy_notify (gpointer ptr)
{
gint *counter = ptr;
GST_DEBUG ("buffer destroyed");
*counter += 1;
}
/* Track when a buffer is destroyed. The counter will be increased if the
* buffer is finalized (but not if it was re-surrected in dispose and put
* back into the buffer pool. */
static void
buffer_track_destroy (GstBuffer * buf, gint * counter)
{
gst_mini_object_set_qdata (GST_MINI_OBJECT (buf),
g_quark_from_static_string ("TestTracker"),
counter, buffer_destroy_notify);
}
GST_START_TEST (test_new_buffer_from_empty_pool)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstBuffer *buf = NULL;
gst_buffer_pool_set_active (pool, TRUE);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
fail_if (buf == NULL, "acquiring buffer returned NULL");
gst_buffer_unref (buf);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
}
GST_END_TEST;
GST_START_TEST (test_buffer_is_recycled)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstBuffer *buf = NULL, *prev;
gint dcount = 0;
gst_buffer_pool_set_active (pool, TRUE);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
prev = buf;
buffer_track_destroy (buf, &dcount);
gst_buffer_unref (buf);
/* buffer should not have been freed, but have been recycled */
fail_unless (dcount == 0);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
fail_unless (buf == prev, "got a fresh buffer instead of previous");
gst_buffer_unref (buf);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
/* buffer should now be gone */
fail_unless (dcount == 1);
}
GST_END_TEST;
GST_START_TEST (test_buffer_out_of_order_reuse)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstBuffer *buf1 = NULL, *buf2 = NULL, *prev;
gint dcount1 = 0, dcount2 = 0;
gst_buffer_pool_set_active (pool, TRUE);
gst_buffer_pool_acquire_buffer (pool, &buf1, NULL);
buffer_track_destroy (buf1, &dcount1);
gst_buffer_pool_acquire_buffer (pool, &buf2, NULL);
buffer_track_destroy (buf2, &dcount2);
prev = buf2;
gst_buffer_unref (buf2);
/* buffer should not have been freed, but have been recycled */
fail_unless (dcount2 == 0);
gst_buffer_pool_acquire_buffer (pool, &buf2, NULL);
fail_unless (buf2 == prev, "got a fresh buffer instead of previous");
gst_buffer_unref (buf1);
gst_buffer_unref (buf2);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
fail_unless (dcount1 == 1);
fail_unless (dcount2 == 1);
}
GST_END_TEST;
GST_START_TEST (test_pool_config_buffer_size)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstBuffer *buf = NULL;
gst_buffer_pool_set_active (pool, TRUE);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
ck_assert_int_eq (gst_buffer_get_size (buf), 10);
gst_buffer_unref (buf);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
}
GST_END_TEST;
GST_START_TEST (test_inactive_pool_returns_flushing)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstFlowReturn ret;
GstBuffer *buf = NULL;
ret = gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
ck_assert_int_eq (ret, GST_FLOW_FLUSHING);
gst_object_unref (pool);
}
GST_END_TEST;
GST_START_TEST (test_buffer_modify_discard)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstBuffer *buf = NULL, *prev;
GstMemory *mem;
gint dcount = 0;
gst_buffer_pool_set_active (pool, TRUE);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
fail_unless (buf != NULL);
buffer_track_destroy (buf, &dcount);
/* remove all memory, pool should not reuse this buffer */
gst_buffer_remove_all_memory (buf);
gst_buffer_unref (buf);
/* buffer should've been destroyed instead of going back into pool */
fail_unless_equals_int (dcount, 1);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
buffer_track_destroy (buf, &dcount);
/* do resize, as we didn't modify the memory, pool should reuse this buffer */
gst_buffer_resize (buf, 8, 2);
gst_buffer_unref (buf);
/* buffer should've gone back into pool */
fail_unless_equals_int (dcount, 1);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
prev = buf;
fail_unless (buf == prev, "got a fresh buffer instead of previous");
/* keep ref to memory, not exclusive so pool should reuse this buffer */
mem = gst_buffer_get_memory (buf, 0);
gst_buffer_unref (buf);
gst_memory_unref (mem);
/* buffer should not have been destroyed and gone back into pool */
fail_unless_equals_int (dcount, 1);
gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
fail_unless (buf == prev, "got a fresh buffer instead of previous");
/* we're already did track_destroy on this buf, so no need to do it again */
mem = gst_buffer_get_memory (buf, 0);
/* exclusive lock so pool should not reuse this buffer */
gst_memory_lock (mem, GST_LOCK_FLAG_EXCLUSIVE);
gst_buffer_unref (buf);
gst_memory_unlock (mem, GST_LOCK_FLAG_EXCLUSIVE);
gst_memory_unref (mem);
/* buffer should have been destroyed and not gone back into pool because
* of the exclusive lock */
fail_unless_equals_int (dcount, 2);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
}
GST_END_TEST;
GST_START_TEST (test_pool_activation_and_config)
{
GstBufferPool *pool = gst_buffer_pool_new ();
GstStructure *config = gst_buffer_pool_get_config (pool);
GstCaps *caps = gst_caps_new_empty_simple ("test/data");
/* unconfigured pool cannot be activated */
fail_if (gst_buffer_pool_set_active (pool, TRUE));
gst_buffer_pool_config_set_params (config, caps, 10, 10, 0);
fail_unless (gst_buffer_pool_set_config (pool, config));
fail_unless (gst_buffer_pool_set_active (pool, TRUE));
/* setting the same config on an active pool is ok */
config = gst_buffer_pool_get_config (pool);
fail_unless (gst_buffer_pool_set_config (pool, config));
/* setting a different config on active pool should fail */
config = gst_buffer_pool_get_config (pool);
gst_buffer_pool_config_set_params (config, caps, 12, 10, 0);
fail_if (gst_buffer_pool_set_config (pool, config));
fail_unless (gst_buffer_pool_is_active (pool));
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
gst_caps_unref (caps);
}
GST_END_TEST;
GST_START_TEST (test_pool_config_validate)
{
GstBufferPool *pool = create_pool (5, 4, 30);
GstStructure *config = gst_buffer_pool_get_config (pool);
GstCaps *caps = gst_caps_new_empty_simple ("test/data");
fail_unless (gst_buffer_pool_config_validate_params (config, caps, 5, 4, 0));
fail_unless (gst_buffer_pool_config_validate_params (config, caps, 5, 2, 0));
fail_unless (gst_buffer_pool_config_validate_params (config, caps, 4, 4, 0));
fail_if (gst_buffer_pool_config_validate_params (config, caps, 5, 6, 0));
gst_caps_unref (caps);
caps = gst_caps_new_empty_simple ("test/data2");
fail_if (gst_buffer_pool_config_validate_params (config, caps, 5, 4, 0));
gst_caps_unref (caps);
gst_structure_free (config);
gst_object_unref (pool);
}
GST_END_TEST;
GST_START_TEST (test_flushing_pool_returns_flushing)
{
GstBufferPool *pool = create_pool (10, 0, 0);
GstFlowReturn ret;
GstBuffer *buf = NULL;
gst_buffer_pool_set_active (pool, TRUE);
gst_buffer_pool_set_flushing (pool, TRUE);
ret = gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
ck_assert_int_eq (ret, GST_FLOW_FLUSHING);
gst_buffer_pool_set_flushing (pool, FALSE);
ret = gst_buffer_pool_acquire_buffer (pool, &buf, NULL);
ck_assert_int_eq (ret, GST_FLOW_OK);
gst_buffer_unref (buf);
gst_object_unref (pool);
}
GST_END_TEST;
static gpointer
unref_buf (gpointer p)
{
GstBuffer *buf = (GstBuffer *) p;
/* remove all memory, pool should not reuse this buffer */
gst_buffer_remove_all_memory (buf);
gst_buffer_unref (buf);
return NULL;
}
GST_START_TEST (test_no_deadlock_for_buffer_discard)
{
GstBufferPool *pool;
GstBuffer *buf1, *buf2;
GThread *thread;
pool = create_pool (1, 1, 1);
fail_unless (pool);
gst_buffer_pool_set_active (pool, TRUE);
fail_unless (gst_buffer_pool_acquire_buffer (pool, &buf1,
NULL) == GST_FLOW_OK);
thread = g_thread_new (NULL, unref_buf, buf1);
fail_unless (thread);
/* we will be blocked here until buf1 unrefed */
fail_unless (gst_buffer_pool_acquire_buffer (pool, &buf2,
NULL) == GST_FLOW_OK);
gst_buffer_unref (buf2);
g_thread_join (thread);
gst_object_unref (pool);
}
GST_END_TEST;
GST_START_TEST (test_parent_meta)
{
GstBufferPool *pool;
GstBuffer *buf1, *buf2, *buf3;
GstMemory *mem;
gint buf1_dcount = 0;
gint buf2_dcount = 0;
pool = create_pool (1, 0, 0);
fail_unless (pool);
gst_buffer_pool_set_active (pool, TRUE);
fail_unless (gst_buffer_pool_acquire_buffer (pool, &buf1,
NULL) == GST_FLOW_OK);
buffer_track_destroy (buf1, &buf1_dcount);
/* Create a 2nd buffer reffing the same memory. Set parent meta to make sure
* buf1 does not return to pool until buf2 is destroyed. */
mem = gst_buffer_get_memory (buf1, 0);
buf2 = gst_buffer_new ();
gst_buffer_append_memory (buf2, mem);
gst_buffer_add_parent_buffer_meta (buf2, buf1);
buffer_track_destroy (buf2, &buf2_dcount);
/* buf1 is still reffed by the meta */
gst_buffer_unref (buf1);
fail_unless_equals_int (buf1_dcount, 0);
fail_unless_equals_int (buf2_dcount, 0);
/* buf2 gets destroyed and buf1 returns to pool */
gst_buffer_unref (buf2);
fail_unless_equals_int (buf1_dcount, 0);
fail_unless_equals_int (buf2_dcount, 1);
/* buf1 should be recycled with the same memory */
fail_unless (gst_buffer_pool_acquire_buffer (pool, &buf3,
NULL) == GST_FLOW_OK);
fail_unless_equals_pointer (buf1, buf3);
fail_unless_equals_pointer (mem, gst_buffer_peek_memory (buf3, 0));
gst_buffer_unref (buf3);
fail_unless_equals_int (buf1_dcount, 0);
fail_unless_equals_int (buf2_dcount, 1);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
fail_unless_equals_int (buf1_dcount, 1);
fail_unless_equals_int (buf2_dcount, 1);
}
GST_END_TEST;
GST_START_TEST (test_make_writable_parent_meta)
{
GstBufferPool *pool;
GstBuffer *buf1, *buf2, *buf3;
GstMemory *mem1, *mem2;
gint buf1_dcount = 0;
gint buf2_dcount = 0;
pool = create_pool (1, 0, 0);
fail_unless (pool);
gst_buffer_pool_set_active (pool, TRUE);
fail_unless (gst_buffer_pool_acquire_buffer (pool, &buf1,
NULL) == GST_FLOW_OK);
buffer_track_destroy (buf1, &buf1_dcount);
/* Make buf1 not writable and copy it */
gst_buffer_ref (buf1);
buf2 = gst_buffer_make_writable (buf1);
buffer_track_destroy (buf2, &buf2_dcount);
fail_unless (buf1 != buf2);
fail_unless (gst_buffer_is_writable (buf2));
/* buf1 and buf2 should be sharing the same memory */
mem1 = gst_buffer_peek_memory (buf1, 0);
mem2 = gst_buffer_peek_memory (buf2, 0);
fail_unless_equals_pointer (mem1, mem2);
/* buf1 is still reffed by the meta */
gst_buffer_unref (buf1);
fail_unless_equals_int (buf1_dcount, 0);
fail_unless_equals_int (buf2_dcount, 0);
/* buf2 gets destroyed and buf1 returns to pool */
gst_buffer_unref (buf2);
fail_unless_equals_int (buf1_dcount, 0);
fail_unless_equals_int (buf2_dcount, 1);
/* buf1 should be recycled with the same memory */
fail_unless (gst_buffer_pool_acquire_buffer (pool, &buf3,
NULL) == GST_FLOW_OK);
fail_unless_equals_pointer (buf1, buf3);
fail_unless_equals_pointer (mem1, gst_buffer_peek_memory (buf3, 0));
gst_buffer_unref (buf3);
fail_unless_equals_int (buf1_dcount, 0);
fail_unless_equals_int (buf2_dcount, 1);
gst_buffer_pool_set_active (pool, FALSE);
gst_object_unref (pool);
fail_unless_equals_int (buf1_dcount, 1);
fail_unless_equals_int (buf2_dcount, 1);
}
GST_END_TEST;
static Suite *
gst_buffer_pool_suite (void)
{
Suite *s = suite_create ("GstBufferPool");
TCase *tc_chain = tcase_create ("buffer_pool tests");
tcase_set_timeout (tc_chain, 0);
suite_add_tcase (s, tc_chain);
tcase_add_test (tc_chain, test_new_buffer_from_empty_pool);
tcase_add_test (tc_chain, test_buffer_is_recycled);
tcase_add_test (tc_chain, test_buffer_out_of_order_reuse);
tcase_add_test (tc_chain, test_pool_config_buffer_size);
tcase_add_test (tc_chain, test_inactive_pool_returns_flushing);
tcase_add_test (tc_chain, test_buffer_modify_discard);
tcase_add_test (tc_chain, test_pool_activation_and_config);
tcase_add_test (tc_chain, test_pool_config_validate);
tcase_add_test (tc_chain, test_flushing_pool_returns_flushing);
tcase_add_test (tc_chain, test_no_deadlock_for_buffer_discard);
tcase_add_test (tc_chain, test_parent_meta);
tcase_add_test (tc_chain, test_make_writable_parent_meta);
return s;
}
GST_CHECK_MAIN (gst_buffer_pool);