diff --git a/osu-framework.sln.DotSettings b/osu-framework.sln.DotSettings
index bad7c7804d..98156ad692 100644
--- a/osu-framework.sln.DotSettings
+++ b/osu-framework.sln.DotSettings
@@ -342,6 +342,7 @@
AABB
API
ARGB
+ BBH
BPM
CG
FBO
@@ -991,6 +992,7 @@ private void load()
True
True
True
+ True
True
True
True
diff --git a/osu.Framework.Benchmarks/BenchmarkPathBBHProgressUpdate.cs b/osu.Framework.Benchmarks/BenchmarkPathBBHProgressUpdate.cs
new file mode 100644
index 0000000000..8d4f23ef81
--- /dev/null
+++ b/osu.Framework.Benchmarks/BenchmarkPathBBHProgressUpdate.cs
@@ -0,0 +1,84 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using osu.Framework.Graphics.Lines;
+using osuTK;
+
+namespace osu.Framework.Benchmarks
+{
+ public class BenchmarkPathBBHProgressUpdate : BenchmarkTest
+ {
+ private readonly PathBBH bbh100 = new PathBBH();
+ private readonly PathBBH bbh1K = new PathBBH();
+ private readonly PathBBH bbh10K = new PathBBH();
+ private readonly PathBBH bbh100K = new PathBBH();
+ private readonly PathBBH bbh1M = new PathBBH();
+
+ private readonly Random random = new Random(1);
+
+ public override void SetUp()
+ {
+ base.SetUp();
+
+ List vertices100 = new List(100);
+ List vertices1K = new List(1_000);
+ List vertices10K = new List(10_000);
+ List vertices100K = new List(100_000);
+ List vertices1M = new List(1_000_000);
+
+ for (int i = 0; i < vertices100.Capacity; i++)
+ vertices100.Add(new Vector2(random.NextSingle(), random.NextSingle()));
+
+ for (int i = 0; i < vertices1K.Capacity; i++)
+ vertices1K.Add(new Vector2(random.NextSingle(), random.NextSingle()));
+
+ for (int i = 0; i < vertices10K.Capacity; i++)
+ vertices10K.Add(new Vector2(random.NextSingle(), random.NextSingle()));
+
+ for (int i = 0; i < vertices100K.Capacity; i++)
+ vertices100K.Add(new Vector2(random.NextSingle(), random.NextSingle()));
+
+ for (int i = 0; i < vertices1M.Capacity; i++)
+ vertices1M.Add(new Vector2(random.NextSingle(), random.NextSingle()));
+
+ bbh100.SetVertices(vertices100, 10);
+ bbh1K.SetVertices(vertices1K, 10);
+ bbh10K.SetVertices(vertices10K, 10);
+ bbh100K.SetVertices(vertices100K, 10);
+ bbh1M.SetVertices(vertices1M, 10);
+ }
+
+ [Benchmark]
+ public void SetStartProgressBBH100()
+ {
+ bbh100.StartProgress = random.NextSingle();
+ }
+
+ [Benchmark]
+ public void SetStartProgressBBH1K()
+ {
+ bbh1K.StartProgress = random.NextSingle();
+ }
+
+ [Benchmark]
+ public void SetStartProgressBBH10K()
+ {
+ bbh10K.StartProgress = random.NextSingle();
+ }
+
+ [Benchmark]
+ public void SetStartProgressBBH100K()
+ {
+ bbh100K.StartProgress = random.NextSingle();
+ }
+
+ [Benchmark]
+ public void SetStartProgressBBH1M()
+ {
+ bbh1M.StartProgress = random.NextSingle();
+ }
+ }
+}
diff --git a/osu.Framework.Benchmarks/BenchmarkPathReceivePositionalInputAt.cs b/osu.Framework.Benchmarks/BenchmarkPathReceivePositionalInputAt.cs
new file mode 100644
index 0000000000..eb9d923fee
--- /dev/null
+++ b/osu.Framework.Benchmarks/BenchmarkPathReceivePositionalInputAt.cs
@@ -0,0 +1,85 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using osu.Framework.Graphics.Lines;
+using osuTK;
+
+namespace osu.Framework.Benchmarks
+{
+ public class BenchmarkPathReceivePositionalInputAt : BenchmarkTest
+ {
+ // We deliberately set path radius to 0 to introduce worst-case scenario in which with any position given we won't land on a path.
+ private readonly Path path100 = new Path { PathRadius = 0f };
+ private readonly Path path1K = new Path { PathRadius = 0f };
+ private readonly Path path10K = new Path { PathRadius = 0f };
+ private readonly Path path100K = new Path { PathRadius = 0f };
+ private readonly Path path1M = new Path { PathRadius = 0f };
+
+ private readonly Random random = new Random(1);
+
+ public override void SetUp()
+ {
+ base.SetUp();
+
+ List vertices100 = new List(100);
+ List vertices1K = new List(1_000);
+ List vertices10K = new List(10_000);
+ List vertices100K = new List(100_000);
+ List vertices1M = new List(1_000_000);
+
+ for (int i = 0; i < vertices100.Capacity; i++)
+ vertices100.Add(new Vector2((float)i / vertices100.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices1K.Capacity; i++)
+ vertices1K.Add(new Vector2((float)i / vertices1K.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices10K.Capacity; i++)
+ vertices10K.Add(new Vector2((float)i / vertices10K.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices100K.Capacity; i++)
+ vertices100K.Add(new Vector2((float)i / vertices100K.Capacity * 100, random.NextSingle() * 100));
+
+ for (int i = 0; i < vertices1M.Capacity; i++)
+ vertices1M.Add(new Vector2((float)i / vertices1M.Capacity * 100, random.NextSingle() * 100));
+
+ path100.Vertices = vertices100;
+ path1K.Vertices = vertices1K;
+ path10K.Vertices = vertices10K;
+ path100K.Vertices = vertices100K;
+ path1M.Vertices = vertices1M;
+ }
+
+ [Benchmark]
+ public void Contains100()
+ {
+ path100.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains1K()
+ {
+ path1K.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains10K()
+ {
+ path10K.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains100K()
+ {
+ path100K.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+
+ [Benchmark]
+ public void Contains1M()
+ {
+ path1M.ReceivePositionalInputAt(new Vector2(random.NextSingle() * 100, random.NextSingle() * 100));
+ }
+ }
+}
diff --git a/osu.Framework.Benchmarks/BenchmarkPathSegmentCreation.cs b/osu.Framework.Benchmarks/BenchmarkPathSegmentCreation.cs
new file mode 100644
index 0000000000..79426cd1ec
--- /dev/null
+++ b/osu.Framework.Benchmarks/BenchmarkPathSegmentCreation.cs
@@ -0,0 +1,87 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using BenchmarkDotNet.Engines;
+using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osuTK;
+
+namespace osu.Framework.Benchmarks
+{
+ public partial class BenchmarkPathSegmentCreation : BenchmarkTest
+ {
+ private readonly List vertices100 = new List(100);
+ private readonly List vertices1K = new List(1_000);
+ private readonly List vertices10K = new List(10_000);
+ private readonly List vertices100K = new List(100_000);
+ private readonly List vertices1M = new List(1_000_000);
+
+ private readonly BenchPath path = new BenchPath();
+ private readonly Consumer consumer = new Consumer();
+
+ public override void SetUp()
+ {
+ base.SetUp();
+
+ var rng = new Random(1);
+
+ for (int i = 0; i < vertices100.Capacity; i++)
+ vertices100.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices1K.Capacity; i++)
+ vertices1K.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices10K.Capacity; i++)
+ vertices10K.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices100K.Capacity; i++)
+ vertices100K.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+
+ for (int i = 0; i < vertices1M.Capacity; i++)
+ vertices1M.Add(new Vector2(rng.NextSingle(), rng.NextSingle()));
+ }
+
+ [Benchmark]
+ public void Compute100Segments()
+ {
+ path.Vertices = vertices100;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute1KSegments()
+ {
+ path.Vertices = vertices1K;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute10KSegments()
+ {
+ path.Vertices = vertices10K;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute100KSegments()
+ {
+ path.Vertices = vertices100K;
+ consumer.Consume(path.Segments);
+ }
+
+ [Benchmark]
+ public void Compute1MSegments()
+ {
+ path.Vertices = vertices1M;
+ consumer.Consume(path.Segments);
+ }
+
+ private partial class BenchPath : Path
+ {
+ public IEnumerable Segments => BBH.Segments;
+ }
+ }
+}
diff --git a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs
index ea688a65dc..c628e7c1d2 100644
--- a/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs
+++ b/osu.Framework.Tests/Visual/Drawables/TestSceneInteractivePathDrawing.cs
@@ -1,12 +1,18 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osuTK.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Lines;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Graphics.Rendering;
+using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osuTK.Input;
using osu.Framework.Utils;
@@ -18,14 +24,17 @@ namespace osu.Framework.Tests.Visual.Drawables
public partial class TestSceneInteractivePathDrawing : FrameworkTestScene
{
private readonly Path rawDrawnPath;
- private readonly Path approximatedDrawnPath;
+ private readonly TestPath approximatedDrawnPath;
private readonly Path controlPointPath;
private readonly Container controlPointViz;
+ private readonly BoundingBoxVisualizer bbViz;
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder();
public TestSceneInteractivePathDrawing()
{
+ Circle curveAtProgressVis;
+
Child = new Container
{
RelativeSizeAxes = Axes.Both,
@@ -36,7 +45,7 @@ public TestSceneInteractivePathDrawing()
Colour = Color4.DeepPink,
PathRadius = 5,
},
- approximatedDrawnPath = new Path
+ approximatedDrawnPath = new TestPath
{
Colour = Color4.Blue,
PathRadius = 3,
@@ -52,31 +61,62 @@ public TestSceneInteractivePathDrawing()
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f,
},
+ bbViz = new BoundingBoxVisualizer
+ {
+ RelativeSizeAxes = Axes.Both,
+ },
+ curveAtProgressVis = new Circle
+ {
+ Origin = Anchor.Centre,
+ Size = new Vector2(10),
+ Colour = Color4.Yellow
+ }
}
};
- updateViz();
- OnUpdate += _ => updateViz();
-
AddStep("Reset path", () =>
{
bSplineBuilder.Clear();
+ updateViz();
});
AddSliderStep($"{nameof(bSplineBuilder.Degree)}", 1, 4, 3, v =>
{
bSplineBuilder.Degree = v;
+ updateViz();
});
AddSliderStep($"{nameof(bSplineBuilder.Tolerance)}", 0f, 3f, 2f, v =>
{
bSplineBuilder.Tolerance = v;
+ updateViz();
});
AddSliderStep($"{nameof(bSplineBuilder.CornerThreshold)}", 0f, 1f, 0.4f, v =>
{
bSplineBuilder.CornerThreshold = v;
+ updateViz();
+ });
+ AddSliderStep($"{nameof(approximatedDrawnPath.StartProgress)}", 0f, 1f, 0f, v =>
+ {
+ approximatedDrawnPath.StartProgress = v;
+ });
+
+ AddSliderStep($"{nameof(approximatedDrawnPath.EndProgress)}", 0f, 1f, 1f, v =>
+ {
+ approximatedDrawnPath.EndProgress = v;
+ });
+
+ AddSliderStep($"{nameof(approximatedDrawnPath.CurvePositionAt)}", 0f, 1f, 0, v =>
+ {
+ curveAtProgressVis.Position = approximatedDrawnPath.CurvePositionAt(v);
});
}
+ [BackgroundDependencyLoader]
+ private void load(IRenderer renderer)
+ {
+ bbViz.Texture = renderer.WhitePixel;
+ }
+
private void updateControlPointsViz()
{
controlPointPath.Vertices = bSplineBuilder.ControlPoints.SelectMany(o => o).ToArray();
@@ -117,9 +157,18 @@ private void updateViz()
updateControlPointsViz();
}
+ protected override void Update()
+ {
+ base.Update();
+
+ approximatedDrawnPath.CollectBoundingBoxes(bbViz.Boxes);
+ bbViz.Invalidate(Invalidation.DrawNode);
+ }
+
protected override void OnDrag(DragEvent e)
{
bSplineBuilder.AddLinearPoint(rawDrawnPath.ToLocalSpace(ToScreenSpace(e.MousePosition)));
+ updateViz();
}
protected override void OnDragEnd(DragEndEvent e)
@@ -129,5 +178,89 @@ protected override void OnDragEnd(DragEndEvent e)
base.OnDragEnd(e);
}
+
+ private partial class TestPath : Path
+ {
+ public void CollectBoundingBoxes(List list) => BBH.CollectBoundingBoxes(list);
+ }
+
+ private partial class BoundingBoxVisualizer : Sprite
+ {
+ public readonly List Boxes = [];
+
+ public BoundingBoxVisualizer()
+ {
+ RelativeSizeAxes = Axes.Both;
+ }
+
+ protected override DrawNode CreateDrawNode() => new BoundingBoxDrawNode(this);
+
+ private class BoundingBoxDrawNode : SpriteDrawNode
+ {
+ public new BoundingBoxVisualizer Source => (BoundingBoxVisualizer)base.Source;
+
+ public BoundingBoxDrawNode(BoundingBoxVisualizer source)
+ : base(source)
+ {
+ }
+
+ private readonly List boxes = new List();
+
+ public override void ApplyState()
+ {
+ base.ApplyState();
+
+ boxes.Clear();
+ boxes.AddRange(Source.Boxes);
+ }
+
+ protected override void Blit(IRenderer renderer)
+ {
+ ColourInfo colourInfo = DrawColourInfo.Colour;
+ colourInfo.ApplyChild(Color4.Red);
+
+ foreach (var box in boxes)
+ {
+ var drawQuad = new Quad(
+ Vector2Extensions.Transform(box.TopLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.TopRight, DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.TopLeft + new Vector2(0, 1), DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.TopRight + new Vector2(0, 1), DrawInfo.Matrix)
+ );
+
+ renderer.DrawQuad(Texture, drawQuad, colourInfo);
+
+ drawQuad = new Quad(
+ Vector2Extensions.Transform(box.BottomLeft - new Vector2(0, 1), DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomRight - new Vector2(0, 1), DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomRight, DrawInfo.Matrix)
+ );
+
+ renderer.DrawQuad(Texture, drawQuad, colourInfo);
+
+ drawQuad = new Quad(
+ Vector2Extensions.Transform(box.TopLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.TopLeft + new Vector2(1, 0), DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomLeft, DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomLeft + new Vector2(1, 0), DrawInfo.Matrix)
+ );
+
+ renderer.DrawQuad(Texture, drawQuad, colourInfo);
+
+ drawQuad = new Quad(
+ Vector2Extensions.Transform(box.TopRight - new Vector2(1, 0), DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.TopRight, DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomRight - new Vector2(1, 0), DrawInfo.Matrix),
+ Vector2Extensions.Transform(box.BottomRight, DrawInfo.Matrix)
+ );
+
+ renderer.DrawQuad(Texture, drawQuad, colourInfo);
+ }
+ }
+
+ protected internal override bool CanDrawOpaqueInterior => false;
+ }
+ }
}
}
diff --git a/osu.Framework/Graphics/Lines/Path.cs b/osu.Framework/Graphics/Lines/Path.cs
index 85c69475d9..c8245f4145 100644
--- a/osu.Framework/Graphics/Lines/Path.cs
+++ b/osu.Framework/Graphics/Lines/Path.cs
@@ -48,8 +48,7 @@ public IReadOnlyList Vertices
vertices.Clear();
vertices.AddRange(value);
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -72,8 +71,7 @@ public virtual float PathRadius
pathRadius = value;
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -168,49 +166,34 @@ public override Vector2 Size
}
}
- private readonly Cached vertexBoundsCache = new Cached();
-
- private RectangleF vertexBounds
+ public float StartProgress
{
- get
+ get => BBH.StartProgress;
+ set
{
- if (vertexBoundsCache.IsValid)
- return vertexBoundsCache.Value;
-
- if (vertices.Count > 0)
- {
- float minX = 0;
- float minY = 0;
- float maxX = 0;
- float maxY = 0;
-
- foreach (var v in vertices)
- {
- minX = Math.Min(minX, v.X - PathRadius);
- minY = Math.Min(minY, v.Y - PathRadius);
- maxX = Math.Max(maxX, v.X + PathRadius);
- maxY = Math.Max(maxY, v.Y + PathRadius);
- }
-
- return vertexBoundsCache.Value = new RectangleF(minX, minY, maxX - minX, maxY - minY);
- }
-
- return vertexBoundsCache.Value = new RectangleF(0, 0, 0, 0);
+ BBH.StartProgress = value;
+ Invalidate(Invalidation.DrawSize);
}
}
- public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ public float EndProgress
{
- var localPos = ToLocalSpace(screenSpacePos);
- float pathRadiusSquared = PathRadius * PathRadius;
-
- foreach (var t in segments)
+ get => BBH.EndProgress;
+ set
{
- if (t.DistanceSquaredToPoint(localPos) <= pathRadiusSquared)
- return true;
+ BBH.EndProgress = value;
+ Invalidate(Invalidation.DrawSize);
}
+ }
- return false;
+ private RectangleF vertexBounds => BBH.VertexBounds;
+
+ public Vector2 CurvePositionAt(float progress) => BBH.CurvePositionAt(progress)?.position ?? Vector2.Zero;
+
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
+ {
+ var localPos = ToLocalSpace(screenSpacePos);
+ return BBH.Contains(localPos);
}
public Vector2 PositionInBoundingBox(Vector2 pos) => pos - vertexBounds.TopLeft;
@@ -222,8 +205,7 @@ public void ClearVertices()
vertices.Clear();
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -232,8 +214,7 @@ public void AddVertex(Vector2 pos)
{
vertices.Add(pos);
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
@@ -242,29 +223,21 @@ public void ReplaceVertex(int index, Vector2 pos)
{
vertices[index] = pos;
- vertexBoundsCache.Invalidate();
- segmentsCache.Invalidate();
+ bbhCache.Invalidate();
Invalidate(Invalidation.DrawSize);
}
- private readonly List segmentsBacking = new List();
- private readonly Cached segmentsCache = new Cached();
- private List segments => segmentsCache.IsValid ? segmentsBacking : generateSegments();
+ private readonly PathBBH bbhBacking = new PathBBH();
+ private readonly Cached bbhCache = new Cached();
- private List generateSegments()
- {
- segmentsBacking.Clear();
+ protected PathBBH BBH => bbhCache.IsValid ? bbhBacking : computeBBH();
- if (vertices.Count > 1)
- {
- Vector2 offset = vertexBounds.TopLeft;
- for (int i = 0; i < vertices.Count - 1; ++i)
- segmentsBacking.Add(new Line(vertices[i] - offset, vertices[i + 1] - offset));
- }
-
- segmentsCache.Validate();
- return segmentsBacking;
+ private PathBBH computeBBH()
+ {
+ bbhBacking.SetVertices(vertices, pathRadius);
+ bbhCache.Validate();
+ return bbhBacking;
}
private Texture texture;
@@ -353,6 +326,7 @@ protected override void Dispose(bool isDisposing)
texture = null;
sharedData.Dispose();
+ bbhBacking?.Dispose();
}
}
}
diff --git a/osu.Framework/Graphics/Lines/PathBBH.cs b/osu.Framework/Graphics/Lines/PathBBH.cs
new file mode 100644
index 0000000000..5b1709da98
--- /dev/null
+++ b/osu.Framework/Graphics/Lines/PathBBH.cs
@@ -0,0 +1,508 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Buffers;
+using System.Collections.Generic;
+using osu.Framework.Graphics.Primitives;
+using osu.Framework.Utils;
+using osuTK;
+
+namespace osu.Framework.Graphics.Lines
+{
+ ///
+ /// A Bounding Box Hierarchy of a set of vertices which when drawn consecutively represent a path.
+ ///
+ public class PathBBH : IDisposable
+ {
+ public IEnumerable Segments
+ {
+ get
+ {
+ if (segmentCount > 0)
+ {
+ for (int i = firstLeafIndex; i <= lastLeafIndex; i++)
+ yield return new Line(nodes[i].StartPoint, nodes[i].EndPoint);
+ }
+ }
+ }
+
+ private float startProgress;
+
+ public float StartProgress
+ {
+ get => startProgress;
+ set
+ {
+ if (startProgress == value || segmentCount == 0)
+ return;
+
+ startProgress = Math.Clamp(value, 0, endProgress - float.Epsilon);
+ updateStartProgress(startProgress);
+ VertexBounds = RectangleF.Union(nodes[0].Bounds, RectangleF.Empty);
+ }
+ }
+
+ private float endProgress;
+
+ public float EndProgress
+ {
+ get => endProgress;
+ set
+ {
+ if (endProgress == value || segmentCount == 0)
+ return;
+
+ endProgress = Math.Clamp(value, startProgress + float.Epsilon, 1);
+ updateEndProgress(endProgress);
+ VertexBounds = RectangleF.Union(nodes[0].Bounds, RectangleF.Empty);
+ }
+ }
+
+ public int TreeVersion { get; private set; }
+
+ public Line FirstSegment => nodes[modifiedStartNodeIndex ?? firstLeafIndex].CurrentSegment;
+ public Line LastSegment => nodes[modifiedEndNodeIndex ?? lastLeafIndex].CurrentSegment;
+
+ public int RangeStart => (modifiedStartNodeIndex ?? firstLeafIndex) - firstLeafIndex;
+ public int RangeEnd => segmentCount - (lastLeafIndex - (modifiedEndNodeIndex ?? lastLeafIndex)) - 1;
+
+ public RectangleF VertexBounds { get; private set; } = RectangleF.Empty;
+
+ private float radius;
+ private BBHNode[] nodes = null!;
+ private int treeDepth;
+ private int firstLeafIndex;
+ private int lastLeafIndex;
+ private int segmentCount;
+ private float totalLength;
+ private int? modifiedStartNodeIndex;
+ private int? modifiedEndNodeIndex;
+ private bool rented;
+
+ public void SetVertices(IReadOnlyList vertices, float pathRadius)
+ {
+ radius = pathRadius;
+ TreeVersion++;
+
+ startProgress = 0;
+ endProgress = 1;
+ modifiedStartNodeIndex = null;
+ modifiedEndNodeIndex = null;
+ totalLength = 0;
+
+ segmentCount = Math.Max(vertices.Count - 1, 0);
+ // Definition of a leaf here is a node containing a segment
+ int maxLeafCount = Math.Max((int)System.Numerics.BitOperations.RoundUpToPowerOf2((uint)segmentCount), 1);
+ treeDepth = (int)Math.Log2(maxLeafCount);
+ int arrayLength = segmentCount;
+
+ int nodesOnDepth = segmentCount;
+
+ for (int i = treeDepth - 1; i >= 0; i--)
+ {
+ nodesOnDepth = (nodesOnDepth + 1) / 2;
+ arrayLength += nodesOnDepth;
+ }
+
+ firstLeafIndex = arrayLength - segmentCount;
+ lastLeafIndex = arrayLength - 1;
+
+ if (rented)
+ {
+ if (nodes.Length < arrayLength)
+ {
+ ArrayPool.Shared.Return(nodes);
+ nodes = ArrayPool.Shared.Rent(arrayLength);
+ }
+ }
+ else
+ {
+ nodes = ArrayPool.Shared.Rent(arrayLength);
+ rented = true;
+ }
+
+ switch (vertices.Count)
+ {
+ case 0:
+ VertexBounds = RectangleF.Empty;
+ break;
+
+ case 1:
+ VertexBounds = RectangleF.Union(new RectangleF(vertices[0] - new Vector2(radius), new Vector2(radius * 2)), RectangleF.Empty);
+ break;
+
+ default:
+ {
+ for (int i = 0; i < vertices.Count - 1; i++)
+ {
+ var segment = new Line(vertices[i], vertices[i + 1]);
+ totalLength += segment.Rho;
+
+ nodes[firstLeafIndex + i] = new BBHNode
+ {
+ Bounds = lineAABB(segment, radius),
+ StartPoint = segment.StartPoint,
+ EndPoint = segment.EndPoint,
+ CumulativeLength = totalLength,
+ IsLeaf = true
+ };
+ }
+
+ computeParentNodes();
+
+ VertexBounds = RectangleF.Union(nodes[0].Bounds, RectangleF.Empty);
+ break;
+ }
+ }
+ }
+
+ private void computeParentNodes()
+ {
+ if (lastLeafIndex == 0) // bounds are already computed for a node containing a segment
+ return;
+
+ int nodesOnCurrentDepth = segmentCount;
+ int currentNodeIndex = lastLeafIndex - segmentCount;
+
+ for (int i = treeDepth - 1; i >= 0; i--)
+ {
+ int nodesOnNextDepth = nodesOnCurrentDepth;
+ nodesOnCurrentDepth = Math.Max((nodesOnCurrentDepth + 1) / 2, 1);
+
+ for (int j = nodesOnCurrentDepth - 1; j >= 0; j--)
+ {
+ int offset = (nodesOnCurrentDepth - j) + 2 * j;
+ int left = currentNodeIndex + offset;
+ int rightOffset = offset + 1;
+
+ // Right child exists
+ if (rightOffset <= nodesOnNextDepth)
+ {
+ int right = currentNodeIndex + rightOffset;
+
+ nodes[currentNodeIndex] = new BBHNode
+ {
+ Bounds = RectangleF.Union(nodes[left].Bounds, nodes[right].Bounds),
+ Left = left,
+ Right = right,
+ CumulativeLength = nodes[right].CumulativeLength
+ };
+
+ nodes[right].Parent = currentNodeIndex;
+ }
+ else
+ {
+ nodes[currentNodeIndex] = new BBHNode
+ {
+ Bounds = nodes[left].Bounds,
+ Left = left,
+ CumulativeLength = nodes[left].CumulativeLength
+ };
+ }
+
+ nodes[left].Parent = currentNodeIndex;
+
+ currentNodeIndex--;
+ }
+ }
+ }
+
+ private void updateStartProgress(float newStartProgress)
+ {
+ if (modifiedStartNodeIndex.HasValue)
+ {
+ int n = modifiedStartNodeIndex.Value;
+ nodes[n].InterpolatedSegmentStart = null;
+ nodes[n].Bounds = lineAABB(nodes[n].CurrentSegment, radius);
+
+ while (true)
+ {
+ if (nodes[n].Parent is not int parent)
+ break;
+
+ n = parent;
+ int left = nodes[n].Left;
+ int? right = nodes[n].Right;
+
+ nodes[left].Disabled = false;
+ nodes[n].Bounds = !right.HasValue || nodes[right.Value].Disabled ? nodes[left].Bounds : RectangleF.Union(nodes[left].Bounds, nodes[right.Value].Bounds);
+ }
+
+ modifiedStartNodeIndex = null;
+ }
+
+ if (newStartProgress == 0)
+ return;
+
+ var positionAt = CurvePositionAt(newStartProgress);
+
+ if (!positionAt.HasValue)
+ return;
+
+ nodes[positionAt.Value.index].InterpolatedSegmentStart = positionAt.Value.position;
+ nodes[positionAt.Value.index].Bounds = lineAABB(nodes[positionAt.Value.index].CurrentSegment, radius);
+ modifiedStartNodeIndex = positionAt.Value.index;
+
+ int i = modifiedStartNodeIndex.Value;
+
+ while (true)
+ {
+ if (nodes[i].Parent is not int parent)
+ break;
+
+ int modifiedChild = i;
+ i = parent;
+ int left = nodes[i].Left;
+ int? right = nodes[i].Right;
+
+ if (modifiedChild == left)
+ {
+ nodes[i].Bounds = !right.HasValue || nodes[right.Value].Disabled ? nodes[left].Bounds : RectangleF.Union(nodes[left].Bounds, nodes[right.Value].Bounds);
+ }
+ else
+ {
+ nodes[left].Disabled = true;
+ nodes[i].Bounds = nodes[right!.Value].Bounds;
+ }
+ }
+ }
+
+ private void updateEndProgress(float newEndProgress)
+ {
+ if (modifiedEndNodeIndex.HasValue)
+ {
+ int n = modifiedEndNodeIndex.Value;
+ nodes[n].InterpolatedSegmentEnd = null;
+ nodes[n].Bounds = lineAABB(nodes[n].CurrentSegment, radius);
+
+ while (true)
+ {
+ if (nodes[n].Parent is not int parent)
+ break;
+
+ n = parent;
+ int left = nodes[n].Left;
+ int? right = nodes[n].Right;
+
+ if (right.HasValue)
+ nodes[right.Value].Disabled = false;
+
+ nodes[n].Bounds = nodes[left].Disabled ? nodes[right!.Value].Bounds : (!right.HasValue ? nodes[left].Bounds : RectangleF.Union(nodes[left].Bounds, nodes[right.Value].Bounds));
+ }
+
+ modifiedEndNodeIndex = null;
+ }
+
+ if (newEndProgress == 1)
+ return;
+
+ var positionAt = CurvePositionAt(newEndProgress);
+
+ if (!positionAt.HasValue)
+ return;
+
+ nodes[positionAt.Value.index].InterpolatedSegmentEnd = positionAt.Value.position;
+ nodes[positionAt.Value.index].Bounds = lineAABB(nodes[positionAt.Value.index].CurrentSegment, radius);
+ modifiedEndNodeIndex = positionAt.Value.index;
+
+ int i = modifiedEndNodeIndex.Value;
+
+ while (true)
+ {
+ if (nodes[i].Parent is not int parent)
+ break;
+
+ int modifiedChild = i;
+ i = parent;
+ int left = nodes[i].Left;
+ int? right = nodes[i].Right;
+
+ if (modifiedChild == right)
+ {
+ nodes[i].Bounds = nodes[left].Disabled ? nodes[right.Value].Bounds : RectangleF.Union(nodes[left].Bounds, nodes[right.Value].Bounds);
+ }
+ else
+ {
+ if (right.HasValue)
+ nodes[right.Value].Disabled = true;
+
+ nodes[i].Bounds = nodes[left].Bounds;
+ }
+ }
+ }
+
+ public (int index, Vector2 position)? CurvePositionAt(float progress)
+ {
+ if (segmentCount == 0)
+ return null;
+
+ if (progress == 0)
+ return (firstLeafIndex, nodes[firstLeafIndex].StartPoint);
+
+ if (progress == 1)
+ return (lastLeafIndex, nodes[lastLeafIndex].EndPoint);
+
+ float lengthAtProgress = totalLength * progress;
+ int i = 0;
+
+ while (true)
+ {
+ if (nodes[i].IsLeaf)
+ {
+ float segmentLength = nodes[i].CumulativeLength - (i > firstLeafIndex ? nodes[i - 1].CumulativeLength : 0);
+ float lengthFromEnd = nodes[i].CumulativeLength - lengthAtProgress;
+ return (i, Precision.AlmostEquals(segmentLength, 0) ? nodes[i].EndPoint : nodes[i].EndPoint + (nodes[i].StartPoint - nodes[i].EndPoint) * lengthFromEnd / segmentLength);
+ }
+
+ int left = nodes[i].Left;
+ int? right = nodes[i].Right;
+
+ if (lengthAtProgress > nodes[left].CumulativeLength)
+ {
+ if (right.HasValue)
+ i = right.Value;
+ else
+ return (lastLeafIndex, nodes[lastLeafIndex].EndPoint);
+ }
+ else
+ i = left;
+ }
+ }
+
+ public bool Contains(Vector2 pos)
+ {
+ if (segmentCount == 0)
+ return false;
+
+ return contains(pos + VertexBounds.TopLeft, 0);
+ }
+
+ private bool contains(Vector2 position, int? index)
+ {
+ if (!index.HasValue)
+ return false;
+
+ BBHNode node = nodes[index.Value];
+
+ if (node.Disabled || !node.Bounds.Contains(position))
+ return false;
+
+ if (node.IsLeaf)
+ return node.CurrentSegment.DistanceSquaredToPoint(position) < radius * radius;
+
+ return contains(position, node.Left) || contains(position, node.Right);
+ }
+
+ public void CollectBoundingBoxes(List boxes)
+ {
+ boxes.Clear();
+
+ if (segmentCount == 0)
+ return;
+
+ collectBoundingBoxes(0, boxes);
+ }
+
+ private void collectBoundingBoxes(int? index, List boxes)
+ {
+ if (!index.HasValue)
+ return;
+
+ BBHNode node = nodes[index.Value];
+
+ if (node.Disabled)
+ return;
+
+ boxes.Add(new RectangleF(node.Bounds.TopLeft - VertexBounds.TopLeft, node.Bounds.Size));
+
+ if (node.IsLeaf)
+ return;
+
+ collectBoundingBoxes(node.Left, boxes);
+ collectBoundingBoxes(node.Right, boxes);
+ }
+
+ private static RectangleF lineAABB(Line line, float radius)
+ {
+ float minX = MathUtils.BranchlessMin(line.StartPoint.X, line.EndPoint.X);
+ float minY = MathUtils.BranchlessMin(line.StartPoint.Y, line.EndPoint.Y);
+ float maxX = line.StartPoint.X + line.EndPoint.X - minX;
+ float maxY = line.StartPoint.Y + line.EndPoint.Y - minY;
+ return new RectangleF(minX - radius, minY - radius, maxX - minX + radius * 2, maxY - minY + radius * 2);
+ }
+
+ public void Dispose()
+ {
+ if (rented)
+ ArrayPool.Shared.Return(nodes);
+
+ GC.SuppressFinalize(this);
+ }
+
+ private struct BBHNode
+ {
+ ///
+ /// Index of a left child of this in the tree array.
+ ///
+ public int Left { get; init; }
+
+ ///
+ /// Index of a right child of this in the tree array. Null if no right child exists.
+ ///
+ public int? Right { get; init; }
+
+ ///
+ /// Index of a parent of this in the tree array.
+ ///
+ public int? Parent { get; set; }
+
+ ///
+ /// Whether this should not be considered for bounding box calculations.
+ ///
+ public bool Disabled { get; set; }
+
+ ///
+ /// Whether this contains a path segment.
+ ///
+ public bool IsLeaf { get; init; }
+
+ ///
+ /// The line which represents a (modified) path segment in case when this is marked as a .
+ ///
+ public Line CurrentSegment => new Line(InterpolatedSegmentStart ?? StartPoint, InterpolatedSegmentEnd ?? EndPoint);
+
+ ///
+ /// Start position of a segment of this .
+ ///
+ public Vector2 StartPoint { get; init; }
+
+ ///
+ /// End position of a segment of this .
+ ///
+ public Vector2 EndPoint { get; init; }
+
+ ///
+ /// Modified start point of a segment of this . Returns null if no such modification has taken place.
+ ///
+ public Vector2? InterpolatedSegmentStart { get; set; }
+
+ ///
+ /// Modified end point of a segment of this . Returns null if no such modification has taken place.
+ ///
+ public Vector2? InterpolatedSegmentEnd { get; set; }
+
+ ///
+ /// If - sum of lengths of all the s (including the segment of this ) to the left of this .
+ /// Otherwise - max between and nodes.
+ ///
+ public required float CumulativeLength { get; init; }
+
+ ///
+ /// If - bounding box of the .
+ /// Otherwise - combined bounding box of and nodes.
+ ///
+ public required RectangleF Bounds { get; set; }
+ }
+ }
+}
diff --git a/osu.Framework/Graphics/Lines/Path_DrawNode.cs b/osu.Framework/Graphics/Lines/Path_DrawNode.cs
index 3a9c106b83..605a1bd801 100644
--- a/osu.Framework/Graphics/Lines/Path_DrawNode.cs
+++ b/osu.Framework/Graphics/Lines/Path_DrawNode.cs
@@ -30,6 +30,12 @@ private class PathDrawNode : DrawNode
private Vector2 drawSize;
private float radius;
private IShader? pathShader;
+ private int treeVersion;
+ private int rangeStart;
+ private int rangeEnd;
+ private Vector2 offset;
+ private Line firstSegment;
+ private Line lastSegment;
private IVertexBatch? triangleBatch;
@@ -42,8 +48,28 @@ public override void ApplyState()
{
base.ApplyState();
- segments.Clear();
- segments.AddRange(Source.segments);
+ var bbh = Source.BBH;
+
+ int newTreeVersion = bbh.TreeVersion;
+
+ if (newTreeVersion != treeVersion)
+ {
+ segments.Clear();
+ segments.AddRange(bbh.Segments);
+
+ treeVersion = newTreeVersion;
+ }
+
+ rangeStart = bbh.RangeStart;
+ rangeEnd = bbh.RangeEnd;
+
+ if (segments.Count > 0)
+ {
+ firstSegment = bbh.FirstSegment;
+ lastSegment = bbh.LastSegment;
+ }
+
+ offset = bbh.VertexBounds.TopLeft;
texture = Source.Texture;
drawSize = Source.DrawSize;
@@ -269,8 +295,19 @@ private void updateVertexBuffer()
SegmentStartLocation modifiedLocation = SegmentStartLocation.Outside;
SegmentWithThickness? lastDrawnSegment = null;
- for (int i = 0; i < segments.Count; i++)
+ for (int i = rangeStart; i <= rangeEnd; i++)
{
+ Line line;
+
+ if (i == rangeStart)
+ line = firstSegment;
+ else if (i == rangeEnd)
+ line = lastSegment;
+ else
+ line = segments[i];
+
+ line = new Line(line.StartPoint - offset, line.EndPoint - offset);
+
if (segmentToDraw.HasValue)
{
float segmentToDrawLength = segmentToDraw.Value.Rho;
@@ -278,15 +315,15 @@ private void updateVertexBuffer()
// If segment is too short, make its end point equal start point of a new segment
if (segmentToDrawLength < 1f)
{
- segmentToDraw = new Line(segmentToDraw.Value.StartPoint, segments[i].EndPoint);
+ segmentToDraw = new Line(segmentToDraw.Value.StartPoint, line.EndPoint);
continue;
}
- float progress = progressFor(segmentToDraw.Value, segmentToDrawLength, segments[i].EndPoint);
+ float progress = progressFor(segmentToDraw.Value, segmentToDrawLength, line.EndPoint);
Vector2 closest = segmentToDraw.Value.At(progress);
- // Expand segment if next end point is located within a line passing through it
- if (Precision.AlmostEquals(closest, segments[i].EndPoint, 0.1f))
+ // Expand segment if new segment end is located within a line passing through it
+ if (Precision.AlmostEquals(closest, line.EndPoint, 0.1f))
{
if (progress < 0)
{
@@ -309,14 +346,14 @@ private void updateVertexBuffer()
lastDrawnSegment = s;
// Figure out at which point within currently drawn segment the new one starts
- float p = progressFor(segmentToDraw.Value, segmentToDrawLength, segments[i].StartPoint);
- segmentToDraw = segments[i];
+ float p = progressFor(segmentToDraw.Value, segmentToDrawLength, line.StartPoint);
+ segmentToDraw = line;
location = modifiedLocation = Precision.AlmostEquals(p, 1f) ? SegmentStartLocation.End : Precision.AlmostEquals(p, 0f) ? SegmentStartLocation.Start : SegmentStartLocation.Middle;
}
}
else
{
- segmentToDraw = segments[i];
+ segmentToDraw = line;
}
}
diff --git a/osu.Framework/Graphics/Primitives/RectangleF.cs b/osu.Framework/Graphics/Primitives/RectangleF.cs
index d1dad42586..afbd29c50b 100644
--- a/osu.Framework/Graphics/Primitives/RectangleF.cs
+++ b/osu.Framework/Graphics/Primitives/RectangleF.cs
@@ -8,6 +8,7 @@
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
+using osu.Framework.Utils;
using osuTK;
namespace osu.Framework.Graphics.Primitives
@@ -314,10 +315,10 @@ public bool IntersectsWith(RectangleI rect) =>
/// 1
public static RectangleF Union(RectangleF a, RectangleF b)
{
- float x = Math.Min(a.X, b.X);
- float num2 = Math.Max(a.X + a.Width, b.X + b.Width);
- float y = Math.Min(a.Y, b.Y);
- float num4 = Math.Max(a.Y + a.Height, b.Y + b.Height);
+ float x = MathUtils.BranchlessMin(a.X, b.X);
+ float num2 = MathUtils.BranchlessMax(a.X + a.Width, b.X + b.Width);
+ float y = MathUtils.BranchlessMin(a.Y, b.Y);
+ float num4 = MathUtils.BranchlessMax(a.Y + a.Height, b.Y + b.Height);
return new RectangleF(x, y, num2 - x, num4 - y);
}
diff --git a/osu.Framework/Utils/MathUtils.cs b/osu.Framework/Utils/MathUtils.cs
index b32f497e22..ff337118b1 100644
--- a/osu.Framework/Utils/MathUtils.cs
+++ b/osu.Framework/Utils/MathUtils.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
+
namespace osu.Framework.Utils
{
public static class MathUtils
@@ -15,5 +17,17 @@ public static int DivideRoundUp(int value, int divisor)
{
return (value + divisor - 1) / divisor;
}
+
+ public static float BranchlessMin(float value1, float value2)
+ {
+ int b = Convert.ToInt32(value1 < value2);
+ return b * value1 + (1 - b) * value2;
+ }
+
+ public static float BranchlessMax(float value1, float value2)
+ {
+ int b = Convert.ToInt32(value1 > value2);
+ return b * value1 + (1 - b) * value2;
+ }
}
}