Files
UnrealEngine/Engine/Source/Programs/CSVTools/PerfReportTool/Summaries/MapOverlaySummary.cs
2025-05-18 13:04:45 +08:00

332 lines
12 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using PerfReportTool;
using CSVStats;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.Versioning;
namespace PerfSummaries
{
class MapOverlaySummary : Summary
{
class MapOverlayEvent
{
public MapOverlayEvent(string inName)
{
name = inName;
}
public MapOverlayEvent(XElement element)
{
}
public string name;
public string summaryStatName;
public string shortName;
public string lineColor;
};
class MapOverlay
{
public MapOverlay(XElement element, XmlVariableMappings vars)
{
positionStatNames[0] = element.GetSafeAttribute<string>(vars, "xStat");
positionStatNames[1] = element.GetSafeAttribute<string>(vars, "yStat");
positionStatNames[2] = element.GetSafeAttribute<string>(vars, "zStat");
summaryStatNamePrefix = element.GetSafeAttribute<string>(vars, "summaryStatNamePrefix"); // unused!
lineColor = element.GetSafeAttribute<string>(vars, "lineColor", "#ffffff");
foreach (XElement eventEl in element.Elements("event"))
{
MapOverlayEvent ev = new MapOverlayEvent(eventEl.GetRequiredAttribute<string>(vars, "name"));
ev.shortName = eventEl.GetSafeAttribute<string>(vars, "shortName");
ev.summaryStatName = eventEl.GetSafeAttribute<string>(vars, "summaryStatName"); // unused!
ev.lineColor = eventEl.GetSafeAttribute<string>(vars, "lineColor");
if (eventEl.GetSafeAttribute<bool>(vars, "isStartEvent", false))
{
if (startEvent != null)
{
throw new Exception("Can't have multiple start events!");
}
startEvent = ev;
}
events.Add(ev);
}
}
public string[] positionStatNames = new string[3];
public string summaryStatNamePrefix;
public MapOverlayEvent startEvent;
public string lineColor;
public List<MapOverlayEvent> events = new List<MapOverlayEvent>();
}
public MapOverlaySummary(XElement element, XmlVariableMappings vars, string baseXmlDirectory)
{
ReadStatsFromXML(element, vars);
if (stats.Count != 0)
{
throw new Exception("<stats> element is not supported");
}
sourceImagePath = element.GetSafeAttribute<string>(vars, "sourceImage");
if (baseXmlDirectory == null)
{
throw new Exception("BaseXmlDirectory not specified");
}
if (!System.IO.Path.IsPathRooted(sourceImagePath))
{
sourceImagePath = System.IO.Path.GetFullPath(System.IO.Path.Combine(baseXmlDirectory, sourceImagePath));
}
string transformtext = element.GetSafeAttribute(vars, "transform", "");
if (transformtext.Length == 0)
{
// support for previous metadata
float offsetX = element.GetSafeAttribute<float>(vars, "offsetX", 0.0f);
float offsetY = element.GetSafeAttribute<float>(vars, "offsetY", 0.0f);
float scale = element.GetSafeAttribute<float>(vars, "scale", 1.0f);
transform[0] = scale * 0.5f;
transform[1] = 0.0f;
transform[2] = 0.0f;
transform[3] = -scale * 0.5f;
transform[4] = offsetX * 0.5f + 0.5f;
transform[5] = -offsetY * 0.5f + 0.5f;
}
else
{
transform = Array.ConvertAll(transformtext.Split(' ', StringSplitOptions.RemoveEmptyEntries), float.Parse);
}
title = element.GetSafeAttribute(vars, "title", "Events");
destImageFilename = element.GetRequiredAttribute<string>(vars, "destImage");
imageWidth = element.GetSafeAttribute<float>(vars, "width", 250.0f);
imageHeight = element.GetSafeAttribute<float>(vars, "height", 250.0f);
framesPerLineSegment = element.GetSafeAttribute<int>(vars, "framesPerLineSegment", 5);
lineSplitDistanceThreshold = element.GetSafeAttribute<float>(vars, "lineSplitDistanceThreshold", float.MaxValue);
foreach (XElement overlayEl in element.Elements("overlay"))
{
MapOverlay overlay = new MapOverlay(overlayEl, vars);
overlays.Add(overlay);
stats.Add(overlay.positionStatNames[0]);
stats.Add(overlay.positionStatNames[1]);
stats.Add(overlay.positionStatNames[2]);
}
}
public MapOverlaySummary() { }
public override string GetName() { return "mapoverlay"; }
int toSvgX(float worldX, float worldY)
{
float svgX = worldX * transform[0] + worldY * transform[1] + transform[4];
svgX *= imageWidth;
return (int)(svgX + 0.5f);
}
int toSvgY(float worldX, float worldY)
{
float svgY = worldX * transform[2] + worldY * transform[3] + transform[5];
svgY *= imageHeight;
return (int)(svgY + 0.5f);
}
private void CopyAndResizeImage(string sourceImagePath, string destImagePath, int destWidth, int destHeight)
{
if (!OperatingSystem.IsWindows())
{
Console.WriteLine("CopyAndResizeImage is not supported on this platform!");
return;
}
Console.WriteLine("Downsampling map image.\n Source: " + sourceImagePath + "\n Dest : " + destImagePath);
using (FileStream fileStream = new FileStream(sourceImagePath, FileMode.Open, FileAccess.Read))
{
Console.WriteLine("Reading source image");
using (var image = Image.FromStream(fileStream))
{
Console.WriteLine("Generating downsampled image");
var thumbnail = image.GetThumbnailImage(destWidth, destHeight, null, IntPtr.Zero);
using (var destImageStream = new FileStream(destImagePath, FileMode.OpenOrCreate, FileAccess.Write))
{
Console.WriteLine("Saving downsampled map image: " + destImagePath);
thumbnail.Save(destImageStream, ImageFormat.Jpeg);
}
}
}
}
public override HtmlSection WriteSummaryData(bool bWriteHtml, CsvStats csvStats, CsvStats csvStatsUnstripped, bool bWriteSummaryCsv, SummaryTableRowData rowData, string htmlFileName)
{
HtmlSection htmlSection = null;
// Output HTML
if (bWriteHtml)
{
htmlSection = new HtmlSection(title, bStartCollapsed);
string outputDirectory = System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(htmlFileName));
string outputMapFilename = System.IO.Path.Combine(outputDirectory, destImageFilename);
// Skip the copy if the output file already exists
if (!File.Exists(outputMapFilename))
{
if (File.Exists(sourceImagePath))
{
// Copy the file to the reports folder and reset attributes to ensure it's not readonly if the source is
// File.Copy(sourceImagePath, outputMapFilename);
// File.SetAttributes(outputMapFilename, FileAttributes.Normal);
CopyAndResizeImage(sourceImagePath, outputMapFilename, (int)imageWidth, (int)imageHeight);
}
else
{
Console.WriteLine("[Warning] Can't find source map image: " + sourceImagePath);
}
}
// Check if the file exists in the output directory
htmlSection.WriteLine("<svg version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' width='" + imageWidth + "' height='" + imageHeight + "'>");
htmlSection.WriteLine("<image href='" + destImageFilename + "' width='" + imageWidth + "' height='" + imageHeight + "' />");
// Draw the overlays
foreach (MapOverlay overlay in overlays)
{
StatSamples xStat = csvStats.GetStat(overlay.positionStatNames[0]);
StatSamples yStat = csvStats.GetStat(overlay.positionStatNames[1]);
if (xStat == null || yStat == null)
{
continue;
}
// If a startevent is specified, update the start frame
int startFrame = 0;
if (overlay.startEvent != null)
{
foreach (CsvEvent ev in csvStats.Events)
{
if (CsvStats.DoesSearchStringMatch(ev.Name, overlay.startEvent.name))
{
startFrame = ev.Frame;
break;
}
}
}
// Make a mapping from frame to map indices
List<KeyValuePair<int, MapOverlayEvent>> frameEvents = new List<KeyValuePair<int, MapOverlayEvent>>();
foreach (MapOverlayEvent mapEvent in overlay.events)
{
foreach (CsvEvent ev in csvStats.Events)
{
if (CsvStats.DoesSearchStringMatch(ev.Name, mapEvent.name))
{
frameEvents.Add(new KeyValuePair<int, MapOverlayEvent>(ev.Frame, mapEvent));
}
}
}
frameEvents.Sort((pair0, pair1) => pair0.Key.CompareTo(pair1.Key));
int eventIndex = 0;
// Draw the lines
string currentLineColor = overlay.lineColor;
string lineStartTemplate = "<polyline style='fill:none;stroke-width:1.3;stroke:{LINECOLOUR}' points='";
htmlSection.Write(lineStartTemplate.Replace("{LINECOLOUR}", currentLineColor));
float adjustedLineSplitDistanceThreshold = lineSplitDistanceThreshold * framesPerLineSegment;
float oldx = 0;
float oldy = 0;
int lastFrameIndex = -1;
for (int i = startFrame; i < xStat.samples.Count; i += framesPerLineSegment)
{
float x = xStat.samples[i];
float y = yStat.samples[i];
string lineCoordsStr = toSvgX(x, y) + "," + toSvgY(x, y) + " ";
// Figure out which event we're up to so we can do color changes
bool restartLineStrip = false;
while (eventIndex < frameEvents.Count && lastFrameIndex < frameEvents[eventIndex].Key && i >= frameEvents[eventIndex].Key)
{
MapOverlayEvent mapEvent = frameEvents[eventIndex].Value;
string newLineColor = mapEvent.lineColor != null ? mapEvent.lineColor : overlay.lineColor;
// If we changed color, restart the line strip
if (newLineColor != currentLineColor)
{
currentLineColor = newLineColor;
restartLineStrip = true;
}
eventIndex++;
}
// If the distance between this point and the last is over the threshold, restart the line strip
float maxManhattanDist = Math.Max(Math.Abs(x - oldx), Math.Abs(y - oldy));
if (maxManhattanDist > adjustedLineSplitDistanceThreshold)
{
restartLineStrip = true;
}
else
{
htmlSection.Write(lineCoordsStr);
}
if (restartLineStrip)
{
htmlSection.WriteLine("'/>");
htmlSection.Write(lineStartTemplate.Replace("{LINECOLOUR}", currentLineColor));
htmlSection.Write(lineCoordsStr);
}
oldx = x;
oldy = y;
lastFrameIndex = i;
}
htmlSection.WriteLine("'/>");
// Plot the events
float circleRadius = 3;
string eventColourString = "#ffffff";
foreach (MapOverlayEvent mapEvent in overlay.events)
{
foreach (CsvEvent ev in csvStats.Events)
{
if (CsvStats.DoesSearchStringMatch(ev.Name, mapEvent.name))
{
string eventText = mapEvent.shortName != null ? mapEvent.shortName : ev.Name;
float x = xStat.samples[ev.Frame];
float y = yStat.samples[ev.Frame];
int svgX = toSvgX(x, y);
int svgY = toSvgY(x, y);
htmlSection.Write("<circle cx='" + svgX + "' cy='" + svgY + "' r='" + circleRadius + "' fill='" + eventColourString + "' fill-opacity='1.0'/>");
htmlSection.WriteLine("<text x='" + (svgX + 5) + "' y='" + svgY + "' text-anchor='left' style='font-family: Verdana;fill: #ffffff; font-size: " + 9 + "px;'>" + eventText + "</text>");
}
}
}
}
//htmlSection.WriteLine("<text x='50%' y='" + (imageHeight * 0.05) + "' text-anchor='middle' style='font-family: Verdana;fill: #FFFFFF; stroke: #C0C0C0; font-size: " + 20 + "px;'>" + title + "</text>");
htmlSection.WriteLine("</svg>");
}
// Output row data
if (rowData != null)
{
}
return htmlSection;
}
public override void PostInit(ReportTypeInfo reportTypeInfo, CsvStats csvStats)
{
}
string title;
string sourceImagePath;
float[] transform = new float[6];
string destImageFilename;
float imageWidth;
float imageHeight;
float lineSplitDistanceThreshold;
int framesPerLineSegment;
List<MapOverlay> overlays = new List<MapOverlay>();
};
}