|
|
|
06-25-2019, 07:28 PM
|
#1
|
Human being with feelings
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
|
Deeply confused about JSFX serialization and undo (bug?)
Suppose you: - have a JSFX that maintains and serializes an internal variable
- have a Lua script that adjusts a parameter on the JSFX within an Undo block
- execute the Lua script action
- undo the action via ctrl-z
Expected result: after the undo, the internal (serialized) variable should be restored to its value at the moment of Undo_BeginBlock2()
Actual result: after the undo, the internal variable is restored to the value it had roughly at instantiation time.
Attached is an example JSFX and Lua action that demonstrates this.
The attached JSFX maintains a counter that increments inside @block. The counter is read and written in @serialize.
At first initialization:
This is a fresh instance. We see no attempts to deserialize (given that there's nothing to deserialize), with 2 serializations, where the value of the counter at the time of last serialization was 2 (i.e. two executions of @block).
Now I execute the attached Lua script action, which increments the control slider value within an undo block:
By the time I got around to executing the action, the counter had run up to 5653. This was the value of the counter during last serialization, and we can see the number of serializations has increased by 1. Makes sense. And the control slider has incremented by 1, which is what the action does within the undo block.
And the action in appears as expected in the undo history:
But now when I undo the action with ctrl-z, this is the result:
We see the number of deserializations has increased, which is expected as the undo action will deserialize the previous serialized state. However, notice the last read value wasn't the last written value (or at least the value of the counter at last serialization -- I can't be certain what was actually written): it was the value of counter at the previous serialization time (i.e. 2).
The control slider has returned to its original state as expected, but the internal serialized counter variable was restored to the value not at the time of Undo_BeginBlock2() but rather at a moment much earlier, closer to the time it was initially instantiated.
Can someone help me make sense of this? I'm afraid I'm in need of a cluebatting.
Much obliged.
Last edited by tack; 06-25-2019 at 07:37 PM.
|
|
|
06-26-2019, 12:48 AM
|
#2
|
Human being with feelings
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 346
|
That behaviour makes sense to me, because slider changes made in @block (incrementing the counter) aren't really a "user action", and therefore wouldn't need to be preserved by the undo history.
Any state which changes without user interaction only gets saved when the next undo-able user change happens. So, when "undo" is triggered, it returns to the state saved at the last meaningful user interaction, which in this case is right when the effect started running.
What happens if you add this to @gfx?
Code:
(mouse_cap&1) && !(prev_mouse_cap&1) ? (
// Mouse-down event
sliderchange(-1);
);
prev_mouse_cap = mouse_cap;
I would expect that this being called from @gfx counts as a real "user action", therefore triggering the @serialize block, and "undo" would then return to that saved point. I haven't tested it myself, though.
TL;DR: Undo-state is saved just after user changes, not just before.
Last edited by geraintluff; 06-26-2019 at 01:09 AM.
|
|
|
06-26-2019, 05:42 AM
|
#3
|
Human being with feelings
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
|
Quote:
Originally Posted by geraintluff
That behaviour makes sense to me, because slider changes made in @block (incrementing the counter) aren't really a "user action", and therefore wouldn't need to be preserved by the undo history.
|
Which I'd actually be fine with, but it should be consistent: if it's not going to serialize the counter (and other internal values) as part of the undo context, then nor should it deserialize the counter (et al) when the undo state is restored.
Here we have the worst of both worlds, because it doesn't seem to be storing those serialized values as part of the undo block, and instead restores them to an ancient state upon undo.
It has the effect that the plugin is left in a broken and confused state.
Quote:
Originally Posted by geraintluff
What happens if you add this to @gfx?
Code:
(mouse_cap&1) && !(prev_mouse_cap&1) ? (
// Mouse-down event
sliderchange(-1);
);
prev_mouse_cap = mouse_cap;
|
Same behavior there, in that when I click the mouse in the gfx area, we are serialized and the value of the counter during serialization is current (let's call it 1000), but when I undo that interaction with ctrl-z, it deserializes the original value at instantiation time (let's call it 2).
Quote:
Originally Posted by geraintluff
TL;DR: Undo-state is saved just after user changes, not just before.
|
The mystery here for me is I don't understand the state it either chooses to store as part of the undo context or chooses to restore as part of undoing the action.
IMO it should be consistent and retain everything that is serialized during the @serialize block and deserialize all that when the action is undone.
Is anyone aware of a workaround?
Thanks for sharing your insights, Geraint.
|
|
|
06-26-2019, 09:33 AM
|
#4
|
Human being with feelings
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 346
|
It is storing those values - but it doesn't do a last-minute @serialize just before you're about to make changes. It does a @serialize just after you've made the changes.
The two approaches produce the same results apart from any state changes which happen on their own, without user interaction. I've marked these in blue in this diagram:
A key detail: the second save-state in the top diagram corresponds to the first save-state in the bottom one.
Geraint
Last edited by geraintluff; 06-26-2019 at 09:43 AM.
|
|
|
06-26-2019, 10:34 AM
|
#5
|
Human being with feelings
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
|
Quote:
Originally Posted by geraintluff
It is storing those values - but it doesn't do a last-minute @serialize just before you're about to make changes. It does a @serialize just after you've made the changes.
|
Aha, I see what you mean. And thanks for the diagram. Your interpretation of my (mis)understanding of when serialization took place was correct. But the behavior I'm seeing is consistent with your description. And this is corroborated by the fact that you tell reaper what things to retain in the undo context in the call to Undo_EndBlock() rather than Undo_BeginBlock().
This makes sense when the JSFX isn't keeping internal state that changes autonomously (i.e. without user interaction), but unfortunately my JSFX doesn't fall into that category. Consequently when there is an undo, the internal state is restored to an obsolete point in time, which for me leaves the plugin all kinds of broken.
The question now is whether I can induce a serialization step before the action that's invoked from the Lua script, without that itself generating a new undo point. I understand this was the experiment you suggested (with the gfx code) but that does create an undo point. The goal would be to force it to serialize current state without affecting undo history, then immediately execute the action which itself creates the undo point.
At least now that I understand how things work I'll be in much better shape to try to hack it into submission. So thanks for that, Geraint.
Ideas most welcome on how to accomplish that.
|
|
|
06-26-2019, 04:31 PM
|
#6
|
Human being with feelings
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
|
Quote:
Originally Posted by tack
The goal would be to force it to serialize current state without affecting undo history, then immediately execute the action which itself creates the undo point.
|
So far I'm coming up short here. I think this is going to require some knowledge of the internals.
Justin, schwa, if you're watching, any ideas?
|
|
|
06-27-2019, 04:09 AM
|
#7
|
Human being with feelings
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 346
|
So, I'm still not sure I understand the end-goal here - e.g. in your example, I'd ask "what are you doing with the block-counter, such that resetting it to an earlier state is a problem?".
Quote:
Originally Posted by tack
Ideas most welcome on how to accomplish that.
|
So... I have an idea, but I haven't tried it, and don't know if it would actually work.
If it's a modest amount of data, you could maintain your own undo history for some sub-set of your state (in particular, the values which change without user-interaction).
Here's how it could work, with a simple @block-counting example similar to yours:
Code:
@init
// Circular buffer of undo states
undo_memory_length = 1000;
undo_memory = {memory offset};
undo_counter = 1;
@block
block_counter += 1;
@serialize
file_var(0, block_counter);
file_avail(0) >= 0 ? (
// Load old counter from state
file_var(0, undo_counter);
// Restore the a more recent value than the state actually contains
undo_index = undo_counter%undo_memory_length;
block_counter = undo_memory[undo_index];
) : (
// Save the block_counter for the _previous_ undo index
// This could be done in @block instead, but this feels cleaner to me
undo_index = undo_counter%undo_memory_length;
undo_memory[undo_index] = block_counter;
undo_counter += 1;
file_var(0, undo_counter);
);
So, the key idea is to maintain our own counter for the undo-steps.
When serializing, we increment and then save that counter position as part of our undo state - but before we increment, use the old undo_counter to store a value in our own undo_memory. When we re-load a previous state (e.g. from undo), we use the restored undo_counter, and look up the corresponding values from undo_memory (which will be more recent, because it was captured at the time of the next change).
To be robust, we'd need to handle the case when our undo_buffer isn't complete - e.g. the user hits "undo" more than 1000 times, or if the effect is unloaded/reloaded and loses internal state.
This isn't in the example above, but I think a good way to handle this would be to save the undo_counter value into undo_memory as well - so when we pull the value out, we can check it matches what we actually expect.
As I said, I haven't tried it.
Geraint
Last edited by geraintluff; 06-27-2019 at 05:55 AM.
|
|
|
07-09-2019, 06:50 PM
|
#8
|
Human being with feelings
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
|
Quote:
Originally Posted by geraintluff
If it's a modest amount of data, you could maintain your own undo history for some sub-set of your state (in particular, the values which change without user-interaction).
|
Just wanted to thank you for the idea. Funny enough, I did actually start early on with an internal ring buffer for essential undo state, but quickly abandoned it because I had somehow convinced myself it couldn't work.
But your example made it clear I can indeed detect the undo scenario in @serialize and skip deserialization in that case, restoring instead from the history ring buffer. Sometimes you just need to see it realized in code for things to click.
I have enough of it implemented in Reaticulate now that I believe it can work. Thanks very much for the consult, Geraint!
|
|
|
Thread Tools |
|
Display Modes |
Linear Mode
|
Posting Rules
|
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts
HTML code is Off
|
|
|
All times are GMT -7. The time now is 05:11 AM.
|