Skip to content

Persist coroutines state #296

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
not-fl3 opened this issue Sep 23, 2021 · 7 comments
Open

Persist coroutines state #296

not-fl3 opened this issue Sep 23, 2021 · 7 comments

Comments

@not-fl3
Copy link
Owner

not-fl3 commented Sep 23, 2021

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?

@oli-obk
Copy link
Contributor

oli-obk commented Sep 23, 2021

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.

@not-fl3
Copy link
Owner Author

not-fl3 commented Sep 23, 2021

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).

@oli-obk
Copy link
Contributor

oli-obk commented Sep 28, 2021

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

@not-fl3
Copy link
Owner Author

not-fl3 commented Oct 4, 2021

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.

rollback.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.

@oli-obk
Copy link
Contributor

oli-obk commented Oct 5, 2021

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.

@not-fl3
Copy link
Owner Author

not-fl3 commented Oct 5, 2021

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.

@oli-obk
Copy link
Contributor

oli-obk commented Jun 11, 2022

Minimal step towards this: rust-lang/rust#95360

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants