From cf0e73e75ee8cb92b50faccfac9f25efb0946f9e Mon Sep 17 00:00:00 2001 From: RoosterDragon Date: Fri, 14 Jun 2024 16:55:50 +0100 Subject: [PATCH] Improve performance of copy-paste in map editor. - EditorActorLayer now tracks previews on map with a SpatiallyPartitioned instead of a Dictionary. This allows the copy-paste logic to call an efficient PreviewsInCellRegion method, instead of asking for previews cell-by-cell. - EditorActorPreview subscribes to the CellEntryChanged methods on the map. Previously the preview was refreshed regardless of which cell changed. Now the preview only regenerates if the preview's footprint has been affected. --- OpenRA.Game/Map/CellCoordsRegion.cs | 5 + .../Traits/World/TSEditorResourceLayer.cs | 2 +- .../EditorBrushes/EditorCopyPasteBrush.cs | 6 +- .../EditorBrushes/EditorDefaultBrush.cs | 2 +- .../Traits/World/EditorActorLayer.cs | 94 +++++++++---------- .../Traits/World/EditorActorPreview.cs | 13 ++- .../Logic/Editor/MapEditorSelectionLogic.cs | 3 +- 7 files changed, 63 insertions(+), 62 deletions(-) diff --git a/OpenRA.Game/Map/CellCoordsRegion.cs b/OpenRA.Game/Map/CellCoordsRegion.cs index 6ec0d928b2..2f6914f2c8 100644 --- a/OpenRA.Game/Map/CellCoordsRegion.cs +++ b/OpenRA.Game/Map/CellCoordsRegion.cs @@ -64,6 +64,11 @@ namespace OpenRA BottomRight = bottomRight; } + public bool Contains(CPos cell) + { + return cell.X >= TopLeft.X && cell.X <= BottomRight.X && cell.Y >= TopLeft.Y && cell.Y <= BottomRight.Y; + } + public override string ToString() { return $"{TopLeft}->{BottomRight}"; diff --git a/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs b/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs index 3b3e7ed294..3bf3b92e9a 100644 --- a/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs +++ b/OpenRA.Mods.Cnc/Traits/World/TSEditorResourceLayer.cs @@ -46,7 +46,7 @@ namespace OpenRA.Mods.Cnc.Traits return false; // Cell is automatically valid if it contains a veinhole actor - if (actorLayer.PreviewsAt(neighbour).Any(a => info.VeinholeActors.Contains(a.Info.Name))) + if (actorLayer.PreviewsAtCell(neighbour).Any(a => info.VeinholeActors.Contains(a.Info.Name))) return true; // Neighbour must be flat or a cardinal slope, unless the resource cell itself is a slope diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs b/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs index 1c5bc12052..23de19fe38 100644 --- a/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs +++ b/OpenRA.Mods.Common/EditorBrushes/EditorCopyPasteBrush.cs @@ -165,7 +165,7 @@ namespace OpenRA.Mods.Common.Widgets tiles.Add(cell, new ClipboardTile(mapTiles[cell], mapResources[cell], resourceLayerContents, mapHeight[cell])); if (copyFilters.HasFlag(MapCopyFilters.Actors)) - foreach (var preview in selection.CellCoords.SelectMany(editorActorLayer.PreviewsAt).Distinct()) + foreach (var preview in editorActorLayer.PreviewsInCellRegion(selection.CellCoords)) previews.TryAdd(preview.ID, preview); } @@ -187,7 +187,7 @@ namespace OpenRA.Mods.Common.Widgets // Clear any existing actors in the paste cells. var selectionSize = clipboard.CellRegion.BottomRight - clipboard.CellRegion.TopLeft; var pasteRegion = new CellRegion(map.Grid.Type, pastePosition, pastePosition + selectionSize); - foreach (var regionActor in pasteRegion.CellCoords.SelectMany(editorActorLayer.PreviewsAt).ToHashSet()) + foreach (var regionActor in editorActorLayer.PreviewsInCellRegion(pasteRegion.CellCoords).ToList()) editorActorLayer.Remove(regionActor); } @@ -244,7 +244,7 @@ namespace OpenRA.Mods.Common.Widgets if (copyFilters.HasFlag(MapCopyFilters.Actors)) { // Clear existing actors. - foreach (var regionActor in undoClipboard.CellRegion.CellCoords.SelectMany(editorActorLayer.PreviewsAt).Distinct().ToList()) + foreach (var regionActor in editorActorLayer.PreviewsInCellRegion(undoClipboard.CellRegion.CellCoords).ToList()) editorActorLayer.Remove(regionActor); } diff --git a/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs b/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs index 612babc24b..ba3decbc12 100644 --- a/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs +++ b/OpenRA.Mods.Common/EditorBrushes/EditorDefaultBrush.cs @@ -120,7 +120,7 @@ namespace OpenRA.Mods.Common.Widgets worldPixel = worldRenderer.Viewport.ViewToWorldPx(mi.Location); var cell = worldRenderer.Viewport.ViewToWorld(mi.Location); - var underCursor = editorLayer.PreviewsAt(worldPixel).MinByOrDefault(CalculateActorSelectionPriority); + var underCursor = editorLayer.PreviewsAtWorldPixel(worldPixel).MinByOrDefault(CalculateActorSelectionPriority); var resourceUnderCursor = resourceLayer?.GetResource(cell).Type; if (underCursor != null) diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs index e1acae47be..0b730c0e45 100644 --- a/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs +++ b/OpenRA.Mods.Common/Traits/World/EditorActorLayer.cs @@ -40,8 +40,9 @@ namespace OpenRA.Mods.Common.Traits { readonly EditorActorLayerInfo info; readonly List previews = new(); - readonly Dictionary> cellMap = new(); + int2 cellOffset; + SpatiallyPartitioned cellMap; SpatiallyPartitioned screenMap; WorldRenderer worldRenderer; @@ -74,6 +75,12 @@ namespace OpenRA.Mods.Common.Traits foreach (var pr in Players.Players.Values) wr.UpdatePalettesForPlayer(pr.Name, pr.Color, false); + cellOffset = new int2(world.Map.AllCells.Min(c => c.X), world.Map.AllCells.Min((c) => c.Y)); + var cellOffsetMax = new int2(world.Map.AllCells.Max(c => c.X), world.Map.AllCells.Max((c) => c.Y)); + var mapCellSize = cellOffsetMax - cellOffset; + cellMap = new SpatiallyPartitioned( + mapCellSize.X, mapCellSize.Y, Exts.IntegerDivisionRoundingAwayFromZero(info.BinSize, world.Map.Grid.TileSize.Width)); + var ts = world.Map.Grid.TileSize; var width = world.Map.MapSize.X * ts.Width; var height = world.Map.MapSize.Y * ts.Height; @@ -102,7 +109,7 @@ namespace OpenRA.Mods.Common.Traits if (wr.World.Type != WorldType.Editor) return NoRenderables; - return PreviewsInBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) + return PreviewsInScreenBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) .SelectMany(p => p.Render()); } @@ -117,7 +124,7 @@ namespace OpenRA.Mods.Common.Traits if (wr.World.Type != WorldType.Editor) return NoRenderables; - return PreviewsInBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) + return PreviewsInScreenBox(wr.Viewport.TopLeft, wr.Viewport.BottomRight) .SelectMany(p => p.RenderAnnotations()); } @@ -147,13 +154,9 @@ namespace OpenRA.Mods.Common.Traits if (!preview.Bounds.IsEmpty) screenMap.Add(preview, preview.Bounds); - // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint - var footprint = preview.Footprint.Select(kv => kv.Key).ToArray(); - if (footprint.Length == 0) - footprint = new[] { worldRenderer.World.Map.CellContaining(preview.CenterPosition) }; - - foreach (var cell in footprint) - AddPreviewLocation(preview, cell); + var cellFootprintBounds = Footprint(preview).Select( + cell => new Rectangle(cell.X - cellOffset.X, cell.Y - cellOffset.Y, 1, 1)).Union(); + cellMap.Add(preview, cellFootprintBounds); preview.AddedToEditor(); @@ -166,26 +169,20 @@ namespace OpenRA.Mods.Common.Traits } } + IEnumerable Footprint(EditorActorPreview preview) + { + // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint + if (preview.Footprint.Count == 0) + return new[] { worldRenderer.World.Map.CellContaining(preview.CenterPosition) }; + return preview.Footprint.Keys; + } + public void Remove(EditorActorPreview preview) { previews.Remove(preview); screenMap.Remove(preview); - // Fallback to the actor's CenterPosition for the ActorMap if it has no Footprint - var footprint = preview.Footprint.Select(kv => kv.Key).ToArray(); - if (footprint.Length == 0) - footprint = new[] { worldRenderer.World.Map.CellContaining(preview.CenterPosition) }; - - foreach (var cell in footprint) - { - if (!cellMap.TryGetValue(cell, out var list)) - continue; - - list.Remove(preview); - - if (list.Count == 0) - cellMap.Remove(cell); - } + cellMap.Remove(preview); preview.RemovedFromEditor(); UpdateNeighbours(preview.Footprint); @@ -236,49 +233,46 @@ namespace OpenRA.Mods.Common.Traits { // Include actors inside the footprint too var cells = Util.ExpandFootprint(footprint.Keys, true); - foreach (var p in cells.SelectMany(c => PreviewsAt(c))) + foreach (var p in cells.SelectMany(PreviewsAtCell)) p.ReplaceInit(new RuntimeNeighbourInit(NeighbouringPreviews(p.Footprint))); } - void AddPreviewLocation(EditorActorPreview preview, CPos location) - { - if (!cellMap.TryGetValue(location, out var list)) - { - list = new List(); - cellMap.Add(location, list); - } - - list.Add(preview); - } - Dictionary NeighbouringPreviews(IReadOnlyDictionary footprint) { var cells = Util.ExpandFootprint(footprint.Keys, true).Except(footprint.Keys); - return cells.ToDictionary(c => c, c => PreviewsAt(c).Select(p => p.Info.Name).ToArray()); + return cells.ToDictionary(c => c, c => PreviewsAtCell(c).Select(p => p.Info.Name).ToArray()); } - public IEnumerable PreviewsInBox(int2 a, int2 b) + public IEnumerable PreviewsInScreenBox(int2 a, int2 b) { - return screenMap.InBox(Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y))); + return PreviewsInScreenBox(Rectangle.FromLTRB(Math.Min(a.X, b.X), Math.Min(a.Y, b.Y), Math.Max(a.X, b.X), Math.Max(a.Y, b.Y))); } - public IEnumerable PreviewsInBox(Rectangle r) + public IEnumerable PreviewsInScreenBox(Rectangle r) { return screenMap.InBox(r); } - public IEnumerable PreviewsAt(CPos cell) + public IEnumerable PreviewsInCellRegion(CellCoordsRegion region) { - if (cellMap.TryGetValue(cell, out var list)) - return list; + return cellMap.InBox(Rectangle.FromLTRB( + region.TopLeft.X - cellOffset.X, + region.TopLeft.Y - cellOffset.Y, + region.BottomRight.X - cellOffset.X + 1, + region.BottomRight.Y - cellOffset.Y + 1)) + .Where(p => Footprint(p).Any(region.Contains)); + } - return Enumerable.Empty(); + public IEnumerable PreviewsAtCell(CPos cell) + { + return cellMap.At(new int2(cell.X - cellOffset.X, cell.Y - cellOffset.Y)) + .Where(p => Footprint(p).Any(fp => fp == cell)); } public SubCell FreeSubCellAt(CPos cell) { var map = worldRenderer.World.Map; - var previews = PreviewsAt(cell).ToList(); + var previews = PreviewsAtCell(cell).ToList(); if (previews.Count == 0) return map.Grid.DefaultSubCell; @@ -293,7 +287,7 @@ namespace OpenRA.Mods.Common.Traits return SubCell.Invalid; } - public IEnumerable PreviewsAt(int2 worldPx) + public IEnumerable PreviewsAtWorldPixel(int2 worldPx) { return screenMap.At(worldPx); } @@ -325,9 +319,9 @@ namespace OpenRA.Mods.Common.Traits public void PopulateRadarSignatureCells(Actor self, List<(CPos Cell, Color Color)> destinationBuffer) { - foreach (var previewsForCell in cellMap) - foreach (var preview in previewsForCell.Value) - destinationBuffer.Add((previewsForCell.Key, preview.RadarColor)); + foreach (var preview in cellMap.Items) + foreach (var cell in Footprint(preview)) + destinationBuffer.Add((cell, preview.RadarColor)); } public EditorActorPreview this[string id] diff --git a/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs b/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs index 0613a45362..fcef483161 100644 --- a/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs +++ b/OpenRA.Mods.Common/Traits/World/EditorActorPreview.cs @@ -68,8 +68,8 @@ namespace OpenRA.Mods.Common.Traits if (!world.Map.Rules.Actors.TryGetValue(reference.Type.ToLowerInvariant(), out Info)) throw new InvalidDataException($"Actor {id} of unknown type {reference.Type.ToLowerInvariant()}"); - UpdateFromCellChange(); GenerateFootprint(); + UpdateFromCellChange(null); tooltip = Info.TraitInfos().FirstOrDefault(info => info.EnabledByDefault) as TooltipInfoBase ?? Info.TraitInfos().FirstOrDefault(info => info.EnabledByDefault); @@ -79,8 +79,7 @@ namespace OpenRA.Mods.Common.Traits terrainRadarColorInfo = Info.TraitInfoOrDefault(); UpdateRadarColor(); - // TODO: updating all actors on the map is not very efficient. - onCellEntryChanged = _ => UpdateFromCellChange(); + onCellEntryChanged = cell => UpdateFromCellChange(cell); } public EditorActorPreview WithId(string id) @@ -88,8 +87,11 @@ namespace OpenRA.Mods.Common.Traits return new EditorActorPreview(worldRenderer, id, reference.Clone(), Owner); } - void UpdateFromCellChange() + void UpdateFromCellChange(CPos? cellChanged) { + if (cellChanged != null && !Footprint.ContainsKey(cellChanged.Value)) + return; + CenterPosition = PreviewPosition(worldRenderer.World, reference); GeneratePreviews(); GenerateBounds(); @@ -169,8 +171,8 @@ namespace OpenRA.Mods.Common.Traits foreach (var notify in Info.TraitInfos()) editorData[notify] = notify.AddedToEditor(this, worldRenderer.World); - // TODO: this should subscribe to ramp cell map as well. worldRenderer.World.Map.Height.CellEntryChanged += onCellEntryChanged; + worldRenderer.World.Map.Ramp.CellEntryChanged += onCellEntryChanged; } public void RemovedFromEditor() @@ -179,6 +181,7 @@ namespace OpenRA.Mods.Common.Traits kv.Key.RemovedFromEditor(this, worldRenderer.World, kv.Value); worldRenderer.World.Map.Height.CellEntryChanged -= onCellEntryChanged; + worldRenderer.World.Map.Ramp.CellEntryChanged -= onCellEntryChanged; } public void AddInit(T init) where T : ActorInit diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorSelectionLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorSelectionLogic.cs index 2d7432201b..d2c73a3d2e 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorSelectionLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/MapEditorSelectionLogic.cs @@ -11,7 +11,6 @@ using System; using System.Collections.Generic; -using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.EditorBrushes; using OpenRA.Mods.Common.Traits; @@ -123,7 +122,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic tiles.Add(cell, new ClipboardTile(mapTiles[cell], mapResources[cell], resourceLayer?.GetResource(cell), mapHeight[cell])); if (copyFilters.HasFlag(MapCopyFilters.Actors)) - foreach (var preview in selection.CellCoords.SelectMany(editorActorLayer.PreviewsAt).Distinct()) + foreach (var preview in editorActorLayer.PreviewsInCellRegion(selection.CellCoords)) previews.TryAdd(preview.ID, preview); }