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

Refactor storyboard commands structure and add framework-based transform loop support #27539

Merged
merged 32 commits into from
May 6, 2024

Conversation

frenzibyte
Copy link
Member

The need for a refactor sparked while I was reviewing #27482, a PR that intended to replace the ad-hoc transform loop logic with an actual framework-based looping transform sequence.

The PR has shown that the current structure of the storyboard commands flow was not flexible enough to have looping easily supported, for multiple reasons:

  1. A transform sequence is required for GeneratedCommand<T> to append a loop operation on the transforms, but it's not possible to return a transform sequence from the transform delegates, unless another generic parameter is added to the delegate, making code extremely awful.
    • attempted somewhat in previous PR: (8b03acd)
  2. Moving the transform application logic to the GeneratedCommand<T> will avoid the necessity of adding generic parameters, but at the cost of using TransformTo and tracking both the property name and property grouping (technical detail in Transforms).

And generally, I did not like that everything was being served to StoryboardSprite as flat data, and StoryboardSprite taking on the role of doing literally everything by itself. If I understand correctly, that shows a lack of encapsulation and polymorphism, destroying code readability as well as quality/flexibility.

My refactor intends to address all of the problems above by removing the command generation logic in ApplyTransforms and use polymorphism for transform application (instead of handling that case-by-case in StoryboardSprite.ApplyTransforms and the helper methods surrounding it). The refactor also simplifies most of the structure and merges CommandTimeline and CommandTimelineGroup together (removing the iteration-based properties in CommandTimelineGroup in the process).

In the process, I've already supported framework-based transform loops in this PR. I can split it to a second PR, but I'm not sure there's much reason to. If it helps reviewing and ensuring stability, I can split it out.

Feedback on the efficiency of the new structure with respect to performance would be appreciated, and/or an idea on how to make benchmarks for this. I'm not quite familiar with the performance side of things, but I hope the new structure isn't overly inefficient for some reason.

I've also done a general pass on the naming of all relevant classes to read better for me. Terms like Timeline combined with Group makes me mistake the classes for something entirely different. Open for feedback.

EVAST9919 and others added 22 commits March 3, 2024 20:50
`LoopStartTime` is now baked into each `IStoryboardCommand`.
Not sure when this happened >.>
I had them shuffled around in the middle of the refactor.
…emory footprint

Mutation should be done only with the methods exposed by `StoryboardCommandGroup`.
@frenzibyte
Copy link
Member Author

As an update, this PR is ready for review. Would appreciate testing with any storyboards that may have potential edge cases. I've already tested thoroughly with world.execute(me) and Ayane - Nageki no Mori, and while both of them look to work correctly according to my eyesight, a second pair would be helpful.

Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

basic structure pass. a lot more palatable structurally than #27482 but i've got the same issue here of not actually being able to see where the fix is super well

Comment on lines +36 to +40
public abstract void ApplyInitialValue<TDrawable>(TDrawable d)
where TDrawable : Drawable, IFlippable, IVectorScalable;

public abstract TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
where TDrawable : Drawable, IFlippable, IVectorScalable;
Copy link
Collaborator

@bdach bdach Mar 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than have generics here I'd prefer what #27482 did and have IDrawableStoryboardElement : IFlippable, IVectorScalable if possible. I just don't really see that much point in an extra generic type param here.

(I did initially say in that pull that that felt arbitrary but I guess this explains it better than that diff ever did.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not possible to use any interface as a generic type argument for TransformSequence because it's constrained to Drawable classes, that's why I have the generic parameter here constrained to Drawables.

We can change the TransformSequence constraint to IDrawable, but I'm not sure if the effort is worth it, so I bailed out and added a pretty local generic here.

osu.Game/Storyboards/Commands/StoryboardLoopingGroup.cs Outdated Show resolved Hide resolved
Comment on lines 139 to 144
// For performance reasons, we need to apply the commands in chronological order.
// Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
IEnumerable<IStoryboardCommand> commands = Commands.AllCommands;
commands = commands.Concat(loopingGroups.SelectMany(l => l.AllCommands));

foreach (var command in generated.OrderBy(g => g.StartTime))
command.ApplyTo(drawable);
}

private void generateCommands<T>(List<IGeneratedCommand> resultList, IEnumerable<CommandTimeline<T>.TypedCommand> commands,
DrawablePropertyInitializer<T> initializeProperty, DrawableTransformer<T> transform, bool alwaysInitialize = true)
{
bool initialized = false;

foreach (var command in commands)
foreach (var command in commands.OrderBy(c => c.StartTime))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks... expensive...

  • AllCommands, which is a SelectManyIterator
  • which is then linq-concatenated with another SelectManyIterator
  • and then finally linq-sorted

Was linq overhead measured here? It looks like it would be massive. I guess this is only ran during load which makes it less of a killer, but surely this can be improved by materialising somewhere?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have been going quite easy on the code because it's a once-off, yeah. I will revise it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Normally I would agree but given that storyboards have thousands of these at a time I really would like to ensure this doesn't spiral out of control.

Comment on lines +56 to +63
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
{
if (loopingGroup.TotalIterations == 0)
return command.ApplyTransforms(d);

double loopingGroupDuration = loopingGroup.Duration;
return command.ApplyTransforms(d).Loop(loopingGroupDuration - Duration, loopingGroup.TotalIterations);
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdach if I understand what you are asking, the fix lies here specifically. normally, with my refactor but without the fix applied, this code would be a for-loop manually applying the transform until TotalIteration times (causing memory allocation issues etc.).

@bdach
Copy link
Collaborator

bdach commented Apr 5, 2024

Merge conflicts are probably going to be "fun" to resolve here post-#27797.

@Coppertine
Copy link

Coppertine commented Apr 17, 2024

As an update, this PR is ready for review. Would appreciate testing with any storyboards that may have potential edge cases. I've already tested thoroughly with world.execute(me) and Ayane - Nageki no Mori, and while both of them look to work correctly according to my eyesight, a second pair would be helpful.

Large post incoming..
Transformations look pretty good on my end for uncanny long arms, Reality Distortion and the Liquicity Yearmix storyboard (albiet to some lag, but is expected, and you can not change the current time on the timeline in auto due to how the stars are setup so i might refactor the storyboard). as well as some other storyboards I have tried out with. (let me know for the list, there are some that are affected by the non-None easings case below)
However i would like to note that NOMA - Louder Machine (due to the second half) likes to run at around 30fps (on a 144hz monitor set to 8x refresh), possibly due to the ~170k commands / ~10k sprites in that section.

Outdated issue Another thing I noticed was that easings on opacity changes on sprites don't appear during gameplay. Here's a sprite code of one of my storyboards ([Astsumei - Tenshi no Kikyou (feat. L4hee)](https://osu.ppy.sh/beatmapsets/2036657#osu/4247350)) for example. (it applies to other storyboards, but this one's the easiest to examine) ``` Sprite,Foreground,Centre,"sb/p.png",320,240 V,0,87333,,853,480 F,19,87333,88333,1,0 F,16,89999,90415,1,0 F,16,90499,90999,1,0 F,16,90999,91665,1,0 F,19,103332,103532,1,0 F,19,108665,109165,1,0 F,15,71333,72833,1,0 F,18,92665,93499,1,0 F,19,183332,186332,1,0 F,19,195665,197665,1,0 F,19,199998,201998,1,0 F,19,203332,203593,1,0 F,19,203679,203979,1,0 F,19,204080,206080,1,0 F,19,121999,123999,1,0 F,19,113999,115999,1,0 F,19,124665,134665,1,0 F,19,177998,187998,1,0 F,0,295430,296430,1,0 F,19,300763,301763,1,0 F,19,303430,304430,1,0 F,19,306096,309096,1,0 F,19,348763,351763,1,0 F,19,393130,403130,1,0 ```

…nt errors

Stable supports these on videos already from a quick read on code (since videos are generally a subclass of sprites).
@frenzibyte
Copy link
Member Author

@Coppertine I cannot find anything odd with the beatmap you attached, I've checked on one of the sprites of the storyboard that changes opacity, and the easing is populated correctly:

CleanShot 2024-05-02 at 00 25 53

If the issue happens on master as well, it should be taken to an issue thread to be tackled separately.

Co-authored-by: Bartłomiej Dach <dach.bartlomiej@gmail.com>
double earliestStartTime = TimelineGroup.StartTime;
foreach (var l in loops)
double earliestStartTime = Commands.StartTime;
foreach (var l in loopingGroups)
earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously l.StartTime referred to the start time of the loop command offset from the start time of the loop group, so the loop group start time was supposed to be added to l.StartTime in here for earliestStartTime to correctly return the start time of the command...

The behaviour is now different in this PR because l.StartTime correctly refers to the absolute start time of the command, making this code more correct.

@bdach
Copy link
Collaborator

bdach commented May 3, 2024

Another thing I noticed was that easings on opacity changes on sprites don't appear during gameplay.
Here's a sprite code of one of my storyboards (Astsumei - Tenshi no Kikyou (feat. L4hee)) for example. (it applies to other storyboards, but this one's the easiest to examine)

@Coppertine Thanks for the testing but I can't see anything wrong either. If you're going to make such claims then please provide timestamps of the specific moment that looks off to you, screenshots, comparisons, anything. We cannot be expected to comb over every single minute sprite in the storyboard.

@Coppertine
Copy link

Apologies, opacity changing sprites was fixed in a later commit. Edited to reflect current pr state.

@bdach
Copy link
Collaborator

bdach commented May 3, 2024

I dunno what to do with this pull. I tried but the size of the diff and the complexities involved make it literally unreviewable as far as my powers go. About all I have to go on is that it does appear to fix the game gobbling up memory on beatmaps with those stupid loop counts and @Coppertine's empirical testing. But having the game not die on OOM does seem like a major positive so I'm tempted to just merge this with minimal review and deal with whatever fallout happens later. @ppy/team-client objections?

@peppy peppy self-requested a review May 3, 2024 15:10
@peppy
Copy link
Sponsor Member

peppy commented May 3, 2024

Yeah I can agree with that. This seems to be doing the correct thing. Given how localised storyboard code is I'm inclined to get it in. Worst case we revert or rewrite the whole thing.

Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yolo

@bdach bdach merged commit cf22fc1 into ppy:master May 6, 2024
13 of 17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Large loopcounts in storyboard loops may allocate lots of memory
6 participants