Old 06-25-2019, 07:28 PM   #1
tack
Human being with feelings
 
tack's Avatar
 
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
Default Deeply confused about JSFX serialization and undo (bug?)

Suppose you:
  1. have a JSFX that maintains and serializes an internal variable
  2. have a Lua script that adjusts a parameter on the JSFX within an Undo block
  3. execute the Lua script action
  4. 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.
Attached Files
File Type: lua Test Serialize.lua (407 Bytes, 121 views)
File Type: txt Serialize Test.jsfx.txt (1.0 KB, 114 views)

Last edited by tack; 06-25-2019 at 07:37 PM.
tack is offline   Reply With Quote
Old 06-26-2019, 12:48 AM   #2
geraintluff
Human being with feelings
 
geraintluff's Avatar
 
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 346
Default

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.
__________________
JSFX set | Bandcamp/SoundCloud/Spotify

Last edited by geraintluff; 06-26-2019 at 01:09 AM.
geraintluff is offline   Reply With Quote
Old 06-26-2019, 05:42 AM   #3
tack
Human being with feelings
 
tack's Avatar
 
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
Default

Quote:
Originally Posted by geraintluff View Post
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 View Post
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 View Post
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.
tack is offline   Reply With Quote
Old 06-26-2019, 09:33 AM   #4
geraintluff
Human being with feelings
 
geraintluff's Avatar
 
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 346
Default

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
Attached Images
File Type: png undo diagram.png (34.2 KB, 305 views)
__________________
JSFX set | Bandcamp/SoundCloud/Spotify

Last edited by geraintluff; 06-26-2019 at 09:43 AM.
geraintluff is offline   Reply With Quote
Old 06-26-2019, 10:34 AM   #5
tack
Human being with feelings
 
tack's Avatar
 
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
Default

Quote:
Originally Posted by geraintluff View Post
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.
tack is offline   Reply With Quote
Old 06-26-2019, 04:31 PM   #6
tack
Human being with feelings
 
tack's Avatar
 
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
Default

Quote:
Originally Posted by tack View Post
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?
tack is offline   Reply With Quote
Old 06-27-2019, 04:09 AM   #7
geraintluff
Human being with feelings
 
geraintluff's Avatar
 
Join Date: Nov 2009
Location: mostly inside my own head
Posts: 346
Default

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 View Post
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
__________________
JSFX set | Bandcamp/SoundCloud/Spotify

Last edited by geraintluff; 06-27-2019 at 05:55 AM.
geraintluff is offline   Reply With Quote
Old 07-09-2019, 06:50 PM   #8
tack
Human being with feelings
 
tack's Avatar
 
Join Date: Jan 2014
Location: Ontario, Canada
Posts: 1,618
Default

Quote:
Originally Posted by geraintluff View Post
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!
tack is offline   Reply With Quote
Reply

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

BB code is On
Smilies are On
[IMG] code is On
HTML code is Off

Forum Jump


All times are GMT -7. The time now is 05:11 AM.


Powered by vBulletin® Version 3.8.11
Copyright ©2000 - 2024, vBulletin Solutions Inc.