From f6e3777cc6d54507bf151337025c02853cfeb1aa Mon Sep 17 00:00:00 2001 From: CarnifexOptimus <156172553+CarnifexOptimus@users.noreply.github.com> Date: Fri, 21 Jun 2024 12:44:05 +0200 Subject: [PATCH] performance improvements --- BossMod/BossModule/AOEShapes.cs | 32 ++----- BossMod/BossModule/ArenaBounds.cs | 19 ++-- BossMod/BossModule/Shapes.cs | 16 ++-- .../DRS7StygimolochLord/Border.cs | 23 ++--- BossMod/Util/Polygon.cs | 47 +++++++++ BossMod/Util/ShapeDistance.cs | 96 ++++++++++--------- 6 files changed, 136 insertions(+), 97 deletions(-) diff --git a/BossMod/BossModule/AOEShapes.cs b/BossMod/BossModule/AOEShapes.cs index bbdf4b408a..6a29a8392f 100644 --- a/BossMod/BossModule/AOEShapes.cs +++ b/BossMod/BossModule/AOEShapes.cs @@ -163,10 +163,11 @@ public sealed record class AOEShapeCustom(IEnumerable UnionShapes, IEnume private static readonly Dictionary<(string, bool), RelSimplifiedComplexPolygon> _polygonCache = []; private readonly Dictionary<(string, WPos, WPos, Angle, bool), bool> _checkCache = []; private static readonly Dictionary<(string, WPos, Angle, bool), Func> _distanceFuncCache = []; + private readonly string sha512key = CreateCacheKey(UnionShapes, DifferenceShapes ?? []); private RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) { - var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), InvertForbiddenZone); + var cacheKey = (sha512key, InvertForbiddenZone); if (_polygonCache.TryGetValue(cacheKey, out var cachedResult)) return cachedResult; @@ -180,14 +181,13 @@ private RelSimplifiedComplexPolygon GetCombinedPolygon(WPos origin) var clipper = new PolygonClipper(); var finalResult = clipper.Difference(unionOperands, differenceOperands); - _polygonCache[cacheKey] = finalResult; return finalResult; } public override bool Check(WPos position, WPos origin, Angle rotation) { - var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), position, origin, rotation, InvertForbiddenZone); + var cacheKey = (sha512key, position, origin, rotation, InvertForbiddenZone); if (_checkCache.TryGetValue(cacheKey, out var cachedResult)) return cachedResult; var combinedPolygon = GetCombinedPolygon(origin); @@ -205,7 +205,7 @@ private static string CreateCacheKey(IEnumerable unionShapes, IEnumerable return Shape.ComputeSHA512(combinedKey); } - public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneRelPoly(CreateCacheKey(UnionShapes, DifferenceShapes ?? []), GetCombinedPolygon(origin), color); + public override void Draw(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.AOE) => arena.ZoneRelPoly(sha512key, GetCombinedPolygon(origin), color); public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint color = ArenaColor.Danger) { @@ -239,27 +239,11 @@ public override void Outline(MiniArena arena, WPos origin, Angle rotation, uint public override Func Distance(WPos origin, Angle rotation) { - // TODO: Distance maps should probably be cloned instead of being saved in a dictionary - var cacheKey = (CreateCacheKey(UnionShapes, DifferenceShapes ?? []), origin, rotation, InvertForbiddenZone); + var cacheKey = (sha512key, origin, rotation, InvertForbiddenZone); if (_distanceFuncCache.TryGetValue(cacheKey, out var cachedFunc)) return cachedFunc; - var unionDistanceFuncs = UnionShapes.Select(shape => shape.Distance()).ToList(); - var differenceDistanceFuncs = (DifferenceShapes ?? []).Select(shape => shape.Distance()).ToList(); - - float combinedDistanceFunc(WPos p) - { - var minUnionDist = unionDistanceFuncs.Min(func => func(p)); - var maxDifferenceDist = differenceDistanceFuncs.Count != 0 ? differenceDistanceFuncs.Max(func => -func(p)) : float.MinValue; - return Math.Max(minUnionDist, maxDifferenceDist); - } - - float finalFunc(WPos p) - { - var result = combinedDistanceFunc(p); - return InvertForbiddenZone ? -result : result; - } - - _distanceFuncCache[cacheKey] = finalFunc; - return finalFunc; + var result = InvertForbiddenZone ? ShapeDistance.InvertedPolygonWithHoles(origin, GetCombinedPolygon(origin)) : ShapeDistance.PolygonWithHoles(origin, GetCombinedPolygon(origin)); + _distanceFuncCache[cacheKey] = result; + return result; } } diff --git a/BossMod/BossModule/ArenaBounds.cs b/BossMod/BossModule/ArenaBounds.cs index d8392443ca..5cc6117cf0 100644 --- a/BossMod/BossModule/ArenaBounds.cs +++ b/BossMod/BossModule/ArenaBounds.cs @@ -277,6 +277,7 @@ private Pathfinding.Map BuildMap() var (halfWidth, halfHeight) = CalculatePolygonProperties(polygon); var map = new Pathfinding.Map(MapResolution, Center, halfWidth, halfHeight); + // due to being an embarassingly parallel problem this is faster than using a proper ShapeDistance func Parallel.ForEach(map.EnumeratePixels(), (pixel) => { var (x, y, pos) = pixel; @@ -285,7 +286,6 @@ private Pathfinding.Map BuildMap() var allPointsInside = samplePoints.All(polygon.Contains); map.Pixels[y * map.Width + x].MaxG = allPointsInside ? float.MaxValue : 0; }); - return map; } @@ -312,22 +312,23 @@ private List GenerateSamplePoints(WDir relativeCenter, float resolution) // for convenience third list will optionally perform additional unions at the end public record class ArenaBoundsComplex : ArenaBoundsCustom { + public ArenaBoundsComplex(IEnumerable UnionShapes, IEnumerable? DifferenceShapes = null, IEnumerable? AdditionalShapes = null, float MapResolution = 0.5f, float Offset = 0) - : base(BuildBounds(UnionShapes, DifferenceShapes ?? [], AdditionalShapes ?? [], MapResolution, Offset)) + : base(BuildBounds(UnionShapes, DifferenceShapes, AdditionalShapes, MapResolution, Offset, out var center)) { - var properties = CalculatePolygonProperties(UnionShapes, DifferenceShapes ?? [], AdditionalShapes ?? []); - Center = properties.Center; + Center = center; } - private static ArenaBoundsCustom BuildBounds(IEnumerable unionShapes, IEnumerable differenceShapes, IEnumerable additionalShapes, float mapResolution, float offset) + private static ArenaBoundsCustom BuildBounds(IEnumerable unionShapes, IEnumerable? differenceShapes, IEnumerable? additionalShapes, float mapResolution, float offset, out WPos center) { - var props = CalculatePolygonProperties(unionShapes, differenceShapes, additionalShapes); - return new ArenaBoundsCustom(props.Radius, props.Poly, mapResolution, offset); + var cacheKey = CreateCacheKey(unionShapes, differenceShapes ?? [], additionalShapes ?? []); + var properties = CalculatePolygonProperties(cacheKey, unionShapes, differenceShapes ?? [], additionalShapes ?? []); + center = properties.Center; + return new ArenaBoundsCustom(properties.Radius, properties.Poly, mapResolution, offset); } - private static (WPos Center, float Radius, RelSimplifiedComplexPolygon Poly) CalculatePolygonProperties(IEnumerable unionShapes, IEnumerable differenceShapes, IEnumerable additionalShapes) + private static (WPos Center, float Radius, RelSimplifiedComplexPolygon Poly) CalculatePolygonProperties(string cacheKey, IEnumerable unionShapes, IEnumerable differenceShapes, IEnumerable additionalShapes) { - var cacheKey = CreateCacheKey(unionShapes, differenceShapes, additionalShapes); if (StaticCache.TryGetValue(cacheKey, out var cachedResult)) return ((WPos, float, RelSimplifiedComplexPolygon))cachedResult; diff --git a/BossMod/BossModule/Shapes.cs b/BossMod/BossModule/Shapes.cs index 6b837e5c6b..f465a40783 100644 --- a/BossMod/BossModule/Shapes.cs +++ b/BossMod/BossModule/Shapes.cs @@ -87,11 +87,11 @@ private bool IsConvex() return isConvex; } - private bool IsCounterClockwise() + private bool IsClockwise() { - var hash = ComputeHash() + "IsCounterClockwise"; - if (propertyCache.TryGetValue(hash, out var isCounterClockwise)) - return isCounterClockwise; + var hash = ComputeHash() + "IsClockwise"; + if (propertyCache.TryGetValue(hash, out var isClockwise)) + return isClockwise; var vertices = Vertices.ToList(); float area = 0; @@ -101,12 +101,12 @@ private bool IsCounterClockwise() var p1 = vertices[(i + 1) % vertices.Count]; area += (p1.X - p0.X) * (p1.Z + p0.Z); } - isCounterClockwise = area > 0; - propertyCache[hash] = isCounterClockwise; - return isCounterClockwise; + isClockwise = area < 0; + propertyCache[hash] = isClockwise; + return isClockwise; } - public override Func Distance() => IsConvex() ? ShapeDistance.ConvexPolygon(Vertices, !IsCounterClockwise()) : ShapeDistance.ConcavePolygon(Vertices); + public override Func Distance() => IsConvex() ? ShapeDistance.ConvexPolygon(Vertices, IsClockwise()) : ShapeDistance.ConcavePolygon(Vertices); public override string ComputeHash() { diff --git a/BossMod/Modules/Shadowbringers/Foray/DelubrumReginaeSavage/DRS7StygimolochLord/Border.cs b/BossMod/Modules/Shadowbringers/Foray/DelubrumReginaeSavage/DRS7StygimolochLord/Border.cs index b8391c0f48..97c1b73d3b 100644 --- a/BossMod/Modules/Shadowbringers/Foray/DelubrumReginaeSavage/DRS7StygimolochLord/Border.cs +++ b/BossMod/Modules/Shadowbringers/Foray/DelubrumReginaeSavage/DRS7StygimolochLord/Border.cs @@ -10,8 +10,7 @@ class Border(BossModule module) : Components.GenericAOEs(module) private const float _alcoveDepth = 1; private const float _alcoveWidth = 2; private bool Active; - private static readonly List labyrinth = [new PolygonCustom(ConvertToWPos(InDanger())), - new PolygonCustom(ConvertToWPos(MidDanger())), new PolygonCustom(ConvertToWPos(OutDanger()))]; + private static readonly List labyrinth = [new PolygonCustom(InDanger()), new PolygonCustom(MidDanger()), new PolygonCustom(OutDanger())]; public static readonly AOEShapeCustom customShape = new(labyrinth); public static readonly ArenaBounds labPhase = new ArenaBoundsComplex([new Circle(BoundsCenter, 34.5f)], labyrinth); @@ -30,23 +29,23 @@ public override void OnCastFinished(Actor caster, ActorCastInfo spell) } } - private static IEnumerable RingBorder(Angle centerOffset, float ringRadius, bool innerBorder) + private static IEnumerable RingBorder(Angle centerOffset, float ringRadius, bool innerBorder) { float offsetMultiplier = innerBorder ? -1 : 1; var halfWidth = (_alcoveWidth / ringRadius).Radians(); for (var i = 0; i < 8; ++i) { var centerAlcove = centerOffset + i * 45.Degrees(); - foreach (var p in CurveApprox.CircleArc(ringRadius + offsetMultiplier * (_ringHalfWidth + _alcoveDepth), centerAlcove - halfWidth, centerAlcove + halfWidth, Shape.MaxApproxError)) + foreach (var p in CurveApprox.CircleArc(BoundsCenter, ringRadius + offsetMultiplier * (_ringHalfWidth + _alcoveDepth), centerAlcove - halfWidth, centerAlcove + halfWidth, Shape.MaxApproxError)) yield return p; - foreach (var p in CurveApprox.CircleArc(ringRadius + offsetMultiplier * _ringHalfWidth, centerAlcove + halfWidth, centerAlcove + 45.Degrees() - halfWidth, Shape.MaxApproxError)) + foreach (var p in CurveApprox.CircleArc(BoundsCenter, ringRadius + offsetMultiplier * _ringHalfWidth, centerAlcove + halfWidth, centerAlcove + 45.Degrees() - halfWidth, Shape.MaxApproxError)) yield return p; } } - private static IEnumerable RepeatFirst(IEnumerable pts) + private static IEnumerable RepeatFirst(IEnumerable pts) { - WDir? first = null; + WPos? first = null; foreach (var p in pts) { first ??= p; @@ -56,21 +55,19 @@ private static IEnumerable RepeatFirst(IEnumerable pts) yield return first.Value; } - private static IEnumerable InDanger() => RingBorder(22.5f.Degrees(), _innerRingRadius, true); + private static IEnumerable InDanger() => RingBorder(22.5f.Degrees(), _innerRingRadius, true); - private static IEnumerable MidDanger() + private static IEnumerable MidDanger() { var outerRing = RepeatFirst(RingBorder(0.Degrees(), _outerRingRadius, true)); var innerRing = RepeatFirst(RingBorder(22.5f.Degrees(), _innerRingRadius, false)).Reverse(); return outerRing.Concat(innerRing); } - private static IEnumerable OutDanger() + private static IEnumerable OutDanger() { - var outerBoundary = RepeatFirst(CurveApprox.Circle(34.6f, Shape.MaxApproxError)); + var outerBoundary = RepeatFirst(CurveApprox.Circle(BoundsCenter, 34.6f, Shape.MaxApproxError)); var innerRing = RepeatFirst(RingBorder(0.Degrees(), _outerRingRadius, false)).Reverse(); return outerBoundary.Concat(innerRing); } - - private static IEnumerable ConvertToWPos(IEnumerable directions) => directions.Select(dir => new WPos(BoundsCenter.X + dir.X, BoundsCenter.Z + dir.Z)); } diff --git a/BossMod/Util/Polygon.cs b/BossMod/Util/Polygon.cs index 8abfa1b8ff..b020b398e3 100644 --- a/BossMod/Util/Polygon.cs +++ b/BossMod/Util/Polygon.cs @@ -337,4 +337,51 @@ public static bool IsConvex(ReadOnlySpan contour) } return cross != 0; } + + public static bool IsPointInsideConcavePolygon(WPos point, IEnumerable vertices) + { + var intersections = 0; + var verticesList = vertices.ToList(); + for (var i = 0; i < verticesList.Count; i++) + { + var a = verticesList[i]; + var b = verticesList[(i + 1) % verticesList.Count]; + if (RayIntersectsEdge(point, a, b)) + intersections++; + } + return intersections % 2 != 0; + } + + public static bool RayIntersectsEdge(WPos point, WPos a, WPos b) + { + if (a.Z > b.Z) + (b, a) = (a, b); + if (point.Z == a.Z || point.Z == b.Z) + point = new WPos(point.X, point.Z + 0.0001f); + if (point.Z > b.Z || point.Z < a.Z || point.X >= Math.Max(a.X, b.X)) + return false; + if (point.X < Math.Min(a.X, b.X)) + return true; + var red = (point.Z - a.Z) / (b.Z - a.Z); + var blue = (b.X - a.X) * red + a.X; + return point.X < blue; + } + + public static float DistanceToEdge(WPos point, (WPos p1, WPos p2) edge) + { + var (p1, p2) = edge; + var edgeVector = p2 - p1; + var pointVector = point - p1; + var edgeLengthSquared = edgeVector.LengthSq(); + + var t = Math.Max(0, Math.Min(1, pointVector.Dot(edgeVector) / edgeLengthSquared)); + var projection = p1 + t * edgeVector; + return (point - projection).Length(); + } + + public static (WPos, WPos) ConvertToWPos(WPos origin, (WDir, WDir) edge) + { + var (p1, p2) = edge; + return (new WPos(origin.X + p1.X, origin.Z + p1.Z), new WPos(origin.X + p2.X, origin.Z + p2.Z)); + } } diff --git a/BossMod/Util/ShapeDistance.cs b/BossMod/Util/ShapeDistance.cs index 0cfb0e0bc5..2307cb86f5 100644 --- a/BossMod/Util/ShapeDistance.cs +++ b/BossMod/Util/ShapeDistance.cs @@ -2,8 +2,23 @@ // shapes can be defined by distance from point to shape's border; distance is positive for points outside shape and negative for points inside shape // union is min, intersection is max + public static class ShapeDistance { + // for concave polygons this seems to provide a big speed up (seen 10x uplift), for others not so much or even detrimental. not sure why. + public static Func CacheFunction(Func func) + { + var cache = new ConcurrentDictionary(); + return p => + { + if (cache.TryGetValue(p, out var cachedValue)) + return cachedValue; + var result = func(p); + cache[p] = result; + return result; + }; + } + public static Func HalfPlane(WPos point, WDir normal) => p => normal.Dot(p - point); public static Func Circle(WPos origin, float radius) => radius <= 0 ? (_ => float.MaxValue) : (p => (p - origin).Length() - radius); @@ -191,75 +206,70 @@ public static Func ConvexPolygon(IEnumerable<(WPos, WPos)> edges, b Func edge((WPos p1, WPos p2) e) { if (e.p1 == e.p2) - return _ => float.MinValue; + return CacheFunction(_ => float.MinValue); var dir = (e.p2 - e.p1).Normalized(); var normal = cw ? dir.OrthoL() : dir.OrthoR(); - return p => normal.Dot(p - e.p1); + return CacheFunction(p => normal.Dot(p - e.p1)); } - return Intersection([.. edges.Select(edge)], offset); + return CacheFunction(Intersection([.. edges.Select(edge)], offset)); } + public static Func ConvexPolygon(IEnumerable vertices, bool cw, float offset = 0) => ConvexPolygon(PolygonUtil.EnumerateEdges(vertices), cw, offset); public static Func InvertedConvexPolygon(IEnumerable vertices, bool cw, float offset = 0) { - var convexpolygon = ConvexPolygon(vertices, cw, offset); - return p => -convexpolygon(p); + var convexPolygon = ConvexPolygon(vertices, cw, offset); + return p => -convexPolygon(p); } public static Func ConcavePolygon(IEnumerable vertices) { var edges = PolygonUtil.EnumerateEdges(vertices).ToList(); - return p => + return CacheFunction(p => { - var isInside = IsPointInsideConcavePolygon(p, vertices); - var minDistance = edges.Min(e => DistanceToEdge(p, e)); + var isInside = PolygonUtil.IsPointInsideConcavePolygon(p, vertices); + var minDistance = edges.Min(e => PolygonUtil.DistanceToEdge(p, e)); return isInside ? -minDistance : minDistance; - }; + }); } - private static bool IsPointInsideConcavePolygon(WPos point, IEnumerable vertices) + public static Func InvertedConcavePolygon(IEnumerable vertices) { - var intersections = 0; - var verticesList = vertices.ToList(); - for (var i = 0; i < verticesList.Count; i++) - { - var a = verticesList[i]; - var b = verticesList[(i + 1) % verticesList.Count]; - if (RayIntersectsEdge(point, a, b)) - { - intersections++; - } - } - return intersections % 2 != 0; + var concavePolygon = ConcavePolygon(vertices); + return p => -concavePolygon(p); } - private static bool RayIntersectsEdge(WPos point, WPos a, WPos b) + public static Func PolygonWithHoles(WPos origin, RelSimplifiedComplexPolygon polygon) { - if (a.Z > b.Z) - (b, a) = (a, b); - if (point.Z == a.Z || point.Z == b.Z) - point = new WPos(point.X, point.Z + 0.0001f); - if (point.Z > b.Z || point.Z < a.Z || point.X >= Math.Max(a.X, b.X)) - return false; - if (point.X < Math.Min(a.X, b.X)) - return true; - var red = (point.Z - a.Z) / (b.Z - a.Z); - var blue = (b.X - a.X) * red + a.X; - return point.X < blue; + float distanceFunc(WPos p) + { + var localPoint = new WDir(p.X - origin.X, p.Z - origin.Z); + var isInside = polygon.Contains(localPoint); + var minDistance = polygon.Parts.SelectMany(part => part.ExteriorEdges) + .Min(edge => PolygonUtil.DistanceToEdge(p, PolygonUtil.ConvertToWPos(origin, edge))); + + Parallel.ForEach(polygon.Parts, part => + { + Parallel.ForEach(part.Holes, holeIndex => + { + var holeMinDistance = part.InteriorEdges(holeIndex) + .Min(edge => PolygonUtil.DistanceToEdge(p, PolygonUtil.ConvertToWPos(origin, edge))); + lock (polygon) + minDistance = Math.Min(minDistance, holeMinDistance); + }); + }); + return isInside ? -minDistance : minDistance; + } + return CacheFunction(distanceFunc); } - private static float DistanceToEdge(WPos point, (WPos p1, WPos p2) edge) + public static Func InvertedPolygonWithHoles(WPos origin, RelSimplifiedComplexPolygon polygon) { - var (p1, p2) = edge; - var edgeVector = p2 - p1; - var pointVector = point - p1; - var edgeLengthSquared = edgeVector.LengthSq(); - - var t = Math.Max(0, Math.Min(1, pointVector.Dot(edgeVector) / edgeLengthSquared)); - var projection = p1 + t * edgeVector; - return (point - projection).Length(); + var polygonWithHoles = PolygonWithHoles(origin, polygon); + return p => -polygonWithHoles(p); } private static Func Intersection(List> funcs, float offset = 0) => p => funcs.Max(e => e(p)) - offset; private static Func Union(List> funcs, float offset = 0) => p => funcs.Min(e => e(p)) - offset; + }