macroquad icon indicating copy to clipboard operation
macroquad copied to clipboard

Persist coroutines state

Open not-fl3 opened this issue 4 years ago • 7 comments

Coroutines are a nice way to express sequences of actions. We can gradually, frame by frame control advancement of coroutines. But what we cant do - serialize the coroutine state and then load it. Say, save the progress of the cutscene in a save file and jump right to the middle of the coroutine after game load.

Use cases:

  • save load systems
  • time rewind games
  • network rollback

Minimized use case, do not depend on macroquad at all for simplicity: https://gist.github.com/not-fl3/e6c796fb4701e408c39fc3a03b7b9b72

As far as I understand, official rust's position here: futures are one-time-use by design and this is not possible. But, maybe, with some magic applied there is a way?

not-fl3 avatar Sep 23 '21 04:09 not-fl3

futures are one-time-use by design and this is not possible

well, in a very pedantic way on could say that Future + Serialize bounds are possible, but only handwritten impls actually work. The moment you involve async blocks or functions, you're out of luck.

I don't think Rust will support this, even if I can vaguely see a path forward on the implementation side. The only solution I can think of right now would be to only capture the initial state of the coroutine, and then fast forward it to the current frame.

oli-obk avatar Sep 23 '21 12:09 oli-obk

Future + Serialize will work, but it will not work for desired sequences notation:

spawn_fx();
wait_seconds(5.0).await;
play_animation(Jump).await;
pos += 5;
wait_seconds(1.).await;

Persisting state just before running the coroutine and then re-running it with the same deltas for the same amount of frames will work, but its a lot of extra computations and additional complexity on maintaining code within the coroutine 100% deterministic.

But, I read a little bit on future's implementation with generators, and, it looks like this terrible crime may work: https://gist.github.com/not-fl3/ba67e22ba7554f1850e25aaf3f3c07e8#file-past2-rs-L108 It will not work for filesystem-persisting, but going back just a few frames - for a short time rewind, or, more importantly, network rollback situation - it may work (on a good day).

not-fl3 avatar Sep 23 '21 22:09 not-fl3

But, I read a little bit on future's implementation with generators, and, it looks like this terrible crime may work:

heh, yea, but this is easy to abuse for unsoundness (even by accident) if anyone holds non-copy types across an await. While I don't see Serialize/Deserialize ever happening, I think we could reasonably allow cloning async blocks. It's not that different from cloning closures. But that takes a while until we have it stable (probably a year?), there may be pre-existing discussions on this.

So... if you guarded the "cloning" of the future with needs_drop, we could get this to be comfortable: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c2d46a3b92fce1d7ff493b338f701b85

oli-obk avatar Sep 28 '21 13:09 oli-obk

Well, I actually tried this and it works (kind of) (sort of) (but it should not)

https://github.com/not-fl3/macroquad/blob/persist_coroutines/examples/rollback.rs

This example is "animation-driven" player movement - on "Arrow" keys the red square is doing some long and non-linear movement pattern. Input is blocked until the "animation" finishes.

When paused - it allows going back in time, restoring "player's" state. And after unpause, coroutine finishes the movement, exactly from the state it was persisted.

https://user-images.githubusercontent.com/910977/135926746-bccb39a8-e1b7-47cc-b90c-567334054c85.mp4

This gives hope, gives light...

The problem: https://github.com/not-fl3/macroquad/blob/persist_coroutines/src/experimental/coroutines.rs#L100 - this assert always triggers, the only coroutine that passes: async move {}. So soundness is the user's responsibility, and there are no exact rules which coroutine will cross the unsoundess line.

Trait objects manipulations are, probably, also unsound, but I believe this part is fixable.

not-fl3 avatar Oct 04 '21 21:10 not-fl3

so... in essence: It would be best if we had language support for cloning async blocks/functions. I believe it should be doable, but I haven't read up on previous discussions on this (if there are any). There may be a fundamental problem, but I don't see what it would be. It may be very compiler-cycle prone, but we should be able to make it work for the cases proposed here.

oli-obk avatar Oct 05 '21 07:10 oli-obk

Yes, basically I am doing exactly a "clone", but in a really unsafe way.

I think the only feature missing to make it relatively safe - being able to ask the future if it has any references persisted in the current state. I am really not sure if it is possible, maybe some rust's tracking issues I should follow?

It looks like doing a "clone" of a future without references is safe enough. I wonder if I am missing something?

I am considering to keep using futures for action chains like this:

tweens::approach_linear(player_pos, 10).await;
tweens::approach_lerp(player_pos, 15).await;

But make a checklist of things illegal for a coroutine. And eventually, somehow move the checks into runtime/comptime.

not-fl3 avatar Oct 05 '21 19:10 not-fl3

Minimal step towards this: https://github.com/rust-lang/rust/issues/95360

oli-obk avatar Jun 11 '22 15:06 oli-obk