Skip to content
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

core: Simplifiy goto logic and use ratio field #5431

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

Herschel
Copy link
Member

@Herschel Herschel commented Oct 5, 2021

  • When seeking, step through frames as normal instead of folding goto commands into a final delta.
  • Remove all GotoPlaceObject and goto_* machinery in MovieClip.
  • Remove place_frame, and instead use ratio field to determine if a display object survives a rewinding goto (fix Handle PlaceObject ratio field for non-morph shapes #1291).

Simplified goto

Previously when seeking, Ruffle would aggregate the frame-by-frame changes into a final delta, and then execute the final delta. This avoided creating objects on the intermediate frames only to destroy them immediately if they didn't exist on the final frame.

Unfortunately, this is too clever, because Flash seems to actually instantiate these transient display objects while seeking. This can be observed in a few ways:

  • AVM1/AVM2: The default instance name will increase due to the intermediate display objects. For example, instance1 will increase to instance4 even if only one new object appears on the final frame. (example)
  • AVM1: Unload clip events will run for transient display objects. (example)
  • AVM1/AVM2: Event sounds on frame 1 of intermediate display objects will play when seeking, both on rewind and fast-forward. (example)
  • AVM2 w/ SWFv9: Constructors for intermediate display objects are run on fast-forward (not rewind). This was fixed w/ improved timeline behavior in SWFv10+. (example)

Nested clips within the transient objects also affect the above, so I don't think there's a clever way to cheat this. We need to do the "dumb" way and step through the frames normally, instantiating and destroying objects as we go. The upside is that this removes all of the complicated logic to fold the PlaceObject tags into a final delta (GotoPlaceObject, etc.). The downside is that this will certainly be slower, requiring more allocations and GC churn.

Use the ratio field during rewinding gotos

Because Flash timelines are stored as deltas from frame to frame, rewinding to a previous frame requires clearing everything, restarting from frame 1, and stepping forward. However, any object that was created before the target frame should "survive" the goto and be reused, not re-created.

Previously, every display object stored a place_frame indicating the frame that the object was created on. When rewinding, an object would survive if place_frame was <= the frame we are seeking to.

However, #1291 shows that Flash uses the ratio field of the PlaceObject tag to handle this. If a depth is occupied both before and after a rewind, the ratio field is used to decide whether the two objects are the "same". If the ratios are equal, the original object should survive the goto and be reused instead of the new object.

The Flash IDE always exports SWFs with the object's creation frame stored in the ratio field, which matches our current place_frame behavior, so this hasn't been much of an issue. But the ratio can be any arbitrary value, and this behavior can be observed in AVM2 by storing a reference to the object before the seek, and comparing it afterwards
(example). 3rd party tools also export SWFs with weirdo ratio fields (#1060).

This PR removes DisplayObject::place_frame in favor of DisplayObject::ratio. The downside is that we can't know if a display object will survive the rewind until after the rewind is finished, because we have to compare it to a potential new object at the same depth. The survival check compares multiple properties in addition to the ratio depending on the display object type. From experimentation, Flash seems to have three categories:

  • "Shapes" including Shape, MorphShape, and Text are recreated if they differ in ratio, character id, clip depth, or transform. These could not be accessed directly in AVM1, so they don't really have any important state. It's safe to trash and recreate them.
  • "Objects" such as Buttons and EditText are recreated if the ratio, character id, or clip depth differ. These can have properties assigned to them, so a user expects these properties to survive after a rewind.
  • MovieClips are recreated only if the ratio differs. Not 100% sure why the character id is not considered here -- I guess because an SWF can be loaded into a movieclip, erasing its character ID, but it's still otherwise be the same clip.

This check is handled by DisplayObject::survives_rewind.

Which properties must match in order to survive a rewind, in table form:

ratio id clip_depth matrix color_transform
Shape, MorphShape, Text ✔️ ✔️ ✔️ ✔️
Button, EditText, Bitmap, Video ✔️ ✔️ ✔️
MovieClip ✔️

(example)

Fixes #1060, #1291.
Supersedes #1305.

TODO:

  • Handling the event order in AVM2 feels clumsy. I settled on adding a SEEKING flag to disable AVM2 events firing while a clip is seeking, and then manually firing the events in the proper order when the seek is complete.
  • I believe the official player runs the constructors for intermediate display objects during a goto in SWFv9 (sort-of buggy behavior). This was fixed in SWF10+.
  • Some properties of a PlaceObject tag (such as name) should only apply to a newly instantiated object (PlaceObjectAction::Place, not PlaceObjectAction::Modify). This will be for a future PR.

@Herschel Herschel marked this pull request as draft October 5, 2021 23:30
@Herschel Herschel force-pushed the goto-ratio branch 3 times, most recently from d125945 to e770fe5 Compare October 6, 2021 09:03
@Toad06
Copy link
Member

Toad06 commented Oct 6, 2021

This also fixes the last bit of #815. 👍

There are also regressions unfortunately:

Crystal Island
Mario
Critical Zone
Steppenwolf 1-1
These games are no longer playable.

Missile Game 3D
Unexpected things happen after you click to start the level 1.

Luigi's Day

  • Move Luigi and hit the "?" block, then move the character to the entrance that just appeared.
  • Press the up arrow and hold the key down. Notice that Luigi unexpectedly moved to his start position.

@Herschel
Copy link
Member Author

Herschel commented Oct 6, 2021

Thanks for the helpful testing, as always!

Pushed a fix for these cases (was mistakenly checking the parent: !self.transformed_by_script() instead of child: !dst.transformed_by_script()).

@Toad06
Copy link
Member

Toad06 commented Oct 7, 2021

This fix changed everything (there were issues in other games that I haven't mentioned yesterday and they're now fixed). 👍

I've made more testings today and I only observed one issue/regression which affects a Steppenwolf game (for a change 🙃): the character gets stuck forever after you use a ladder (she still reacts to keyboard inputs but acts as if she was stuck in an invisible wall).

Steppenwolf 1-2
Here are the steps to follow to get to the scene that uses this ladder.

  • Skip intro.
  • Climb the liana through the window and make the totem fall.
  • Use the other liana to get down and take the knife the guard has. Cut the rope to lower the bridge.
  • Go down and go to the bottom of the screen to reach the scene where the ladder is.

steppen

@Herschel
Copy link
Member Author

Herschel commented Oct 7, 2021

We should add TAS playthroughs of the Steppenwolf games as automated tests someday after we add input recording+playback 😆

@Herschel
Copy link
Member Author

Herschel commented Oct 7, 2021

This was a bug when goto-ing one frame past the total # of frames in the movieclip -- should be fixed now.

@Toad06
Copy link
Member

Toad06 commented Oct 8, 2021

Everything looks good to me. 👍

@Herschel
Copy link
Member Author

Herschel commented Oct 9, 2021

I'm a little hesitant to pull the trigger on this as the goto simplification affects performance quite a bit, so I think I'll split it into two PRs: one doing the ratio changes, which is the main fix and I am confident in merging, and one doing the simplification changes (I'm still thinking through if it's possible to optimize this in some way).

@danielhjacobs danielhjacobs added A-core Area: Core player, where no other category fits T-fix Type: Bug fix (in something that's supposed to work already) T-refactor Type: Refactor / Cleanup labels Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-core Area: Core player, where no other category fits T-fix Type: Bug fix (in something that's supposed to work already) T-refactor Type: Refactor / Cleanup
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Handle PlaceObject ratio field for non-morph shapes ridesims.com Taron Simulator Frame Skipping?
3 participants