3 name:
"FPS Diagnostic",
4 description:
"Collect FPS all over the terrain and create a heatmap of it",
5 shortcut:
"Ctrl+Alt+Shift+F",
6 wbModules: {
"WorldEditor" },
7 awesomeFontCode: 0xF625)]
8class SCR_FPSDiagnosticPlugin : WorldEditorPlugin
14 [
Attribute(
category:
"Camera", defvalue:
"0 " + CAMERA_DEFAULT_ALTITUDE +
" 0",
desc:
"Camera offset from terrain/ocean position (default " + CAMERA_DEFAULT_ALTITUDE +
"m above terrain/ocean)")]
15 protected vector m_vPositionOffset;
18 protected bool m_bAnalyseOnOcean;
21 desc:
"If checked, positions are randomised, otherwise they are done in order (so the area is usually preloaded)"
22 +
"\nDoes not work if the total amount of positions is greater than " +
SCR_Math.
MAX_RANDOM)]
23 protected bool m_bRandomisePositions;
25 [
Attribute(
category:
"Camera", uiwidget:
UIWidgets.Slider,
desc:
"Camera angles in degrees from which to get FPS\nX: 0 = horizon, 90 = look straight up, -30 = look down 30°, etc\nY: 0 = North, -90/270 = West, etc\nZ: banking angle"
26 +
"\n\nIf left empty, 4 cardinal direction cameras with " + CAMERA_DEFAULT_PITCH +
" degrees pitch will be used",
params:
"-180 360")]
27 protected ref array<vector> m_aOrientations;
29 [
Attribute(
category:
"Camera", defvalue:
"2",
desc:
"Delay in seconds before capture starts (to allow proper loading and setting fullscreen)",
params:
"0 30 0.5",
precision: 1)]
30 protected float m_fStartDelay;
33 protected float m_fScenePause;
42 [
Attribute(
category:
"Heatmap", defvalue:
"0", uiwidget:
UIWidgets.ComboBox,
desc:
"- Greyscale: from black to white\n- Thermal: from blue to green to red\n- Alpha: from transparent to white", enums:
SCR_ParamEnumArray.
FromString(
"Greyscale,From black to white;Thermal,From blue to green to red;Alpha,From transparent to white"))]
43 protected int m_iHeatmapColourMode;
45 [
Attribute(
category:
"Heatmap", defvalue:
"0",
desc:
"Invert pixel value (except for max value), e.g black = most dense, white = least dense instead of the opposite")]
46 protected bool m_bHeatmapValueInversion;
48 [
Attribute(
category:
"Heatmap", defvalue:
"1", uiwidget:
UIWidgets.ComboBox,
desc:
"This setting can prevent a density peak from \"darkening\" the image everywhere else - it also offers to highlight pixels above the 0..2×median range", enums:
SCR_ParamEnumArray.
FromString(
"Raw value;2 × average;2 × median"))]
49 protected int m_iHeatmapMaxValueMode;
51 [
Attribute(
category:
"Heatmap", defvalue:
"1",
desc:
"Highlight values above max (when Heat Map Max Value Mode is not Raw)\n- Greyscale: red pixels\n- Thermal: white pixels")]
52 protected bool m_bHeatmapHighlightValuesAboveMax;
54 [
Attribute(
category:
"Heatmap", defvalue: DEFINITION_DEFAULT.ToString(), uiwidget:
UIWidgets.ComboBox,
desc:
"Number of detection squares in terrain height/width", enums:
SCR_ParamEnumArray.
FromString(
"4,4×4,;8,8×8,;16,16×16,;32,32×32,;64,64×64,;128,128×128,;256,256×256,;512,512×512,;1024,1024×1024,;2048,2048×2048,;4096,4096×4096,;1080,1080×1080 (wallpaper),;1440,1440×1440 (wallpaper),;2160,2160×2160 (wallpaper),"))]
55 protected int m_iHeatmapDefinition;
58 protected int m_iHeatmapResolutionFactor;
64 [
Attribute(
category:
"Mode", defvalue:
"1",
desc:
"Use Game mode instead of Workbench mode (Workbench has performance overhead due to edit mode)\nIgnored when Fake Data is used")]
65 protected bool m_bUseGameMode;
67 [
Attribute(
category:
"Mode", defvalue:
"1",
desc:
"Use fullscreen (if Use Game Mode above is ticked)")]
68 protected bool m_bUseFullScreen;
75 protected int m_iOpenHeatmap;
77 [
Attribute(
category:
"Misc", defvalue:
"30", uiwidget:
UIWidgets.Slider,
desc:
"Force progress bar estimate refresh and print a time estimate in the log console every X seconds - 0 = disabled (if used, progress bar still updates its estimate every percent)",
params:
"0 3600 30")]
78 protected int m_iTimeEstimateFrequency;
84 [
Attribute(
category:
"Debug", defvalue:
"0",
desc:
"Use fake (randomised) data - useful to debug water detection\nIgnores Use Game Mode (Workbench itself generates the fake data)")]
85 protected bool m_bUseFakeData;
87 protected bool m_bInternalUseGameMode;
88 protected int m_iLastEstimateTick;
90 protected static const float CAMERA_DEFAULT_ALTITUDE = 7.5;
91 protected static const float CAMERA_DEFAULT_PITCH = -10;
92 protected static const int CAMERA_DEFAULT_ANGLE_COUNT = 4;
94 protected static const int DEFINITION_DEFAULT = 64;
96 protected static const float FPS_UNFOCUSED = 3.0;
97 protected static const float FPS_UNFOCUSED_DELTA = 0.01;
98 protected static const float FPS_UNFOCUSED_DURATION_MS = 0;
99 protected static const int MAX_FPS = 500;
100 protected static const float FPS_HIGH_DURATION_MS = 2000;
101 protected static const int FPS_CHECK_FREQUENCY_MS = 100;
103 protected static const int FPS_RANDOM_MIN = 0;
104 protected static const int FPS_RANDOM_MID = 45;
105 protected static const int FPS_RANDOM_MAX = 60;
107 protected static const float ESTIMATE_FACTOR = 1.10;
110 protected static const string OUTPUT_HEATMAP_NAME =
"Heatmap_FPS_%1_%2_%3x%3.dds";
113 protected override void Run()
115 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
129 if (!SCR_WorldEditorToolHelper.IsWorldLoaded())
135 if (worldEditor.IsPrefabEditMode())
141 IEntitySource terrainEntity = SCR_WorldEditorToolHelper.GetTerrainEntitySource();
148 if (Workbench.ScriptDialog(
"FPS Diagnostic",
"",
this) == 0)
152 if (!terrainEntity.Get(
"coords", terrainOrigin))
155 vector terrainDimensions = SCR_WorldEditorToolHelper.GetTerrainDimensions();
157 if (m_iHeatmapDefinition < 1)
158 m_iHeatmapDefinition = DEFINITION_DEFAULT;
160 float pixelWidth = terrainDimensions[0] / m_iHeatmapDefinition;
161 float pixelHeight = terrainDimensions[2] / m_iHeatmapDefinition;
163 if (pixelWidth < 0.1 || pixelHeight < 0.1)
169 BaseWorld world = worldEditorAPI.GetWorld();
171 bool isOceanEnabled = world.IsOcean();
172 float oceanBaseHeight;
174 oceanBaseHeight = world.GetOceanBaseHeight();
176 array<int> oceanIndices;
177 if (!m_bAnalyseOnOcean && isOceanEnabled)
182 for (
int z = m_iHeatmapDefinition - 1; z >= 0; --z)
184 float zPos = terrainOrigin[2] + (z + 0.5) * pixelHeight;
185 for (
int x; x < m_iHeatmapDefinition; ++x)
187 float xPos = terrainOrigin[0] + (x + 0.5) * pixelWidth;
189 float yPos = worldEditorAPI.GetTerrainSurfaceY(xPos, zPos);
190 if (!m_bAnalyseOnOcean && isOceanEnabled && yPos < oceanBaseHeight)
191 oceanIndices.Insert(pixelIndex);
193 positionsMap.Insert(pixelIndex, { xPos, yPos, zPos });
198 int positionsCount = positionsMap.Count();
199 if (positionsCount < 1)
205 int oceanIndicesCount;
207 oceanIndicesCount = oceanIndices.Count();
209 int relevantPositionsCount = positionsCount - oceanIndicesCount;
210 if (relevantPositionsCount < 1)
216 int orientationsCount = m_aOrientations.Count();
217 if (orientationsCount < 1)
219 if (CAMERA_DEFAULT_ANGLE_COUNT < 1)
225 for (
int i; i < CAMERA_DEFAULT_ANGLE_COUNT; ++i)
227 m_aOrientations.Insert({ CAMERA_DEFAULT_PITCH, i * 360 / CAMERA_DEFAULT_ANGLE_COUNT, 0 });
232 int relevantScenesCount = relevantPositionsCount * orientationsCount;
233 if (relevantScenesCount < 1)
239 string captionPrefix;
241 captionPrefix =
"[DEBUG] ";
243 if (Workbench.ScriptDialog(
244 captionPrefix +
"FPS Diagnostic",
246 "The plugin will process %1 positions in %2 angle(s) for a total of %3 scenes (every %4m)",
247 relevantPositionsCount,
250 pixelWidth.ToString(-1, 1))
252 "\nHeatmap definition: %1×%1 stretched ×%2 to %3×%3",
253 m_iHeatmapDefinition,
254 m_iHeatmapResolutionFactor,
255 m_iHeatmapDefinition * m_iHeatmapResolutionFactor)
257 "\n\n%1 estimated duration with %2s waiting time per scene (theoretical duration ×%3)"
258 +
"\n\nDo NOT touch or close the Workbench during that time"
259 +
"\nDo NOT unfocus the Workbench (unless -forceUpdate is used or you want to abort the benchmark)"
260 +
"\n\nA waiting time of %4s will happen before the benchmark begins.",
261 SCR_FormatHelper.FormatTime(m_fStartDelay + relevantScenesCount * m_fScenePause * ESTIMATE_FACTOR),
265 new SCR_OKCancelWorkbenchDialog()) == 0)
268 float lowestFPS =
float.MAX;
271 array<float> fpsArray = {};
272 fpsArray.Resize(positionsCount * orientationsCount);
275 array<int> resolutionList = {};
276 resolutionList.Reserve(relevantScenesCount * 2);
278 int validPositionsDone;
281 array<int> keys = SCR_MapHelperT<int, vector>.GetKeys(positionsMap);
282 if (m_bRandomisePositions)
285 SCR_ArrayHelperT<int>.Shuffle(keys);
291 string estimatedDuration =
SCR_FormatHelper.FormatTime(relevantScenesCount * m_fScenePause * ESTIMATE_FACTOR);
292 Print(
"Estimated duration: " + estimatedDuration,
LogLevel.NORMAL);
294 float prevProgress, currProgress;
296 if (!m_bInternalUseGameMode)
299 progressText =
"[DEBUG] Generating scenes's performance...\nEstimated time left: %1";
301 progressText =
"Capturing scenes's performance...\nEstimated time left: %1";
304 WBProgressDialog progress;
306 m_bInternalUseGameMode = m_bUseGameMode && !m_bUseFakeData;
308 vector cameraPos, traceEnd, cameraDir;
309 int screenWidth = worldEditorAPI.GetScreenWidth();
310 int screenHeight = worldEditorAPI.GetScreenHeight();;
311 if (!m_bInternalUseGameMode)
312 worldEditorAPI.TraceWorldPos(screenWidth * 0.5, screenHeight * 0.5,
TraceFlags.WORLD, cameraPos, traceEnd, cameraDir);
317 if (m_bInternalUseGameMode)
319 worldEditor.SwitchToGameMode(fullScreen: m_bUseFullScreen);
321 while (!worldEditorAPI.IsGameMode())
323 if (
float.AlmostEqual(
System.GetFPS(), FPS_UNFOCUSED))
325 Print(
"Switching to Game mode got unfocused - capture cancelled",
LogLevel.ERROR);
326 worldEditor.SwitchToEditMode();
337 worldEditor.SwitchToEditMode();
345 worldEditor.SwitchToEditMode();
349 cameraManager.SetCamera(camera);
353 progress =
new WBProgressDialog(
string.Format(progressText, estimatedDuration), worldEditor);
356 if (!m_bUseFakeData && m_fStartDelay > 0)
358 PlaceCamera(worldEditorAPI, camera, terrainOrigin + m_vPositionOffset, -
vector.Up);
360 Print(
"Starting in " + m_fStartDelay +
"s, unfocus the Workbench to cancel",
LogLevel.NORMAL);
361 if (!WaitFocused(m_fStartDelay))
363 PlaceCamera(worldEditorAPI, camera, cameraPos, cameraDir);
366 if (m_bInternalUseGameMode)
367 worldEditor.SwitchToEditMode();
375 if (
float.AlmostEqual(
System.GetFPS(), FPS_UNFOCUSED, FPS_UNFOCUSED_DELTA))
378 if (m_bInternalUseGameMode)
379 worldEditor.SwitchToEditMode();
384 const int startTime =
System.GetTickCount();
385 m_iLastEstimateTick = startTime;
387 Debug.BeginTimeMeasure();
390 foreach (
int index : keys)
394 if (m_iTimeEstimateFrequency > 0 &&
System.GetTickCount(m_iLastEstimateTick) >= m_iTimeEstimateFrequency * 1000)
396 int now =
System.GetTickCount();
397 int spentTime = now - startTime;
399 if (currProgress > 0)
401 float timePerPercentage = (spentTime - m_fStartDelay) / currProgress;
402 float estimatedRemaining = timePerPercentage * (1 - currProgress);
406 progress =
new WBProgressDialog(
string.Format(progressText,
SCR_FormatHelper.FormatTime(estimatedRemaining * 0.001 * ESTIMATE_FACTOR)), worldEditor);
407 progress.SetProgress(currProgress);
411 "Progress: %1%%; Spent time: %2 Estimated remaining time: %3",
412 (currProgress * 100).
ToString(-1, 2),
417 m_iLastEstimateTick = now;
422 if (oceanIndices && oceanIndices.Contains(
index))
424 for (
int i; i < orientationsCount; ++i)
426 fpsArray[
index * orientationsCount + i] = -1;
432 ++validPositionsDone;
433 currProgress = validPositionsDone / relevantPositionsCount;
434 if (currProgress - prevProgress >= 0.01)
438 if (currProgress > 0)
440 int now =
System.GetTickCount();
441 int spentTime = now - startTime;
442 float timePerPercentage = (spentTime - m_fStartDelay) / currProgress;
443 float estimatedRemaining = timePerPercentage * (1 - currProgress);
444 progress =
new WBProgressDialog(
string.Format(progressText,
SCR_FormatHelper.FormatTime(estimatedRemaining * 0.001 * ESTIMATE_FACTOR)), worldEditor);
447 progress.SetProgress(currProgress);
450 prevProgress = currProgress;
453 foreach (
int orientationIndex,
vector orientation : m_aOrientations)
458 if (
float.AlmostEqual(
System.GetFPS(), FPS_UNFOCUSED))
461 worldEditor.SwitchToEditMode();
469 if (m_bInternalUseGameMode && !worldEditorAPI.IsGameMode())
476 float tmp = orientation[0];
477 orientation[0] = orientation[1];
478 orientation[1] = tmp;
480 PlaceCamera(worldEditorAPI, camera,
position + m_vPositionOffset, orientation.AnglesToVector());
482 if (!WaitFocused(m_fScenePause))
484 PlaceCamera(worldEditorAPI, camera,
position + m_vPositionOffset, orientation.AnglesToVector());
488 if (m_bInternalUseGameMode)
489 worldEditor.SwitchToEditMode();
494 PlaceCamera(worldEditorAPI, camera,
position + m_vPositionOffset, orientation.AnglesToVector());
495 sceneFPS =
System.GetFPS();
496 int waitTime = FPS_HIGH_DURATION_MS;
497 while (sceneFPS > MAX_FPS)
499 Sleep(FPS_CHECK_FREQUENCY_MS);
500 PlaceCamera(worldEditorAPI, camera,
position + m_vPositionOffset, orientation.AnglesToVector());
501 sceneFPS =
System.GetFPS();
502 waitTime -= FPS_CHECK_FREQUENCY_MS;
505 worldEditorAPI.SetCamera(cameraPos, cameraDir);
508 if (m_bInternalUseGameMode)
509 worldEditor.SwitchToEditMode();
515 waitTime = FPS_UNFOCUSED_DURATION_MS;
516 while (
float.AlmostEqual(sceneFPS, FPS_UNFOCUSED, FPS_UNFOCUSED_DELTA))
518 Sleep(FPS_CHECK_FREQUENCY_MS);
519 PlaceCamera(worldEditorAPI, camera,
position + m_vPositionOffset, orientation.AnglesToVector());
520 sceneFPS =
System.GetFPS();
521 waitTime -= FPS_CHECK_FREQUENCY_MS;
524 PlaceCamera(worldEditorAPI, camera, cameraPos, cameraDir);
528 if (m_bInternalUseGameMode)
529 worldEditor.SwitchToEditMode();
536 fpsArray[
index * orientationsCount + orientationIndex] = sceneFPS;
537 addedFPS += sceneFPS;
539 if (sceneFPS < lowestFPS)
540 lowestFPS = sceneFPS;
542 if (sceneFPS > highestFPS)
543 highestFPS = sceneFPS;
545 resolutionList.Insert(worldEditorAPI.GetScreenWidth());
546 resolutionList.Insert(worldEditorAPI.GetScreenHeight());
550 Debug.EndTimeMeasure(
"Measuring " + relevantScenesCount +
" scenes");
552 if (m_bInternalUseGameMode)
553 worldEditor.SwitchToEditMode();
555 PlaceCamera(worldEditorAPI, camera, cameraPos, cameraDir);
557 array<float> medianArray = {};
558 medianArray.Copy(fpsArray);
559 medianArray.RemoveItem(-1);
563 if (!medianArray.IsEmpty())
564 medianFPS = medianArray[medianArray.Count() * 0.5];
566 Print(
"Average FPS: " + addedFPS / relevantScenesCount,
LogLevel.NORMAL);
571 int resolutionListCount = resolutionList.Count();
573 bool differentResolutions;
574 for (
int i; i < resolutionListCount; i += 2)
578 width = resolutionList[0];
579 height = resolutionList[1];
583 if (width != resolutionList[i] || height != resolutionList[i + 1])
585 differentResolutions =
true;
591 if (differentResolutions)
593 int avgWidth, avgHeight;
594 for (
int i; i < resolutionListCount; i += 2)
596 avgWidth += resolutionList[i];
597 avgHeight += resolutionList[i + 1];
600 PrintFormat(
"Resolution was changed during the FPS test! Started with %1×%2. Average resolution: %3×%4", screenWidth, screenHeight, avgWidth / relevantScenesCount, avgHeight / relevantScenesCount, level:
LogLevel.WARNING);
609 colourMode =
"Alpha";
617 if (m_bHeatmapValueInversion)
620 string worldName = SCR_WorldEditorToolHelper.GetWorldName();
621 string fileName =
string.Format(OUTPUT_HEATMAP_NAME, worldName, colourMode, m_iHeatmapDefinition);
622 string absoluteFileName;
623 if (!Workbench.GetAbsolutePath(fileName, absoluteFileName,
false) || !CreateImage(absoluteFileName, m_iHeatmapDefinition, fpsArray))
625 Print(
"Heatmap cannot be created at " + absoluteFileName,
LogLevel.ERROR);
629 Print(
"Heatmap successfully created at " + absoluteFileName,
LogLevel.NORMAL);
630 absoluteFileName.Replace(
"/",
"\\");
631 if (m_iOpenHeatmap == 1 || m_iOpenHeatmap == 2)
632 Workbench.RunCmd(
"explorer \"" +
FilePath.StripFileName(absoluteFileName) +
"\"");
634 if (m_iOpenHeatmap == 0 || m_iOpenHeatmap == 2)
635 Workbench.RunCmd(
"explorer \"file:/" +
SCR_StringHelper.DOUBLE_SLASH + absoluteFileName +
"\"");
641 protected bool WaitFocused(
float secondsToWait)
643 int waitTime = secondsToWait * 1000;
647 Sleep(FPS_CHECK_FREQUENCY_MS);
648 waitTime -= FPS_CHECK_FREQUENCY_MS;
650 if (!
float.AlmostEqual(
System.GetFPS(), FPS_UNFOCUSED, FPS_UNFOCUSED_DELTA))
656 unfocusDuration += FPS_CHECK_FREQUENCY_MS;
657 if (unfocusDuration >= FPS_UNFOCUSED_DURATION_MS)
667 if (m_bInternalUseGameMode)
680 camera.SetOrigin(
position + m_vPositionOffset);
695 protected bool CreateImage(
string imagePath,
int definition, notnull array<float> fpsArray)
697 int definitionSq = definition * definition;
698 array<int> imageData = {};
699 imageData.Resize(definitionSq);
701 int scenePerPosition = fpsArray.Count() / definitionSq;
702 if (scenePerPosition < 1)
704 PrintFormat(
"[SCR_FPSDiagnosticPlugin.CreateImage] scenePerPosition %1/%2 = 0!", fpsArray.Count(), definitionSq, level:
LogLevel.ERROR);
708 Debug.BeginTimeMeasure();
710 int fpsArrayCount = fpsArray.Count();
712 for (
int i; i < definitionSq; ++i)
714 float avgOrientationsFPS;
716 for (
int fpsI; fpsI < scenePerPosition; ++fpsI)
718 float fps = fpsArray[i * scenePerPosition + fpsI];
722 avgOrientationsFPS += fps;
727 if (measuresCount != scenePerPosition)
730 if (avgOrientationsFPS >= 0 && measuresCount > 0)
731 avgOrientationsFPS /= measuresCount;
733 imageData[i] =
Math.Round(avgOrientationsFPS);
736 Debug.EndTimeMeasure(
738 "Processing %1 scene FPS for image creation (%2×%2 = %3 pixels)",
739 definition * scenePerPosition,
743 if (m_bUseFakeData && definition < 16)
745 int maxValue = imageData[0];
746 for (
int i; i < fpsArrayCount; i += definition)
749 for (
int j; j < definition; ++j)
751 int datum = imageData[i + j];
752 if (datum > maxValue)
756 toPrint += datum.ToString(2);
758 toPrint +=
"," + datum.ToString(2);
766 Print(
"Image made of " + fpsArrayCount +
" pixels");
767 Print(
"Min = 0, Max = " + maxValue);
773 m_iHeatmapColourMode,
774 m_bHeatmapValueInversion,
775 m_iHeatmapMaxValueMode,
776 m_bHeatmapHighlightValuesAboveMax,
777 m_iHeatmapResolutionFactor);
782 protected int ButtonDiagnose()
789 protected int ButtonClose()
ArmaReforgerScripted GetGame()
IEntity SpawnEntity(ResourceName entityResourceName, notnull IEntity slotOwner)
SCR_DestructionSynchronizationComponentClass ScriptComponentClass int index
UI Textures DeployMenu Briefing conflict_HintBanner_1_UI desc
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
static string GetResolutionFactorEnum(array< int > multipliers=null)
static bool CreateHeatmapImageFromData(string imagePath, notnull array< int > imageData, int colourMode=COLOUR_MODE_GREYSCALE, bool invertColour=false, int maxValueMode=MAX_MODE_RAW, bool highlightValuesAboveMax=false, int resolutionFactor=1)
static float RandomGaussFloat(float min, float mid, float max)
static const int MAX_RANDOM
static ParamEnumArray FromString(string input)
static void PrintDialog(string message, string caption="", LogLevel level=LogLevel.WARNING)
static void PrintFormatDialog(string message, string param1, string param2="", string param3="", string caption="", LogLevel level=LogLevel.WARNING)
proto void Print(void var, LogLevel level=LogLevel.NORMAL)
Prints content of variable to console/log.
LogLevel
Enum with severity of the logging message.
proto void PrintFormat(string fmt, void param1=NULL, void param2=NULL, void param3=NULL, void param4=NULL, void param5=NULL, void param6=NULL, void param7=NULL, void param8=NULL, void param9=NULL, LogLevel level=LogLevel.NORMAL)
SCR_FieldOfViewSettings Attribute
proto external string ToString()
Plain C++ pointer, no weak pointers, no memory management.