Persist coroutines state
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?
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.
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).
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
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.
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.
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.
Minimal step towards this: https://github.com/rust-lang/rust/issues/95360