1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 14:32:55 +08:00

Merge pull request #16608 from peppy/playlist-overlay-optimisations

Optimise `PlaylistOverlay` and reduce startup overhead of `MusicController`
This commit is contained in:
Dan Balasescu 2022-01-26 20:28:33 +09:00 committed by GitHub
commit cf9ee43b17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 126 additions and 128 deletions

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Music;
using osu.Game.Tests.Resources;
@ -18,11 +19,11 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene
{
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>();
private PlaylistOverlay playlistOverlay;
private BeatmapSetInfo first;
private Live<BeatmapSetInfo> first;
[SetUp]
public void Setup() => Schedule(() =>
@ -45,7 +46,7 @@ namespace osu.Game.Tests.Visual.UserInterface
for (int i = 0; i < 100; i++)
{
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo());
beatmapSets.Add(TestResources.CreateTestBeatmapSetInfo().ToLiveUnmanaged());
}
first = beatmapSets.First();
@ -60,7 +61,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("hold 1st item handle", () =>
{
var handle = this.ChildrenOfType<OsuRearrangeableListItem<BeatmapSetInfo>.PlaylistItemHandle>().First();
var handle = this.ChildrenOfType<OsuRearrangeableListItem<Live<BeatmapSetInfo>>.PlaylistItemHandle>().First();
InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre);
InputManager.PressButton(MouseButton.Left);
});
@ -68,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("drag to 5th", () =>
{
var item = this.ChildrenOfType<PlaylistItem>().ElementAt(4);
InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.Centre);
InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.BottomLeft);
});
AddAssert("song 1 is 5th", () => beatmapSets[4].Equals(first));

View File

@ -25,7 +25,7 @@ namespace osu.Game.Collections
/// <summary>
/// The beatmaps contained by the collection.
/// </summary>
public readonly BindableList<IBeatmapInfo> Beatmaps = new BindableList<IBeatmapInfo>();
public readonly BindableList<BeatmapInfo> Beatmaps = new BindableList<BeatmapInfo>();
/// <summary>
/// The date when this collection was last modified.

View File

@ -38,7 +38,7 @@ namespace osu.Game.Collections
}
private readonly IBindableList<BeatmapCollection> collections = new BindableList<BeatmapCollection>();
private readonly IBindableList<IBeatmapInfo> beatmaps = new BindableList<IBeatmapInfo>();
private readonly IBindableList<BeatmapInfo> beatmaps = new BindableList<BeatmapInfo>();
private readonly BindableList<CollectionFilterMenuItem> filters = new BindableList<CollectionFilterMenuItem>();
[Resolved(CanBeNull = true)]
@ -196,7 +196,7 @@ namespace osu.Game.Collections
private IBindable<WorkingBeatmap> beatmap { get; set; }
[CanBeNull]
private readonly BindableList<IBeatmapInfo> collectionBeatmaps;
private readonly BindableList<BeatmapInfo> collectionBeatmaps;
[NotNull]
private readonly Bindable<string> collectionName;

View File

@ -7,16 +7,17 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Overlays.Music
{
public class Playlist : OsuRearrangeableListContainer<BeatmapSetInfo>
public class Playlist : OsuRearrangeableListContainer<Live<BeatmapSetInfo>>
{
public Action<BeatmapSetInfo> RequestSelection;
public Action<Live<BeatmapSetInfo>> RequestSelection;
public readonly Bindable<BeatmapSetInfo> SelectedSet = new Bindable<BeatmapSetInfo>();
public readonly Bindable<Live<BeatmapSetInfo>> SelectedSet = new Bindable<Live<BeatmapSetInfo>>();
public new MarginPadding Padding
{
@ -26,23 +27,23 @@ namespace osu.Game.Overlays.Music
public void Filter(FilterCriteria criteria)
{
var items = (SearchContainer<RearrangeableListItem<BeatmapSetInfo>>)ListContainer;
var items = (SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>)ListContainer;
foreach (var item in items.OfType<PlaylistItem>())
item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.Equals(b.BeatmapSet)) ?? true;
item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => item.Model.ID == b.BeatmapSet?.ID) ?? true;
items.SearchTerm = criteria.SearchText;
}
public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
public Live<BeatmapSetInfo> FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter);
protected override OsuRearrangeableListItem<BeatmapSetInfo> CreateOsuDrawable(BeatmapSetInfo item) => new PlaylistItem(item)
protected override OsuRearrangeableListItem<Live<BeatmapSetInfo>> CreateOsuDrawable(Live<BeatmapSetInfo> item) => new PlaylistItem(item)
{
SelectedSet = { BindTarget = SelectedSet },
RequestSelection = set => RequestSelection?.Invoke(set)
};
protected override FillFlowContainer<RearrangeableListItem<BeatmapSetInfo>> CreateListFillFlowContainer() => new SearchContainer<RearrangeableListItem<BeatmapSetInfo>>
protected override FillFlowContainer<RearrangeableListItem<Live<BeatmapSetInfo>>> CreateListFillFlowContainer() => new SearchContainer<RearrangeableListItem<Live<BeatmapSetInfo>>>
{
Spacing = new Vector2(0, 3),
LayoutDuration = 200,

View File

@ -10,17 +10,18 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Overlays.Music
{
public class PlaylistItem : OsuRearrangeableListItem<BeatmapSetInfo>, IFilterable
public class PlaylistItem : OsuRearrangeableListItem<Live<BeatmapSetInfo>>, IFilterable
{
public readonly Bindable<BeatmapSetInfo> SelectedSet = new Bindable<BeatmapSetInfo>();
public readonly Bindable<Live<BeatmapSetInfo>> SelectedSet = new Bindable<Live<BeatmapSetInfo>>();
public Action<BeatmapSetInfo> RequestSelection;
public Action<Live<BeatmapSetInfo>> RequestSelection;
private TextFlowContainer text;
private ITextPart titlePart;
@ -28,12 +29,10 @@ namespace osu.Game.Overlays.Music
[Resolved]
private OsuColour colours { get; set; }
public PlaylistItem(BeatmapSetInfo item)
public PlaylistItem(Live<BeatmapSetInfo> item)
: base(item)
{
Padding = new MarginPadding { Left = 5 };
FilterTerms = item.Metadata.GetSearchableTerms();
}
[BackgroundDependencyLoader]
@ -46,47 +45,52 @@ namespace osu.Game.Overlays.Music
{
base.LoadComplete();
SelectedSet.BindValueChanged(set =>
Model.PerformRead(m =>
{
if (set.OldValue?.Equals(Model) != true && set.NewValue?.Equals(Model) != true)
return;
var metadata = m.Metadata;
updateSelectionState(false);
}, true);
var title = new RomanisableString(metadata.TitleUnicode, metadata.Title);
var artist = new RomanisableString(metadata.ArtistUnicode, metadata.Artist);
titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular));
titlePart.DrawablePartsRecreated += _ => updateSelectionState(true);
text.AddText(@" "); // to separate the title from the artist.
text.AddText(artist, sprite =>
{
sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
sprite.Colour = colours.Gray9;
sprite.Padding = new MarginPadding { Top = 1 };
});
SelectedSet.BindValueChanged(set =>
{
bool newSelected = set.NewValue?.Equals(Model) == true;
if (newSelected == selected)
return;
selected = newSelected;
updateSelectionState(false);
});
updateSelectionState(true);
});
}
private bool selected;
private void updateSelectionState(bool instant)
{
foreach (Drawable s in titlePart.Drawables)
s.FadeColour(SelectedSet.Value?.Equals(Model) == true ? colours.Yellow : Color4.White, instant ? 0 : FADE_DURATION);
s.FadeColour(selected ? colours.Yellow : Color4.White, instant ? 0 : FADE_DURATION);
}
protected override Drawable CreateContent() => text = new OsuTextFlowContainer
protected override Drawable CreateContent() => new DelayedLoadWrapper(text = new OsuTextFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
};
protected override void LoadAsyncComplete()
{
base.LoadAsyncComplete();
var title = new RomanisableString(Model.Metadata.TitleUnicode, Model.Metadata.Title);
var artist = new RomanisableString(Model.Metadata.ArtistUnicode, Model.Metadata.Artist);
titlePart = text.AddText(title, sprite => sprite.Font = OsuFont.GetFont(weight: FontWeight.Regular));
updateSelectionState(true);
titlePart.DrawablePartsRecreated += _ => updateSelectionState(true);
text.AddText(@" "); // to separate the title from the artist.
text.AddText(artist, sprite =>
{
sprite.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold);
sprite.Colour = colours.Gray9;
sprite.Padding = new MarginPadding { Top = 1 };
});
}
});
protected override bool OnClick(ClickEvent e)
{
@ -109,7 +113,7 @@ namespace osu.Game.Overlays.Music
}
}
public IEnumerable<string> FilterTerms { get; }
public IEnumerable<string> FilterTerms => Model.PerformRead(m => m.Metadata.GetSearchableTerms());
private bool matchingFilter = true;

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -10,9 +11,11 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
using Realms;
namespace osu.Game.Overlays.Music
{
@ -21,15 +24,20 @@ namespace osu.Game.Overlays.Music
private const float transition_duration = 600;
private const float playlist_height = 510;
public IBindableList<BeatmapSetInfo> BeatmapSets => beatmapSets;
public IBindableList<Live<BeatmapSetInfo>> BeatmapSets => beatmapSets;
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
private readonly BindableList<Live<BeatmapSetInfo>> beatmapSets = new BindableList<Live<BeatmapSetInfo>>();
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
private IDisposable beatmapSubscription;
private FilterControl filter;
private Playlist list;
@ -77,13 +85,16 @@ namespace osu.Game.Overlays.Music
filter.Search.OnCommit += (sender, newText) =>
{
BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps.FirstOrDefault();
if (toSelect != null)
list.FirstVisibleSet.PerformRead(set =>
{
beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect);
beatmap.Value.Track.Restart();
}
BeatmapInfo toSelect = set.Beatmaps.FirstOrDefault();
if (toSelect != null)
{
beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect);
beatmap.Value.Track.Restart();
}
});
};
}
@ -91,8 +102,29 @@ namespace osu.Game.Overlays.Music
{
base.LoadComplete();
// tests might bind externally, in which case we don't want to involve realm.
if (beatmapSets.Count == 0)
beatmapSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => !s.DeletePending), beatmapsChanged);
list.Items.BindTo(beatmapSets);
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo, true);
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
{
if (changes == null)
{
beatmapSets.Clear();
// must use AddRange to avoid RearrangeableList sort overhead per add op.
beatmapSets.AddRange(sender.Select(b => b.ToLive(realm)));
return;
}
foreach (int i in changes.InsertedIndices)
beatmapSets.Insert(i, sender[i].ToLive(realm));
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i))
beatmapSets.RemoveAt(i);
}
protected override void PopIn()
@ -112,16 +144,25 @@ namespace osu.Game.Overlays.Music
this.FadeOut(transition_duration);
}
private void itemSelected(BeatmapSetInfo set)
private void itemSelected(Live<BeatmapSetInfo> beatmapSet)
{
if (set.Equals((beatmap.Value?.BeatmapSetInfo)))
beatmapSet.PerformRead(set =>
{
beatmap.Value?.Track.Seek(0);
return;
}
if (set.Equals((beatmap.Value?.BeatmapSetInfo)))
{
beatmap.Value?.Track.Seek(0);
return;
}
beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First());
beatmap.Value.Track.Restart();
beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First());
beatmap.Value.Track.Restart();
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapSubscription?.Dispose();
}
}
}

View File

@ -16,7 +16,6 @@ using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets.Mods;
using Realms;
namespace osu.Game.Overlays
{
@ -25,20 +24,14 @@ namespace osu.Game.Overlays
/// </summary>
public class MusicController : CompositeDrawable
{
private IDisposable beatmapSubscription;
[Resolved]
private BeatmapManager beatmaps { get; set; }
public IBindableList<BeatmapSetInfo> BeatmapSets => beatmapSets;
/// <summary>
/// Point in time after which the current track will be restarted on triggering a "previous track" action.
/// </summary>
private const double restart_cutoff_point = 5000;
private readonly BindableList<BeatmapSetInfo> beatmapSets = new BindableList<BeatmapSetInfo>();
/// <summary>
/// Whether the user has requested the track to be paused. Use <see cref="IsPlaying"/> to determine whether the track is still playing.
/// </summary>
@ -71,50 +64,11 @@ namespace osu.Game.Overlays
mods.BindValueChanged(_ => ResetTrackAdjustments(), true);
}
private IQueryable<BeatmapSetInfo> queryRealmBeatmapSets() =>
realm.Realm
.All<BeatmapSetInfo>()
.Where(s => !s.DeletePending);
protected override void LoadComplete()
{
base.LoadComplete();
beatmapSubscription = realm.RegisterForNotifications(r => queryRealmBeatmapSets(), beatmapsChanged);
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
{
if (changes == null)
{
beatmapSets.Clear();
foreach (var s in sender)
beatmapSets.Add(s.Detach());
return;
}
foreach (int i in changes.InsertedIndices)
beatmapSets.Insert(i, sender[i].Detach());
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i))
beatmapSets.RemoveAt(i);
}
/// <summary>
/// Forcefully reload the current <see cref="WorkingBeatmap"/>'s track from disk.
/// </summary>
public void ReloadCurrentTrack() => changeTrack();
/// <summary>
/// Change the position of a <see cref="BeatmapSetInfo"/> in the current playlist.
/// </summary>
/// <param name="beatmapSetInfo">The beatmap to move.</param>
/// <param name="index">The new position.</param>
public void ChangeBeatmapSetPosition(BeatmapSetInfo beatmapSetInfo, int index)
{
beatmapSets.Remove(beatmapSetInfo);
beatmapSets.Insert(index, beatmapSetInfo);
}
/// <summary>
/// Returns whether the beatmap track is playing.
/// </summary>
@ -240,11 +194,12 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Prev;
var playable = BeatmapSets.TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault() ?? BeatmapSets.LastOrDefault();
var playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current.BeatmapSetInfo)).LastOrDefault()
?? getBeatmapSets().LastOrDefault();
if (playable != null)
if (playableSet != null)
{
changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First()));
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Beatmaps.First()));
restartTrack();
return PreviousTrackResult.Previous;
}
@ -271,7 +226,9 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Next;
var playableSet = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault();
var playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current.BeatmapSetInfo)).ElementAtOrDefault(1)
?? getBeatmapSets().FirstOrDefault();
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault();
if (playableBeatmap != null)
@ -295,6 +252,8 @@ namespace osu.Game.Overlays
private TrackChangeDirection? queuedDirection;
private IQueryable<BeatmapSetInfo> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending);
private void beatmapChanged(ValueChangedEvent<WorkingBeatmap> beatmap) => changeBeatmap(beatmap.NewValue);
private void changeBeatmap(WorkingBeatmap newWorking)
@ -322,8 +281,8 @@ namespace osu.Game.Overlays
else
{
// figure out the best direction based on order in playlist.
int last = BeatmapSets.TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count();
int next = newWorking == null ? -1 : BeatmapSets.TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count();
int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count();
int next = newWorking == null ? -1 : getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count();
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
}
@ -437,13 +396,6 @@ namespace osu.Game.Overlays
mod.ApplyToTrack(CurrentTrack);
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
beatmapSubscription?.Dispose();
}
}
public enum TrackChangeDirection

View File

@ -197,7 +197,6 @@ namespace osu.Game.Overlays
{
dragContainer.Add(playlist);
playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
togglePlaylist();