Compare commits

...

60 Commits

Author SHA1 Message Date
metalgearsloth
5069b0ccf9 Version: 189.0.0 2023-12-10 12:44:00 +11:00
chromiumboy
12cfdb2175 Tweaked how ExpandPvsEvent is raised (#4665) 2023-12-10 12:35:00 +11:00
metalgearsloth
b5e079815d Add MIDI semaphore (#4643) 2023-12-10 12:34:24 +11:00
metalgearsloth
ca3a3279c5 Set priorGain to 0 if gain is 0 (#4680) 2023-12-10 12:31:06 +11:00
Leon Friedrich
003752a161 Improve yaml linter error messages (#4683) 2023-12-10 12:18:52 +11:00
Leon Friedrich
55e51cba9c Fix client-side entity error spam (#4673) 2023-12-10 12:17:46 +11:00
Leon Friedrich
2cd829f4f6 SpriteView Modulation (#4584) 2023-12-10 12:02:21 +11:00
metalgearsloth
43138669ec Network base not adjusted audio params (#4679) 2023-12-09 17:12:09 +11:00
metalgearsloth
3fe30bc00f Version: 188.0.0 2023-12-09 15:17:37 +11:00
metalgearsloth
3ccbdeac6a Fix GetDimensions for screenhandle (#4677) 2023-12-09 15:04:45 +11:00
metalgearsloth
9eb9c91da6 Fix predicted audio not using adjust params (#4676) 2023-12-09 14:59:22 +11:00
metalgearsloth
28d2b47a2c Log errors on spawning audio to deleting ents (#4629) 2023-12-09 14:30:18 +11:00
metalgearsloth
049ffa05e4 Remove EntityQuery<T> from MapVelocity API (#4648) 2023-12-09 14:12:04 +11:00
metalgearsloth
2f36a0a5fc Change midi volume to gain (#4639) 2023-12-09 14:03:01 +11:00
Kelrak
41d03db59d Possible fix to some audio issues (#4640) 2023-12-09 13:39:09 +11:00
Łukasz Mędrek
68df887a65 Fix sorting order in entity spawn panel (#3767) (#4671) 2023-12-09 12:46:57 +11:00
metalgearsloth
e0bbcd7b08 Return null buffered audio on exception (#4624) 2023-12-09 12:44:16 +11:00
metalgearsloth
525815427e Version: 187.2.0 2023-12-06 20:07:30 +11:00
Tom Richardson
70224ac100 Update map physics after events (#4660)
Co-authored-by: metalgearsloth <comedian_vs_clown@hotmail.com>
2023-12-06 20:02:10 +11:00
metalgearsloth
dabb090dc2 Version: 187.1.2 2023-12-06 14:00:54 +11:00
metalgearsloth
ace8334a3e Bandaid physics contacts getting modified during collision (#4666) 2023-12-06 13:36:33 +11:00
metalgearsloth
d8e70b4d52 Version: 187.1.1 2023-12-05 00:44:56 +11:00
metalgearsloth
2fca0e03ee Don't RegenerateContacts for disabled bodies (#4658) 2023-12-05 00:42:09 +11:00
metalgearsloth
b6980964b6 Revert physics jobs (#4663) 2023-12-05 00:30:02 +11:00
metalgearsloth
34d02256fd Version: 187.1.0 2023-12-03 00:59:49 +11:00
metalgearsloth
34637fb430 Avoid recontruction broadphase job every tick (#4653) 2023-12-03 00:49:05 +11:00
metalgearsloth
d905ef2a50 Apply default audio to MIDIs (#4626) 2023-12-03 00:34:28 +11:00
metalgearsloth
c3f7ef1b5c Version: 187.0.0 2023-12-02 19:38:56 +11:00
metalgearsloth
ced2a5c6cd Make PVS overrides less bad and fix audio (#4656) 2023-12-02 19:36:55 +11:00
metalgearsloth
9ec927543f Remove audio logging (#4654) 2023-12-02 10:43:06 +11:00
Pieter-Jan Briers
215fc8c229 Begone, toolshed logs
They're verbose now
2023-12-01 22:30:44 +01:00
Pieter-Jan Briers
f82ff9e581 Improve error message for network failing to initialize.
The "make sure port XXXX is open" thing now only appears if the error was an actual "address in use" error.
2023-12-01 22:18:38 +01:00
metalgearsloth
906f4598a2 Version: 186.1.0 2023-12-01 20:37:02 +11:00
metalgearsloth
0eb3c37bd8 Add detailed audio logging (#4652) 2023-12-01 20:33:29 +11:00
metalgearsloth
9cc7cf80ba Version: 186.0.0 2023-12-01 19:14:21 +11:00
metalgearsloth
7ba02b5ca6 Store audio on its own map (#4651) 2023-12-01 18:48:33 +11:00
metalgearsloth
3ca7121f5b Don't stop playing audio on game disposal (#4649) 2023-12-01 18:45:37 +11:00
metalgearsloth
d3b31c1d58 Fix out of range MIDI (#4650) 2023-12-01 17:36:58 +11:00
metalgearsloth
92b5bb4660 Version: 185.2.0 2023-12-01 00:18:51 +11:00
metalgearsloth
33caf9c1ba Cap MIDI update rate (#4644) 2023-11-30 21:56:56 +11:00
metalgearsloth
58da8a6001 Fix deleted entity spam for midis (#4647) 2023-11-30 21:56:11 +11:00
metalgearsloth
962f5dc650 Version: 185.1.1 2023-11-30 11:24:34 +11:00
metalgearsloth
c324562513 Fix audio z-offset not applying correctly (#4641) 2023-11-30 10:10:33 +11:00
metalgearsloth
f5a2a710f0 Nuke some non-approx grid queries (#4637) 2023-11-29 22:55:33 +11:00
metalgearsloth
357283e2bc Version: 185.1.0 2023-11-29 16:40:44 +11:00
metalgearsloth
5991bfa106 Fix audio position floating point imprecision (#4634) 2023-11-29 16:39:23 +11:00
metalgearsloth
4160b120e0 Set listener velocity for audio (#4635) 2023-11-29 16:31:22 +11:00
metalgearsloth
98a1fa1fba Version: 185.0.0 2023-11-29 11:02:53 +11:00
metalgearsloth
fb08451849 Replace Parallel.For with ParallelManager (#4588) 2023-11-29 10:57:52 +11:00
metalgearsloth
ebea0d7572 Add grid audio flag (#4632) 2023-11-29 10:19:17 +11:00
metalgearsloth
eb6f28cce0 Version: 184.1.0 2023-11-28 23:55:55 +11:00
metalgearsloth
a1d02d7c55 Add gain setter for audio params + API cleanup (#4627) 2023-11-28 22:54:19 +11:00
metalgearsloth
777ab85cff Version: 184.0.1 2023-11-28 20:46:53 +11:00
metalgearsloth
d33a8465b0 Fix global audio (#4625) 2023-11-28 20:44:40 +11:00
metalgearsloth
6572fdb404 Midi tweaks (#4618) 2023-11-28 20:39:59 +11:00
Uriende
6273b1b80d Only change the offset if has already started (#4619) 2023-11-28 20:29:19 +11:00
Nemanja
a09a60efe9 Adjust how KeyBindUp retreives the focused control (#4620) 2023-11-28 19:22:05 +11:00
metalgearsloth
d3339964ee Version: 184.0.0 2023-11-28 19:13:18 +11:00
metalgearsloth
3ffef625ec Pool MsgState streams (#4582) 2023-11-28 19:10:30 +11:00
metalgearsloth
4fd9b2bc3b Add another GetEntitiesInRange overload (#4587) 2023-11-28 14:18:40 +11:00
63 changed files with 1509 additions and 549 deletions

View File

@@ -1,4 +1,4 @@
<Project>
<!-- This file automatically reset by Tools/version.py -->
<!-- This file automatically reset by Tools/version.py -->

View File

@@ -54,6 +54,184 @@ END TEMPLATE-->
*None yet*
## 189.0.0
### Breaking changes
* Use the base AudioParams for networking not the z-offset adjusted ones.
* Modulate SpriteView sprites by the control's color modulation.
### New features
* Improve YAML linter error messages for parent nodes.
### Bugfixes
* Client clientside entity error spam.
### Internal
* Set priorGain to 0 where no EFX is supported for audio rather than 0.5.
* Try to hotfix MIDI lock contention more via a semaphore.
## 188.0.0
### Breaking changes
* Return null buffered audio if there's an exception and use the dummy instance internally.
* Use entity name then suffix for entity spawn window ordering.
* Change MidiManager volume to gain.
* Remove EntityQuery from the MapVelocity API.
### Bugfixes
* Potentially fix some audio issues by setting gain to half where EFX not found and the prior gain was 0.
* Log errors upon trying to spawn audio attached to deleted entities instead of trying to spawn them and erroring later.
* Fixed predicted audio spawns not applying the adjusted audio params.
* Fix GetDimensions for the screenhandle where the text is only a single line.
## 187.2.0
### New features
* Added a cancellable bool to physics sleeping events where we may wish to cancel it.
### Bugfixes
* Fix corrupted physics awake state leading to client mispredicts.
## 187.1.2
### Bugfixes
* Hotfix contact nullrefs if they're modified during manifold generation.
## 187.1.1
### Bugfixes
* Revert physics solver job to fix crashes until box2d v3 rolls around.
* Don't RegenerateContacts if the body isn't collidable to avoid putting non-collidable proxies on the movebuffer.
## 187.1.0
### Bugfixes
* Apply default audio params to all audio sources not just non-buffered ones.
* Avoid re-allocating broadphase job every tick and maybe fix a rare nullref for it.
## 187.0.0
### New features
* Improved error message for network failing to initialize.
### Bugfixes
* Fix not being able to add multiple PVS session overrides in a single tick without overwriting each one. This should fix issues with audio filters.
### Other
* Changed toolshed initialisation logs to verbose.
## 186.1.0
### New features
* Add public method to get PVS session overrides for a specific session.
### Internal
* Add temporary audio debugging.
## 186.0.0
### Breaking changes
* Global audio is now stored on its own map to avoid contamination issues with nullspace.
### Bugfixes
* Fix MIDIs playing cross-map
* Only dispose audio on game closure and don't stop playing if it's disposed elsewhere i.e. MIDIs.
## 185.2.0
### Bugfixes
* Bandaid deleted MIDI source entities spamming velocity error logs.
### Other
* Reverted MIDI audio not updating every frame due to lock contention with the MIDI renderer for now.
## 185.1.1
### Bugfixes
* Fix Z-Offset for audio not being applied on initialization.
### Internal
* Flag some internal queries as approximate to avoid unnecessary AABB checks. Some of these are already covered off with TestOverlap calls and the rest will need updating to do so in a future update.
## 185.1.0
### New features
* Audio listener's velocity is set using the attached entity's velocity rather than ignored.
### Bugfixes
* Fix imprecision on audio position
## 185.0.0
### Breaking changes
* Added a flag for grid-based audio rather than implicitly doing it.
### New features
* Added IRobustJob and IParallelRobustJob (which splits out into IRobustJob). These can be passed to ParallelManager for work to be run on the threadpool without relying upon Task.Run / Parallel.For which can allocate significantly more. It also has conveniences such as being able to specify batch sizing via the interface implementation.
## 184.1.0
### New features
* Add API to get gain / volume for a provided value on SharedAudioSystem.
* Make GetOcclusion public for AudioSystem.
* Add SharedAudioSystem.SetGain to complement SharedAudioSystem.SetVolume
## 184.0.1
### Bugfixes
* Update MIDI position and occlusion every frame instead of at set intervals.
* Fix global audio not being global.
## 184.0.0
### Internal
* Add RobustMemoryManager with RecyclableIOMemoryStream to significantly reduce MsgState allocations until better memory management is implemented.
## 183.0.0
### Breaking changes

View File

@@ -60,6 +60,12 @@ internal partial class AudioManager
}
}
/// <inheritdoc/>
public void SetVelocity(Vector2 velocity)
{
AL.Listener(ALListener3f.Velocity, velocity.X, velocity.Y, 0f);
}
/// <inheritdoc/>
public void SetPosition(Vector2 position)
{
@@ -208,9 +214,9 @@ internal partial class AudioManager
return new AudioStream(handle, length, channels, name);
}
public void SetMasterVolume(float newVolume)
public void SetMasterGain(float newGain)
{
AL.Listener(ALListenerf.Gain, newVolume);
AL.Listener(ALListenerf.Gain, newGain);
}
public void SetAttenuation(Attenuation attenuation)
@@ -272,25 +278,37 @@ internal partial class AudioManager
var audioSource = new AudioSource(this, source, stream);
_audioSources.Add(source, new WeakReference<BaseAudioSource>(audioSource));
ApplyDefaultParams(audioSource);
return audioSource;
}
public IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false)
/// <inheritdoc/>
public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio=false)
{
var source = AL.GenSource();
if (!AL.IsSource(source))
{
OpenALSawmill.Error("Failed to generate source. Too many simultaneous audio streams? {0}", Environment.StackTrace);
return null;
}
// ReSharper disable once PossibleInvalidOperationException
var audioSource = new BufferedAudioSource(this, source, AL.GenBuffers(buffers), floatAudio);
_bufferedAudioSources.Add(source, new WeakReference<BufferedAudioSource>(audioSource));
ApplyDefaultParams(audioSource);
return audioSource;
}
private void ApplyDefaultParams(IAudioSource source)
{
source.MaxDistance = AudioParams.Default.MaxDistance;
source.Pitch = AudioParams.Default.Pitch;
source.ReferenceDistance = AudioParams.Default.ReferenceDistance;
source.RolloffFactor = AudioParams.Default.RolloffFactor;
}
/// <inheritdoc />
public void StopAllAudio()
{
@@ -318,7 +336,6 @@ internal partial class AudioManager
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
target.Dispose();
}
}
@@ -329,7 +346,6 @@ internal partial class AudioManager
{
if (source.TryGetTarget(out var target))
{
target.Playing = false;
target.Dispose();
}
}

View File

@@ -115,7 +115,7 @@ internal sealed partial class AudioManager : IAudioInternal
IsEfxSupported = HasAlDeviceExtension("ALC_EXT_EFX");
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterVolume, true);
_cfg.OnValueChanged(CVars.AudioMasterVolume, SetMasterGain, true);
}
internal bool IsMainThread()

View File

@@ -69,7 +69,7 @@ public sealed class AudioOverlay : Overlay
var screenPos = args.ViewportControl.WorldToScreen(audioPos);
var distance = audioPos - listenerPos.Position;
var posOcclusion = _audio.GetOcclusion(uid, listenerPos, distance, distance.Length());
var posOcclusion = _audio.GetOcclusion(listenerPos, distance, distance.Length(), uid);
output.Clear();
output.AppendLine("Audio Source");

View File

@@ -2,9 +2,9 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Threading.Tasks;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Player;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Audio;
@@ -23,7 +23,6 @@ using Robust.Shared.Player;
using Robust.Shared.Replays;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
using AudioComponent = Robust.Shared.Audio.Components.AudioComponent;
namespace Robust.Client.Audio;
@@ -34,6 +33,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
* but exposing the whole thing in an easy way is a lot of effort.
*/
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IReplayRecordingManager _replayRecording = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
@@ -49,25 +49,52 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
private readonly List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> _streams = new();
private EntityUid? _listenerGrid;
private UpdateAudioJob _updateAudioJob;
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<PhysicsComponent> _physicsQuery;
private EntityQuery<TransformComponent> _xformQuery;
private float _maxRayLength;
public override float ZOffset
{
get => _zOffset;
protected set
{
_zOffset = value;
_audio.SetZOffset(value);
var query = AllEntityQuery<AudioComponent>();
while (query.MoveNext(out var audio))
{
// Pythagoras back to normal then adjust.
var maxDistance = GetAudioDistance(audio.Params.MaxDistance);
var refDistance = GetAudioDistance(audio.Params.ReferenceDistance);
audio.MaxDistance = maxDistance;
audio.ReferenceDistance = refDistance;
}
}
}
private float _zOffset;
/// <inheritdoc />
public override void Initialize()
{
base.Initialize();
_updateAudioJob = new UpdateAudioJob
{
System = this,
Streams = _streams,
};
UpdatesOutsidePrediction = true;
// Need to run after Eye updates so we have an accurate listener position.
UpdatesAfter.Add(typeof(EyeSystem));
_gridQuery = GetEntityQuery<MapGridComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<AudioComponent, ComponentStartup>(OnAudioStartup);
SubscribeLocalEvent<AudioComponent, ComponentShutdown>(OnAudioShutdown);
@@ -103,13 +130,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// </summary>
public void SetMasterVolume(float value)
{
_audio.SetMasterVolume(value);
}
protected override void SetZOffset(float value)
{
base.SetZOffset(value);
_audio.SetZOffset(value);
_audio.SetMasterGain(value);
}
public override void Shutdown()
@@ -140,7 +161,6 @@ public sealed partial class AudioSystem : SharedAudioSystem
if (!TryGetAudio(component.FileName, out var audioResource))
{
Log.Error($"Error creating audio source for {audioResource}, can't find file {component.FileName}");
component.Source = new DummyAudioSource();
return;
}
@@ -150,20 +170,22 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
Log.Error($"Error creating audio source for {audioResource}");
DebugTools.Assert(false);
source = new DummyAudioSource();
source = component.Source;
}
// Need to set all initial data for first frame.
component.Source = source;
// Need to set all initial data for first frame.
ApplyAudioParams(component.Params, component);
component.Global = component.Global;
source.Global = component.Global;
// Don't play until first frame so occlusion etc. are correct.
component.Gain = 0f;
// If audio came into range then start playback at the correct position.
var offset = (Timing.CurTime - component.AudioStart).TotalSeconds % GetAudioLength(component.FileName).TotalSeconds;
if (offset != 0)
if (offset > 0)
{
component.PlaybackPosition = (float) offset;
}
@@ -188,11 +210,19 @@ public sealed partial class AudioSystem : SharedAudioSystem
public override void FrameUpdate(float frameTime)
{
var eye = _eyeManager.CurrentEye;
var localEntity = _playerManager.LocalEntity;
Vector2 listenerVelocity;
if (localEntity != null)
listenerVelocity = _physics.GetMapLinearVelocity(localEntity.Value);
else
listenerVelocity = Vector2.Zero;
_audio.SetVelocity(listenerVelocity);
_audio.SetRotation(eye.Rotation);
_audio.SetPosition(eye.Position.Position);
var ourPos = eye.Position;
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parMan.ParallelProcessCount };
var ourPos = GetListenerCoordinates();
var query = AllEntityQuery<AudioComponent, TransformComponent>();
_streams.Clear();
@@ -207,7 +237,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
try
{
Parallel.ForEach(_streams, opts, comp => ProcessStream(comp.Entity, comp.Component, comp.Xform, ourPos));
_updateAudioJob.OurPosition = ourPos;
_parMan.ProcessNow(_updateAudioJob, _streams.Count);
}
catch (Exception e)
{
@@ -216,6 +247,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
}
}
public MapCoordinates GetListenerCoordinates()
{
return _eyeManager.CurrentEye.Position;
}
private void ProcessStream(EntityUid entity, AudioComponent component, TransformComponent xform, MapCoordinates listener)
{
// TODO:
@@ -253,7 +289,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
var gridUid = xform.ParentUid;
// Handle grid audio differently by using nearest-edge instead of entity centre.
if (_gridQuery.HasComponent(gridUid))
if ((component.Flags & AudioFlags.GridAudio) != 0x0)
{
// It's our grid so max volume.
if (_listenerGrid == gridUid)
@@ -309,8 +345,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
return;
}
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
// Update audio occlusion
var occlusion = GetOcclusion(entity, listener, delta, distance);
var occlusion = GetOcclusion(listener, delta, distance, entity);
component.Occlusion = occlusion;
// Update audio positions.
@@ -321,12 +364,15 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
// This actually gets the tracked entity's xform & iterates up though the parents for the second time. Bit
// inefficient.
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform, _xformQuery, _physicsQuery);
var velocity = _physics.GetMapLinearVelocity(entity, physicsComp, xform);
component.Velocity = velocity;
}
}
internal float GetOcclusion(EntityUid entity, MapCoordinates listener, Vector2 delta, float distance)
/// <summary>
/// Gets the audio occlusion from the target audio entity to the listener's position.
/// </summary>
public float GetOcclusion(MapCoordinates listener, Vector2 delta, float distance, EntityUid? ignoredEnt = null)
{
float occlusion = 0;
@@ -334,7 +380,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
var rayLength = MathF.Min(distance, _maxRayLength);
var ray = new CollisionRay(listener.Position, delta / distance, OcclusionCollisionMask);
occlusion = _physics.IntersectRayPenetration(listener.MapId, ray, rayLength, entity);
occlusion = _physics.IntersectRayPenetration(listener.MapId, ray, rayLength, ignoredEnt);
}
return occlusion;
@@ -452,6 +498,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayEntity(AudioStream stream, EntityUid entity, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(entity))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(entity)}");
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, new EntityCoordinates(entity, Vector2.Zero));
@@ -487,6 +539,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <param name="audioParams"></param>
private (EntityUid Entity, AudioComponent Component)? PlayStatic(AudioStream stream, EntityCoordinates coordinates, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
var playing = CreateAndStartPlayingStream(audioParams, stream);
_xformSys.SetCoordinates(playing.Entity, coordinates);
return playing;
@@ -559,6 +617,7 @@ public sealed partial class AudioSystem : SharedAudioSystem
offset = Math.Clamp(offset, 0f, (float) stream.Length.TotalSeconds - 0.01f);
source.PlaybackPosition = offset;
// For server we will rely on the adjusted one but locally we will have to adjust it ourselves.
ApplyAudioParams(audioP, comp);
comp.Params = audioP;
source.StartPlaying();
@@ -573,8 +632,8 @@ public sealed partial class AudioSystem : SharedAudioSystem
source.Pitch = audioParams.Pitch;
source.Volume = audioParams.Volume;
source.RolloffFactor = audioParams.RolloffFactor;
source.MaxDistance = audioParams.MaxDistance;
source.ReferenceDistance = audioParams.ReferenceDistance;
source.MaxDistance = GetAudioDistance(audioParams.MaxDistance);
source.ReferenceDistance = GetAudioDistance(audioParams.ReferenceDistance);
source.Looping = audioParams.Loop;
}
@@ -597,4 +656,25 @@ public sealed partial class AudioSystem : SharedAudioSystem
{
return _resourceCache.GetResource<AudioResource>(filename).AudioStream.Length;
}
#region Jobs
private record struct UpdateAudioJob : IParallelRobustJob
{
public int BatchSize => 2;
public AudioSystem System;
public MapCoordinates OurPosition;
public List<(EntityUid Entity, AudioComponent Component, TransformComponent Xform)> Streams;
public void Execute(int index)
{
var comp = Streams[index];
System.ProcessStream(comp.Entity, comp.Component, comp.Xform, OurPosition);
}
}
#endregion
}

View File

@@ -35,11 +35,16 @@ internal sealed class HeadlessAudioManager : IAudioInternal
}
/// <inheritdoc />
public IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio = false)
public IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio = false)
{
return DummyBufferedAudioSource.Instance;
}
/// <inheritdoc />
public void SetVelocity(Vector2 velocity)
{
}
/// <inheritdoc />
public void SetPosition(Vector2 position)
{
@@ -51,7 +56,7 @@ internal sealed class HeadlessAudioManager : IAudioInternal
}
/// <inheritdoc />
public void SetMasterVolume(float value)
public void SetMasterGain(float newGain)
{
}

View File

@@ -11,7 +11,7 @@ namespace Robust.Client.Audio;
/// <summary>
/// Handles clientside audio.
/// </summary>
internal interface IAudioInternal
internal interface IAudioInternal : IAudioManager
{
void InitializePostWindowing();
void Shutdown();
@@ -23,7 +23,16 @@ internal interface IAudioInternal
IAudioSource? CreateAudioSource(AudioStream stream);
IBufferedAudioSource CreateBufferedAudioSource(int buffers, bool floatAudio=false);
/// <summary>
/// Returns a buffered audio source.
/// </summary>
/// <returns>null if unable to create the source.</returns>
IBufferedAudioSource? CreateBufferedAudioSource(int buffers, bool floatAudio=false);
/// <summary>
/// Sets the velocity for the audio listener.
/// </summary>
void SetVelocity(Vector2 velocity);
/// <summary>
/// Sets position for the audio listener.
@@ -35,7 +44,6 @@ internal interface IAudioInternal
/// </summary>
void SetRotation(Angle angle);
void SetMasterVolume(float value);
void SetAttenuation(Attenuation attenuation);
/// <summary>

View File

@@ -0,0 +1,9 @@
namespace Robust.Client.Audio;
/// <summary>
/// Public audio API for stuff that can't go through <see cref="AudioSystem"/>
/// </summary>
public interface IAudioManager
{
void SetMasterGain(float gain);
}

View File

@@ -17,11 +17,9 @@ public interface IMidiManager
bool IsAvailable { get; }
/// <summary>
/// Volume, in db.
/// Gain of audio.
/// </summary>
float Volume { get; set; }
public int OcclusionCollisionMask { get; set; }
float Gain { get; set; }
/// <summary>
/// This method tries to return a midi renderer ready to be used.

View File

@@ -1,17 +1,15 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using NFluidsynth;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared;
using Robust.Shared.Asynchronous;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Midi;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Exceptions;
@@ -20,7 +18,6 @@ using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Threading;
@@ -34,14 +31,7 @@ internal sealed partial class MidiManager : IMidiManager
{
public const string SoundfontEnvironmentVariable = "ROBUST_SOUNDFONT_OVERRIDE";
private int _minRendererParallel;
private float _occlusionUpdateDelay;
private float _positionUpdateDelay;
[ViewVariables] private TimeSpan _nextOcclusionUpdate = TimeSpan.Zero;
[ViewVariables] private TimeSpan _nextPositionUpdate = TimeSpan.Zero;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IConfigurationManager _cfgMan = default!;
@@ -50,9 +40,10 @@ internal sealed partial class MidiManager : IMidiManager
[Dependency] private readonly ILogManager _logger = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly IRuntimeLog _runtime = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private AudioSystem _audioSys = default!;
private SharedPhysicsSystem _broadPhaseSystem = default!;
private SharedTransformSystem _xformSystem = default!;
public IReadOnlyList<IMidiRenderer> Renderers
{
@@ -79,24 +70,32 @@ internal sealed partial class MidiManager : IMidiManager
[ViewVariables] private readonly List<IMidiRenderer> _renderers = new();
// To avoid lock contention until some kind of MIDI refactor.
private TimeSpan _nextUpdate;
private TimeSpan _updateFrequency = TimeSpan.FromSeconds(0.1f);
private SemaphoreSlim _updateSemaphore = new(1);
private bool _alive = true;
[ViewVariables] private Settings? _settings;
private Thread? _midiThread;
private ISawmill _midiSawmill = default!;
private float _volume = 0f;
private float _gain = 0f;
private bool _volumeDirty = true;
// Not reliable until Fluidsynth is initialized!
[ViewVariables(VVAccess.ReadWrite)]
public float Volume
public float Gain
{
get => _volume;
get => _gain;
set
{
if (MathHelper.CloseToPercent(_volume, value))
var clamped = Math.Clamp(value, 0f, 1f);
if (MathHelper.CloseToPercent(_gain, clamped))
return;
_cfgMan.SetCVar(CVars.MidiVolume, value);
_cfgMan.SetCVar(CVars.MidiVolume, clamped);
_volumeDirty = true;
}
}
@@ -133,10 +132,9 @@ internal sealed partial class MidiManager : IMidiManager
private NFluidsynth.Logger.LoggerDelegate _loggerDelegate = default!;
private ISawmill _fluidsynthSawmill = default!;
private float _maxCastLength;
[ViewVariables(VVAccess.ReadWrite)]
public int OcclusionCollisionMask { get; set; }
private MidiUpdateJob _updateJob;
public MidiManager()
{
@@ -149,19 +147,10 @@ internal sealed partial class MidiManager : IMidiManager
_cfgMan.OnValueChanged(CVars.MidiVolume, value =>
{
_volume = value;
_gain = value;
_volumeDirty = true;
}, true);
_cfgMan.OnValueChanged(CVars.MidiMinRendererParallel,
value => _minRendererParallel = value, true);
_cfgMan.OnValueChanged(CVars.MidiOcclusionUpdateDelay,
value => _occlusionUpdateDelay = value, true);
_cfgMan.OnValueChanged(CVars.MidiPositionUpdateDelay,
value => _positionUpdateDelay = value, true);
_midiSawmill = _logger.GetSawmill("midi");
#if DEBUG
_midiSawmill.Level = LogLevel.Debug;
@@ -215,8 +204,17 @@ internal sealed partial class MidiManager : IMidiManager
_midiThread = new Thread(ThreadUpdate);
_midiThread.Start();
_updateJob = new MidiUpdateJob()
{
Manager = this,
Renderers = _renderers,
};
_audioSys = _entityManager.EntitySysManager.GetEntitySystem<AudioSystem>();
_broadPhaseSystem = _entityManager.EntitySysManager.GetEntitySystem<SharedPhysicsSystem>();
_cfgMan.OnValueChanged(CVars.AudioRaycastLength, OnRaycastLengthChanged, true);
_xformSystem = _entityManager.System<SharedTransformSystem>();
_entityManager.GetEntityQuery<PhysicsComponent>();
_entityManager.GetEntityQuery<TransformComponent>();
FluidsynthInitialized = true;
}
@@ -233,11 +231,6 @@ internal sealed partial class MidiManager : IMidiManager
_midiSawmill.Debug($"Synth Polyphony: {_settings["synth.polyphony"].IntValue}");
}
private void OnRaycastLengthChanged(float value)
{
_maxCastLength = value;
}
private void LoggerDelegate(NFluidsynth.Logger.LogLevel level, string message, IntPtr data)
{
var rLevel = level switch
@@ -352,7 +345,7 @@ internal sealed partial class MidiManager : IMidiManager
renderer.LoadSoundfont(file.ToString());
}
renderer.Source.Volume = _volume;
renderer.Source.Gain = _gain;
lock (_renderers)
{
@@ -373,44 +366,33 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
// Update positions of streams every frame.
if (_nextUpdate > _timing.RealTime)
return;
_nextUpdate = _timing.RealTime + _updateFrequency;
// Update positions of streams occasionally.
// This has a lot of code duplication with AudioSystem.FrameUpdate(), and they should probably be combined somehow.
// so TRUE
lock (_renderers)
{
if (_renderers.Count == 0)
return;
_updateJob.OurPosition = _audioSys.GetListenerCoordinates();
var transQuery = _entityManager.GetEntityQuery<TransformComponent>();
var physicsQuery = _entityManager.GetEntityQuery<PhysicsComponent>();
var opts = new ParallelOptions { MaxDegreeOfParallelism = _parallel.ParallelProcessCount };
// This semaphore is here to avoid lock contention as much as possible.
_updateSemaphore.Wait();
if (_renderers.Count > _minRendererParallel)
{
Parallel.ForEach(_renderers, opts, renderer => UpdateRenderer(renderer, transQuery, physicsQuery));
}
else
{
foreach (var renderer in _renderers)
{
UpdateRenderer(renderer, transQuery, physicsQuery);
}
}
// The ONLY time this should be contested is with ThreadUpdate.
// If that becomes NOT the case then just lock this, remove the semaphore, and drop the update frequency even harder.
// ReSharper disable once InconsistentlySynchronizedField
_parallel.ProcessNow(_updateJob, _renderers.Count);
}
if (_nextOcclusionUpdate < _timing.RealTime)
_nextOcclusionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_occlusionUpdateDelay));
if (_nextPositionUpdate < _timing.RealTime)
_nextPositionUpdate = _timing.RealTime.Add(TimeSpan.FromSeconds(_positionUpdateDelay));
_updateSemaphore.Release();
_volumeDirty = false;
}
private void UpdateRenderer(IMidiRenderer renderer, EntityQuery<TransformComponent> transQuery,
EntityQuery<PhysicsComponent> physicsQuery)
private void UpdateRenderer(IMidiRenderer renderer, MapCoordinates listener)
{
// TODO: This should be sharing more code with AudioSystem.
try
{
if (renderer.Disposed)
@@ -418,7 +400,7 @@ internal sealed partial class MidiManager : IMidiManager
if (_volumeDirty)
{
renderer.Source.Volume = Volume;
renderer.Source.Gain = Gain;
}
if (!renderer.Mono)
@@ -427,66 +409,82 @@ internal sealed partial class MidiManager : IMidiManager
return;
}
if (_nextPositionUpdate < _timing.RealTime)
MapCoordinates mapPos;
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
{
if (renderer.TrackingEntity is {} trackedEntity && !_entityManager.Deleted(trackedEntity))
{
renderer.TrackingCoordinates = transQuery.GetComponent(renderer.TrackingEntity!.Value).MapPosition;
}
else if (renderer.TrackingCoordinates == null)
renderer.TrackingCoordinates = _xformSystem.GetMapCoordinates(renderer.TrackingEntity.Value);
// Pause it if the attached entity is paused.
if (_entityManager.IsPaused(renderer.TrackingEntity))
{
renderer.Source.Pause();
return;
}
var position = renderer.TrackingCoordinates.Value;
if (position.MapId == MapId.Nullspace)
{
return;
}
renderer.Source.Position = position.Position;
var vel = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity!.Value,
xformQuery: transQuery, physicsQuery: physicsQuery);
renderer.Source.Velocity = vel;
}
else if (renderer.TrackingCoordinates == null)
{
renderer.Source.Pause();
return;
}
if (renderer.TrackingCoordinates != null && renderer.TrackingCoordinates.Value.MapId == _eyeManager.CurrentMap)
mapPos = renderer.TrackingCoordinates.Value;
// If it's on a different map then just mute it, not pause.
if (mapPos.MapId == MapId.Nullspace || mapPos.MapId != listener.MapId)
{
if (_nextOcclusionUpdate >= _timing.RealTime)
return;
renderer.Source.Gain = 0f;
return;
}
var pos = renderer.TrackingCoordinates.Value;
// Was previously muted maybe so try unmuting it?
if (renderer.Source.Gain == 0f)
{
renderer.Source.Gain = Gain;
}
var sourceRelative = pos.Position - _eyeManager.CurrentEye.Position.Position;
var occlusion = 0f;
if (sourceRelative.Length() > 0)
{
occlusion = _broadPhaseSystem.IntersectRayPenetration(
pos.MapId,
new CollisionRay(
_eyeManager.CurrentEye.Position.Position,
sourceRelative.Normalized(),
OcclusionCollisionMask),
MathF.Min(sourceRelative.Length(), _maxCastLength),
renderer.TrackingEntity);
}
var worldPos = mapPos.Position;
var delta = worldPos - listener.Position;
var distance = delta.Length();
renderer.Source.Occlusion = occlusion;
// Update position
// Out of range so just clip it for us.
if (distance > renderer.Source.MaxDistance)
{
// Still keeps the source playing, just with no volume.
renderer.Source.Gain = 0f;
return;
}
// Same imprecision suppression as audiosystem.
if (distance > 0f && distance < 0.01f)
{
worldPos = listener.Position;
delta = Vector2.Zero;
distance = 0f;
}
renderer.Source.Position = worldPos;
// Update velocity (doppler).
if (!_entityManager.Deleted(renderer.TrackingEntity))
{
var velocity = _broadPhaseSystem.GetMapLinearVelocity(renderer.TrackingEntity.Value);
renderer.Source.Velocity = velocity;
}
else
{
renderer.Source.Occlusion = float.MaxValue;
renderer.Source.Velocity = Vector2.Zero;
}
// Update occlusion
var occlusion = _audioSys.GetOcclusion(listener, delta, distance, renderer.TrackingEntity);
renderer.Source.Occlusion = occlusion;
}
catch (Exception ex)
{
_runtime.LogException(ex, _midiSawmill.Name);
}
}
/// <summary>
@@ -498,21 +496,39 @@ internal sealed partial class MidiManager : IMidiManager
{
lock (_renderers)
{
var toRemove = new ValueList<IMidiRenderer>();
for (var i = 0; i < _renderers.Count; i++)
{
var renderer = _renderers[i];
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
renderer.Master = null;
renderer.Render();
lock (renderer)
{
if (!renderer.Disposed)
{
if (renderer.Master is { Disposed: true })
renderer.Master = null;
renderer.Render();
}
else
{
toRemove.Add(renderer);
}
}
else
}
if (toRemove.Count > 0)
{
_updateSemaphore.Wait();
foreach (var renderer in toRemove)
{
renderer.InternalDispose();
_renderers.Remove(renderer);
}
_updateSemaphore.Release();
}
}
@@ -683,4 +699,31 @@ internal sealed partial class MidiManager : IMidiManager
}
}
#region Jobs
private record struct MidiUpdateJob : IParallelRobustJob
{
public int MinimumBatchParallel => 2;
public int BatchSize => 1;
public MidiManager Manager;
public MapCoordinates OurPosition;
public List<IMidiRenderer> Renderers;
public void Execute(int index)
{
// The indices shouldn't be able to be touched while this job is running, just the renderer itself getting locked.
var renderer = Renderers[index];
lock (renderer)
{
Manager.UpdateRenderer(renderer, OurPosition);
}
}
}
#endregion
}

View File

@@ -255,7 +255,7 @@ internal sealed class MidiRenderer : IMidiRenderer
_taskManager = taskManager;
_midiSawmill = midiSawmill;
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true);
Source = clydeAudio.CreateBufferedAudioSource(Buffers, true) ?? DummyBufferedAudioSource.Instance;
Source.SampleRate = SampleRate;
_settings = settings;
_soundFontLoader = soundFontLoader;

View File

@@ -5,6 +5,7 @@ using OpenTK.Audio.OpenAL.Extensions.Creative.EFX;
using Robust.Client.Audio.Effects;
using Robust.Shared.Audio.Effects;
using Robust.Shared.Audio.Sources;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Maths;
namespace Robust.Client.Audio.Sources;
@@ -164,10 +165,10 @@ internal abstract class BaseAudioSource : IAudioSource
get
{
var gain = Gain;
var volume = 10f * MathF.Log10(gain);
var volume = SharedAudioSystem.GainToVolume(gain);
return volume;
}
set => Gain = MathF.Pow(10, value / 10);
set => Gain = SharedAudioSystem.VolumeToGain(value);
}
/// <inheritdoc />
@@ -187,7 +188,8 @@ internal abstract class BaseAudioSource : IAudioSource
if (!IsEfxSupported)
{
AL.GetSource(SourceHandle, ALSourcef.Gain, out var priorGain);
priorOcclusion = priorGain / _gain;
// Default to 0 to avoid spiking audio, just means it will be muted for a frame in this case.
priorOcclusion = _gain == 0 ? 0f : priorGain / _gain;
}
_gain = value;

View File

@@ -20,8 +20,6 @@ internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSourc
private bool _float = false;
private int FilterHandle;
private float _gain;
public int SampleRate { get; set; } = 44100;
private bool IsEfxSupported => _master.IsEfxSupported;
@@ -37,7 +35,6 @@ internal sealed class BufferedAudioSource : BaseAudioSource, IBufferedAudioSourc
BufferMap[bufferHandle] = i;
}
_float = floatAudio;
AL.GetSource(sourceHandle, ALSourcef.Gain, out _gain);
}
/// <inheritdoc />

View File

@@ -107,6 +107,7 @@ namespace Robust.Client
deps.Register<IClyde, ClydeHeadless>();
deps.Register<IClipboardManager, ClydeHeadless>();
deps.Register<IClydeInternal, ClydeHeadless>();
deps.Register<IAudioManager, HeadlessAudioManager>();
deps.Register<IAudioInternal, HeadlessAudioManager>();
deps.Register<IInputManager, InputManager>();
deps.Register<IFileDialogManager, DummyFileDialogManager>();
@@ -116,6 +117,7 @@ namespace Robust.Client
deps.Register<IClyde, Clyde>();
deps.Register<IClipboardManager, Clyde>();
deps.Register<IClydeInternal, Clyde>();
deps.Register<IAudioManager, AudioManager>();
deps.Register<IAudioInternal, AudioManager>();
deps.Register<IInputManager, ClydeInputManager>();
deps.Register<IFileDialogManager, FileDialogManager>();

View File

@@ -119,6 +119,7 @@ namespace Robust.Client
Options.DefaultWindowTitle ?? _resourceManifest!.DefaultWindowTitle ?? "RobustToolbox");
_taskManager.Initialize();
_parallelMgr.Initialize();
_fontManager.SetFontDpi((uint)_configurationManager.GetCVar(CVars.DisplayFontDpi));
// Load optional Robust modules.
@@ -360,7 +361,6 @@ namespace Robust.Client
ProfileOptSetup.Setup(_configurationManager);
_parallelMgr.Initialize();
_prof.Initialize();
_resManager.Initialize(Options.LoadConfigAndUserData ? userDataDir : null);

View File

@@ -1,3 +1,4 @@
using Robust.Client.Audio;
using Robust.Client.Audio.Midi;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -8,6 +9,13 @@ namespace Robust.Client.GameObjects
{
[Dependency] private readonly IMidiManager _midiManager = default!;
public override void Initialize()
{
base.Initialize();
// AudioSystem sets eye position and rotation so rely on those.
UpdatesAfter.Add(typeof(AudioSystem));
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);

View File

@@ -174,19 +174,21 @@ namespace Robust.Client.GameStates
private void OnComponentAdded(AddedComponentEventArgs args)
{
if (_resettingPredictedEntities)
{
var comp = args.ComponentType;
if (!_resettingPredictedEntities)
return;
if (comp.NetID == null)
return;
var comp = args.ComponentType;
if (comp.NetID == null)
return;
_sawmill.Error($"""
Added component {comp.Name} with net id {comp.NetID} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
if (_entityManager.IsClientSide(args.BaseArgs.Owner))
return;
_sawmill.Error($"""
Added component {comp.Name} to entity {_entityManager.ToPrettyString(args.BaseArgs.Owner)} while resetting predicted entities.
Stack trace:
{Environment.StackTrace}
""");
}
/// <inheritdoc />

View File

@@ -116,7 +116,6 @@ namespace Robust.Client.Graphics
{
baseLine.Y += lineHeight;
advanceTotal.Y += lineHeight;
advanceTotal.X = Math.Max(advanceTotal.X, baseLine.X);
baseLine.X = 0f;
continue;
}
@@ -128,6 +127,7 @@ namespace Robust.Client.Graphics
var advance = metrics.Value.Advance;
baseLine += new Vector2(advance, 0);
advanceTotal.X = MathF.Max(baseLine.X, advanceTotal.X);
}
return advanceTotal;

View File

@@ -37,7 +37,7 @@ namespace Robust.Client.Graphics
Vector2 scale,
Angle? worldRot,
Angle eyeRotation = default,
Shared.Maths.Direction? overrideDirection = null,
Direction? overrideDirection = null,
SpriteComponent? sprite = null,
TransformComponent? xform = null,
SharedTransformSystem? xformSystem = null);

View File

@@ -213,7 +213,12 @@ public sealed class EntitySpawningUIController : UIController
_shownEntities.Add(prototype);
}
_shownEntities.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal));
_shownEntities.Sort((a, b) => {
var namesComparation = string.Compare(a.Name, b.Name, StringComparison.Ordinal);
if (namesComparation == 0)
return string.Compare(a.EditorSuffix, b.EditorSuffix, StringComparison.Ordinal);
return namesComparation;
});
_window.PrototypeList.TotalItemCount = _shownEntities.Count;
_window.PrototypeScrollContainer.SetScrollValue(new Vector2(0, 0));

View File

@@ -13,17 +13,15 @@ namespace Robust.Client.UserInterface.Controls
[Virtual]
public class SpriteView : Control
{
private SpriteSystem? _spriteSystem;
private SpriteSystem? _sprite;
private SharedTransformSystem? _transform;
IEntityManager _entMan;
[ViewVariables]
public SpriteComponent? Sprite { get; private set; }
public SpriteComponent? Sprite => Entity?.Comp1;
[ViewVariables]
public EntityUid? Entity { get; private set; }
public Entity<SpriteComponent>? Ent => Entity == null || Sprite == null ? null : (Entity.Value, Sprite);
public Entity<SpriteComponent, TransformComponent>? Entity { get; private set; }
/// <summary>
/// This field configures automatic scaling of the sprite. This automatic scaling is done before
@@ -118,23 +116,30 @@ namespace Robust.Client.UserInterface.Controls
public SpriteView()
{
_entMan = IoCManager.Resolve<IEntityManager>();
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
{
Sprite = sprite;
}
IoCManager.Resolve(ref _entMan);
RectClipContent = true;
}
public SpriteView(EntityUid uid, IEntityManager entMan)
{
_entMan = entMan;
RectClipContent = true;
SetEntity(uid);
}
public void SetEntity(EntityUid? uid)
{
Entity = uid;
if (Entity?.Owner == uid)
return;
if (_entMan.TryGetComponent(Entity, out SpriteComponent? sprite))
if (!_entMan.TryGetComponent(uid, out SpriteComponent? sprite)
|| !_entMan.TryGetComponent(uid, out TransformComponent? xform))
{
Sprite = sprite;
Entity = null;
return;
}
Entity = new(uid.Value, sprite, xform);
}
protected override Vector2 MeasureOverride(Vector2 availableSize)
@@ -146,13 +151,13 @@ namespace Robust.Client.UserInterface.Controls
private void UpdateSize()
{
if (Entity == null || Sprite == null)
if (Entity is not { } ent)
{
_spriteSize = default;
return;
}
var spriteBox = Sprite.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation)
var spriteBox = ent.Comp1.CalculateRotatedBoundingBox(default, _worldRotation ?? Angle.Zero, _eyeRotation)
.CalcBoundingBox();
if (!SpriteOffset)
@@ -194,18 +199,22 @@ namespace Robust.Client.UserInterface.Controls
internal override void DrawInternal(IRenderHandle renderHandle)
{
if (Entity is not {} uid || Sprite == null)
if (Entity == null)
return;
if (Sprite.Deleted)
var (uid, sprite, xform) = Entity.Value;
if (sprite.Deleted)
{
SetEntity(null);
return;
}
_sprite ??= _entMan.System<SpriteSystem>();
_transform ??= _entMan.System<TransformSystem>();
// Ensure the sprite is animated despite possible not being visible in any viewport.
_spriteSystem ??= _entMan.System<SpriteSystem>();
_spriteSystem.ForceUpdate(uid);
_sprite.ForceUpdate(uid);
var stretchVec = Stretch switch
{
@@ -217,11 +226,18 @@ namespace Robust.Client.UserInterface.Controls
var offset = SpriteOffset
? Vector2.Zero
: - (-_eyeRotation).RotateVec(Sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
: - (-_eyeRotation).RotateVec(sprite.Offset) * new Vector2(1, -1) * EyeManager.PixelsPerMeter;
var position = PixelSize / 2 + offset * stretch * UIScale;
var scale = Scale * UIScale * stretch;
renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, Sprite);
// control modulation is applied automatically to the screen handle, but here we need to use the world handle
var world = renderHandle.DrawingHandleWorld;
var oldModulate = world.Modulate;
world.Modulate *= Modulate * ActualModulateSelf;
renderHandle.DrawEntity(uid, position, scale, _worldRotation, _eyeRotation, OverrideDirection, sprite, xform, _transform);
world.Modulate = oldModulate;
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Robust.Client.Graphics;
@@ -40,6 +41,7 @@ internal partial class UserInterfaceManager
private bool _showingTooltip;
private Control? _suppliedTooltip;
private const float TooltipDelay = 0.25f;
private readonly Dictionary<BoundKeyFunction, Control> _focusedControls = new();
private WindowRoot? _focusedRoot;
@@ -109,13 +111,13 @@ internal partial class UserInterfaceManager
args.Handle();
}
_focusedControls[args.Function] = control;
OnKeyBindDown?.Invoke(control);
}
public void KeyBindUp(BoundKeyEventArgs args)
{
var control = ControlFocused ?? KeyboardFocused ?? MouseGetControl(args.PointerLocation);
if (control == null)
if (!_focusedControls.TryGetValue(args.Function, out var control))
{
return;
}
@@ -129,6 +131,7 @@ internal partial class UserInterfaceManager
// Always mark this as handled.
// The only case it should not be is if we do not have a control to click on,
// in which case we never reach this.
_focusedControls.Remove(args.Function);
args.Handle();
}

View File

@@ -74,15 +74,18 @@ public sealed partial class AudioSystem : SharedAudioSystem
var entity = Spawn("Audio", MapCoordinates.Nullspace);
var audio = SetupAudio(entity, filename, audioParams);
AddAudioFilter(entity, audio, playerFilter);
audio.Global = true;
return (entity, audio);
}
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayEntity(string filename, Filter playerFilter, EntityUid uid, bool recordReplay, AudioParams? audioParams = null)
{
if (!Exists(uid))
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}");
return null;
}
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
@@ -94,8 +97,11 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityUid uid, AudioParams? audioParams = null)
{
if (!Exists(uid))
if (TerminatingOrDeleted(uid))
{
Log.Error($"Tried to play audio on a terminating / deleted entity {ToPrettyString(uid)}");
return null;
}
var entity = Spawn("Audio", new EntityCoordinates(uid, Vector2.Zero));
var audio = SetupAudio(entity, filename, audioParams);
@@ -106,6 +112,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
/// <inheritdoc />
public override (EntityUid Entity, AudioComponent Component)? PlayStatic(string filename, Filter playerFilter, EntityCoordinates coordinates, bool recordReplay, AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
if (!coordinates.IsValid(EntityManager))
return null;
@@ -120,6 +132,12 @@ public sealed partial class AudioSystem : SharedAudioSystem
public override (EntityUid Entity, AudioComponent Component)? PlayPvs(string filename, EntityCoordinates coordinates,
AudioParams? audioParams = null)
{
if (TerminatingOrDeleted(coordinates.EntityId))
{
Log.Error($"Tried to play coordinates audio on a terminating / deleted entity {ToPrettyString(coordinates.EntityId)}");
return null;
}
if (!coordinates.IsValid(EntityManager))
return null;

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Threading;
using Prometheus;
using Robust.Server.Console;
@@ -204,8 +205,6 @@ namespace Robust.Server
ProfileOptSetup.Setup(_config);
_parallelMgr.Initialize();
//Sets up Logging
_logHandlerFactory = logHandlerFactory;
@@ -268,6 +267,7 @@ namespace Robust.Server
// Has to be done early because this guy's in charge of the main thread Synchronization Context.
_taskManager.Initialize();
_parallelMgr.Initialize();
LoadSettings();
@@ -279,12 +279,15 @@ namespace Robust.Server
_network.Initialize(true);
_network.StartServer();
}
catch (Exception e)
catch (SocketException e) when (e.SocketErrorCode == SocketError.AddressAlreadyInUse)
{
var port = _network.Port;
_logger.Fatal(
"Unable to setup networking manager. Check port {0} is not already in use and that all binding addresses are correct!\n{1}",
port, e);
_logger.Fatal("Unable to setup networking manager. Make sure that you aren't running the server twice and that port {0} is not in use by another application.\n{1}", port, e);
return true;
}
catch (Exception e)
{
_logger.Fatal("Unable to setup networking manager!\n{0}", e);
return true;
}

View File

@@ -226,10 +226,15 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
gridLoc.Add(index);
dirtyChunks.Add(gridChunkLocation);
break;
case SessionOverride sessionOverride:
if (!_sessionOverrides.TryGetValue(sessionOverride.Session, out var set))
return;
set.Add(index);
case SessionsOverride sessionOverride:
foreach (var sesh in sessionOverride.Sessions)
{
if (!_sessionOverrides.TryGetValue(sesh, out var set))
continue;
set.Add(index);
}
break;
case MapChunkLocation mapChunkLocation:
// might be gone due to map-deletions
@@ -259,8 +264,11 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
case GridChunkLocation gridChunkLocation:
_gridChunkContents[gridChunkLocation.GridId][gridChunkLocation.ChunkIndices].Remove(index);
break;
case SessionOverride sessionOverride:
_sessionOverrides.GetValueOrDefault(sessionOverride.Session)?.Remove(index);
case SessionsOverride sessionOverride:
foreach (var sesh in sessionOverride.Sessions)
{
_sessionOverrides.GetValueOrDefault(sesh)?.Remove(index);
}
break;
case MapChunkLocation mapChunkLocation:
_mapChunkContents[mapChunkLocation.MapId][mapChunkLocation.ChunkIndices].Remove(index);
@@ -410,7 +418,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
return;
}
if (!removeFromOverride && oldLocation is SessionOverride)
if (!removeFromOverride && oldLocation is SessionsOverride)
return;
if (oldLocation is GlobalOverride global &&
@@ -433,20 +441,29 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
{
if (!TryGetLocation(index, out var oldLocation))
{
RegisterUpdate(index, new SessionOverride(session));
RegisterUpdate(index, new SessionsOverride(new HashSet<ICommonSession>()
{
session
}));
return;
}
if (!removeFromOverride && oldLocation is GlobalOverride)
return;
if (oldLocation is SessionOverride local &&
(!removeFromOverride || local.Session == session))
if (oldLocation is SessionsOverride local)
{
if (!removeFromOverride || local.Sessions.Contains(session))
return;
local.Sessions.Add(session);
return;
}
RegisterUpdate(index, new SessionOverride(session));
RegisterUpdate(index, new SessionsOverride(new HashSet<ICommonSession>()
{
session
}));
}
/// <summary>
@@ -459,7 +476,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
{
if (!removeFromOverride
&& TryGetLocation(index, out var oldLocation)
&& oldLocation is GlobalOverride or SessionOverride)
&& oldLocation is GlobalOverride or SessionsOverride)
{
return;
}
@@ -509,7 +526,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
_indexLocations.TryGetValue(index, out var oldLocation);
//removeFromOverride is false 99% of the time.
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionOverride && !removeFromOverride)
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionsOverride && !removeFromOverride)
return;
if (oldLocation is GridChunkLocation oldGrid &&
@@ -540,7 +557,7 @@ public sealed class PVSCollection<TIndex> : IPVSCollection where TIndex : ICompa
_indexLocations.TryGetValue(index, out var oldLocation);
//removeFromOverride is false 99% of the time.
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionOverride && !removeFromOverride)
if ((bufferedLocation ?? oldLocation) is GlobalOverride or SessionsOverride && !removeFromOverride)
return;
// Is this entity just returning to its old location?
@@ -640,14 +657,17 @@ public struct GlobalOverride : IIndexLocation
}
}
public struct SessionOverride : IIndexLocation
/// <summary>
/// Adds overrides for the specified sessions for this entity.
/// </summary>
public struct SessionsOverride : IIndexLocation
{
public SessionOverride(ICommonSession session)
public SessionsOverride(HashSet<ICommonSession> sessions)
{
Session = session;
Sessions = sessions;
}
public readonly ICommonSession Session;
public readonly HashSet<ICommonSession> Sessions;
}
#endregion

View File

@@ -1,3 +1,4 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
@@ -11,6 +12,21 @@ public sealed class PvsOverrideSystem : EntitySystem
{
[Shared.IoC.Dependency] private readonly PvsSystem _pvs = default!;
/// <summary>
/// Yields the NetEntity session overrides for the specified session.
/// </summary>
/// <param name="session"></param>
public IEnumerable<NetEntity> GetSessionOverrides(ICommonSession session)
{
var enumerator = _pvs.EntityPVSCollection.GetSessionOverrides(session);
while (enumerator.MoveNext())
{
var current = enumerator.Current;
yield return current;
}
}
/// <summary>
/// Used to ensure that an entity is always sent to every client. By default this overrides any client-specific overrides.
/// </summary>

View File

@@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.GameObjects;
using Robust.Shared.Player;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
@@ -31,11 +31,33 @@ internal sealed partial class PvsSystem
/// </summary>
internal void ProcessQueuedAcks()
{
var opts = new ParallelOptions {MaxDegreeOfParallelism = _parallelManager.ParallelProcessCount};
Parallel.ForEach(PendingAcks, opts, ProcessQueuedAck);
if (PendingAcks.Count == 0)
return;
_toAck.Clear();
foreach (var session in PendingAcks)
{
_toAck.Add(session);
}
_parallelManager.ProcessNow(_ackJob, _toAck.Count);
PendingAcks.Clear();
}
private record struct PvsAckJob : IParallelRobustJob
{
public int BatchSize => 2;
public PvsSystem System;
public List<ICommonSession> Sessions;
public void Execute(int index)
{
System.ProcessQueuedAck(Sessions[index]);
}
}
/// <summary>
/// Process a given client's queued ack.
/// </summary>

View File

@@ -61,6 +61,12 @@ internal sealed partial class PvsSystem : EntitySystem
/// </summary>
private float _viewSize;
/// <summary>
/// Per-tick ack data to avoid re-allocating.
/// </summary>
private readonly List<ICommonSession> _toAck = new();
private PvsAckJob _ackJob;
/// <summary>
/// If PVS disabled then we'll track if we've dumped all entities on the player.
/// This way any future ticks can be orders of magnitude faster as we only send what changes.
@@ -121,6 +127,12 @@ internal sealed partial class PvsSystem : EntitySystem
{
base.Initialize();
_ackJob = new PvsAckJob()
{
System = this,
Sessions = _toAck,
};
_eyeQuery = GetEntityQuery<EyeComponent>();
_metaQuery = GetEntityQuery<MetaDataComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
@@ -793,7 +805,12 @@ internal sealed partial class PvsSystem : EntitySystem
}
var expandEvent = new ExpandPvsEvent(session);
RaiseLocalEvent(ref expandEvent);
if (session.AttachedEntity != null)
RaiseLocalEvent(session.AttachedEntity.Value, ref expandEvent, true);
else
RaiseLocalEvent(ref expandEvent);
if (expandEvent.Entities != null)
{
foreach (var entityUid in expandEvent.Entities)

View File

@@ -270,44 +270,26 @@ Oldest acked clients: {string.Join(", ", players)}
private PvsData? GetPVSData(ICommonSession[] players)
{
var chunks= _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
const int ChunkBatchSize = 2;
var chunks = _pvs.GetChunks(players, ref _playerChunks, ref _viewerEntities);
var chunksCount = chunks.Count;
var chunkBatches = (int)MathF.Ceiling((float)chunksCount / ChunkBatchSize);
var chunkCache =
new (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[chunksCount];
// Update the reused trees sequentially to avoid having to lock the dictionary per chunk.
var reuse = ArrayPool<bool>.Shared.Rent(chunksCount);
Parallel.For(0, chunkBatches,
new ParallelOptions { MaxDegreeOfParallelism = _parallelMgr.ParallelProcessCount },
i =>
if (chunksCount > 0)
{
var chunkJob = new PvsChunkJob()
{
var start = i * ChunkBatchSize;
var end = Math.Min(start + ChunkBatchSize, chunksCount);
EntManager = _entityManager,
Pvs = _pvs,
ChunkCache = chunkCache,
Reuse = reuse,
Chunks = chunks,
};
for (var j = start; j < end; ++j)
{
var (visMask, chunkIndexLocation) = chunks[j];
reuse[j] = _pvs.TryCalculateChunk(chunkIndexLocation, visMask, out var chunk);
chunkCache[j] = chunk;
#if DEBUG
if (chunk == null)
continue;
// Each root nodes should simply be a map or a grid entity.
DebugTools.Assert(chunk.Value.tree.RootNodes.Count == 1,
$"Root node count is {chunk.Value.tree.RootNodes.Count} instead of 1.");
var nent = chunk.Value.tree.RootNodes.FirstOrDefault();
var ent = _entityManager.GetEntity(nent);
DebugTools.Assert(_entityManager.EntityExists(ent), $"Root node does not exist. Node {ent}.");
DebugTools.Assert(_entityManager.HasComponent<MapComponent>(ent)
|| _entityManager.HasComponent<MapGridComponent>(ent));
#endif
}
});
_parallelMgr.ProcessNow(chunkJob, chunksCount);
}
_pvs.RegisterNewPreviousChunkTrees(chunks, chunkCache, reuse);
ArrayPool<bool>.Shared.Return(reuse);
@@ -438,5 +420,46 @@ Oldest acked clients: {string.Join(", ", players)}
}
}
}
#region Jobs
/// <summary>
/// Pre-calculates chunk indices (Robust Tree) to be re-used per-player later on.
/// </summary>
private record struct PvsChunkJob : IParallelRobustJob
{
public int BatchSize => 2;
public IEntityManager EntManager;
public PvsSystem Pvs;
public List<(int, IChunkIndexLocation)> Chunks;
public bool[] Reuse;
public (Dictionary<NetEntity, MetaDataComponent> metadata, RobustTree<NetEntity> tree)?[] ChunkCache;
public void Execute(int index)
{
var (visMask, chunkIndexLocation) = Chunks[index];
Reuse[index] = Pvs.TryCalculateChunk(chunkIndexLocation, visMask, out var chunk);
ChunkCache[index] = chunk;
#if DEBUG
if (chunk == null)
return;
// Each root nodes should simply be a map or a grid entity.
DebugTools.Assert(chunk.Value.tree.RootNodes.Count == 1,
$"Root node count is {chunk.Value.tree.RootNodes.Count} instead of 1.");
var nent = chunk.Value.tree.RootNodes.FirstOrDefault();
var ent = EntManager.GetEntity(nent);
DebugTools.Assert(EntManager.EntityExists(ent), $"Root node does not exist. Node {ent}.");
DebugTools.Assert(EntManager.HasComponent<MapComponent>(ent)
|| EntManager.HasComponent<MapGridComponent>(ent));
#endif
}
}
#endregion
}
}

View File

@@ -15,9 +15,12 @@ namespace Robust.Shared.Audio.Components;
/// <summary>
/// Stores the audio data for an audio entity.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedAudioSystem))]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true), Access(typeof(SharedAudioSystem))]
public sealed partial class AudioComponent : Component, IAudioSource
{
[ViewVariables(VVAccess.ReadWrite), AutoNetworkedField, DataField, Access(Other = AccessPermissions.ReadWriteExecute)]
public AudioFlags Flags = AudioFlags.None;
#region Filter
public override bool SessionSpecific => true;
@@ -28,8 +31,6 @@ public sealed partial class AudioComponent : Component, IAudioSource
[DataField(customTypeSerializer:typeof(TimeOffsetSerializer)), AutoNetworkedField]
public TimeSpan AudioStart;
#region Filters
// Don't need to network these as client doesn't care.
/// <summary>
@@ -47,8 +48,6 @@ public sealed partial class AudioComponent : Component, IAudioSource
#endregion
#endregion
// We can't just start playing on audio creation as we don't have the correct position yet.
// As such we'll wait for FrameUpdate before we start playing to avoid the position being cooked.
public bool Started = false;
@@ -68,7 +67,7 @@ public sealed partial class AudioComponent : Component, IAudioSource
/// Audio source that interacts with OpenAL.
/// </summary>
[ViewVariables(VVAccess.ReadOnly)]
internal IAudioSource Source = default!;
internal IAudioSource Source = new DummyAudioSource();
/// <summary>
/// Auxiliary entity to pass audio to.
@@ -115,6 +114,7 @@ public sealed partial class AudioComponent : Component, IAudioSource
/// <see cref="IAudioSource.Global"/>
/// </summary>
[AutoNetworkedField]
[Access(typeof(SharedAudioSystem))]
public bool Global { get; set; }
/// <summary>
@@ -234,3 +234,14 @@ public sealed partial class AudioComponent : Component, IAudioSource
Source.Dispose();
}
}
[Flags]
public enum AudioFlags : byte
{
None = 0,
/// <summary>
/// Should the audio act as if attached to a grid?
/// </summary>
GridAudio = 1 << 0,
}

View File

@@ -1,10 +1,12 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Robust.Shared.Audio.Components;
using Robust.Shared.Configuration;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Player;
@@ -41,7 +43,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
/// </summary>
public int OcclusionCollisionMask { get; set; }
public float ZOffset;
public virtual float ZOffset { get; protected set; }
public override void Initialize()
{
@@ -59,23 +61,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
CfgManager.UnsubValueChanged(CVars.AudioZOffset, SetZOffset);
}
protected virtual void SetZOffset(float value)
protected void SetZOffset(float value)
{
var query = AllEntityQuery<AudioComponent>();
var oldZOffset = ZOffset;
ZOffset = value;
while (query.MoveNext(out var uid, out var audio))
{
// Pythagoras back to normal then adjust.
var maxDistance = MathF.Pow(audio.Params.MaxDistance, 2) - oldZOffset;
var refDistance = MathF.Pow(audio.Params.ReferenceDistance, 2) - oldZOffset;
audio.Params.MaxDistance = maxDistance;
audio.Params.ReferenceDistance = refDistance;
audio.Params = GetAdjustedParams(audio.Params);
Dirty(uid, audio);
}
}
protected virtual void OnAudioUnpaused(EntityUid uid, AudioComponent component, ref EntityUnpausedEvent args)
@@ -87,8 +75,13 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
var playerEnt = args.Player?.AttachedEntity;
if ((component.ExcludedEntity != null && playerEnt == component.ExcludedEntity) ||
(playerEnt != null && component.IncludedEntities != null && !component.IncludedEntities.Contains(playerEnt.Value)))
if (component.ExcludedEntity != null && playerEnt == component.ExcludedEntity)
{
args.Cancelled = true;
return;
}
if (playerEnt != null && component.IncludedEntities != null && !component.IncludedEntities.Contains(playerEnt.Value))
{
args.Cancelled = true;
}
@@ -134,9 +127,9 @@ public abstract partial class SharedAudioSystem : EntitySystem
{
DebugTools.Assert(!string.IsNullOrEmpty(fileName));
audioParams ??= AudioParams.Default;
var comp = AddComp<Components.AudioComponent>(uid);
var comp = AddComp<AudioComponent>(uid);
comp.FileName = fileName;
comp.Params = GetAdjustedParams(audioParams.Value);
comp.Params = audioParams.Value;
comp.AudioStart = Timing.CurTime;
if (!audioParams.Value.Loop)
@@ -151,23 +144,32 @@ public abstract partial class SharedAudioSystem : EntitySystem
return comp;
}
/// <summary>
/// Accounts for ZOffset on audio distance.
/// </summary>
private AudioParams GetAdjustedParams(AudioParams audioParams)
public static float GainToVolume(float value)
{
var maxDistance = GetAudioDistance(audioParams.MaxDistance);
var refDistance = GetAudioDistance(audioParams.ReferenceDistance);
return 10f * MathF.Log10(value);
}
return audioParams
.WithMaxDistance(maxDistance)
.WithReferenceDistance(refDistance);
public static float VolumeToGain(float value)
{
return MathF.Pow(10, value / 10);
}
/// <summary>
/// Sets the audio params volume for an entity.
/// </summary>
public void SetVolume(EntityUid? entity, float value, Components.AudioComponent? component = null)
public void SetGain(EntityUid? entity, float value, AudioComponent? component = null)
{
if (entity == null || !Resolve(entity.Value, ref component))
return;
var volume = GainToVolume(value);
SetVolume(entity, volume, component);
}
/// <summary>
/// Sets the audio params volume for an entity.
/// </summary>
public void SetVolume(EntityUid? entity, float value, AudioComponent? component = null)
{
if (entity == null || !Resolve(entity.Value, ref component))
return;
@@ -176,6 +178,7 @@ public abstract partial class SharedAudioSystem : EntitySystem
return;
component.Params.Volume = value;
component.Volume = value;
Dirty(entity.Value, component);
}

View File

@@ -1030,7 +1030,7 @@ namespace Robust.Shared
/// Master volume for audio output.
/// </summary>
public static readonly CVarDef<float> AudioMasterVolume =
CVarDef.Create("audio.mastervolume", 1.0f, CVar.ARCHIVE | CVar.CLIENTONLY);
CVarDef.Create("audio.mastervolume", 0.50f, CVar.ARCHIVE | CVar.CLIENTONLY);
/// <summary>
/// Maximum raycast distance for audio occlusion.
@@ -1302,16 +1302,7 @@ namespace Robust.Shared
*/
public static readonly CVarDef<float> MidiVolume =
CVarDef.Create("midi.volume", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<int> MidiMinRendererParallel =
CVarDef.Create("midi.min_renderers_parallel_update", 3, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<float> MidiPositionUpdateDelay =
CVarDef.Create("midi.position_update_delay", 0.125f, CVar.CLIENTONLY | CVar.ARCHIVE);
public static readonly CVarDef<float> MidiOcclusionUpdateDelay =
CVarDef.Create("midi.occlusion_update_delay", 0.25f, CVar.CLIENTONLY | CVar.ARCHIVE);
CVarDef.Create("midi.volume", 0.50f, CVar.CLIENTONLY | CVar.ARCHIVE);
/*
* HUB

View File

@@ -0,0 +1,46 @@
using System;
using System.IO;
using Microsoft.IO;
using Robust.Shared.Utility;
namespace Robust.Shared.GameObjects;
/// <summary>
/// Generic memory manager for engine use.
/// </summary>
internal sealed class RobustMemoryManager
{
// Let's be real this is a bandaid for pooling bullshit at an engine level and I don't know what
// good memory management looks like for PVS or the RobustSerializer.
private static readonly RecyclableMemoryStreamManager MemStreamManager = new()
{
ThrowExceptionOnToArray = true,
};
public RobustMemoryManager()
{
MemStreamManager.StreamDoubleDisposed += (sender, args) =>
throw new InvalidOperationException("Found double disposed stream.");
MemStreamManager.StreamFinalized += (sender, args) =>
throw new InvalidOperationException("Stream finalized but not disposed indicating a leak");
MemStreamManager.StreamOverCapacity += (sender, args) =>
throw new InvalidOperationException("Stream over memory capacity");
}
public static MemoryStream GetMemoryStream()
{
var stream = MemStreamManager.GetStream("RobustMemoryManager");
DebugTools.Assert(stream.Position == 0);
return stream;
}
public static MemoryStream GetMemoryStream(int length)
{
var stream = MemStreamManager.GetStream("RobustMemoryManager", length);
DebugTools.Assert(stream.Position == 0);
return stream;
}
}

View File

@@ -40,7 +40,7 @@ public sealed partial class EntityLookupSystem
tuple.intersecting.Add(value.Entity);
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.Static) != 0x0)
@@ -53,7 +53,7 @@ public sealed partial class EntityLookupSystem
tuple.intersecting.Add(value.Entity);
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries)
@@ -63,7 +63,7 @@ public sealed partial class EntityLookupSystem
{
state.Add(value);
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.Sundries) != 0x0)
@@ -73,7 +73,7 @@ public sealed partial class EntityLookupSystem
{
state.Add(value);
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
}
@@ -109,7 +109,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
if (state.found)
return true;
@@ -124,7 +124,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
if (state.found)
return true;
@@ -139,7 +139,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
if (state.found)
return true;
@@ -154,7 +154,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
return state.found;
@@ -270,7 +270,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
});
}, approx: true);
if (state.found)
return true;
@@ -314,7 +314,7 @@ public sealed partial class EntityLookupSystem
}
return true;
});
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);
@@ -345,7 +345,7 @@ public sealed partial class EntityLookupSystem
return false;
}
return true;
});
}, approx: true);
if (state.found)
return true;
@@ -373,7 +373,7 @@ public sealed partial class EntityLookupSystem
{
tuple.lookup.AddEntitiesIntersecting(uid, tuple.intersecting, tuple.worldBounds, tuple.flags);
return true;
});
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);
@@ -411,7 +411,7 @@ public sealed partial class EntityLookupSystem
}
return true;
});
}, approx: true);
var mapUid = _mapManager.GetMapEntityId(mapID);
return AnyEntitiesIntersecting(mapUid, worldAABB, flags, uid);
@@ -446,7 +446,7 @@ public sealed partial class EntityLookupSystem
}
return true;
});
}, approx: true);
var mapUid = _mapManager.GetMapEntityId(mapPos.MapId);
return AnyEntitiesIntersecting(mapUid, worldAABB, flags, uid);
@@ -464,6 +464,17 @@ public sealed partial class EntityLookupSystem
return intersecting;
}
public void GetEntitiesInRange(EntityUid uid, float range, HashSet<EntityUid> entities, LookupFlags flags = DefaultFlags)
{
var mapPos = _transform.GetMapCoordinates(uid);
if (mapPos.MapId == MapId.Nullspace)
return;
GetEntitiesInRange(mapPos.MapId, mapPos.Position, range, entities, flags);
entities.Remove(uid);
}
public HashSet<EntityUid> GetEntitiesIntersecting(EntityUid uid, LookupFlags flags = DefaultFlags)
{
var xform = _xformQuery.GetComponent(uid);
@@ -635,7 +646,7 @@ public sealed partial class EntityLookupSystem
}
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.Static) != 0x0)
@@ -650,7 +661,7 @@ public sealed partial class EntityLookupSystem
}
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries)
@@ -741,7 +752,7 @@ public sealed partial class EntityLookupSystem
{
tuple.callback(uid, tuple._broadQuery.GetComponent(uid));
return true;
});
}, approx: true);
}
#endregion

View File

@@ -40,7 +40,7 @@ public sealed partial class EntityLookupSystem
tuple.intersecting.Add((value.Entity, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & (LookupFlags.Static)) != 0x0)
@@ -52,7 +52,7 @@ public sealed partial class EntityLookupSystem
tuple.intersecting.Add((value.Entity, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries)
@@ -64,7 +64,7 @@ public sealed partial class EntityLookupSystem
tuple.intersecting.Add((value, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.Sundries) != 0x0)
@@ -76,7 +76,7 @@ public sealed partial class EntityLookupSystem
tuple.intersecting.Add((value, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
}
@@ -123,7 +123,7 @@ public sealed partial class EntityLookupSystem
state.Intersecting.Add((value.Entity, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & (LookupFlags.Static)) != 0x0)
@@ -144,7 +144,7 @@ public sealed partial class EntityLookupSystem
state.Intersecting.Add((value.Entity, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.StaticSundries) == LookupFlags.StaticSundries)
@@ -184,7 +184,7 @@ public sealed partial class EntityLookupSystem
state.Intersecting.Add((value, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
if ((flags & LookupFlags.Sundries) != 0x0)
@@ -225,7 +225,7 @@ public sealed partial class EntityLookupSystem
state.Intersecting.Add((value, comp));
return true;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
}
@@ -251,7 +251,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
if (state.found)
return true;
@@ -267,7 +267,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
if (state.found)
return true;
@@ -283,7 +283,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
if (state.found)
return true;
@@ -299,7 +299,7 @@ public sealed partial class EntityLookupSystem
tuple.found = true;
return false;
}, localAABB, (flags & LookupFlags.Approximate) != 0x0);
}, localAABB, true);
}
return state.found;
@@ -439,7 +439,7 @@ public sealed partial class EntityLookupSystem
return true;
tuple.found = true;
return false;
}, (flags & LookupFlags.Approximate) != 0x0);
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);
@@ -489,7 +489,7 @@ public sealed partial class EntityLookupSystem
{
tuple.system.AddEntitiesIntersecting(uid, tuple.intersecting, tuple.worldAABB, tuple.flags, tuple.query);
return true;
}, (flags & LookupFlags.Approximate) != 0x0);
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);
@@ -529,7 +529,7 @@ public sealed partial class EntityLookupSystem
{
tuple.system.AddEntitiesIntersecting(uid, tuple.intersecting, tuple.worldAABB, tuple.flags, tuple.query);
return true;
}, (flags & LookupFlags.Approximate) != 0x0);
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);
@@ -609,7 +609,7 @@ public sealed partial class EntityLookupSystem
{
state.Lookup.AddEntitiesIntersecting(uid, state.Intersecting, state.Shape, state.WorldAABB, state.Flags, state.Query);
return true;
}, (flags & LookupFlags.Approximate) != 0x0);
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);
@@ -684,7 +684,7 @@ public sealed partial class EntityLookupSystem
{
tuple.system.AddEntitiesIntersecting(uid, tuple.intersecting, tuple.shape, tuple.worldAABB, tuple.flags, tuple.query);
return true;
}, (flags & LookupFlags.Approximate) != 0x0);
}, approx: true);
// Get map entities
var mapUid = _mapManager.GetMapEntityId(mapId);

View File

@@ -1,5 +1,6 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -17,7 +18,8 @@ namespace Robust.Shared.Network.Messages
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
int length = buffer.ReadVariableInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
Text = serializer.Deserialize<FormattedMessage>(stream);
}

View File

@@ -33,8 +33,9 @@ namespace Robust.Shared.Network.Messages
{
case EntityMessageType.SystemMessage:
{
int length = buffer.ReadVariableInt32();
using var stream = buffer.ReadAlignedMemory(length);
var length = buffer.ReadVariableInt32();
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
SystemMessage = serializer.Deserialize<EntityEventArgs>(stream);
}
break;

View File

@@ -1,5 +1,6 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -28,7 +29,8 @@ namespace Robust.Shared.Network.Messages
{
buffer.ReadPadBits();
var length = buffer.ReadVariableInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
serializer.DeserializeDirect(stream, out Echo);
serializer.DeserializeDirect(stream, out Response);
}

View File

@@ -1,10 +1,9 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.GameStates;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -38,29 +37,30 @@ namespace Robust.Shared.Network.Messages
// State is compressed.
if (compressedLength > 0)
{
var stream = buffer.ReadAlignedMemory(compressedLength);
var stream = RobustMemoryManager.GetMemoryStream(compressedLength);
buffer.ReadAlignedMemory(stream, compressedLength);
using var decompressStream = new ZStdDecompressStream(stream);
var decompressedStream = new MemoryStream(uncompressedLength);
decompressStream.CopyTo(decompressedStream, uncompressedLength);
decompressedStream.Position = 0;
finalStream = decompressedStream;
finalStream = RobustMemoryManager.GetMemoryStream(uncompressedLength);
finalStream.SetLength(uncompressedLength);
decompressStream.CopyTo(finalStream, uncompressedLength);
finalStream.Position = 0;
}
// State is uncompressed.
else
{
var stream = buffer.ReadAlignedMemory(uncompressedLength);
finalStream = stream;
finalStream = RobustMemoryManager.GetMemoryStream(uncompressedLength);
buffer.ReadAlignedMemory(finalStream, uncompressedLength);
}
serializer.DeserializeDirect(finalStream, out State);
finalStream.Dispose();
State.PayloadSize = uncompressedLength;
finalStream.Dispose();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
var stateStream = new MemoryStream();
using var stateStream = RobustMemoryManager.GetMemoryStream();
serializer.SerializeDirect(stateStream, State);
buffer.WriteVariableInt32((int)stateStream.Length);
@@ -87,7 +87,6 @@ namespace Robust.Shared.Network.Messages
{
// 0 means that the state isn't compressed.
buffer.WriteVariableInt32(0);
buffer.Write(stateStream.AsSpan());
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -42,12 +43,14 @@ namespace Robust.Shared.Network.Messages
SessionId = buffer.ReadUInt32();
{
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
PropertyIndex = serializer.Deserialize<object[]>(stream);
}
{
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
Value = serializer.Deserialize(stream);
}
ReinterpretValue = buffer.ReadBoolean();

View File

@@ -1,5 +1,6 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -31,7 +32,8 @@ namespace Robust.Shared.Network.Messages
{
RequestId = buffer.ReadUInt32();
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
Blob = serializer.Deserialize<ViewVariablesBlob>(stream);
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -37,7 +38,8 @@ namespace Robust.Shared.Network.Messages
RequestId = buffer.ReadUInt32();
SessionId = buffer.ReadUInt32();
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
RequestMeta = serializer.Deserialize<ViewVariablesRequest>(stream);
}

View File

@@ -1,5 +1,6 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -32,7 +33,8 @@ namespace Robust.Shared.Network.Messages
{
RequestId = buffer.ReadUInt32();
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
Selector = serializer.Deserialize<ViewVariablesObjectSelector>(stream);
}

View File

@@ -6,6 +6,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Robust.Shared.Network
{
@@ -96,16 +97,17 @@ namespace Robust.Shared.Network
/// <exception cref="ArgumentException">
/// Thrown if the current read position of the message is not byte-aligned.
/// </exception>
public static MemoryStream ReadAlignedMemory(this NetIncomingMessage message, int length)
public static void ReadAlignedMemory(this NetIncomingMessage message, MemoryStream memStream, int length)
{
if ((message.Position & 7) != 0)
{
throw new ArgumentException("Read position in message must be byte-aligned", nameof(message));
}
var stream = new MemoryStream(message.Data, message.PositionInBytes, length, false);
DebugTools.Assert(memStream.Position == 0);
memStream.Write(message.Data, message.PositionInBytes, length);
memStream.Position = 0;
message.Position += length * 8;
return stream;
}
public static TimeSpan ReadTimeSpan(this NetIncomingMessage message)

View File

@@ -355,21 +355,27 @@ namespace Robust.Shared.Physics.Dynamics.Contacts
internal enum ContactFlags : byte
{
None = 0,
/// <summary>
/// Is the contact pending its first manifold generation.
/// </summary>
PreInit = 1 << 0,
/// <summary>
/// Has this contact already been added to an island?
/// </summary>
Island = 1 << 0,
Island = 1 << 1,
/// <summary>
/// Does this contact need re-filtering?
/// </summary>
Filter = 1 << 1,
Filter = 1 << 2,
/// <summary>
/// Is this a special contact for grid-grid collisions
/// </summary>
Grid = 1 << 2,
Grid = 1 << 3,
Deleting = 1 << 3,
Deleting = 1 << 4,
}
}

View File

@@ -1,11 +1,16 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Components;
namespace Robust.Shared.Physics
{
[ByRefEvent]
public readonly record struct PhysicsWakeEvent(EntityUid Entity, PhysicsComponent Body);
namespace Robust.Shared.Physics;
[ByRefEvent]
public readonly record struct PhysicsSleepEvent(EntityUid Entity, PhysicsComponent Body);
}
[ByRefEvent]
public readonly record struct PhysicsWakeEvent(EntityUid Entity, PhysicsComponent Body);
[ByRefEvent]
public record struct PhysicsSleepEvent(EntityUid Entity, PhysicsComponent Body)
{
/// <summary>
/// Marks the entity as still being awake and cancels sleeping.
/// </summary>
public bool Cancelled;
};

View File

@@ -48,15 +48,22 @@ namespace Robust.Shared.Physics.Systems
/// </summary>
private float _broadphaseExpand;
private const int PairBufferParallel = 8;
private ObjectPool<List<FixtureProxy>> _bufferPool =
new DefaultObjectPool<List<FixtureProxy>>(new ListPolicy<FixtureProxy>(), 2048);
private BroadphaseJob _broadphaseJob;
public override void Initialize()
{
base.Initialize();
_broadphaseJob = new BroadphaseJob()
{
Broadphase = this,
BroadphaseExpand = _broadphaseExpand,
MapManager = _mapManager,
};
_broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
_gridQuery = GetEntityQuery<MapGridComponent>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
@@ -188,50 +195,16 @@ namespace Robust.Shared.Physics.Systems
foreach (var (proxy, aabb) in moveBuffer)
{
// TODO: This should just be done inline in the job.
contactBuffer[idx] = _bufferPool.Get();
pMoveBuffer[idx++] = (proxy, aabb);
}
var options = new ParallelOptions
{
MaxDegreeOfParallelism = _parallel.ParallelProcessCount,
};
_broadphaseJob.ContactBuffer = contactBuffer;
_broadphaseJob.PMoveBuffer = pMoveBuffer;
_broadphaseJob.MapId = mapId;
var batches = (int)MathF.Ceiling((float) count / PairBufferParallel);
Parallel.For(0, batches, options, i =>
{
var start = i * PairBufferParallel;
var end = Math.Min(start + PairBufferParallel, count);
for (var j = start; j < end; j++)
{
var (proxy, worldAABB) = pMoveBuffer[j];
var buffer = contactBuffer[j];
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (this, proxy, worldAABB, buffer);
// Get every broadphase we may be intersecting.
_mapManager.FindGridsIntersecting(mapId, worldAABB.Enlarged(_broadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
Box2 worldAABB,
List<FixtureProxy> pairBuffer) tuple) =>
{
ref var buffer = ref tuple.pairBuffer;
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
return true;
});
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
FindPairs(proxy, worldAABB, _mapManager.GetMapEntityId(mapId), buffer);
}
});
_parallel.ProcessNow(_broadphaseJob, count);
for (var i = 0; i < count; i++)
{
@@ -432,6 +405,13 @@ namespace Robust.Shared.Physics.Systems
public void RegenerateContacts(EntityUid uid, PhysicsComponent body, FixturesComponent? fixtures = null, TransformComponent? xform = null)
{
// If it can't collide then we can't touch proxies and add them to the movebuffer anyway.
if (!body.CanCollide)
{
// Sleep body may still have contacts around.
return;
}
_physicsSystem.DestroyContacts(body);
if (!Resolve(uid, ref xform, ref fixtures))
return;
@@ -514,5 +494,50 @@ namespace Robust.Shared.Physics.Systems
}
}
}
#region Jobs
private record struct BroadphaseJob : IParallelRobustJob
{
public int BatchSize => 8;
public IMapManager MapManager;
public SharedBroadphaseSystem Broadphase;
public MapId MapId;
public float BroadphaseExpand;
public List<FixtureProxy>[] ContactBuffer;
public (FixtureProxy Proxy, Box2 AABB)[] PMoveBuffer;
public void Execute(int index)
{
var (proxy, worldAABB) = PMoveBuffer[index];
var buffer = ContactBuffer[index];
var proxyBody = proxy.Body;
DebugTools.Assert(!proxyBody.Deleted);
var state = (Broadphase, proxy, worldAABB, buffer);
// Get every broadphase we may be intersecting.
MapManager.FindGridsIntersecting(MapId, worldAABB.Enlarged(BroadphaseExpand), ref state,
static (EntityUid uid, MapGridComponent _, ref (
SharedBroadphaseSystem system,
FixtureProxy proxy,
Box2 worldAABB,
List<FixtureProxy> pairBuffer) tuple) =>
{
ref var buffer = ref tuple.pairBuffer;
tuple.system.FindPairs(tuple.proxy, tuple.worldAABB, uid, buffer);
return true;
}, approx: true, includeMap: false);
// Struct ref moment, I have no idea what's fastest.
buffer = state.buffer;
Broadphase.FindPairs(proxy, worldAABB, MapManager.GetMapEntityId(MapId), buffer);
}
}
#endregion
}
}

View File

@@ -399,12 +399,30 @@ public partial class SharedPhysicsSystem
{
var ev = new PhysicsSleepEvent(uid, body);
RaiseLocalEvent(uid, ref ev, true);
// Reset the sleep timer.
if (ev.Cancelled)
{
if (updateSleepTime)
SetSleepTime(body, 0);
return;
}
ResetDynamics(body);
}
if (updateSleepTime)
SetSleepTime(body, 0);
if (body.Awake != value)
{
Log.Error($"Found a corrupted physics awake state for {ToPrettyString(ent)}! Did you forget to cancel the sleep subscription? Forcing body awake");
DebugTools.Assert(false);
body.Awake = true;
}
UpdateMapAwakeState(uid, body);
Dirty(ent);
}

View File

@@ -31,17 +31,15 @@ using System;
using System.Buffers;
using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.GameObjects;
using Robust.Shared.Maths;
using Robust.Shared.Physics.Collision;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Dynamics.Contacts;
using Robust.Shared.Physics.Events;
using Robust.Shared.Threading;
using Robust.Shared.Utility;
namespace Robust.Shared.Physics.Systems;
@@ -122,6 +120,7 @@ public abstract partial class SharedPhysicsSystem
public bool Return(Contact obj)
{
SetContact(obj,
false,
EntityUid.Invalid, EntityUid.Invalid,
string.Empty, string.Empty,
null, 0,
@@ -132,6 +131,7 @@ public abstract partial class SharedPhysicsSystem
}
private static void SetContact(Contact contact,
bool enabled,
EntityUid uidA, EntityUid uidB,
string fixtureAId, string fixtureBId,
Fixture? fixtureA, int indexA,
@@ -139,9 +139,9 @@ public abstract partial class SharedPhysicsSystem
PhysicsComponent? bodyA,
PhysicsComponent? bodyB)
{
contact.Enabled = true;
contact.Enabled = enabled;
contact.IsTouching = false;
contact.Flags = ContactFlags.None;
contact.Flags = ContactFlags.None | ContactFlags.PreInit;
// TOIFlag = false;
contact.EntityA = uidA;
@@ -229,11 +229,11 @@ public abstract partial class SharedPhysicsSystem
// Edge+Polygon is non-symmetrical due to the way Erin handles collision type registration.
if ((type1 >= type2 || (type1 == ShapeType.Edge && type2 == ShapeType.Polygon)) && !(type2 == ShapeType.Edge && type1 == ShapeType.Polygon))
{
SetContact(contact, uidA, uidB, fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB, bodyA, bodyB);
SetContact(contact, true, uidA, uidB, fixtureAId, fixtureBId, fixtureA, indexA, fixtureB, indexB, bodyA, bodyB);
}
else
{
SetContact(contact, uidB, uidA, fixtureBId, fixtureAId, fixtureB, indexB, fixtureA, indexA, bodyB, bodyA);
SetContact(contact, true, uidB, uidA, fixtureBId, fixtureAId, fixtureB, indexB, fixtureA, indexA, bodyB, bodyA);
}
contact.Type = _registers[(int)type1, (int)type2];
@@ -376,6 +376,12 @@ public abstract partial class SharedPhysicsSystem
var contact = node.Value;
node = node.Next;
// It's possible the contact was destroyed by content in which case we just skip it.
if (!contact.Enabled)
continue;
// No longer pre-init and can be used in the solver.
contact.Flags &= ~ContactFlags.PreInit;
Fixture fixtureA = contact.FixtureA!;
Fixture fixtureB = contact.FixtureB!;
int indexA = contact.ChildIndexA;
@@ -504,6 +510,7 @@ public abstract partial class SharedPhysicsSystem
{
Log.Error($"Insufficient contact length at 429! Index {index} and length is {contacts.Length}. Tell Sloth");
}
contacts[index++] = contact;
}
@@ -524,6 +531,12 @@ public abstract partial class SharedPhysicsSystem
var contact = contacts[i];
// It's possible the contact was disabled above if DestroyContact lead to even more being destroyed.
if (!contact.Enabled)
{
continue;
}
switch (status[i])
{
case ContactStatus.StartTouching:
@@ -582,24 +595,19 @@ public abstract partial class SharedPhysicsSystem
private void BuildManifolds(Contact[] contacts, int count, ContactStatus[] status, Vector2[] worldPoints)
{
if (count == 0)
return;
var wake = ArrayPool<bool>.Shared.Rent(count);
if (count > ContactsPerThread * 2)
_parallel.ProcessNow(new ManifoldsJob()
{
var batches = (int) Math.Ceiling((float) count / ContactsPerThread);
Parallel.For(0, batches, i =>
{
var start = i * ContactsPerThread;
var end = Math.Min(start + ContactsPerThread, count);
UpdateContacts(contacts, start, end, status, wake, worldPoints);
});
}
else
{
UpdateContacts(contacts, 0, count, status, wake, worldPoints);
}
Physics = this,
Status = status,
WorldPoints = worldPoints,
Contacts = contacts,
Wake = wake,
}, count);
// Can't do this during UpdateContacts due to IoC threading issues.
for (var i = 0; i < count; i++)
@@ -620,35 +628,49 @@ public abstract partial class SharedPhysicsSystem
ArrayPool<bool>.Shared.Return(wake);
}
private void UpdateContacts(Contact[] contacts, int start, int end, ContactStatus[] status, bool[] wake, Vector2[] worldPoints)
private record struct ManifoldsJob : IParallelRobustJob
{
for (var i = start; i < end; i++)
public int BatchSize => ContactsPerThread;
public SharedPhysicsSystem Physics;
public Contact[] Contacts;
public ContactStatus[] Status;
public Vector2[] WorldPoints;
public bool[] Wake;
public void Execute(int index)
{
var contact = contacts[i];
Physics.UpdateContact(Contacts, index, Status, Wake, WorldPoints);
}
}
// TODO: Temporary measure. When Box2D 3.0 comes out expect a major refactor
// of everything
if (contact.FixtureA == null || contact.FixtureB == null)
{
Log.Error($"Found a null contact for contact at {i}");
status[i] = ContactStatus.NoContact;
wake[i] = false;
DebugTools.Assert(false);
continue;
}
private void UpdateContact(Contact[] contacts, int index, ContactStatus[] status, bool[] wake, Vector2[] worldPoints)
{
var contact = contacts[index];
var uidA = contact.EntityA;
var uidB = contact.EntityB;
var bodyATransform = GetPhysicsTransform(uidA, Transform(uidA));
var bodyBTransform = GetPhysicsTransform(uidB, Transform(uidB));
// TODO: Temporary measure. When Box2D 3.0 comes out expect a major refactor
// of everything
// It's okay past sloth it can't hurt you anymore.
// This can happen if DestroyContact is called and content deletes contacts that were already processed.
if (!contact.Enabled)
{
status[index] = ContactStatus.NoContact;
wake[index] = false;
return;
}
var contactStatus = contact.Update(bodyATransform, bodyBTransform, out wake[i]);
status[i] = contactStatus;
var uidA = contact.EntityA;
var uidB = contact.EntityB;
var bodyATransform = GetPhysicsTransform(uidA);
var bodyBTransform = GetPhysicsTransform(uidB);
if (contactStatus == ContactStatus.StartTouching)
{
worldPoints[i] = Physics.Transform.Mul(bodyATransform, contacts[i].Manifold.LocalPoint);
}
var contactStatus = contact.Update(bodyATransform, bodyBTransform, out wake[index]);
status[index] = contactStatus;
if (contactStatus == ContactStatus.StartTouching)
{
worldPoints[index] = Physics.Transform.Mul(bodyATransform, contacts[index].Manifold.LocalPoint);
}
}

View File

@@ -385,8 +385,8 @@ public abstract partial class SharedPhysicsSystem
var contact = node.Value;
node = node.Next;
// Has this contact already been added to an island?
if ((contact.Flags & ContactFlags.Island) != 0x0) continue;
// Has this contact already been added to an island / is it pre-init?
if ((contact.Flags & (ContactFlags.Island | ContactFlags.PreInit)) != 0x0) continue;
// Is this contact solid and touching?
if (!contact.Enabled || !contact.IsTouching) continue;

View File

@@ -1,4 +1,5 @@
using System.Numerics;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
@@ -15,13 +16,18 @@ public abstract partial class SharedPhysicsSystem
/// <summary>
/// Gets the linear velocity of a particular body at the specified point.
/// </summary>
[Pure]
[PublicAPI]
public Vector2 GetLinearVelocity(
EntityUid uid,
Vector2 point,
PhysicsComponent? component = null,
TransformComponent? xform = null)
{
if (!Resolve(uid, ref component, ref xform))
if (!PhysicsQuery.Resolve(uid, ref component))
return Vector2.Zero;
if (!_xformQuery.Resolve(uid, ref xform))
return Vector2.Zero;
var velocity = component.LinearVelocity;
@@ -29,6 +35,43 @@ public abstract partial class SharedPhysicsSystem
return velocity + angVelocity;
}
/// <summary>
/// This is the total rate of change of the coordinate's map position.
/// </summary>
[Pure]
[PublicAPI]
public Vector2 GetMapLinearVelocity(EntityCoordinates coordinates)
{
if (!coordinates.IsValid(EntityManager))
return Vector2.Zero;
var mapUid = coordinates.GetMapUid(EntityManager);
var parent = coordinates.EntityId;
var localPos = coordinates.Position;
var velocity = Vector2.Zero;
var angularComponent = Vector2.Zero;
while (parent != mapUid && parent.IsValid())
{
// Could make this a method with the below one but ehh
// then you get a method bigger than this block with a billion out args and who wants that.
var xform = _xformQuery.GetComponent(parent);
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
velocity += body.LinearVelocity;
angularComponent += Vector2Helpers.Cross(body.AngularVelocity, localPos - body.LocalCenter);
angularComponent = xform.LocalRotation.RotateVec(angularComponent);
}
localPos = xform.LocalPosition + xform.LocalRotation.RotateVec(localPos);
parent = xform.ParentUid;
}
return velocity;
}
/// <summary>
/// This is the total rate of change of the entity's map-position, resulting from the linear and angular
/// velocities of this entity and any parents.
@@ -36,17 +79,16 @@ public abstract partial class SharedPhysicsSystem
/// <remarks>
/// Use <see cref="GetMapVelocities"/> if you need linear and angular at the same time.
/// </remarks>
[Pure]
[PublicAPI]
public Vector2 GetMapLinearVelocity(
EntityUid uid,
PhysicsComponent? component = null,
TransformComponent? xform = null,
EntityQuery<TransformComponent>? xformQuery = null,
EntityQuery<PhysicsComponent>? physicsQuery = null)
TransformComponent? xform = null)
{
xformQuery ??= EntityManager.GetEntityQuery<TransformComponent>();
physicsQuery ??= EntityManager.GetEntityQuery<PhysicsComponent>();
if (!_xformQuery.Resolve(uid, ref xform))
return Vector2.Zero;
xform ??= xformQuery.Value.GetComponent(uid);
var parent = xform.ParentUid;
var localPos = xform.LocalPosition;
@@ -55,9 +97,9 @@ public abstract partial class SharedPhysicsSystem
while (parent != xform.MapUid && parent.IsValid())
{
xform = xformQuery.Value.GetComponent(parent);
xform = _xformQuery.GetComponent(parent);
if (physicsQuery.Value.TryGetComponent(parent, out var body))
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
// add linear velocity of parent relative to it's own parent (again, in map coordinates)
velocity += body.LinearVelocity;
@@ -82,48 +124,48 @@ public abstract partial class SharedPhysicsSystem
/// <remarks>
/// Consider using <see cref="GetMapVelocities"/> if you need linear and angular at the same time.
/// </remarks>
[Pure]
[PublicAPI]
public float GetMapAngularVelocity(
EntityUid uid,
PhysicsComponent? component = null,
TransformComponent? xform = null,
EntityQuery<TransformComponent>? xformQuery = null,
EntityQuery<PhysicsComponent>? physicsQuery = null)
TransformComponent? xform = null)
{
if (!Resolve(uid, ref component))
if (!PhysicsQuery.Resolve(uid, ref component))
return 0;
xformQuery ??= EntityManager.GetEntityQuery<TransformComponent>();
physicsQuery ??= EntityManager.GetEntityQuery<PhysicsComponent>();
xform ??= xformQuery.Value.GetComponent(uid);
if (!_xformQuery.Resolve(uid, ref xform))
return 0f;
var angularVelocity = component.AngularVelocity;
while (xform.ParentUid != xform.MapUid && xform.ParentUid.IsValid())
{
if (physicsQuery.Value.TryGetComponent(xform.ParentUid, out var body))
if (PhysicsQuery.TryGetComponent(xform.ParentUid, out var body))
angularVelocity += body.AngularVelocity;
xform = xformQuery.Value.GetComponent(xform.ParentUid);
xform = _xformQuery.GetComponent(xform.ParentUid);
}
return angularVelocity;
}
/// <summary>
/// Gets the linear and angular velocity for this entity in map terms.
/// </summary>
[Pure]
[PublicAPI]
public (Vector2, float) GetMapVelocities(
EntityUid uid,
PhysicsComponent? component = null,
TransformComponent? xform = null,
EntityQuery<TransformComponent>? xformQuery = null,
EntityQuery<PhysicsComponent>? physicsQuery = null)
TransformComponent? xform = null)
{
if (!Resolve(uid, ref component))
if (!PhysicsQuery.Resolve(uid, ref component))
return (Vector2.Zero, 0);
xformQuery ??= EntityManager.GetEntityQuery<TransformComponent>();
physicsQuery ??= EntityManager.GetEntityQuery<PhysicsComponent>();
if (!_xformQuery.Resolve(uid, ref xform))
return (Vector2.Zero, 0);
xform ??= xformQuery.Value.GetComponent(uid);
var parent = xform.ParentUid;
var localPos = xform.LocalPosition;
@@ -134,9 +176,9 @@ public abstract partial class SharedPhysicsSystem
while (parent != xform.MapUid && parent.IsValid())
{
xform = xformQuery.Value.GetComponent(parent);
xform = _xformQuery.GetComponent(parent);
if (physicsQuery.Value.TryGetComponent(parent, out var body))
if (PhysicsQuery.TryGetComponent(parent, out var body))
{
angularVelocity += body.AngularVelocity;
@@ -180,7 +222,7 @@ public abstract partial class SharedPhysicsSystem
FixturesComponent? manager = null;
// for the new velocities (that need to be updated), we can just use the existing function:
var (newLinear, newAngular) = GetMapVelocities(uid, physics, xform, xformQuery, physicsQuery);
var (newLinear, newAngular) = GetMapVelocities(uid, physics, xform);
// for the old velocities, we need to re-implement this function while using the old parent and old local position:
if (args.OldParent is not { Valid: true } parent)

View File

@@ -75,8 +75,6 @@ namespace Robust.Shared.Physics.Systems
_xformQuery = GetEntityQuery<TransformComponent>();
SubscribeLocalEvent<GridAddEvent>(OnGridAdd);
SubscribeLocalEvent<PhysicsWakeEvent>(OnWake);
SubscribeLocalEvent<PhysicsSleepEvent>(OnSleep);
SubscribeLocalEvent<CollisionChangeEvent>(OnCollisionChange);
SubscribeLocalEvent<PhysicsComponent, EntGotRemovedFromContainerMessage>(HandleContainerRemoved);
SubscribeLocalEvent<EntParentChangedMessage>(OnParentChange);
@@ -254,26 +252,20 @@ namespace Robust.Shared.Physics.Systems
_configManager.UnsubValueChanged(CVars.AutoClearForces, OnAutoClearChange);
}
private void OnWake(ref PhysicsWakeEvent @event)
private void UpdateMapAwakeState(EntityUid uid, PhysicsComponent body)
{
var mapId = EntityManager.GetComponent<TransformComponent>(@event.Entity).MapID;
var mapId = EntityManager.GetComponent<TransformComponent>(uid).MapID;
if (mapId == MapId.Nullspace)
return;
var tempQualifier = _mapManager.GetMapEntityId(mapId);
AddAwakeBody(@event.Entity, @event.Body, tempQualifier);
}
private void OnSleep(ref PhysicsSleepEvent @event)
{
var mapId = EntityManager.GetComponent<TransformComponent>(@event.Entity).MapID;
if (mapId == MapId.Nullspace)
return;
var tempQualifier = _mapManager.GetMapEntityId(mapId);
RemoveSleepBody(@event.Entity, @event.Body, tempQualifier);
if (body.Awake)
{
AddAwakeBody(uid, body, tempQualifier);
}
else
{
RemoveSleepBody(uid, body, tempQualifier);
}
}
private void HandleContainerRemoved(EntityUid uid, PhysicsComponent physics, EntGotRemovedFromContainerMessage message)

View File

@@ -523,35 +523,43 @@ namespace Robust.Shared.Prototypes
void ProcessItem(string id, InheritancePushDatum datum)
{
if (tree.TryGetParents(id, out var parents))
try
{
var parentNodes = new MappingDataNode[parents.Length];
for (var i = 0; i < parents.Length; i++)
if (tree.TryGetParents(id, out var parents))
{
parentNodes[i] = results[parents[i]].Result;
var parentNodes = new MappingDataNode[parents.Length];
for (var i = 0; i < parents.Length; i++)
{
parentNodes[i] = results[parents[i]].Result;
}
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
parentNodes,
datum.Result);
}
datum.Result = _serializationManager.PushCompositionWithGenericNode(
kind,
parentNodes,
datum.Result);
}
if (tree.TryGetChildren(id, out var children))
{
foreach (var child in children)
if (tree.TryGetChildren(id, out var children))
{
var childDatum = results[child];
var val = Interlocked.Decrement(ref childDatum.CountParentsRemaining);
if (val == 0)
foreach (var child in children)
{
ThreadPool.QueueUserWorkItem(_ => { ProcessItem(child, childDatum); });
var childDatum = results[child];
var val = Interlocked.Decrement(ref childDatum.CountParentsRemaining);
if (val == 0)
{
ThreadPool.QueueUserWorkItem(_ => { ProcessItem(child, childDatum); });
}
}
}
}
// ReSharper disable once AccessToDisposedClosure
countDown.Signal();
// ReSharper disable once AccessToDisposedClosure
countDown.Signal();
}
catch (Exception e)
{
Sawmill.Error($"Failed to push composition for {kind.Name} prototype with id: {id}. Exception: {e}");
throw;
}
}
await WaitHandleHelpers.WaitOneAsync(countDown.WaitHandle);

View File

@@ -9,6 +9,7 @@
<PackageReference Include="JetBrains.Annotations" Version="2021.3.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="6.0.2" />
<PackageReference Include="Microsoft.ILVerification" Version="6.0.0" PrivateAssets="compile" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.3.2" />
<PackageReference Include="Nett" Version="0.15.0" PrivateAssets="compile" />
<PackageReference Include="NVorbis" Version="0.10.1" PrivateAssets="compile" />
<PackageReference Include="Pidgin" Version="2.5.0" />

View File

@@ -49,6 +49,7 @@ namespace Robust.Shared
deps.Register<IParallelManagerInternal, ParallelManager>();
deps.Register<ToolshedManager>();
deps.Register<HttpClientHolder>();
deps.Register<RobustMemoryManager>();
}
}
}

View File

@@ -0,0 +1,16 @@
namespace Robust.Shared.Threading;
/// <summary>
/// Runs the job with the specified batch size per thread; Execute is still called per index.
/// </summary>
public interface IParallelRobustJob
{
/// <summary>
/// Minimum amount of batches required to engage in parallelism.
/// </summary>
int MinimumBatchParallel => 2;
int BatchSize => 1;
void Execute(int index);
}

View File

@@ -0,0 +1,10 @@
using System.Threading;
namespace Robust.Shared.Threading;
/// <summary>
/// Implement for code that needs to be runnable on a threadpool.
/// </summary>
public interface IRobustJob : IThreadPoolWorkItem
{
}

View File

@@ -1,10 +1,8 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Shared.Threading;
@@ -18,6 +16,27 @@ public interface IParallelManager
/// Add the delegate to <see cref="ParallelCountChanged"/> and immediately invoke it.
/// </summary>
void AddAndInvokeParallelCountChanged(Action changed);
/// <summary>
/// Takes in a job that gets flushed.
/// </summary>
/// <param name="job"></param>
WaitHandle Process(IRobustJob job);
/// <summary>
/// Takes in a parallel job and runs it the specified amount.
/// </summary>
void ProcessNow(IParallelRobustJob jobs, int amount);
/// <summary>
/// Processes a robust job sequentially if desired.
/// </summary>
void ProcessSerialNow(IParallelRobustJob jobs, int amount);
/// <summary>
/// Takes in a parallel job and runs it without blocking.
/// </summary>
WaitHandle Process(IParallelRobustJob jobs, int amount);
}
internal interface IParallelManagerInternal : IParallelManager
@@ -32,6 +51,21 @@ internal sealed class ParallelManager : IParallelManagerInternal
public event Action? ParallelCountChanged;
public int ParallelProcessCount { get; private set; }
// Without pooling it's hard to keep task allocations down for classes
// This lets us avoid re-allocating the ManualResetEventSlims constantly when we just need a way to signal job completion.
private readonly ObjectPool<InternalJob> _jobPool =
new DefaultObjectPool<InternalJob>(new DefaultPooledObjectPolicy<InternalJob>(), 256);
private readonly ObjectPool<InternalParallelJob> _parallelPool =
new DefaultObjectPool<InternalParallelJob>(new DefaultPooledObjectPolicy<InternalParallelJob>(), 256);
/// <summary>
/// Used internally for Parallel jobs, for external callers it gets garbage collected.
/// </summary>
private readonly ObjectPool<ParallelTracker> _trackerPool =
new DefaultObjectPool<ParallelTracker>(new DefaultPooledObjectPolicy<ParallelTracker>());
public void Initialize()
{
_cfg.OnValueChanged(CVars.ThreadParallelCount, UpdateCVar, true);
@@ -43,13 +77,187 @@ internal sealed class ParallelManager : IParallelManagerInternal
changed();
}
private InternalJob GetJob(IRobustJob job)
{
var robustJob = _jobPool.Get();
robustJob.Event.Reset();
robustJob.Set(job, _jobPool);
return robustJob;
}
private InternalParallelJob GetParallelJob(IParallelRobustJob job, int start, int end, ParallelTracker tracker)
{
var internalJob = _parallelPool.Get();
internalJob.Set(job, start, end, tracker, _parallelPool);
return internalJob;
}
private void UpdateCVar(int value)
{
var oldCount = ParallelProcessCount;
ParallelProcessCount = value == 0 ? Environment.ProcessorCount : value;
ThreadPool.GetAvailableThreads(out var oldWorker, out var oldCompletion);
ParallelProcessCount = value == 0 ? oldWorker : value;
if (oldCount != ParallelProcessCount)
{
ParallelCountChanged?.Invoke();
ThreadPool.SetMaxThreads(ParallelProcessCount, oldCompletion);
}
}
/// <inheritdoc/>
public WaitHandle Process(IRobustJob job)
{
var subJob = GetJob(job);
// From what I can tell preferLocal is more of a !forceGlobal flag.
// Also UnsafeQueue should be fine as long as we don't use async locals.
ThreadPool.UnsafeQueueUserWorkItem(subJob, true);
return subJob.Event.WaitHandle;
}
/// <inheritdoc/>
public void ProcessNow(IParallelRobustJob job, int amount)
{
var batches = amount / (float) job.BatchSize;
// Below the threshold so just do it now.
if (batches <= job.MinimumBatchParallel)
{
ProcessSerialNow(job, amount);
return;
}
var tracker = InternalProcess(job, amount);
tracker.Event.WaitHandle.WaitOne();
_trackerPool.Return(tracker);
}
/// <inheritdoc/>
public void ProcessSerialNow(IParallelRobustJob jobs, int amount)
{
for (var i = 0; i < amount; i++)
{
jobs.Execute(i);
}
}
/// <inheritdoc/>
public WaitHandle Process(IParallelRobustJob job, int amount)
{
var tracker = InternalProcess(job, amount);
return tracker.Event.WaitHandle;
}
/// <summary>
/// Runs a parallel job internally. Used so we can pool the tracker task for ProcessParallelNow
/// and not rely on external callers to return it where they don't want to wait.
/// </summary>
private ParallelTracker InternalProcess(IParallelRobustJob job, int amount)
{
var batches = (int) MathF.Ceiling(amount / (float) job.BatchSize);
var batchSize = job.BatchSize;
var tracker = _trackerPool.Get();
// Need to set this up front to avoid firing too early.
tracker.Event.Reset();
tracker.PendingTasks = batches;
for (var i = 0; i < batches; i++)
{
var start = i * batchSize;
var end = Math.Min(start + batchSize, amount);
var subJob = GetParallelJob(job, start, end, tracker);
// From what I can tell preferLocal is more of a !forceGlobal flag.
// Also UnsafeQueue should be fine as long as we don't use async locals.
ThreadPool.UnsafeQueueUserWorkItem(subJob, true);
}
return tracker;
}
#region Jobs
/// <summary>
/// Runs an <see cref="IRobustJob"/> and handles cleanup.
/// </summary>
private sealed class InternalJob : IRobustJob
{
private IRobustJob _robust = default!;
public readonly ManualResetEventSlim Event = new();
private ObjectPool<InternalJob> _parentPool = default!;
public void Set(IRobustJob job, ObjectPool<InternalJob> parentPool)
{
_robust = job;
_parentPool = parentPool;
}
public void Execute()
{
_robust.Execute();
Event.Set();
_parentPool.Return(this);
}
}
/// <summary>
/// Runs an <see cref="IParallelRobustJob"/> and handles cleanup.
/// </summary>
private sealed class InternalParallelJob : IRobustJob
{
private IParallelRobustJob _robust = default!;
private int _start;
private int _end;
private ParallelTracker _tracker = default!;
private ObjectPool<InternalParallelJob> _parentPool = default!;
public void Set(IParallelRobustJob robust, int start, int end, ParallelTracker tracker, ObjectPool<InternalParallelJob> parentPool)
{
_robust = robust;
_start = start;
_end = end;
_tracker = tracker;
_parentPool = parentPool;
}
public void Execute()
{
for (var i = _start; i < _end; i++)
{
_robust.Execute(i);
}
// Set the event and return it to the pool for re-use.
_tracker.Set();
_parentPool.Return(this);
}
}
/// <summary>
/// Tracks jobs internally. This is because WaitHandle has a max limit of 64 tasks.
/// So we'll just decrement PendingTasks in lieu.
/// </summary>
private sealed class ParallelTracker
{
public readonly ManualResetEventSlim Event = new();
public int PendingTasks;
/// <summary>
/// Marks the tracker as having 1 less pending task.
/// </summary>
public void Set()
{
Interlocked.Decrement(ref PendingTasks);
if (PendingTasks <= 0)
Event.Set();
}
}
#endregion
}

View File

@@ -32,19 +32,19 @@ public sealed partial class ToolshedManager
if (t.IsGenericType)
{
_genericTypeParsers.Add(t.GetGenericTypeDefinition(), parserType);
_log.Info($"Setting up {parserType.PrettyName()}, {t.GetGenericTypeDefinition().PrettyName()}");
_log.Verbose($"Setting up {parserType.PrettyName()}, {t.GetGenericTypeDefinition().PrettyName()}");
}
else if (t.IsGenericParameter)
{
_constrainedParsers.Add((t, parserType));
_log.Info($"Setting up {parserType.PrettyName()}, for T where T: {string.Join(", ", t.GetGenericParameterConstraints().Select(x => x.PrettyName()))}");
_log.Verbose($"Setting up {parserType.PrettyName()}, for T where T: {string.Join(", ", t.GetGenericParameterConstraints().Select(x => x.PrettyName()))}");
}
}
else
{
var parser = (ITypeParser) _typeFactory.CreateInstanceUnchecked(parserType, oneOff: true);
parser.PostInject();
_log.Info($"Setting up {parserType.PrettyName()}, {parser.Parses.PrettyName()}");
_log.Verbose($"Setting up {parserType.PrettyName()}, {parser.Parses.PrettyName()}");
_consoleTypeParsers.Add(parser.Parses, parser);
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.IO;
using Lidgren.Network;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
@@ -156,7 +157,8 @@ internal sealed class MsgViewVariablesListPathReq : MsgViewVariablesPathReq
{
base.ReadFromBuffer(buffer, serializer);
var length = buffer.ReadInt32();
using var stream = buffer.ReadAlignedMemory(length);
using var stream = RobustMemoryManager.GetMemoryStream(length);
buffer.ReadAlignedMemory(stream, length);
Options = serializer.Deserialize<VVListPathOptions>(stream);
}

View File

@@ -71,7 +71,6 @@ namespace Robust.UnitTesting.Shared.Timing
public void TestCancellation()
{
var timerManager = IoCManager.Resolve<ITimerManager>();
var taskManager = IoCManager.Resolve<ITaskManager>();
var cts = new CancellationTokenSource();
var ran = false;

View File

@@ -1,18 +1,55 @@
using System;
using System.Threading;
using Robust.Shared.Threading;
namespace Robust.UnitTesting;
/// <summary>
/// Only allows 1 parallel process for testing purposes.
/// </summary>
/// </summary>j
public sealed class TestingParallelManager : IParallelManager
{
public event Action? ParallelCountChanged;
public int ParallelProcessCount => 1;
public void AddAndInvokeParallelCountChanged(Action changed)
{
// Gottem
return;
}
WaitHandle IParallelManager.Process(IRobustJob job)
{
job.Execute();
var ev = new ManualResetEventSlim();
ev.Set();
return ev.WaitHandle;
}
/// <inheritdoc/>
public void ProcessNow(IParallelRobustJob jobs, int amount)
{
for (var i = 0; i < amount; i++)
{
jobs.Execute(i);
}
}
/// <inheritdoc/>
public void ProcessSerialNow(IParallelRobustJob jobs, int amount)
{
for (var i = 0; i < amount; i++)
{
jobs.Execute(i);
}
}
/// <inheritdoc/>
public WaitHandle Process(IParallelRobustJob jobs, int amount)
{
ProcessSerialNow(jobs, amount);
var ev = new ManualResetEventSlim();
ev.Set();
return ev.WaitHandle;
}
}