Skip to content

Commit 003ce96

Browse files
authored
Massively improve performance by rewriting the network core & rendering using burst jobs (#864)
* Add the Burst toolchain and project plumbing Reference Unity.Burst, Unity.Collections, Unity.Mathematics and Unity.Jobs from KSPBurst so the network model can be rewritten against the job system, and pull in Krafs.Publicizer to expose UnityEngine.Object.m_CachedPtr (for thread-safe null checks) and List<T>._items (for an AsSpan helper). Expose internals to the RemoteTech.Tests and RemoteTech.InGameTests assemblies, bump the language version to 13, and replace the .slnx with a plain .sln. * Add the RemoteTech.Collections helpers Later work is going to need a number of utilities. This commit adds: * an ArrayMap class that stores keys/values in arrays internally, * an ArrayMinHeap struct that provides a burst-compatible min-heap, * a number of extension helper methods for various types * an ObjectHandle struct for passing managed types to burst jobs * a IJobParallelForBatchDefer interface and extensions * Rewrite the network model as a Burst job pipeline Replace the managed graph/route computation with an unmanaged pipeline built on the Unity job system. Satellite and antenna state is projected into native arrays (NetworkState) once per update, and connectivity, routing and the render geometry are computed by Burst jobs (NetworkUpdate, Edges, NetworkUpdateMath, and the line/cone/mark mesh jobs). Antennas publish their state through a pinned AntennaData registered with StateManager, so the jobs can read them without touching managed memory. ISatellite gains a flat SatelliteState snapshot and Antennas becomes an IReadOnlyList; NetworkLink/NetworkRoute become readonly struct views over the store rather than heap-allocated objects. The range models are unified behind IRangeModel so the job path and the managed path share one implementation. NetworkManager drives the pipeline and NetworkRenderer draws the computed meshes directly, replacing the per-frame NetworkCone/NetworkLine GameObjects. * Add test harnesses for the network jobs RemoteTech.Tests runs the Burst/NativeArray math headlessly under xunit by backing native arrays with Marshal.AllocHGlobal (NativeArena), covering the range/geometry math. RemoteTech.InGameTests is a KSP.Testing plugin with an in-game runner UI that exercises the jobs against real Unity Collections (adjacency, edges, Dijkstra, mark/cone geometry, hashing). * Migrate the API onto the NetworkState read seam Route the public API methods through the precomputed NetworkState instead of the old on-demand LINQ/route materialization: - Add a GetRoute passthrough to the NetworkManager read seam. - GetFirstHopToKSC / GetControlPath now read the groundOnly route directly via GetRoute rather than filtering + Min()-ing over BuildConnections. - HasDirectGroundStation / GetClosestDirectGroundStation now test the satellite's direct adjacency row (GetLinks) for a ground-station target. This treats any direct link to a ground station as qualifying (not only first hops of a shortest route) and drops the empty/self-route NRE and the empty-sequence Min() throw. - Replace the O(n) Satellites.Where(...).FirstOrDefault() guid scans with the O(1) Satellites[id] indexer; GetName / GetRangeDistance use the Network[id] indexer so ground stations still resolve. GetSignalDelayToSatellite's arbitrary A->B pathfind is left on NetworkPathfinder; NetworkState has no point-to-point equivalent yet. * Gate signal relay on transit rather than link existence Relay capability now determines whether a node may forward a signal (transit), not whether a link exists. IsEdgeTraversable previously required both endpoints of every edge to be relay-capable, which wrongly disconnected powered non-relay endpoints (e.g. passive-SPU vessels) — a route's own endpoints never forward, so they should not need to relay. ComputeAdjacencyLists now includes an edge whenever both endpoints are powered. The relay check moves into a per-node CanTransit bitmask: the multi-source Dijkstra records a node's score but only expands out of it when it can transit, and roots (route endpoints) always expand. * Add a point-to-point signal delay query to the API Replace GetSignalDelayToSatellite's on-demand A* (NetworkPathfinder) with a Burst NetworkPathfindJob run over the persisted NetworkState edge table, exposed as NetworkState.ShortestDelayBetween plus a NetworkManager passthrough. Add the NetworkUpdateMath.EncodePairIndex helper the job uses to index the triangular edge table. * Hide command-station routes rooted at filtered-out vessels The multi-path overlay drew every tree edge in the command-station forest, so a vessel hidden by the map view filter still showed its green route back to control. Seed the on-path set by walking the predecessor forest from each visible vessel instead, so a route is drawn only when a visible vessel sits at its far end. * Remove dead network-model code superseded by NetworkState The migration onto the Burst NetworkState left the old managed routing orphaned. Delete the A* NetworkPathfinder and the explicit-mode half of NetworkRoute it fed (the freestanding-path ctor, Empty, Contains, and the BidirectionalEdge it compared against), leaving the live value-type view into the Dijkstra forests. Drop the unused NetworkManager.FindNeighbors / ConnectionCache / Count, the NetworkState.GetRouteLength / GetRouteOrigin guid wrappers (callers use the internal index versions), the unused VesselSatellite.Connections seam, and the never-referenced Dish type. * Properly install CKAN deps in CI * Use KSPBT for the tests project
1 parent dbb7299 commit 003ce96

88 files changed

Lines changed: 7969 additions & 1507 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,6 @@ GameData/RemoteTech/Plugins/RemoteTech.xml
167167
GameData/RemoteTech/Versioning/RemoteTech.version
168168
GameData/build.txt
169169
src/RemoteTech/RemoteTech.sln.ide/
170+
171+
# In-game test plugin build output (the source lives under tests/)
172+
GameData/RemoteTech.Tests/

RemoteTech.sln

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
Microsoft Visual Studio Solution File, Format Version 12.00
2+
# Visual Studio Version 17
3+
VisualStudioVersion = 17.0.31903.59
4+
MinimumVisualStudioVersion = 10.0.40219.1
5+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTech", "src\RemoteTech\RemoteTech.csproj", "{67470554-F754-4664-AC7C-D002943ACD72}"
6+
EndProject
7+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTech.Tests", "tests\RemoteTech.Tests\RemoteTech.Tests.csproj", "{A7585803-6434-436E-BFC1-B09A520425D7}"
8+
EndProject
9+
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RemoteTech.InGameTests", "tests\RemoteTech.InGameTests\RemoteTech.InGameTests.csproj", "{C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}"
10+
EndProject
11+
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{065AC022-06A2-4079-9DEC-051B87D76A08}"
12+
ProjectSection(SolutionItems) = preProject
13+
CHANGES.md = CHANGES.md
14+
CONTRIBUTING.md = CONTRIBUTING.md
15+
README.md = README.md
16+
EndProjectSection
17+
EndProject
18+
Global
19+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
20+
Debug|Any CPU = Debug|Any CPU
21+
Release|Any CPU = Release|Any CPU
22+
EndGlobalSection
23+
GlobalSection(ProjectConfigurationPlatforms) = postSolution
24+
{67470554-F754-4664-AC7C-D002943ACD72}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
25+
{67470554-F754-4664-AC7C-D002943ACD72}.Debug|Any CPU.Build.0 = Debug|Any CPU
26+
{67470554-F754-4664-AC7C-D002943ACD72}.Release|Any CPU.ActiveCfg = Release|Any CPU
27+
{67470554-F754-4664-AC7C-D002943ACD72}.Release|Any CPU.Build.0 = Release|Any CPU
28+
{A7585803-6434-436E-BFC1-B09A520425D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
29+
{A7585803-6434-436E-BFC1-B09A520425D7}.Debug|Any CPU.Build.0 = Debug|Any CPU
30+
{A7585803-6434-436E-BFC1-B09A520425D7}.Release|Any CPU.ActiveCfg = Release|Any CPU
31+
{A7585803-6434-436E-BFC1-B09A520425D7}.Release|Any CPU.Build.0 = Release|Any CPU
32+
{C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
33+
{C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Debug|Any CPU.Build.0 = Debug|Any CPU
34+
{C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Release|Any CPU.ActiveCfg = Release|Any CPU
35+
{C5C1CBBB-83DD-4850-A6F8-6823AB14B54E}.Release|Any CPU.Build.0 = Release|Any CPU
36+
EndGlobalSection
37+
GlobalSection(SolutionProperties) = preSolution
38+
HideSolutionNode = FALSE
39+
EndGlobalSection
40+
EndGlobal

RemoteTech.slnx

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/RemoteTech/API/API.cs

Lines changed: 83 additions & 58 deletions
Large diffs are not rendered by default.

src/RemoteTech/AddOns/Kerbalism.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
namespace RemoteTech.AddOns
55
{
6-
/// <summary> Simple class to detect if Kerbalism is loaded </summary>
6+
/// <summary>
7+
/// Simple class to detect if Kerbalism is loaded
8+
/// </summary>
79
public static class Kerbalism
810
{
911
private static readonly Type API;
@@ -21,7 +23,9 @@ static Kerbalism()
2123
}
2224
}
2325

24-
/// <summary> Returns true if Kerbalism is detected for the current game </summary>
26+
/// <summary>
27+
/// Returns true if Kerbalism is detected for the current game
28+
/// </summary>
2529
public static bool Exists
2630
{
2731
get { return API != null; }

src/RemoteTech/AntennaManager.cs

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@ public class AntennaManager : IDisposable, IEnumerable<IAntenna>
1515
public event Action<IAntenna> OnRegister = delegate { };
1616
public event Action<IAntenna> OnUnregister = delegate { };
1717

18-
public IEnumerable<IAntenna> this[ISatellite s] { get { return For(s.Guid); } }
19-
public IEnumerable<IAntenna> this[Vessel v] { get { return For(v.id); } }
20-
public IEnumerable<IAntenna> this[Guid g] { get { return For(g); } }
18+
public IReadOnlyList<IAntenna> this[ISatellite s] { get { return For(s.Guid); } }
19+
public IReadOnlyList<IAntenna> this[Vessel v] { get { return For(v.id); } }
20+
public IReadOnlyList<IAntenna> this[Guid g] { get { return For(g); } }
2121

2222
private readonly Dictionary<Guid, List<IAntenna>> mLoadedAntennaCache =
2323
new Dictionary<Guid, List<IAntenna>>();
24-
private readonly Dictionary<Guid, List<IAntenna>> mProtoAntennaCache =
25-
new Dictionary<Guid, List<IAntenna>>();
24+
private readonly Dictionary<Guid, List<ProtoAntenna>> mProtoAntennaCache =
25+
new Dictionary<Guid, List<ProtoAntenna>>();
2626

2727
public AntennaManager()
2828
{
@@ -35,6 +35,15 @@ public AntennaManager()
3535
public void Dispose()
3636
{
3737
GameEvents.onVesselGoOnRails.Remove(OnVesselGoOnRails);
38+
39+
foreach (List<ProtoAntenna> antennas in mProtoAntennaCache.Values)
40+
{
41+
foreach (ProtoAntenna antenna in antennas)
42+
{
43+
antenna.Dispose();
44+
}
45+
}
46+
mProtoAntennaCache.Clear();
3847
}
3948

4049
public void Register(Guid key, IAntenna antenna)
@@ -98,7 +107,7 @@ public void RegisterProtos(Vessel v)
98107
{
99108
if (!mProtoAntennaCache.ContainsKey(key))
100109
{
101-
mProtoAntennaCache[key] = new List<IAntenna>();
110+
mProtoAntennaCache[key] = new List<ProtoAntenna>();
102111
}
103112
ProtoAntenna proto = new ProtoAntenna(v, pps, ppms);
104113
mProtoAntennaCache[key].Add(proto);
@@ -113,25 +122,22 @@ public void UnregisterProtos(Guid key)
113122

114123
if (!mProtoAntennaCache.ContainsKey(key)) return;
115124

116-
foreach (IAntenna a in mProtoAntennaCache[key])
125+
foreach (ProtoAntenna a in mProtoAntennaCache[key])
117126
{
118127
OnUnregister.Invoke(a);
128+
a.Dispose();
119129
}
120130

121131
mProtoAntennaCache.Remove(key);
122132
}
123133

124-
private IEnumerable<IAntenna> For(Guid key)
134+
private IReadOnlyList<IAntenna> For(Guid key)
125135
{
126136
if (mLoadedAntennaCache.ContainsKey(key))
127-
{
128137
return mLoadedAntennaCache[key];
129-
}
130138
if (mProtoAntennaCache.ContainsKey(key))
131-
{
132139
return mProtoAntennaCache[key];
133-
}
134-
return Enumerable.Empty<IAntenna>();
140+
return [];
135141
}
136142

137143
private void OnVesselGoOnRails(Vessel v)
@@ -163,11 +169,6 @@ public static bool IsAntenna(this ProtoPartModuleSnapshot ppms)
163169
ppms.GetBool("IsRTActive");
164170
}
165171

166-
public static bool IsAntenna(this PartModule pm)
167-
{
168-
return pm.Fields.GetValue<bool>("IsRTAntenna") &&
169-
pm.Fields.GetValue<bool>("IsRTPowered") &&
170-
pm.Fields.GetValue<bool>("IsRTActive");
171-
}
172+
public static bool IsAntenna(this PartModule pm) => pm is IAntenna;
172173
}
173174
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
using System;
2+
using System.Collections;
3+
using System.Collections.Generic;
4+
5+
namespace RemoteTech.Collections;
6+
7+
public class ArrayMap<K, V> : IEnumerable<KeyValuePair<K, V>>
8+
{
9+
private readonly Dictionary<K, int> indices;
10+
private readonly List<K> keys;
11+
private readonly List<V> values;
12+
13+
public int Count => values.Count;
14+
public V this[K key]
15+
{
16+
get => values[indices[key]];
17+
set
18+
{
19+
if (indices.TryGetValue(key, out var index))
20+
{
21+
keys[index] = key;
22+
values[index] = value;
23+
}
24+
else
25+
{
26+
index = values.Count;
27+
indices.Add(key, index);
28+
keys.Add(key);
29+
values.Add(value);
30+
}
31+
}
32+
}
33+
34+
public ReadOnlySpan<K> Keys => keys.AsReadOnlySpan();
35+
public Span<V> Values => values.AsSpan();
36+
37+
public ArrayMap()
38+
{
39+
indices = [];
40+
keys = [];
41+
values = [];
42+
}
43+
44+
public ArrayMap(int capacity)
45+
{
46+
indices = new(capacity);
47+
keys = new(capacity);
48+
values = new(capacity);
49+
}
50+
51+
public bool TryGetValue(K key, out V value)
52+
{
53+
value = default;
54+
if (!indices.TryGetValue(key, out var index))
55+
return false;
56+
57+
value = values[index];
58+
return true;
59+
}
60+
61+
public bool ContainsKey(K key) => indices.ContainsKey(key);
62+
63+
public int IndexOf(K key) => indices.TryGetValue(key, out var index) ? index : -1;
64+
65+
public void Add(K key, V value)
66+
{
67+
var count = Count;
68+
indices.Add(key, count);
69+
keys.Add(key);
70+
values.Add(value);
71+
}
72+
73+
public bool TryAdd(K key, V value)
74+
{
75+
if (indices.ContainsKey(key))
76+
return false;
77+
78+
var index = Count;
79+
indices.Add(key, index);
80+
keys.Add(key);
81+
values.Add(value);
82+
return true;
83+
}
84+
85+
public bool Remove(K key)
86+
{
87+
if (!indices.TryGetValue(key, out var index))
88+
return false;
89+
90+
if (index == Count - 1)
91+
{
92+
indices.Remove(key);
93+
keys.RemoveAt(index);
94+
values.RemoveAt(index);
95+
}
96+
else
97+
{
98+
var count = Count;
99+
var other = keys[index] = keys[count - 1];
100+
values[index] = values[count - 1];
101+
102+
indices[other] = index;
103+
indices.Remove(key);
104+
keys.RemoveAt(count - 1);
105+
values.RemoveAt(count - 1);
106+
}
107+
108+
return true;
109+
}
110+
111+
public void Clear()
112+
{
113+
indices.Clear();
114+
keys.Clear();
115+
values.Clear();
116+
}
117+
118+
public Enumerator GetEnumerator() => new(this);
119+
IEnumerator<KeyValuePair<K, V>> IEnumerable<KeyValuePair<K, V>>.GetEnumerator() => GetEnumerator();
120+
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
121+
122+
public struct Enumerator(ArrayMap<K, V> map) : IEnumerator<KeyValuePair<K, V>>
123+
{
124+
List<K>.Enumerator keys = map.keys.GetEnumerator();
125+
List<V>.Enumerator vals = map.values.GetEnumerator();
126+
127+
public KeyValuePair<K, V> Current => new(keys.Current, vals.Current);
128+
object IEnumerator.Current => Current;
129+
130+
public bool MoveNext() => keys.MoveNext() & vals.MoveNext();
131+
132+
void IEnumerator.Reset()
133+
{
134+
DoReset(ref keys);
135+
DoReset(ref vals);
136+
}
137+
138+
public void Dispose() { }
139+
140+
private static void DoReset<T>(ref T enumerator)
141+
where T : IEnumerator
142+
{
143+
enumerator.Reset();
144+
}
145+
}
146+
}

0 commit comments

Comments
 (0)