mirror of
https://github.com/Facepunch/sbox-public.git
synced 2026-04-20 06:19:05 -04:00
* Remove unnecessary static singletons in MainMenu code * Empty SceneWorld delete queues during shutdown * Dresser cancel async load operation on destroy * Use reflection to null references to static native resources on shutdown This way we don't have to remember doing this manually. * Fix SoundOcclusionSystem using static lists to reference resources * Sound System: Use weak references to refer to scenes * Cleanup static logging listeners that kept strong refs to panels * UISystem Cleanup, make sure all panel/stylesheet refs are released * RenderTarget and RenderAttributes shutdown cleanup * Rework AvatarLoader, ThumbLoader & HTTPImageLoader Cache First try to go through ResourceLibrary.WeakIndex which might already hold the texture. If there is no hit, go through a second cache that caches HTTP & Steam API response bytes instead of textures. We want to cache the response bytes rather than the actual Texture, so stuff cached sits in RAM not VRAM. Before avatars and thumbs would reside in VRAM. * Fix rendertarget leak in CommandList.Attr.SetValue GetDepthTarget() / GetColorTarget() return a new strong handle (ref count +1). We need to DestroyStrongHandle() that ref. So handles don't leak. * Call EngineLoop.DrainFrameEndDisposables before shutdown * NativeResourceCache now report leaks on shutdown * Override Resource.Destroy for native resources, kill stronghandles * Deregister SceneWorld from SceneWorld.All during destruction * Ensure async texture loading cancels on shutdown * SkinnedModelRender bonemergetarget deregister from target OnDisabled * Clear shaderMaterials cache during shutdown * Refactor Shutdown code Mostly renaming methods from Clear() -> Shutdown() Adding separate GlobalContext.Shutdown function (more aggressive than GlobalContext.Reset). Clear some static input state. * Deregister surfaces from Surface.All in OnDestroy * RunAllStaticConstructors when loading a mount * Advanced managed resource leak tracker enabled via `resource_leak_tracking 1` Works by never pruning the WeakTable in NativeResourceCache. So we can check for all resources if they are still being held on to and log a callstack.
163 lines
5.1 KiB
C#
163 lines
5.1 KiB
C#
namespace Sandbox;
|
|
|
|
/// <summary>
|
|
/// Updates sound occlusion in parallel during StartFixedUpdate.
|
|
/// This moves the expensive physics traces out of the main sound tick loop
|
|
/// and parallelizes them across all sounds rather than per-sound.
|
|
/// </summary>
|
|
internal sealed class SoundOcclusionSystem : GameObjectSystem<SoundOcclusionSystem>
|
|
{
|
|
/// <summary>
|
|
/// Cached data for parallel occlusion updates.
|
|
/// We cache listener positions on the main thread because Listener.Position
|
|
/// has a main thread assertion and cannot be accessed from worker threads.
|
|
/// </summary>
|
|
record struct PendingOcclusionUpdate( SoundHandle Handle, Audio.SteamAudioSource Source, Vector3 ListenerPosition );
|
|
|
|
readonly List<SoundHandle> _tempHandles = new();
|
|
readonly List<PendingOcclusionUpdate> _pendingUpdates = new();
|
|
readonly List<Audio.Listener> _sceneListeners = new();
|
|
readonly Dictionary<Audio.Mixer, int> _voiceCountByMixer = new();
|
|
|
|
public SoundOcclusionSystem( Scene scene ) : base( scene )
|
|
{
|
|
Listen( Stage.StartFixedUpdate, -50, UpdateSoundOcclusion, "SoundOcclusion" );
|
|
}
|
|
|
|
void UpdateSoundOcclusion()
|
|
{
|
|
using var _ = PerformanceStats.Timings.Audio.Scope();
|
|
|
|
var world = Scene.PhysicsWorld;
|
|
if ( !world.IsValid() ) return;
|
|
|
|
// Get listener positions for this scene - filter directly from ActiveList
|
|
_sceneListeners.Clear();
|
|
var scene = Scene;
|
|
|
|
foreach ( var listener in Audio.Listener.ActiveList )
|
|
{
|
|
if ( listener.Scene == scene )
|
|
{
|
|
_sceneListeners.Add( listener );
|
|
}
|
|
}
|
|
|
|
if ( _sceneListeners.Count == 0 ) return;
|
|
|
|
// Collect all pending updates on the main thread.
|
|
// We must cache listener positions here because Listener.Position
|
|
// requires main thread access (ThreadSafe.AssertIsMainThread).
|
|
_pendingUpdates.Clear();
|
|
GetPendingUpdates( _sceneListeners );
|
|
|
|
if ( _pendingUpdates.Count == 0 ) return;
|
|
|
|
// Process all updates in parallel - each does its own sequential ray traces
|
|
Sandbox.Utility.Parallel.ForEach( _pendingUpdates, update =>
|
|
{
|
|
var handle = update.Handle;
|
|
var targetMixer = handle.TargetMixer ?? Audio.Mixer.Default;
|
|
var position = handle.Transform.Position;
|
|
var occlusionSize = handle.OcclusionRadius;
|
|
var listenerPos = update.ListenerPosition;
|
|
|
|
var occlusion = ComputeOcclusion( position, listenerPos, occlusionSize, targetMixer, world );
|
|
|
|
update.Source.SetTargetOcclusion( occlusion );
|
|
|
|
// Schedule next update based on distance - close sounds update more often
|
|
// Distance threshold: 8192 units (~208 meters)
|
|
// Close sounds: ~7 Hz, Far sounds: ~0.33 Hz
|
|
var distance = Vector3.DistanceBetween( position, listenerPos ).Remap( 0, 8192, 1, 0 ).Clamp( 0, 1 );
|
|
update.Source.TimeUntilNextOcclusionCalc = distance.Remap( 3.0f, 0.15f ) * Random.Shared.Float( 0.9f, 1.1f );
|
|
} );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compute how occluded a sound is. Returns 0 if fully occluded, 1 if not occluded.
|
|
/// </summary>
|
|
static float ComputeOcclusion( Vector3 position, Vector3 listener, float occlusionSize, Audio.Mixer targetMixer, PhysicsWorld world )
|
|
{
|
|
var distance = Vector3.DistanceBetween( position, listener ).Remap( 0, 4096, 1, 0 );
|
|
|
|
int iRays = (occlusionSize.Remap( 0, 64, 1, 32 ) * distance).CeilToInt().Clamp( 1, 32 );
|
|
int iHits = 0;
|
|
var tags = targetMixer.GetOcclusionTags();
|
|
|
|
// tags are defined, but are empty, means hit nothing - so 0% occluded
|
|
// if it is null, then we just use the "world" tag
|
|
if ( tags is not null && tags.Count == 0 ) return 1.0f;
|
|
|
|
for ( int i = 0; i < iRays; i++ )
|
|
{
|
|
var startPos = position + Vector3.Random * occlusionSize * 0.5f;
|
|
|
|
var tq = world.Trace.FromTo( startPos, listener );
|
|
|
|
if ( tags is null )
|
|
{
|
|
tq = tq.WithTag( "world" );
|
|
}
|
|
else
|
|
{
|
|
tq = tq.WithAnyTags( tags );
|
|
}
|
|
|
|
var tr = tq.Run();
|
|
|
|
if ( tr.Hit )
|
|
{
|
|
iHits++;
|
|
}
|
|
}
|
|
|
|
return 1 - (iHits / (float)iRays);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Collect all handle/listener pairs that need occlusion updates this frame.
|
|
/// Must be called on main thread to access Listener.Position.
|
|
/// </summary>
|
|
void GetPendingUpdates( List<Audio.Listener> listeners )
|
|
{
|
|
_tempHandles.Clear();
|
|
SoundHandle.GetActive( _tempHandles );
|
|
|
|
// Sort by creation time descending (newest first) to match mixer priority.
|
|
_tempHandles.Sort( SoundHandle.ByCreatedTimeDescending );
|
|
|
|
// Track voice count per mixer to respect MaxVoices limits
|
|
_voiceCountByMixer.Clear();
|
|
|
|
foreach ( var handle in _tempHandles )
|
|
{
|
|
if ( handle.Scene != Scene ) continue;
|
|
if ( !handle.Occlusion ) continue;
|
|
if ( handle.ListenLocal ) continue;
|
|
|
|
// Use shared CanBeMixed check (same as Mixer.ShouldPlay)
|
|
if ( !handle.CanBeMixed() ) continue;
|
|
|
|
var mixer = handle.GetEffectiveMixer();
|
|
if ( mixer is null ) continue;
|
|
|
|
// Check if we've exceeded MaxVoices for this mixer
|
|
_voiceCountByMixer.TryGetValue( mixer, out var count );
|
|
if ( count >= mixer.MaxVoices ) continue;
|
|
_voiceCountByMixer[mixer] = count + 1;
|
|
|
|
foreach ( var listener in listeners )
|
|
{
|
|
var source = handle.GetSource( listener );
|
|
if ( source is null ) continue;
|
|
|
|
if ( source.TimeUntilNextOcclusionCalc <= 0 )
|
|
{
|
|
_pendingUpdates.Add( new PendingOcclusionUpdate( handle, source, listener.Position ) );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|