Skip to content

Commit fc791dc

Browse files
authored
Faster implementation of TemperatureGaugeSystem (#270)
Complete reimplementation of TemperatureGaugeSystem : - Minimal update overhead when no gauges are active/shown - Vastly reduced update overhead when gauges/highlight are active/shown - Gauges are now instantiated on demand, eliminating (most of) the cost on vessel load - Gauges are now recycled when the vessel is modified / switched, instead of destroying and re-instantiating them immediately
1 parent c848dda commit fc791dc

File tree

1 file changed

+199
-17
lines changed

1 file changed

+199
-17
lines changed

KSPCommunityFixes/Performance/PartSystemsFastUpdate.cs

Lines changed: 199 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using CompoundParts;
22
using Highlighting;
33
using KSP.UI.Screens.Flight;
4+
using KSP.UI.Util;
45
using System;
56
using System.Collections.Generic;
67
using UnityEngine;
@@ -21,45 +22,226 @@ protected override void ApplyPatches()
2122
AddPatch(PatchType.Override, typeof(CModuleLinkedMesh), nameof(CModuleLinkedMesh.TrackAnchor));
2223
}
2324

25+
// Complete reimplementation of TemperatureGaugeSystem :
26+
// - Minimal update overhead when no gauges are active/shown
27+
// - Vastly reduced update overhead when gauges/highlight are active/shown
28+
// - Gauges are now instantiated on demand, eliminating (most of) the cost on vessel load
29+
// - Gauges are now recycled when the vessel is modified / switched, instead of destroying and re-instantiating them immediately
30+
// Gauges were previously always instantiated for every part on the active vessel, and this was pretty slow due to triggering
31+
// a lot of internal UI/layout/graphic dirtying on every gauge instantiation. Overall, the operation can take several hundred
32+
// milliseconds in not-so large part count situations, leading to very significant hiccups
33+
// TemperatureGaugeSystem.Update average frame time with a ~500 part vessel, gauges not shown : Stock 0.6%, KSPCF 0.04%
34+
// TemperatureGaugeSystem.Update average frame time with a ~500 part vessel, ~16 gauges shown : Stock 1.7%, KSPCF 0.3%
35+
// See https://github.com/KSPModdingLibs/KSPCommunityFixes/issues/194
2436
private static void TemperatureGaugeSystem_Update_Override(TemperatureGaugeSystem tgs)
2537
{
2638
if (!FlightGlobals.ready || !HighLogic.LoadedSceneIsFlight)
2739
return;
2840

29-
if (GameSettings.TEMPERATURE_GAUGES_MODE < 1 || CameraManager.Instance.currentCameraMode != CameraManager.CameraMode.Flight)
41+
if (GameSettings.TEMPERATURE_GAUGES_MODE == 0
42+
|| CameraManager.Instance.currentCameraMode != CameraManager.CameraMode.Flight
43+
|| FlightGlobals.ActiveVessel.IsNullOrDestroyed())
3044
{
31-
if (tgs.gaugeCount > 0)
32-
tgs.DestroyGauges();
45+
if (tgs.visibleGauges.Count == 0)
46+
return;
47+
48+
for (int i = tgs.visibleGauges.Count; i-- > 0;)
49+
{
50+
TemperatureGauge gauge = tgs.visibleGauges[i];
51+
if (gauge.gaugeActive)
52+
gauge.progressBar.gameObject.SetActive(false);
53+
54+
if (gauge.highlightActive && gauge.part.IsNotNullOrDestroyed())
55+
gauge.part.SetHighlightDefault();
56+
57+
gauge.part = null;
58+
gauge.gaugeActive = false;
59+
gauge.showGauge = false;
60+
gauge.highlightActive = false;
61+
}
62+
63+
tgs.visibleGauges.Clear();
3364
return;
3465
}
3566

3667
Vessel activeVessel = FlightGlobals.ActiveVessel;
37-
if (activeVessel.NotDestroyedRefNotEquals(tgs.activeVessel))
38-
tgs.CreateGauges();
68+
List<Part> parts = activeVessel.parts;
69+
int partCount = parts.Count;
3970

40-
if (activeVessel.IsNotNullOrDestroyed() && activeVessel.parts.Count != tgs.partCount)
41-
tgs.RebuildGaugeList();
71+
List<TemperatureGauge> gauges = tgs.gauges;
72+
tgs.visibleGauges.Clear();
4273

43-
if (tgs.gaugeCount == 0)
44-
return;
74+
if (tgs.activeVessel.IsNullRef())
75+
{
76+
tgs.activeVessel = activeVessel;
77+
tgs.partCount = partCount;
78+
}
79+
else if (tgs.activeVessel.RefNotEquals(activeVessel) || tgs.partCount != partCount)
80+
{
81+
tgs.activeVessel = activeVessel;
82+
tgs.partCount = partCount;
4583

46-
tgs.visibleGauges.Clear();
47-
for (int i = tgs.gaugeCount; i-- > 0;)
84+
for (int i = gauges.Count; i-- > 0;)
85+
{
86+
TemperatureGauge gauge = gauges[i];
87+
if (gauge.IsNullRef())
88+
continue;
89+
90+
if (gauge.gaugeActive)
91+
gauge.progressBar.gameObject.SetActive(false);
92+
93+
if (gauge.highlightActive && gauge.part.IsNotNullOrDestroyed())
94+
gauge.part.SetHighlightDefault();
95+
96+
gauge.part = null;
97+
gauge.gaugeActive = false;
98+
gauge.showGauge = false;
99+
gauge.highlightActive = false;
100+
}
101+
}
102+
103+
while (gauges.Count < partCount)
104+
gauges.Add(null);
105+
106+
while (gauges.Count > partCount)
48107
{
49-
TemperatureGauge gauge = tgs.gauges[i];
50-
gauge.GaugeUpdate();
51-
if (gauge.gaugeActive)
52-
tgs.visibleGauges.Add(gauge);
108+
int lastGaugeIdx = gauges.Count - 1;
109+
TemperatureGauge gauge = gauges[lastGaugeIdx];
110+
if (gauge.IsNotNullRef())
111+
UnityEngine.Object.Destroy(gauge.gameObject);
112+
gauges.RemoveAt(lastGaugeIdx);
113+
}
114+
115+
float gaugeThreshold = PhysicsGlobals.instance.temperatureGaugeThreshold;
116+
float gaugeHighlightThreshold = PhysicsGlobals.instance.temperatureGaugeHighlightThreshold;
117+
118+
bool gaugesEnabled = (GameSettings.TEMPERATURE_GAUGES_MODE & 1) > 0;
119+
bool highlightsEnabled = (GameSettings.TEMPERATURE_GAUGES_MODE & 2) > 0;
120+
121+
for (int i = 0; i < partCount; i++)
122+
{
123+
Part part = parts[i];
124+
125+
float skinTempFactor = (float)(part.skinTemperature / part.skinMaxTemp);
126+
float tempFactor = (float)(part.temperature / part.maxTemp);
127+
128+
if (skinTempFactor > tempFactor)
129+
tempFactor = skinTempFactor;
130+
131+
TemperatureGauge gauge = gauges[i];
132+
133+
bool gaugeEnabled = gaugesEnabled && tempFactor > gaugeThreshold * part.gaugeThresholdMult;
134+
bool highlightEnabled = highlightsEnabled && tempFactor > gaugeHighlightThreshold * part.edgeHighlightThresholdMult;
135+
136+
if (gaugeEnabled || highlightEnabled)
137+
{
138+
if (gauge.IsNullRef())
139+
{
140+
gauge = UnityEngine.Object.Instantiate(tgs.temperatureGaugePrefab);
141+
gauge.transform.SetParent(tgs.transform, worldPositionStays: false);
142+
gauge.Setup(part, gaugeHighlightThreshold, gaugeThreshold);
143+
gauges[i] = gauge;
144+
}
145+
else if (part.RefNotEquals(gauge.part))
146+
{
147+
gauge.part = part;
148+
}
149+
150+
if (gaugeEnabled)
151+
{
152+
bool showGauge = true;
153+
Vector3 partPos = part.partTransform.position;
154+
// this is the main remaining perf hog
155+
// It should be feasible to grab the camera matrix once and perform the transformation manually, but well...
156+
gauge.uiPos = RectUtil.WorldToUISpacePos(partPos, FlightCamera.fetch.mainCamera, MainCanvasUtil.MainCanvasRect, ref showGauge);
157+
158+
if (showGauge)
159+
{
160+
tgs.visibleGauges.Add(gauge);
161+
162+
gauge.distanceFromCamera = Vector3.SqrMagnitude(FlightCamera.fetch.mainCamera.transform.position - partPos);
163+
gauge.rTrf.localPosition = gauge.uiPos;
164+
165+
const float minValueChange = 1f / 120f; // slider is ~60 pixels at 100% UI scale
166+
float valueChange = Math.Abs(gauge.progressBar.value - tempFactor);
167+
if (valueChange > minValueChange)
168+
{
169+
gauge.sliderFill.color = Color.Lerp(Color.green, Color.red, tempFactor);
170+
gauge.progressBar.value = tempFactor; // setting this is awfully slow, so only set it when the slider is visually changing...
171+
}
172+
}
173+
174+
if (!gauge.gaugeActive || showGauge != gauge.showGauge)
175+
{
176+
gauge.gaugeActive = true;
177+
gauge.showGauge = showGauge;
178+
gauge.progressBar.gameObject.SetActive(showGauge);
179+
}
180+
}
181+
else if (gauge.gaugeActive)
182+
{
183+
gauge.gaugeActive = false;
184+
gauge.showGauge = false;
185+
gauge.progressBar.gameObject.SetActive(false);
186+
}
187+
188+
if (highlightEnabled)
189+
{
190+
if (!gauge.highlightActive)
191+
{
192+
gauge.highlightActive = true;
193+
part.SetHighlightType(Part.HighlightType.AlwaysOn);
194+
part.SetHighlight(active: true, recursive: false);
195+
}
196+
197+
gauge.edgeRatio = Mathf.InverseLerp(gauge.edgeHighlightThreshold * part.edgeHighlightThresholdMult, 1f, tempFactor);
198+
gauge.colorScale = tempFactor;
199+
part.SetHighlightColor(Color.Lerp(XKCDColors.Red * tempFactor, XKCDColors.KSPNotSoGoodOrange * tempFactor, gauge.edgeRatio));
200+
}
201+
else if (gauge.highlightActive)
202+
{
203+
gauge.highlightActive = false;
204+
if (gauge.part.IsNotNullOrDestroyed())
205+
part.SetHighlightDefault();
206+
}
207+
}
208+
else if (gauge.IsNotNullRef() && gauge.gaugeActive)
209+
{
210+
gauge.progressBar.gameObject.SetActive(false);
211+
gauge.gaugeActive = false;
212+
gauge.showGauge = false;
213+
if (gauge.highlightActive)
214+
{
215+
gauge.highlightActive = false;
216+
if (gauge.part.IsNotNullOrDestroyed())
217+
part.SetHighlightDefault();
218+
}
219+
}
53220
}
54221

55222
tgs.visibleGaugeCount = tgs.visibleGauges.Count;
56223

57224
if (tgs.visibleGaugeCount > 0)
58225
{
59-
tgs.visibleGauges.Sort();
226+
// A simple manual insertion sort is an order of magnitude faster than going through the IComparable interface
227+
List<TemperatureGauge> visibleGauges = tgs.visibleGauges;
228+
for (int i = 1; i < tgs.visibleGaugeCount; i++)
229+
{
230+
TemperatureGauge current = visibleGauges[i];
231+
int j = i;
232+
while (j > 0 && visibleGauges[j - 1].distanceFromCamera > current.distanceFromCamera)
233+
visibleGauges[j] = visibleGauges[--j];
234+
235+
visibleGauges[j] = current;
236+
}
60237

61238
for (int i = 0; i < tgs.visibleGaugeCount; i++)
62-
tgs.visibleGauges[i].rTrf.SetSiblingIndex(i);
239+
{
240+
TemperatureGauge visibleGauge = visibleGauges[i];
241+
if (visibleGauge.rTrf.GetSiblingIndex() != i)
242+
visibleGauge.rTrf.SetSiblingIndex(i);
243+
}
244+
63245
}
64246
}
65247

0 commit comments

Comments
 (0)