Anim: Graph Editor - use Map to update TransInfo pointers instead of searching

This is a performance improvement when moving a bunch
of keys on the same `FCurve` in heavy scenes.

When moving keys in such a way that the `BezTriple` array of the FCurve
has to be sorted, the pointers of `TransInfo` also have to be  updated.
This used to happen by doing a nested loop over all `BeztMap` and all `TransData2D`.
There was a bit of optimization with the
`blender::Vector<bool> adjusted` which stored if a `TransData2D` has been fixed yet.
But in general the complexity was still `BeztMap.size() * TransData.size()`.

There are two optimizations that can be done here.
* Skip any BeztMap if `old_index == new_index`.
If the Key is not going to move any pointers to it will still be valid.
* Use a `Map<float *, int>` built beforehand to quickly get
the `TransData2D` that needs updating instead of searching.
The `int` in this case is the index to the `TransData2D` array.

Doing this reduces the complexity to `BeztMap.size() + TransData.size()`.

Measurements of `beztmap_to_data`
| - | Before | After |
| - | - | - |
| Moving 1 key of 1 FCurve | ~24000 ns | ~5800ns |
| Moving ~1000 keys of 1 FCurve | 17ms | 0.02ms |

Measurements of `remake_graph_transdata`
| - | Before | After |
| - | - | - |
| Moving 1 key of 279 FCurves | 290ms | 22ms |
| Moving ~300 keys of 279 FCurves | 82 **SECONDS** | 80ms |

Test file used
https://download.blender.org/ftp/sybren/animation-rigging/heavy_mocap_test.blend

The deeper technical explanation.
`TransInfo` has an array of `TransData`.
`TransData` has pointers to the float arrays of a `BezTriple`.
The `BezTriple` array is sorted by swapping data,
meaning the `TransData` will now point to the wrong data in the array.
This has to be updated and we can do that by using the `BeztMap`.
This is all under the assumption that `BeztMap` is sorted in the exact
same way as `BezTriple` otherwise this method will fail.
But by doing it the same way, we can know at which
index the `BezTriple` is before and after sorting.
Now we just need to find the corresponding `TransData`.
That can be done by comparing pointers. The `BeztMap` stores the
`BezTriple` it represents and from it we can
get the pointers to its `vec` 0, 1 and 2. (key and handles)

Pull Request: https://projects.blender.org/blender/blender/pulls/120816
This commit is contained in:
Christoph Lendenfeld
2024-04-30 10:46:48 +02:00
committed by Christoph Lendenfeld
parent 3d3dfb6518
commit c6c7d3d8c4

View File

@@ -783,79 +783,77 @@ static void sort_time_beztmaps(const blender::MutableSpan<BeztMap> bezms)
}
}
/* This function firstly adjusts the pointers that the transdata has to each BezTriple. */
static void update_transdata_bezt_pointers(TransInfo *t,
FCurve *fcu,
static inline void update_trans_data(TransData *td,
const FCurve *fcu,
const int new_index,
const bool swap_handles)
{
if (td->flag & TD_BEZTRIPLE && td->hdata) {
if (swap_handles) {
td->hdata->h1 = &fcu->bezt[new_index].h2;
td->hdata->h2 = &fcu->bezt[new_index].h1;
}
else {
td->hdata->h1 = &fcu->bezt[new_index].h1;
td->hdata->h2 = &fcu->bezt[new_index].h2;
}
}
}
/* Adjust the pointers that the transdata has to each BezTriple. */
static void update_transdata_bezt_pointers(TransDataContainer *tc,
const blender::Map<float *, int> &trans_data_map,
const FCurve *fcu,
const blender::Span<BeztMap> bezms)
{
TransData2D *td2d;
TransData *td;
/* At this point, beztmaps are already sorted, so their current index is assumed to be what the
* BezTriple index will be after sorting. */
for (const int new_index : bezms.index_range()) {
const BeztMap &bezm = bezms[new_index];
if (new_index == bezm.oldIndex && !bezm.swap_handles) {
/* If the index is the same, any pointers to BezTriple will still point to the correct data.
* Handles might need to be swapped though. */
continue;
}
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
TransData2D *td2d;
TransData *td;
/* Used to mark whether an TransData's pointers have been fixed already, so that we don't
* override ones that are already done. */
blender::Vector<bool> adjusted(tc->data_len, false);
/* For each beztmap item, find if it is used anywhere. */
const BeztMap *bezm;
for (const int i : bezms.index_range()) {
bezm = &bezms[i];
/* Loop through transdata, testing if we have a hit
* for the handles (vec[0]/vec[2]), we must also check if they need to be swapped. */
td2d = tc->data_2d;
td = tc->data;
for (int j = 0; j < tc->data_len; j++, td2d++, td++) {
/* Skip item if already marked. */
if (adjusted[j]) {
continue;
if (const int *trans_data_index = trans_data_map.lookup_ptr(bezm.bezt->vec[0])) {
td2d = &tc->data_2d[*trans_data_index];
if (bezm.swap_handles) {
td2d->loc2d = fcu->bezt[new_index].vec[2];
}
/* Update all transdata pointers, no need to check for selections etc,
* since only points that are really needed were created as transdata. */
if (td2d->loc2d == bezm->bezt->vec[0]) {
if (bezm->swap_handles) {
td2d->loc2d = fcu->bezt[i].vec[2];
}
else {
td2d->loc2d = fcu->bezt[i].vec[0];
}
adjusted[j] = true;
else {
td2d->loc2d = fcu->bezt[new_index].vec[0];
}
else if (td2d->loc2d == bezm->bezt->vec[2]) {
if (bezm->swap_handles) {
td2d->loc2d = fcu->bezt[i].vec[0];
}
else {
td2d->loc2d = fcu->bezt[i].vec[2];
}
adjusted[j] = true;
td = &tc->data[*trans_data_index];
update_trans_data(td, fcu, new_index, bezm.swap_handles);
}
if (const int *trans_data_index = trans_data_map.lookup_ptr(bezm.bezt->vec[2])) {
td2d = &tc->data_2d[*trans_data_index];
if (bezm.swap_handles) {
td2d->loc2d = fcu->bezt[new_index].vec[0];
}
else if (td2d->loc2d == bezm->bezt->vec[1]) {
td2d->loc2d = fcu->bezt[i].vec[1];
/* If only control point is selected, the handle pointers need to be updated as well. */
if (td2d->h1) {
td2d->h1 = fcu->bezt[i].vec[0];
}
if (td2d->h2) {
td2d->h2 = fcu->bezt[i].vec[2];
}
adjusted[j] = true;
else {
td2d->loc2d = fcu->bezt[new_index].vec[2];
}
td = &tc->data[*trans_data_index];
update_trans_data(td, fcu, new_index, bezm.swap_handles);
}
if (const int *trans_data_index = trans_data_map.lookup_ptr(bezm.bezt->vec[1])) {
td2d = &tc->data_2d[*trans_data_index];
td2d->loc2d = fcu->bezt[new_index].vec[1];
/* The handle type pointer has to be updated too. */
if (adjusted[j] && td->flag & TD_BEZTRIPLE && td->hdata) {
if (bezm->swap_handles) {
td->hdata->h1 = &fcu->bezt[i].h2;
td->hdata->h2 = &fcu->bezt[i].h1;
}
else {
td->hdata->h1 = &fcu->bezt[i].h1;
td->hdata->h2 = &fcu->bezt[i].h2;
}
/* If only control point is selected, the handle pointers need to be updated as well. */
if (td2d->h1) {
td2d->h1 = fcu->bezt[new_index].vec[0];
}
if (td2d->h2) {
td2d->h2 = fcu->bezt[new_index].vec[2];
}
td = &tc->data[*trans_data_index];
update_trans_data(td, fcu, new_index, bezm.swap_handles);
}
}
}
@@ -869,6 +867,15 @@ static void remake_graph_transdata(TransInfo *t, const blender::Span<FCurve *> f
SpaceGraph *sipo = (SpaceGraph *)t->area->spacedata.first;
const bool use_handle = (sipo->flag & SIPO_NOHANDLES) == 0;
TransDataContainer *tc = TRANS_DATA_CONTAINER_FIRST_SINGLE(t);
/* Build a map from the data that is being modified to its index. This is used to quickly update
* the pointers to where the data ends up after sorting. */
blender::Map<float *, int> trans_data_map;
for (int i = 0; i < tc->data_len; i++) {
trans_data_map.add(tc->data_2d[i].loc2d, i);
}
/* The grain size of 8 was chosen based on measured runtimes of this function. While 1 is the
* fastest, larger grain sizes are generally preferred and the difference between 1 and 8 was
* only minimal (~330ms to ~336ms). */
@@ -884,7 +891,7 @@ static void remake_graph_transdata(TransInfo *t, const blender::Span<FCurve *> f
/* NOTE: none of these functions use 'use_handle', it could be removed. */
blender::Vector<BeztMap> bezms = bezt_to_beztmaps(fcu->bezt, fcu->totvert);
sort_time_beztmaps(bezms);
update_transdata_bezt_pointers(t, fcu, bezms);
update_transdata_bezt_pointers(tc, trans_data_map, fcu, bezms);
/* Re-sort actual beztriples
* (perhaps this could be done using the beztmaps to save time?). */