Skip to content

Commit f69cd48

Browse files
authored
Navmesh memory optimizations (#3916)
Cuts navmesh memory usage by at least 5 times (about 3GB less in our benchmark scene), enabling much larger scenes to generate successfully. Small Fish’s MSC scene (~3km x 3km) now generates fine, previously ran out of memory and crashed. The new theoretical maximum for a navmeshed scene is now ~50km x 50km. - Packed `CompactCell` data into a single 32-bit field (8 bytes -> 4 bytes). - Added heightfield serialization + compression so `CompactHeightfield` can be stored/transferred as bytes, and tiles store compressed heightfield data instead of large voxel arrays. - **Heightfield serialization + compression is a prerequisite for navmesh baking.** - Tiles are decompressed when generating polymeshes (e.g., to adapt to area changes). - Compression adds a small overhead during navmesh generation. - Fixed load sequence, now properly shows "Generating Navmesh..." in scene loading screen - Reduced allocations during `PolyMesh` generation. - Reduced the permanent memory usage of `PolyMesh`. - Improved parallel tile generation; now properly limits the number of in-flight tasks to prevent OOM crashes. Overall generation will be slower due to the extra compression step, I expect roughly ~30% in our benchmarks. Basically, we're trading generation speed for drastically lower memory usage. Some of the slowdown we will see in the benchmark is also due to the increased tile size. The larger tiles don’t actually make generation slower in real use, but they show up as "slower" because of how our benchmarks are designed/measured.
1 parent 4e85d01 commit f69cd48

11 files changed

Lines changed: 408 additions & 138 deletions

File tree

engine/Sandbox.Engine/Game/Navigation/Generation/CompactHeightField.cs

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,25 @@
55
namespace Sandbox.Navigation.Generation;
66

77
[SkipHotload]
8-
[StructLayout( LayoutKind.Explicit, Size = 8 )]
8+
[StructLayout( LayoutKind.Explicit, Size = 4 )]
99
internal struct CompactCell
1010
{
1111
[FieldOffset( 0 )]
12-
public int Index;
13-
[FieldOffset( 4 )]
14-
public int Count;
12+
private int indexAndCount;
13+
14+
// 24bit
15+
public int Index
16+
{
17+
readonly get => indexAndCount & 0xFFFFFF;
18+
set => indexAndCount = (indexAndCount & ~0xFFFFFF) | (value & 0xFFFFFF);
19+
}
20+
21+
// 8bit
22+
public int Count
23+
{
24+
readonly get => (indexAndCount >> 24) & 0xFF;
25+
set => indexAndCount = (indexAndCount & 0xFFFFFF) | ((value & 0xFF) << 24);
26+
}
1527
}
1628

1729
[SkipHotload]
@@ -27,13 +39,13 @@ internal struct CompactSpan
2739

2840
public int Con
2941
{
30-
get => connectionsAndHeight & 0xFFFFFF;
42+
readonly get => connectionsAndHeight & 0xFFFFFF;
3143
set => connectionsAndHeight = (connectionsAndHeight & ~0xFFFFFF) | (value & 0xFFFFFF);
3244
}
3345

3446
public byte Height
3547
{
36-
get => (byte)((connectionsAndHeight >> 24) & 0xFF);
48+
readonly get => (byte)((connectionsAndHeight >> 24) & 0xFF);
3749
set => connectionsAndHeight = (connectionsAndHeight & 0xFFFFFF) | ((value & 0xFF) << 24);
3850
}
3951

@@ -47,9 +59,11 @@ internal partial class CompactHeightfield : IDisposable
4759
public int SpanCount;
4860
public int WalkableHeight;
4961
public int WalkableClimb;
62+
63+
// Those two probably shouldn't be here they are only used during contour building
5064
public int BorderSize;
51-
public ushort MaxDistance;
5265
public ushort MaxRegions;
66+
5367
public Vector3 BMin;
5468
public Vector3 BMax;
5569
public float CellSize;
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
using static Sandbox.IByteParsable;
2+
3+
namespace Sandbox.Navigation.Generation;
4+
5+
internal partial class CompactHeightfield : IByteParsable<CompactHeightfield>
6+
{
7+
public static CompactHeightfield Read( ref ByteStream stream, ByteParseOptions o = default )
8+
{
9+
var compactHeightfield = GetPooled();
10+
var width = stream.Read<int>();
11+
var height = stream.Read<int>();
12+
var spanCount = stream.Read<int>();
13+
var walkableHeight = stream.Read<int>();
14+
var walkableClimb = stream.Read<int>();
15+
16+
var bMin = stream.Read<Vector3>();
17+
var bMax = stream.Read<Vector3>();
18+
19+
var cellSize = stream.Read<float>();
20+
var cellHeight = stream.Read<float>();
21+
22+
compactHeightfield.Init( width, height, spanCount, walkableHeight, walkableClimb, bMin, bMax, cellSize, cellHeight );
23+
24+
var cells = compactHeightfield.Cells;
25+
ReadCells( ref stream, cells );
26+
27+
var spans = compactHeightfield.Spans;
28+
ReadSpans( ref stream, spans, cells );
29+
30+
var areas = compactHeightfield.Areas;
31+
ReadAreas( ref stream, areas );
32+
33+
return compactHeightfield;
34+
}
35+
36+
public static object ReadObject( ref ByteStream stream, ByteParseOptions o = default )
37+
{
38+
return Read( ref stream, o );
39+
}
40+
41+
public static void Write( ref ByteStream stream, CompactHeightfield value, ByteParseOptions o = default )
42+
{
43+
stream.Write( value.Width );
44+
stream.Write( value.Height );
45+
stream.Write( value.SpanCount );
46+
stream.Write( value.WalkableHeight );
47+
stream.Write( value.WalkableClimb );
48+
stream.Write( value.BMin );
49+
stream.Write( value.BMax );
50+
stream.Write( value.CellSize );
51+
stream.Write( value.CellHeight );
52+
53+
WriteCells( ref stream, value.Cells );
54+
WriteSpans( ref stream, value.Spans, value.Cells );
55+
WriteAreas( ref stream, value.Areas );
56+
}
57+
58+
public static void WriteObject( ref ByteStream stream, object value, ByteParseOptions o = default )
59+
{
60+
Write( ref stream, value as CompactHeightfield, o );
61+
}
62+
63+
private static void ReadCells( ref ByteStream stream, Span<CompactCell> cells )
64+
{
65+
var spanCursor = 0;
66+
for ( var i = 0; i < cells.Length; i++ )
67+
{
68+
var count = stream.Read<int>();
69+
ref var cell = ref cells[i];
70+
cell.Index = spanCursor;
71+
cell.Count = count;
72+
spanCursor += count;
73+
}
74+
}
75+
76+
private static void WriteCells( ref ByteStream stream, ReadOnlySpan<CompactCell> cells )
77+
{
78+
for ( var index = 0; index < cells.Length; index++ )
79+
{
80+
stream.Write( cells[index].Count );
81+
}
82+
}
83+
84+
private static void ReadSpans( ref ByteStream stream, Span<CompactSpan> spans, ReadOnlySpan<CompactCell> cells )
85+
{
86+
var spanIndex = 0;
87+
for ( var i = 0; i < cells.Length; i++ )
88+
{
89+
ref readonly var cell = ref cells[i];
90+
for ( var j = 0; j < cell.Count; j++ )
91+
{
92+
ref var span = ref spans[spanIndex++];
93+
span.StartY = stream.Read<ushort>();
94+
span.Region = stream.Read<ushort>();
95+
96+
var packed = stream.Read<int>();
97+
span.Con = packed & 0xFFFFFF;
98+
span.Height = (byte)(packed >> 24);
99+
}
100+
}
101+
}
102+
103+
private static void WriteSpans( ref ByteStream stream, ReadOnlySpan<CompactSpan> spans, ReadOnlySpan<CompactCell> cells )
104+
{
105+
var spanIndex = 0;
106+
for ( var i = 0; i < cells.Length; i++ )
107+
{
108+
ref readonly var cell = ref cells[i];
109+
for ( var j = 0; j < cell.Count; j++ )
110+
{
111+
ref readonly var span = ref spans[spanIndex++];
112+
stream.Write( span.StartY );
113+
stream.Write( span.Region );
114+
var packed = (span.Con & 0xFFFFFF) | ((span.Height & 0xFF) << 24);
115+
stream.Write( packed );
116+
}
117+
}
118+
}
119+
120+
private static void ReadAreas( ref ByteStream stream, Span<int> areas )
121+
{
122+
var index = 0;
123+
while ( index < areas.Length )
124+
{
125+
var runLength = stream.Read<int>();
126+
var areaValue = stream.Read<int>();
127+
128+
for ( var i = 0; i < runLength && index < areas.Length; i++ )
129+
{
130+
areas[index++] = areaValue;
131+
}
132+
}
133+
}
134+
135+
private static void WriteAreas( ref ByteStream stream, ReadOnlySpan<int> areas )
136+
{
137+
var index = 0;
138+
while ( index < areas.Length )
139+
{
140+
var areaValue = areas[index];
141+
var runLength = 1;
142+
while ( index + runLength < areas.Length && areas[index + runLength] == areaValue )
143+
{
144+
runLength++;
145+
}
146+
147+
stream.Write( runLength );
148+
stream.Write( areaValue );
149+
index += runLength;
150+
}
151+
}
152+
153+
}

engine/Sandbox.Engine/Game/Navigation/Generation/HeightField.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal sealed class Heightfield : IDisposable
3434

3535
private int ColumnCount => Width * Height;
3636

37-
private const int _initialColumnCapacity = 64;
37+
private const int _initialColumnCapacity = 48;
3838

3939
private int _columnCapacity = _initialColumnCapacity;
4040
private int _totalSpanCount = 0;
@@ -101,7 +101,6 @@ public void Dispose()
101101
public void GrowColumns()
102102
{
103103
var newCapacity = _columnCapacity * 2;
104-
Log.Warning( $"Heightfield: Growing column capacity from {_columnCapacity} to {newCapacity}" );
105104
var newSpans = ArrayPool<SpanData>.Shared.Rent( ColumnCount * newCapacity );
106105
for ( int c = 0; c < ColumnCount; c++ )
107106
{
@@ -190,18 +189,19 @@ public void EnsureCompressed()
190189
{
191190
if ( _isCompressed ) return;
192191

193-
var newSpans = ArrayPool<SpanData>.Shared.Rent( _totalSpanCount );
194192
int currentOffset = 0;
195193
for ( int c = 0; c < ColumnCount; c++ )
196194
{
197195
int count = _columnCounts[c];
198196
_compressedColumnStarts[c] = currentOffset;
199-
Array.Copy( _spans, c * _columnCapacity, newSpans, currentOffset, count );
197+
if ( count > 0 )
198+
{
199+
int sourceOffset = c * _columnCapacity;
200+
if ( sourceOffset != currentOffset ) Array.Copy( _spans, sourceOffset, _spans, currentOffset, count );
201+
}
200202
currentOffset += count;
201203
}
202204

203-
ArrayPool<SpanData>.Shared.Return( _spans );
204-
_spans = newSpans;
205205
_isCompressed = true;
206206
}
207207

engine/Sandbox.Engine/Game/Navigation/Generation/PolyMesh.cs

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,6 @@ internal class PolyMesh : IDisposable
2525
public Span<ushort> Polys => _polysArray.AsSpan( 0, maxPolyCount * MaxVertsPerPoly * 2 );
2626
private ushort[] _polysArray;
2727

28-
/// <summary>
29-
/// The region id assigned to each polygon. [Size: #npolys]
30-
/// </summary>
31-
public Span<int> RegionIds => _regionIdsArray.AsSpan( 0, maxPolyCount );
32-
private int[] _regionIdsArray;
33-
34-
/// <summary>
35-
/// The user defined flags assigned to each polygon. [Size: #npolys]
36-
/// </summary>
37-
public Span<ushort> Flags => _flagsArray.AsSpan( 0, maxPolyCount );
38-
private ushort[] _flagsArray;
39-
4028
/// <summary>
4129
/// The area id assigned to each polygon. [Size: #npolys]
4230
/// </summary>
@@ -77,16 +65,6 @@ internal void Init( ContourSet cset, int maxVertsPerPoly, int maxTris, int maxVe
7765
if ( _polysArray != null ) ArrayPool<ushort>.Shared.Return( _polysArray );
7866
_polysArray = ArrayPool<ushort>.Shared.Rent( maxTris * maxVertsPerPoly * 2 * 2 );
7967
}
80-
if ( _regionIdsArray == null || _regionIdsArray.Length < maxTris )
81-
{
82-
if ( _regionIdsArray != null ) ArrayPool<int>.Shared.Return( _regionIdsArray );
83-
_regionIdsArray = ArrayPool<int>.Shared.Rent( maxTris * 2 );
84-
}
85-
if ( _flagsArray == null || _flagsArray.Length < maxTris )
86-
{
87-
if ( _flagsArray != null ) ArrayPool<ushort>.Shared.Return( _flagsArray );
88-
_flagsArray = ArrayPool<ushort>.Shared.Rent( maxTris * 2 );
89-
}
9068
if ( _areasArray == null || _areasArray.Length < maxTris )
9169
{
9270
if ( _areasArray != null ) ArrayPool<int>.Shared.Return( _areasArray );

engine/Sandbox.Engine/Game/Navigation/Generation/PolyMeshBuilder.cs

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ public static PolyMesh BuildPolyMesh( ContourSet cset, int maxVertsPerPoly )
5656
#pragma warning restore CA2000 // Dispose objects before losing scope
5757
mesh.Init( cset, maxVertsPerPoly, maxTris, maxVertices );
5858

59+
using var pooledRegionIds = new PooledSpan<int>( maxTris );
60+
var regionIds = pooledRegionIds.Span;
61+
regionIds.Clear();
62+
5963
using var pooledNextVert = new PooledSpan<int>( maxVertices );
6064
var nextVert = pooledNextVert.Span;
6165
nextVert.Clear();
@@ -205,7 +209,7 @@ public static PolyMesh BuildPolyMesh( ContourSet cset, int maxVertsPerPoly )
205209

206210
pj.CopyTo( mesh.Polys.Slice( mesh.PolyCount * maxVertsPerPoly * 2, maxVertsPerPoly * 2 ) );
207211

208-
mesh.RegionIds[mesh.PolyCount] = cont.Region;
212+
regionIds[mesh.PolyCount] = cont.Region;
209213
mesh.Areas[mesh.PolyCount] = cont.Area;
210214
mesh.PolyCount++;
211215
}
@@ -219,7 +223,7 @@ public static PolyMesh BuildPolyMesh( ContourSet cset, int maxVertsPerPoly )
219223
if ( !CanRemoveVertex( mesh, (ushort)i ) )
220224
continue;
221225

222-
if ( !RemoveVertex( mesh, (ushort)i, mesh.MaxPolys ) )
226+
if ( !RemoveVertex( mesh, regionIds, (ushort)i, mesh.MaxPolys ) )
223227
{
224228
// Failed to remove vertex
225229
Log.Error( $"rcBuildPolyMesh: Failed to remove edge vertex {i}." );
@@ -761,7 +765,7 @@ private static bool CanRemoveVertex( PolyMesh mesh, ushort rem )
761765
return numOpenEdges <= 2;
762766
}
763767

764-
private static bool RemoveVertex( PolyMesh mesh, ushort rem, int maxTris )
768+
private static bool RemoveVertex( PolyMesh mesh, Span<int> regionIds, ushort rem, int maxTris )
765769
{
766770
int nvp = mesh.MaxVertsPerPoly;
767771

@@ -812,7 +816,7 @@ private static bool RemoveVertex( PolyMesh mesh, ushort rem, int maxTris )
812816
int eIndex = nedges * 4;
813817
edges[eIndex + 0] = p2[k];
814818
edges[eIndex + 1] = p2[j];
815-
edges[eIndex + 2] = mesh.RegionIds[i];
819+
edges[eIndex + 2] = regionIds[i];
816820
edges[eIndex + 3] = mesh.Areas[i];
817821
nedges++;
818822
}
@@ -828,7 +832,7 @@ private static bool RemoveVertex( PolyMesh mesh, ushort rem, int maxTris )
828832
// Clear the last half (adjacency info)
829833
mesh.Polys.Slice( (i * nvp * 2) + nvp, nvp ).Fill( Constants.MESH_NULL_IDX );
830834

831-
mesh.RegionIds[i] = mesh.RegionIds[mesh.PolyCount - 1];
835+
regionIds[i] = regionIds[mesh.PolyCount - 1];
832836
mesh.Areas[i] = mesh.Areas[mesh.PolyCount - 1];
833837
mesh.PolyCount--;
834838
--i;
@@ -1068,7 +1072,7 @@ private static bool RemoveVertex( PolyMesh mesh, ushort rem, int maxTris )
10681072
p[j] = polys[i * nvp + j];
10691073

10701074
p.CopyTo( mesh.Polys.Slice( mesh.PolyCount * nvp * 2, nvp * 2 ) );
1071-
mesh.RegionIds[mesh.PolyCount] = pregs[i];
1075+
regionIds[mesh.PolyCount] = pregs[i];
10721076
mesh.Areas[mesh.PolyCount] = pareas[i];
10731077
mesh.PolyCount++;
10741078

0 commit comments

Comments
 (0)