Page MenuHome

GPencil: Undo System
AbandonedPublic

Authored by Falk David (filedescriptor) on Feb 2 2022, 7:16 PM.

Details

Summary

The undo system is described in more detail here: T95450. See T95401 for a description of how the update cache works, which the undo system is based on.

The undo system can be enabled and disabled in the Experimental section (Only visible if Interface>Developer Extras is enabled) under Debugging in the user preferences.
Note that the update-cache has to be enabled for the undo system to work.

Disclaimer: Right now, operations that don't use the update cache will make a full-copy which can lead to large memory usage!

Demo

Intel® Core™ i7-9700 CPU @ 3.00GHz with 8 cores:

Performance

Test file: heavy_test_file.blend (from T91945)
Single GP object with:

  • 86 layers
  • 11k frames
  • 300k stokes
  • 13M points

OS: Windows 10
CPU: Intel(R) Xeon(R) E5-1650 v4 @ 3.60GHz

Best case scenario (drawing strokes, sculpting, vertex paint)

Encoding an undo step:

Step (avg of 10)master (2022-02-04)patch
Encoding0.3192669 s0.0000279 s

Decoding an undo step:

Step (avg of 10)master (2022-02-04)patch
Decoding2.1854196 s0.0000295 s
Depsgraph update after decode1.0886491 s0.0015363 s
Total3.2740687 s0.0015658 s

Worst case scenario (deleting a layer, reordering layers)
Encoding an undo step:

Step (avg of 10)master (2022-02-04)patch
Encoding0.3250911 s0.7913403 s

Decoding an undo step:

Step (avg of 10)master (2022-02-04)patch
Decoding2.161628 s0.8676814 s
Depsgraph update after decode1.1315799 s1.0737802 s
Total3.2932079 s1.9414616 s

Measurements done using --debug-depsgraph-time.

TODOs:

  • Calculate memory size of encoded steps

Co-authored-by: @Yann Lanthony (yann-lty)

Diff Detail

Repository
rB Blender
Branch
patch/gpencil-undo-system (branched from master)
Build Status
Buildable 20317
Build 20317: arc lint + arc unit

Event Timeline

There are a very large number of changes, so older changes are hidden. Show Older Changes
  • Link to the correct task in the userpref options
Falk David (filedescriptor) retitled this revision from [WIP] GPencil: Undo System to GPencil: Undo System.Feb 3 2022, 11:50 AM
Falk David (filedescriptor) edited the summary of this revision. (Show Details)
Falk David (filedescriptor) edited the summary of this revision. (Show Details)
Falk David (filedescriptor) edited the summary of this revision. (Show Details)
  • Tag gpd light update on mode change
  • Fix double free in cache_node_update
  • Use is_final in step_decode for tagging
  • Rename flag
  • Fix crashes when chaining decode steps
  • Enable undo copy fallback if cache is empty
  • Small cleanup
  • Renamings and more code documentation
  • Refactor print update cache function.
  • Correctly encode the first full copy in the chain
  • Add optimization to skip decoding uneccessary steps
Falk David (filedescriptor) added a reviewer: Restricted Project.Feb 7 2022, 3:19 PM
Falk David (filedescriptor) added a project: Restricted Project.
Falk David (filedescriptor) removed a reviewer: Restricted Project.Feb 7 2022, 3:23 PM
  • Merge branch 'patch/gpencil-update-on-write' into patch/gpencil-undo-system
  • Merge branch 'patch/gpencil-update-on-write' into patch/gpencil-undo-system
  • Merge branch 'patch/gpencil-update-on-write' into patch/gpencil-undo-system
  • Merge branch 'patch/gpencil-update-on-write' into patch/gpencil-undo-system
  • Merge branch 'master' into patch/gpencil-undo-system

All my undo tests with the Draw tool worked lot faster. Great improvement

Antonio Vazquez (antoniov) added a reviewer: Restricted Project.Mar 2 2022, 4:10 PM

@Bastien Montagne (mont29) would you mind to take a look at the Undo logic to check if this is correct or not? Thanks!

Bastien Montagne (mont29) removed a reviewer: Restricted Project.Mar 2 2022, 4:17 PM

First of all, please do not add projects as reviewers.

And I'd leave non-global undo reviews to @Campbell Barton (campbellbarton), he is the maintainer of that area currently afaik.

This revision is now accepted and ready to land.Mar 3 2022, 2:04 PM
This revision now requires review to proceed.Mar 3 2022, 2:36 PM
Campbell Barton (campbellbarton) requested changes to this revision.EditedMar 7 2022, 6:01 AM

Some general comments:

  • Calling the data storage in undo steps "Cache" seems odd, normally cache is something that can be regenerated, not the data which is read from (in this case storage for undo steps).
  • Duplicating memory using MEM_dupallocN then clearing some members seems error prone. BKE_gpencil_duplicate_update_cache_and_data can duplicate a bGPdata, only clearing it's runtime.update_cache, leaving many other pointers in that struct left as-is. While this duplicating memory was done before this patch, storing copies of GPencilUpdateCache outside of G.main means there is a higher likelihood of dangling a pointers causing crashes. Instead of duplicating memory I'd suggest to clear the memory and copy individual members, this way an additional pointer in the run-time struct won't cause problems if the undo logic isn't clearing it.
Undo Steps Depending on Cache

Relying on cache generation to detect of an undo step should be written or not seems unreliable, as not every property on a grease pencil data-block requires re-generating cache.

When grease-pencil undo is enabled, quite a few properties no longer store undo steps. For example toggling "Use Lights" or layer "Masks" can't be undone.
It's fine to skip encoding/decoding data in undo-steps if it can be done reliably. However I don't see a good reason to skip writing undo steps.

From what I can tell this punch can be updated not to depend on cache without major changes.

Undo Steps & ID References

The grease pencil material array holds ID's which are part of the Main data-base causing a crash from the following steps.

  • Add a new grease pencil object.
  • Enter edit-mode.
  • Undo 3x times.
  • Redo 3x times.
  • Crash (see ASAN report P2822).

If this is important to hold references to ID's in the undo-step see the step_foreach_ID_ref callback, although this is disabled in mesh-edit mode for e.g. so we could consider disabling this for grease pencil edit-mode too.

Mode Switching

I've noticed some bugs entering/exiting edit-mode. The simplest case is entering edit-mode can't be undone.

Also ran into an assert: BLI_assert failed: source/blender/editors/gpencil/gpencil_undo.c:550, gpencil_undosys_step_decode(), at 'mode_changed'

This can be redone by:

  • Adding a stroke
  • Remove all materials (needed to avoid crashes caused by stale ID references).
  • Press Tab (3x)
  • Press Ctrl-Z (2x)
  • The assert is triggered.

There is also a crash when duplicating grease-pencil in object mode.

  • Factory startup file.
  • Add new grease pencil stroke.
  • Remove all materials from grease pencil objects.
  • Enter & exit edit-mode.
  • Shift-D to duplicate the object.
  • Undo 3x
  • Redo 1x
  • Crash (... or keep redoing until crash)
source/blender/blenkernel/BKE_gpencil.h
227

A summary of how gpd_dst is used would be useful, even after reading the code it's not so clear what the intention is with this exactly.

The destination is read from but may be NULL, in that case a zeroed struct is used.

The doc-string should elaborate on the purpose of passing in an existing destination which may be used as a copy & why a caller want to pass this in.


NOTE: since the semantics of read/write variables can be confusing, I'd suggest not to use a pointer argument and instead use a signature like this:

bGPdata *BKE_gpencil_data_duplicate(Main *bmain, const bGPdata *gpd_src, const bGPdata *gpd_dst_reference).

This way the API reads better (returning a duplicate instead of writing to a pointer argument), and it's clear the gpd_dst_reference is an argument that can be used to initialize the destination struct.

230

Should be gpd_src matching the implementation.

source/blender/blenkernel/BKE_gpencil_update_cache.h
143

Missing doc-strings.

source/blender/blenkernel/BKE_object.h
170

This function isn't defined anywhere.

source/blender/blenkernel/intern/gpencil.c
2876

Clang-tidy will remove the else added in this patch, same for the else below as they both have returns in the previous blocks.

source/blender/blenkernel/intern/gpencil_update_cache.c
94

Logically using the existing <= comparison seems correct. When the flags are equal the current cache should cover the new cache.

Since I assume there is a good reason for this change it should be noted why equal flags can't be used.


NOTE: I don't think it's correct to call this a flag, perhaps it could be copy_level, also - constraints on ordering of the enum should be noted in the declaration of eGPUpdateCacheNodeFlag, since this this is where new values would be added, although this is outside the scope of this patch.
NOTE: The terms DEEP & SHALLOW have a well defined meaning compared with FULL & LIGHT copies, but this should also be handled separately from this patch.
97

is a no copy reads poorly could be written as: is a "no-copy" to make it clear this isn't poor grammar. The same could be done for for `"full-copy" although this doesn't read as badly.

Also use capitals and full-stops.

source/blender/editors/gpencil/gpencil_undo.c
197

Worth noting in this doxy-gen section which object-modes the undo system is used for.

206

Prefer scene_frame or scene_cfra (to make it clear this value is from the scene, not a value from the grease pencil which is the default assumption for undo data storage).

208

Prefer object_mode, otherwise this reads as if there are different undo-modes (without first looking at the declaration).

213

Is there any reason this needs to be allocated?

In most cases it's simpler not to, avoiding the additional pointer indirection, allocation & freeing.

It might even make more sense not to have two separate structs and make UndoStep step; the first member of GPencilUndoData.

Either way, if it's important to allocate this as a separate pointer and explanation for why should be noted here (if they were shared between undo-steps for example).

Otherwise it can either be changed not to be a pointer or the structs merged.

278

No need for else as previous block returns.

317

No need for else as previous block returns.

377

No need for else as previous block returns.

423

Prefer:

ViewLayer *view_layer = CTX_data_view_layer(C);
Object *ob = OBACT(view_layer);

As undo is low-level and should use pointer lookups that can't be overridden by Python or the current editor.

426–428

ID_RECALC_ALL is checked in this function - is the comment still valid? Would rather not leave comments like this unresolved.

Or, at the very least expand on the possible user-visible down side of this being left as-is.

449

Returning false should be an exceptional case, if undo data could not be created because of an internal error for example (if the active object isn't found or there is some unexpected context where it's impossible to encode the undo data) ... where failing to create an undo-step is preferred over crashing.

This is because of user muscle memory, if you perform 3x editing-actions, you would expect undoing 3x times to restore this state - anything else should be avoided unless there are no alternatives.

If the undo step is effectively empty / no-op, that's OK, although in general most user actions should not be a no-op.

NOTE: this should be written in the doc-string for this callback.
453

Typo happened.

524

Use OBACT macro, see comment above.

541

ID_RECALC_AUDIO_MUTE seems an odd tag to use, shouldn't ID_RECALC_FRAME_CHANGE be used instead?

If not, a comment explaining why mute is used instead of frame-change (or tag both, also with a comment explaining why).

557

The logic for gpencil_undosys_step_decode seems like it could be simplified, firstly undo has much more involved logic than redo - which seems strange as they are similar operations.

The use of next/previous here also seems error prone. I'd prefer these undo steps work more like image_undo.c see: image_undosys_step_decode.

Where each undo step uses the is_applied member, seeking forwards/backwards, checking the undo-step type and the value of is_applied depending on undo/redo. As far as I can see something close to this logic can be applied here.

source/blender/makesdna/DNA_gpencil_types.h
811

prefix multi-line comment blocks with *.

This revision now requires changes to proceed.Mar 7 2022, 6:02 AM

Thank you @Campbell Barton (campbellbarton) for the detailed feedback!

Relying on cache generation to detect of an undo step should be written or not seems unreliable, as not every property on a grease pencil data-block requires re-generating cache.

The plan for the update-cache is that it should be used whenever anything inside a grease pencil data-block changes. For simple changes, like toggling a flag, we would only do "light copies".
But when the update-cache is not used, we fall back to a full copy (if the ID has something in the recalc flag set).

When grease-pencil undo is enabled, quite a few properties no longer store undo steps. For example toggling "Use Lights" or layer "Masks" can't be undone.
It's fine to skip encoding/decoding data in undo-steps if it can be done reliably. However I don't see a good reason to skip writing undo steps.

Looks like that is a bug in our current implementation. Going through the RNA system will explicitly tag the data-block for a copy on write which will clear the update cache and then skip the encode. So that needs to be fixed.

From what I can tell this punch can be updated not to depend on cache without major changes.

Could you clarify on what you mean by this? The idea of this undo system is that we try to only encode what changed since the last step, which we have to use the update-cache for. The alternative would be to do full-copies every time. But there is no point in doing that since the memfile undo system already does this more efficiently.

Thank you @Campbell Barton (campbellbarton) for the detailed feedback!

Relying on cache generation to detect of an undo step should be written or not seems unreliable, as not every property on a grease pencil data-block requires re-generating cache.

The plan for the update-cache is that it should be used whenever anything inside a grease pencil data-block changes. For simple changes, like toggling a flag, we would only do "light copies".
But when the update-cache is not used, we fall back to a full copy (if the ID has something in the recalc flag set).

In this case I think it's just confusion about the term "cache", using this as a source of truth seems backwards. However it may be outside the scope of this patch to change that is the term is already in use.

When grease-pencil undo is enabled, quite a few properties no longer store undo steps. For example toggling "Use Lights" or layer "Masks" can't be undone.
It's fine to skip encoding/decoding data in undo-steps if it can be done reliably. However I don't see a good reason to skip writing undo steps.

Looks like that is a bug in our current implementation. Going through the RNA system will explicitly tag the data-block for a copy on write which will clear the update cache and then skip the encode. So that needs to be fixed.

Sounds fine.

From what I can tell this punch can be updated not to depend on cache without major changes.

Could you clarify on what you mean by this? The idea of this undo system is that we try to only encode what changed since the last step, which we have to use the update-cache for. The alternative would be to do full-copies every time. But there is no point in doing that since the memfile undo system already does this more efficiently.

Okay, in that case a comment explaining why cache is used for undo and that cache is tracking changes would be good, perhaps parts of T95450 could be included in the doxygen-section text for the undo system.

source/blender/makesdna/DNA_gpencil_types.h
37–38

Redundant, can be removed.

@Falk David (filedescriptor) I guess we can consider this patch as abandoned because GP 3.0 will solve all these issues, no?

It won't make sense to put more efforts into this I don't think. It's better to focus on better performance for GP 3.0, yes.