Skip to content

Commit 147c8b6

Browse files
authored
Merge branch 'Facepunch:master' into master
2 parents e9a7026 + b5ce772 commit 147c8b6

File tree

14 files changed

+379
-65
lines changed

14 files changed

+379
-65
lines changed

engine/Sandbox.Engine/Game/Navigation/NavMesh/NavMesh.Cache.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ public NavMeshAreaDefinition AreaIdToDefinition( int id )
3232
return areaIdToDefinition[id];
3333
}
3434

35+
public bool HasTile( Vector2Int tilePosition )
36+
{
37+
return tileCache.ContainsKey( tilePosition );
38+
}
39+
3540
public void RemoveTile( Vector2Int tilePosition )
3641
{
3742
tileCache.Remove( tilePosition );

engine/Sandbox.Engine/Game/Navigation/NavMesh/NavMesh.Generate.cs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ await Task.Run( async () =>
167167
{
168168
CompactHeightfield heightField;
169169

170-
// If not yet loaded and we have a cached heightfield from baked data, use it directly
171-
if ( !IsLoaded && tile.IsHeightFieldValid )
170+
// Use baked heightfield data directly if available, avoiding expensive geometry collection
171+
if ( tile.IsBakedHeightField && tile.IsHeightFieldValid )
172172
{
173173
heightField = tile.DecompressCachedHeightField();
174174
}
@@ -227,6 +227,11 @@ internal void LoadTileOnMainThread( NavMeshTile targetTile, DtMeshData data )
227227
{
228228
ThreadSafe.AssertIsMainThread();
229229

230+
// Tile may have been evicted (via UnloadTile) while an async build was in flight.
231+
// Don't re-add it to the navmesh if it's no longer in the cache.
232+
if ( !tileCache.HasTile( targetTile.TilePosition ) )
233+
return;
234+
230235
var tileRef = navmeshInternal.GetTileRefAt( targetTile.TilePosition.x, targetTile.TilePosition.y, 0 );
231236

232237
if ( data == null )
@@ -262,6 +267,79 @@ internal void UnloadTileOnMainThread( Vector2Int tilePosition )
262267
}
263268
}
264269

270+
/// <summary>
271+
/// Removes the navmesh tile at the given world position.
272+
/// </summary>
273+
public void UnloadTile( Vector3 worldPosition )
274+
{
275+
ThreadSafe.AssertIsMainThread();
276+
277+
if ( !IsEnabled ) return;
278+
279+
var tilePosition = WorldPositionToTilePosition( worldPosition );
280+
UnloadTileOnMainThread( tilePosition );
281+
tileCache.RemoveTile( tilePosition );
282+
}
283+
284+
/// <summary>
285+
/// Removes all navmesh tiles overlapping with the given bounds.
286+
/// </summary>
287+
public void UnloadTiles( BBox bounds )
288+
{
289+
ThreadSafe.AssertIsMainThread();
290+
291+
if ( !IsEnabled ) return;
292+
293+
var minMaxTileCoords = CalculateMinMaxTileCoords( bounds );
294+
295+
for ( int x = minMaxTileCoords.Left; x <= minMaxTileCoords.Right; x++ )
296+
{
297+
for ( int y = minMaxTileCoords.Top; y <= minMaxTileCoords.Bottom; y++ )
298+
{
299+
var tilePosition = new Vector2Int( x, y );
300+
UnloadTileOnMainThread( tilePosition );
301+
tileCache.RemoveTile( tilePosition );
302+
}
303+
}
304+
}
305+
306+
/// <summary>
307+
/// Queues the navmesh tile at the given world position for incremental generation
308+
/// over subsequent frames. Fire-and-forget alternative to <see cref="GenerateTile"/>.
309+
/// </summary>
310+
public void RequestTileGeneration( Vector3 worldPosition )
311+
{
312+
ThreadSafe.AssertIsMainThread();
313+
314+
if ( !IsEnabled ) return;
315+
316+
var tilePosition = WorldPositionToTilePosition( worldPosition );
317+
var tile = tileCache.GetOrAddTile( tilePosition );
318+
tile.RequestFullRebuild();
319+
}
320+
321+
/// <summary>
322+
/// Queues all navmesh tiles overlapping with the given bounds for incremental generation
323+
/// over subsequent frames. Fire-and-forget alternative to <see cref="GenerateTiles"/>.
324+
/// </summary>
325+
public void RequestTilesGeneration( BBox bounds )
326+
{
327+
ThreadSafe.AssertIsMainThread();
328+
329+
if ( !IsEnabled ) return;
330+
331+
var minMaxTileCoords = CalculateMinMaxTileCoords( bounds );
332+
333+
for ( int x = minMaxTileCoords.Left; x <= minMaxTileCoords.Right; x++ )
334+
{
335+
for ( int y = minMaxTileCoords.Top; y <= minMaxTileCoords.Bottom; y++ )
336+
{
337+
var tile = tileCache.GetOrAddTile( new Vector2Int( x, y ) );
338+
tile.RequestFullRebuild();
339+
}
340+
}
341+
}
342+
265343
internal Vector2Int WorldPositionToTilePosition( Vector3 worldPosition )
266344
{
267345
var tileLocationFloat = (worldPosition - TileOrigin) / TileSizeWorldSpace;

engine/Sandbox.Engine/Game/Navigation/NavMesh/NavMesh.Serialize.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal JsonObject Serialize()
3232
jso["AgentMaxSlope"] = AgentMaxSlope;
3333
jso["ExcludedBodies"] = Json.ToNode( ExcludedBodies, typeof( TagSet ) );
3434
jso["IncludedBodies"] = Json.ToNode( IncludedBodies, typeof( TagSet ) );
35+
jso["DeferGeneration"] = DeferGeneration;
3536
jso["CustomBounds"] = CustomBounds;
3637
if ( CustomBounds ) jso["Bounds"] = Json.ToNode( Bounds, typeof( BBox ) );
3738

@@ -63,6 +64,7 @@ internal void Deserialize( JsonObject jso )
6364

6465
ExcludedBodies = Json.FromNode<TagSet>( jso["ExcludedBodies"] ) ?? ExcludedBodies;
6566
IncludedBodies = Json.FromNode<TagSet>( jso["IncludedBodies"] ) ?? IncludedBodies;
67+
DeferGeneration = (bool)(jso["DeferGeneration"] ?? DeferGeneration);
6668
CustomBounds = (bool)(jso["CustomBounds"] ?? CustomBounds);
6769
Bounds = CustomBounds && jso["Bounds"] is not null ? Json.FromNode<BBox>( jso["Bounds"] ) : default;
6870

engine/Sandbox.Engine/Game/Navigation/NavMesh/NavMesh.Tile.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ internal class NavMeshTile : IDisposable
1717

1818
public bool IsHeightFieldValid => _compressedHeightField != null;
1919

20+
/// <summary>
21+
/// True if the cached heightfield originates from baked data rather than live geometry.
22+
/// </summary>
23+
public bool IsBakedHeightField { get; private set; }
24+
2025
public byte[] CompressedHeightField => _compressedHeightField;
2126

2227
public void HeightfieldBuildComplete()
@@ -81,11 +86,13 @@ public void SetCachedHeightField( CompactHeightfield chf )
8186
// a window where concurrent readers see null
8287
var compressed = Compress( chf );
8388
_compressedHeightField = compressed;
89+
IsBakedHeightField = false;
8490
}
8591

8692
internal void SetCompressedHeightField( byte[] compressedData )
8793
{
8894
_compressedHeightField = compressedData;
95+
IsBakedHeightField = true;
8996
}
9097

9198
public void DispatchNavmeshBuild( NavMesh navMesh )

engine/Sandbox.Engine/Game/Navigation/NavMesh/NavMesh.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ public bool IsEnabled
8888
/// </summary>
8989
public TagSet IncludedBodies { get; set; } = new();
9090

91+
/// <summary>
92+
/// Skip tile generation during scene load. Tiles can then be generated on demand
93+
/// via <see cref="GenerateTile"/>, <see cref="RequestTileGeneration"/>, etc.
94+
/// </summary>
95+
public bool DeferGeneration { get; set; } = false;
96+
9197
/// <summary>
9298
/// By Default , the navmesh will calculate bounds based on the world geometry, but if you want to override that, you can set custom bounds here.
9399
/// </summary>
@@ -273,11 +279,23 @@ internal async Task<bool> Load( PhysicsWorld world )
273279

274280
Init();
275281

276-
await LoadFromBake();
282+
if ( !DeferGeneration )
283+
{
284+
await LoadFromBake();
277285

278-
if ( !CustomBounds ) Bounds = CalculateWorldBounds( world );
286+
if ( !CustomBounds ) Bounds = CalculateWorldBounds( world );
279287

280-
await GenerateTiles( world, Bounds );
288+
await GenerateTiles( world, Bounds );
289+
}
290+
else
291+
{
292+
if ( !string.IsNullOrEmpty( _bakedDataPath ) )
293+
{
294+
Log.Warning( "NavMesh: Baked data is ignored when DeferGeneration is enabled" );
295+
}
296+
297+
if ( !CustomBounds ) Bounds = CalculateWorldBounds( world );
298+
}
281299
}
282300
finally
283301
{

engine/Sandbox.Engine/Scene/Components/Mesh/PolygonMesh.Serialize.cs

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ public static object JsonRead( ref Utf8JsonReader reader, Type typeToConvert )
3434
var propertyName = reader.GetString();
3535
reader.Read();
3636

37+
// Legacy: Position/Rotation are no longer written. They were world-space values
38+
// that got overwritten by MeshComponent on enable. Kept for backward compat with
39+
// old data that may not have TextureCoord and needs these to reconstruct UVs.
3740
if ( propertyName == "Position" )
3841
mesh._transform = mesh.Transform.WithPosition( JsonSerializer.Deserialize<Vector3>( ref reader ) );
3942

@@ -70,6 +73,9 @@ public static object JsonRead( ref Utf8JsonReader reader, Type typeToConvert )
7073
else if ( propertyName == "TextureRotation" )
7174
mesh.TextureRotationUnused.CopyFrom( JsonSerializer.Deserialize<Rotation[]>( ref reader ) );
7275

76+
// Legacy: Texture parameters are no longer written. They are world-space derived
77+
// values recomputed at runtime from TextureCoord via ComputeFaceTextureParametersFromCoordinates().
78+
// Kept for backward compat with old data that lacks TextureCoord.
7379
else if ( propertyName == nameof( TextureUAxis ) )
7480
mesh.TextureUAxis.CopyFrom( JsonSerializer.Deserialize<Vector3[]>( ref reader ) );
7581

@@ -156,11 +162,10 @@ public static void JsonWrite( object value, Utf8JsonWriter writer )
156162
writer.WritePropertyName( nameof( mesh.Topology ) );
157163
writer.WriteBase64StringValue( ms.ToArray() );
158164

159-
writer.WritePropertyName( "Position" );
160-
JsonSerializer.Serialize( writer, mesh.Transform.Position );
161-
162-
writer.WritePropertyName( "Rotation" );
163-
JsonSerializer.Serialize( writer, mesh.Transform.Rotation );
165+
// Position, Rotation, TextureUAxis, TextureVAxis, TextureScale, TextureOffset are not
166+
// serialized because they are world-space dependent and derived at runtime.
167+
// MeshComponent sets Mesh.Transform = WorldTransform on enable/transform change, which
168+
// triggers ComputeFaceTextureParametersFromCoordinates() to recompute them from TextureCoord.
164169

165170
writer.WritePropertyName( nameof( mesh.Positions ) );
166171
JsonSerializer.Serialize( writer, mesh.Positions );
@@ -174,18 +179,6 @@ public static void JsonWrite( object value, Utf8JsonWriter writer )
174179
writer.WritePropertyName( nameof( mesh.TextureCoord ) );
175180
JsonSerializer.Serialize( writer, mesh.TextureCoord );
176181

177-
writer.WritePropertyName( nameof( mesh.TextureUAxis ) );
178-
JsonSerializer.Serialize( writer, mesh.TextureUAxis );
179-
180-
writer.WritePropertyName( nameof( mesh.TextureVAxis ) );
181-
JsonSerializer.Serialize( writer, mesh.TextureVAxis );
182-
183-
writer.WritePropertyName( nameof( mesh.TextureScale ) );
184-
JsonSerializer.Serialize( writer, mesh.TextureScale );
185-
186-
writer.WritePropertyName( nameof( mesh.TextureOffset ) );
187-
JsonSerializer.Serialize( writer, mesh.TextureOffset );
188-
189182
writer.WritePropertyName( nameof( mesh.MaterialIndex ) );
190183
JsonSerializer.Serialize( writer, mesh.MaterialIndex );
191184

engine/Sandbox.Engine/Scene/Components/PostProcessing/Effects/Bloom.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public enum FilterMode
3232

3333
[Property] public FilterMode Filter { get; set; } = FilterMode.Bilinear;
3434

35-
CommandList command = new CommandList();
35+
CommandList command = new CommandList( "Bloom" );
3636

3737
private static Material Shader = Material.FromShader( "postprocess_bloom.shader" );
3838

engine/Sandbox.Engine/Systems/Networking/Networking.Thread.cs

Lines changed: 8 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
1-
using System.Diagnostics;
2-
using System.Threading;
1+
using System.Threading;
32

43
namespace Sandbox;
54

65
public static partial class Networking
76
{
8-
private static volatile bool s_isClosing;
9-
private static readonly ManualResetEventSlim ShutdownEvent = new( false );
7+
private static bool _isClosing;
108

119
internal static void StartThread()
1210
{
13-
s_isClosing = false;
14-
ShutdownEvent.Reset();
11+
_isClosing = false;
1512

1613
var thread = new Thread( RunThread )
1714
{
@@ -24,25 +21,16 @@ internal static void StartThread()
2421

2522
internal static void StopThread()
2623
{
27-
s_isClosing = true;
28-
ShutdownEvent.Set();
24+
_isClosing = true;
2925
}
3026

31-
static readonly Lock NetworkThreadLock = new Lock();
32-
33-
/// <summary>
34-
/// The target tick rate for the networking thread, updated from the main thread
35-
/// each frame to avoid thread safety issues with ProjectSettings.
36-
/// </summary>
37-
private static volatile int s_threadTickRate = 30;
27+
static Lock NetworkThreadLock = new Lock();
3828

3929
private static void RunThread()
4030
{
4131
try
4232
{
43-
var stopwatch = Stopwatch.StartNew();
44-
45-
while ( !s_isClosing )
33+
while ( !_isClosing )
4634
{
4735
var system = System;
4836

@@ -54,24 +42,10 @@ private static void RunThread()
5442
}
5543
}
5644

57-
var targetMs = 1000.0 / s_threadTickRate;
58-
var elapsed = stopwatch.Elapsed.TotalMilliseconds;
59-
60-
stopwatch.Restart();
61-
62-
var remainingMs = targetMs - elapsed;
63-
64-
if ( remainingMs > 1.0 )
65-
{
66-
ShutdownEvent.Wait( (int)remainingMs );
67-
}
68-
else if ( remainingMs > 0 )
69-
{
70-
Thread.Yield();
71-
}
45+
Thread.Sleep( 1 );
7246
}
7347
}
74-
catch ( Exception e )
48+
catch ( System.Exception e )
7549
{
7650
Log.Error( e, "Network Thread Error" );
7751
}

engine/Sandbox.Engine/Systems/Networking/Networking.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,8 +371,6 @@ private static void UpdateLocalStats()
371371

372372
internal static void PreFrameTick()
373373
{
374-
s_threadTickRate = (int)ProjectSettings.Networking.UpdateRate.Clamp( 1, 500 );
375-
376374
UpdateFakeLag();
377375

378376
try

0 commit comments

Comments
 (0)