videofilt_managingfilterdata - shekh/VirtualDub2 GitHub Wiki
VirtualDub Plugin SDK 1.2
Managing filter data
Once you begin writing more complex video filters, you will want to allocate complex data structures and side buffers that must be deallocated at some point before program exit. The video filter API provides several hooks to do this.
Because a filter can be used multiple times within the same filter
chain, each entry in the filter chain associated with a filter is called
a filter instance, and can have instance data associated with it.
This instance data is pointed to by the fa->filter_data
field. The
instance data is allocated by the host, but is managed by the filter.
The size of the instance data is set in the filter definition. Each
instance of the filter can thus have its own parameters and operating
data.
The easiest way to manage filter data is by storing it in a data structure:
struct MyData {
int fillValue;
};
int runProc(const VDXFilterActivation *fa, const VDXFilterFunctions *ff) {
MyData *data = (MyData *)fa->filter_data;
int w = fa->dst.w;
int h = fa->dst.h;
uint32 *dst = (uint32 *)fa->dst.data;
ptrdiff_t dstpitch = fa->dst.pitch;
// loop over all rows
for(int y=0; y<h; ++y) {
memset(dst, data->fillValue, w * 4);
// advance to next row in destination
dst = (uint32 *)((char *)dst + dstpitch);
}
return 0;
}
static struct FilterDefinition myfilter={
0,0,NULL, // next, prev, and module (set to zero)
"my filter", // name
"Description of my filter.", // description
"Author of my filter.", // author / maker
NULL, // no private data
sizeof(MyData), // instance data size
NULL, // initProc
NULL, // deinitProc
runProc, // runProc
};
While the instance data structure is useful, it would be rather inconvenient if everything in it had to be initialized on first use and there were no convenient place to clean it up.
The most useful entry points to implement are startProc
and endProc
,
which are called at the beginning and end of each rendering operation,
respectively. This is useful for allocating large image buffers that are
only necessary when actually processing video — the buffers can be left
unallocated to save memory, and allocated only when necessary and once
the source image parameters are known. This also has the benefit of
keeping paramProc
simple.
One gotcha with endProc
: it may be called more times than startProc
and is required to be safe in this regard. Therefore, you should
initialize any pointers to NULL on initialization and clear them when
deallocating buffers:
int endProc(VDXFilterActivation *fa, const VDXFilterFunctions *ff) {
MyData *p = (MyData *)fa->filter_data;
if (p->buffer) {
delete p->buffer;
p->buffer = NULL;
}
}
By default, the memory allocated and placed in fa->filter_data
is
zero-initialized, like global variables in C, so any pointers in that
structure should already be NULL.
For data that persists across rendering operations, the initProc
and
deinitProc
entry points should be used. These entry points handle
initialization and shutdown for fa->filter_data
, the data that
persists for the life of the filter instance. initProc
is called
exactly once when the filter instance is initialized, and deinitProc
exactly once before it is destroyed.
There is one nit in the above, and that's the copy operation. In certain cases, the host may need to temporarily copy the filter structure. As a result, by default, the filter data structure must be directly copyable. In other words, it must be OK to copy the structure with memcpy(). VirtualDub does this in its filter list UI. Here are a couple of the ugly cases:
- The user makes a change to filter configuration and clicks OK in the
dialog.
- Filter data A is copied to temporary data B.
- Data B is edited in the config dialog.
- Data B is accepted, so data A is freed.
- Data B is used for rendering.
-
deinitProc
receives data B. - Data B is freed.
- The user makes a change to filter configuration and clicks Cancel in
the dialog.
- Filter data A is copied to temporary data B.
- Data B is edited in the config dialog.
- Data B is rejected, so it is freed.
- Data A is used for rendering.
-
deinitProc
receives data A. - Data A is freed.
For the most part, the easiest way to deal with this is simply to embed data structures directly in the filter data structure as inline fields and arrays.
Obviously, this is inconvenient for all but the simplest of filter data,
so there is a better way: the copyProc
entry point. This entry point
allows the filter to control the copy process so that any cloned filter
data structures are allocated properly. It also enables deinitProc
to
be called on any additional copies created. Therefore, both of the above
scenarios are changed as follows:
- The user makes a change to filter configuration and clicks OK in the
dialog.
- Filter data A is copied to temporary data B.
- Data B is edited in the config dialog.
- Data B is accepted.
-
deinitProc
receives data A. - Data A is freed.
- Data B is used for rendering.
-
deinitProc
receives data B. - Data B is freed.
- The user makes a change to filter configuration and clicks Cancel in
the dialog.
- Filter data A is copied to temporary data B.
- Data B is edited in the config dialog.
- Data B is rejected.
-
deinitProc
receives data B. - Data B is freed.
- Data A is used for rendering.
-
deinitProc
receives data A. - Data A is freed.
With copyProc
, it becomes possible to map the allocation and
deallocation hooks onto traditional C++ object semantics, which makes
filter data management much easier. In particular, the three hooks can
be mapped as follows:
Filter API entry point | Equivalent C++ method |
---|---|
initProc |
Constructor |
deinitProc |
Destructor |
copyProc |
Copy constructor |
The tricky part is that, unlike the standard C++ mechanisms, the host does memory allocation instead of the filter. To solve this problem, placement versions of the above primitives are used. Explaining this aspect of C++ is out of scope for this SDK, so here's an example implementation:
// required to use placement new
#include <new>
struct MyData {
// Normally, you couldn't include an object like this in the filter data, because
// the host would copy it with memcpy() and cause havoc. With copyProc, however, the
// filter invokes the standard copy constructor, and the std::string object is copied
// correctly.
std::string mFilename;
};
int initProc(VDXFilterActivation *fa, const VDXFilterFunctions *ff) {
// Invoke placement new to construct the class object in the filter data memory
// allocated by the host.
new(fa->filter_data) MyData();
return 0;
}
void deinitProc(VDXFilterActivation *fa, const VDXFilterFunctions *ff) {
MyData *p = (MyData *)fa->filter_data;
// Invoke the class destructor. This destroys any sub-object in the object without
// invoking the memory deallocator, as the delete keyword would.
p->~MyData();
}
void copyProc(VDXFilterActivation *fa, const VDXFilterFunctions *ff, void *dst) {
MyData *p = (MyData *)fa->filter_data;
// Invoke the copy constructor directly using placement new syntax, constructing the
// new object instance directly into the supplied memory.
new(dst) MyData(*p);
}
static struct FilterDefinition myfilter={
0,0,NULL, // next, prev, and module (set to zero)
"my filter", // name
"Description of my filter.", // description
"Author of my filter.", // author / maker
NULL, // no private data
sizeof(MyData), // instance data size
initProc, // initProc
deinitProc, // deinitProc
runProc, // runProc
NULL, // no paramProc
NULL, // no configProc
NULL, // no stringProc
NULL, // no startProc
NULL, // no endProc
NULL, // no script object
NULL, // no scriptStringProc
NULL, // no stringProc2
NULL, // no serializeProc
NULL, // no deserializeProc
copyProc // copyProc
};
Copyright (C) 2007-2012 Avery Lee.