// 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(vars, "xStat"); positionStatNames[1] = element.GetSafeAttribute(vars, "yStat"); positionStatNames[2] = element.GetSafeAttribute(vars, "zStat"); summaryStatNamePrefix = element.GetSafeAttribute(vars, "summaryStatNamePrefix"); // unused! lineColor = element.GetSafeAttribute(vars, "lineColor", "#ffffff"); foreach (XElement eventEl in element.Elements("event")) { MapOverlayEvent ev = new MapOverlayEvent(eventEl.GetRequiredAttribute(vars, "name")); ev.shortName = eventEl.GetSafeAttribute(vars, "shortName"); ev.summaryStatName = eventEl.GetSafeAttribute(vars, "summaryStatName"); // unused! ev.lineColor = eventEl.GetSafeAttribute(vars, "lineColor"); if (eventEl.GetSafeAttribute(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 events = new List(); } public MapOverlaySummary(XElement element, XmlVariableMappings vars, string baseXmlDirectory) { ReadStatsFromXML(element, vars); if (stats.Count != 0) { throw new Exception(" element is not supported"); } sourceImagePath = element.GetSafeAttribute(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(vars, "offsetX", 0.0f); float offsetY = element.GetSafeAttribute(vars, "offsetY", 0.0f); float scale = element.GetSafeAttribute(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(vars, "destImage"); imageWidth = element.GetSafeAttribute(vars, "width", 250.0f); imageHeight = element.GetSafeAttribute(vars, "height", 250.0f); framesPerLineSegment = element.GetSafeAttribute(vars, "framesPerLineSegment", 5); lineSplitDistanceThreshold = element.GetSafeAttribute(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(""); htmlSection.WriteLine(""); // 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> frameEvents = new List>(); foreach (MapOverlayEvent mapEvent in overlay.events) { foreach (CsvEvent ev in csvStats.Events) { if (CsvStats.DoesSearchStringMatch(ev.Name, mapEvent.name)) { frameEvents.Add(new KeyValuePair(ev.Frame, mapEvent)); } } } frameEvents.Sort((pair0, pair1) => pair0.Key.CompareTo(pair1.Key)); int eventIndex = 0; // Draw the lines string currentLineColor = overlay.lineColor; string lineStartTemplate = "= 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(""); htmlSection.WriteLine("" + eventText + ""); } } } } //htmlSection.WriteLine("" + title + ""); htmlSection.WriteLine(""); } // 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 overlays = new List(); }; }