Merge pull request #10443 from peppy/editor-timeline-tick-display-performance

Improve TimelineTickDisplay performance
This commit is contained in:
Dan Balasescu
2020-10-09 17:53:12 +09:00
committed by GitHub
3 changed files with 120 additions and 35 deletions

View File

@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -13,7 +12,7 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture] [TestFixture]
public class TestSceneTimelineTickDisplay : TimelineTestScene public class TestSceneTimelineTickDisplay : TimelineTestScene
{ {
public override Drawable CreateTestComponent() => new TimelineTickDisplay(); public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
{ {
@ -12,16 +12,23 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// </summary> /// </summary>
public class PointVisualisation : Box public class PointVisualisation : Box
{ {
public const float WIDTH = 1;
public PointVisualisation(double startTime) public PointVisualisation(double startTime)
: this()
{
X = (float)startTime;
}
public PointVisualisation()
{ {
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.Y;
Width = 1;
EdgeSmoothness = new Vector2(1, 0);
RelativePositionAxes = Axes.X; RelativePositionAxes = Axes.X;
X = (float)startTime; RelativeSizeAxes = Axes.Y;
Width = WIDTH;
EdgeSmoothness = new Vector2(WIDTH, 0);
} }
} }
} }

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -12,7 +14,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{ {
public class TimelineTickDisplay : TimelinePart public class TimelineTickDisplay : TimelinePart<PointVisualisation>
{ {
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } private EditorBeatmap beatmap { get; set; }
@ -31,15 +33,63 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
private readonly Cached tickCache = new Cached();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
beatDivisor.BindValueChanged(_ => createLines(), true); beatDivisor.BindValueChanged(_ => tickCache.Invalidate());
} }
private void createLines() /// <summary>
/// The visible time/position range of the timeline.
/// </summary>
private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
/// <summary>
/// The next time/position value to the left of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMinTick;
/// <summary>
/// The next time/position value to the right of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMaxTick;
[Resolved(canBeNull: true)]
private Timeline timeline { get; set; }
protected override void Update()
{ {
Clear(); base.Update();
if (timeline != null)
{
var newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
visibleRange = newRange;
// actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
tickCache.Invalidate();
}
}
if (!tickCache.IsValid)
createTicks();
}
private void createTicks()
{
int drawableIndex = 0;
int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
nextMinTick = null;
nextMaxTick = null;
for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++) for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++)
{ {
@ -50,41 +100,70 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value) for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value)
{ {
var indexInBeat = beat % beatDivisor.Value; float xPos = (float)t;
if (indexInBeat == 0) if (t < visibleRange.min)
{ nextMinTick = xPos;
Add(new PointVisualisation(t) else if (t > visibleRange.max)
{ nextMaxTick ??= xPos;
Colour = BindableBeatDivisor.GetColourFor(1, colours),
Origin = Anchor.TopCentre,
});
}
else else
{ {
// if this is the first beat in the beatmap, there is no next min tick
if (beat == 0 && i == 0)
nextMinTick = float.MinValue;
var indexInBeat = beat % beatDivisor.Value;
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours); var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f;
Add(new PointVisualisation(t) // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
{ var height = indexInBeat == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
Colour = colour,
Height = height,
Origin = Anchor.TopCentre,
});
Add(new PointVisualisation(t) var topPoint = getNextUsablePoint();
{ topPoint.X = xPos;
Colour = colour, topPoint.Colour = colour;
Anchor = Anchor.BottomLeft, topPoint.Height = height;
Origin = Anchor.BottomCentre, topPoint.Anchor = Anchor.TopLeft;
Height = height, topPoint.Origin = Anchor.TopCentre;
});
var bottomPoint = getNextUsablePoint();
bottomPoint.X = xPos;
bottomPoint.Colour = colour;
bottomPoint.Anchor = Anchor.BottomLeft;
bottomPoint.Origin = Anchor.BottomCentre;
bottomPoint.Height = height;
} }
beat++; beat++;
} }
} }
int usedDrawables = drawableIndex;
// save a few drawables beyond the currently used for edge cases.
while (drawableIndex < Math.Min(usedDrawables + 16, Count))
Children[drawableIndex++].Hide();
// expire any excess
while (drawableIndex < Count)
Children[drawableIndex++].Expire();
tickCache.Validate();
Drawable getNextUsablePoint()
{
PointVisualisation point;
if (drawableIndex >= Count)
Add(point = new PointVisualisation());
else
point = Children[drawableIndex];
drawableIndex++;
point.Show();
return point;
}
} }
} }
} }