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; + } } }