Files
space-station-14/Content.Server/Atmos/EntitySystems/AtmosphereSystem.DeltaPressure.cs
T
2026-06-02 12:52:50 -07:00

312 lines
14 KiB
C#

using System.Buffers;
using System.Collections.Concurrent;
using System.Numerics.Tensors;
using Content.Server.Atmos.Components;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Damage;
using Robust.Shared.Random;
using Robust.Shared.Threading;
namespace Content.Server.Atmos.EntitySystems;
public sealed partial class AtmosphereSystem
{
/// <summary>
/// <para>A queue that handles scheduling of invalid entities to be removed from the entity processing list.</para>
///
/// <para>We cannot change the contents of the list while processing it in parallel as this may create
/// a race condition for other thread pool workers working on different parts of the same list (as removing
/// items from the list will do a substitution of items to fill the gap, which can touch ents
/// other threads may be working on).</para>
///
/// <para>As such, we just delay removal of these entities until after parallel processing.</para>
/// </summary>
private readonly ConcurrentQueue<Entity<DeltaPressureComponent>> _deltaPressureInvalidEntityQueue = new();
/// <summary>
/// The number of pairs of opposing directions we can have.
/// This is Atmospherics.Directions / 2, since we always compare opposing directions
/// (e.g. North vs South, East vs West, etc.).
/// Used to determine the size of the opposing groups when processing delta pressure entities.
/// </summary>
private const int DeltaPressurePairCount = Atmospherics.Directions / 2;
/// <summary>
/// Bulk processes a range of <see cref="DeltaPressureComponent"/> entities on a <see cref="GridAtmosphereComponent"/>
/// from a starting index to an ending index,
/// determining the pressures they're experiencing and applying damage based on that.
/// </summary>
/// <param name="gridAtmosComp">The <see cref="GridAtmosphereComponent"/> that belongs to the entity's GridUid.</param>
/// <param name="start">The starting index in the DeltaPressureEntities list to process from.</param>
/// <param name="end">The ending index in the DeltaPressureEntities list to process to.</param>
private void ProcessDeltaPressureEntityBulk(GridAtmosphereComponent gridAtmosComp, int start, int end)
{
/*
To make our comparisons a little bit faster, we take advantage of SIMD-accelerated methods.
This requires that we load our data into spans to process them in bulk.
This code takes advantage of ArrayPool so we can super easily reuse memory per tick
in threading contexts, otherwise this will literally obliterate GC with a nuclear bomb.
*/
var entList = gridAtmosComp.DeltaPressureEntities;
var len = end - start;
const int dirs = Atmospherics.Directions;
// Total number of tiles to gather = number of entities * number of directions.
var lenDirs = len * dirs;
// Total number of opposing direction pairs to process = number of entities * number of opposing direction pairs.
var pairLen = len * DeltaPressurePairCount;
// Memory is meant to be used. Allocate a bunch of it.
// Note that the arrays returned by ArrayPool are usually larger than requested
// (ASK MY DEBUGGER HOW I KNOW) so we ask Span to give us a slice of the correct length.
var tilesArr = ArrayPool<TileAtmosphere?>.Shared.Rent(lenDirs);
var tiles = tilesArr.AsSpan(0, lenDirs);
// I really hate dict lookups in hot loops so we keep AirtightComps on us.
// This is the only array that isn't sliced since we do not have any operation that
// requires SIMD on it, either directly or indirectly
// (SIMD requires that arrays are same length or else indexes/supporting code will likely overstep).
var airtightCompsArr = ArrayPool<AirtightComponent>.Shared.Rent(len);
var groupAArr = ArrayPool<float>.Shared.Rent(pairLen);
var groupBArr = ArrayPool<float>.Shared.Rent(pairLen);
var groupMaxArr = ArrayPool<float>.Shared.Rent(pairLen);
var groupA = groupAArr.AsSpan(0, pairLen);
var groupB = groupBArr.AsSpan(0, pairLen);
var groupMax = groupMaxArr.AsSpan(0, pairLen);
var pressuresArr = ArrayPool<float>.Shared.Rent(lenDirs);
var pressures = pressuresArr.AsSpan(0, lenDirs);
try
{
// Gather tiles & airtight components.
for (var i = 0; i < len; i++)
{
var ent = entList[start + i];
// Ensure that the list still only contains valid airtight entities.
// We cannot remove them here, so we enqueue them for later removal.
if (!_airtightQuery.TryComp(ent, out var airtightComp))
{
_deltaPressureInvalidEntityQueue.Enqueue(ent);
Log.Error($"DeltaPressure entity without an AirtightComponent found in processing list! Ent: {ent}");
return;
}
airtightCompsArr[i] = airtightComp;
var currentPos = airtightComp.LastPosition.Tile;
var tileBase = i * dirs;
for (var j = 0; j < dirs; j++)
{
var direction = (AtmosDirection)(1 << j);
var offset = currentPos.Offset(direction);
tiles[tileBase + j] = gridAtmosComp.Tiles.GetValueOrDefault(offset);
}
}
GetBulkTileAtmospherePressures(tiles, pressures);
/*
This entity could be airtight but still be able to contain air on the tile it's on (ex. directional windows).
As such, substitute the pressure of the pressure on top of the entity for the directions that it can accept air from.
(Or rather, don't do so for directions that it blocks air from.)
*/
for (var i = 0; i < len; i++)
{
var airtight = airtightCompsArr[i];
if (airtight.NoAirWhenFullyAirBlocked)
continue;
var currentPos = airtight.LastPosition.Tile;
var localPressure = 0f;
// microopting one less nullcheck lmao
if (gridAtmosComp.Tiles.TryGetValue(currentPos, out var tile) && tile.Air is { } mixture)
localPressure = mixture.TotalMoles * Atmospherics.R * mixture.Temperature / mixture.Volume;
var presBase = i * dirs;
for (var j = 0; j < dirs; j++)
{
var direction = (AtmosDirection)(1 << j);
if (!airtight.AirBlockedDirection.IsFlagSet(direction))
pressures[presBase + j] = localPressure;
}
}
/*
In order to perform SIMD ops we load the values into opposing pairs, where:
groupA: North, East
groupB: South, West
That way NumericsHelpers can just do vectorized operations on them super easily.
In a sense this means that the arrays, per index, would look like:
0, 1 - 0, 2
0, 1 - 1, 3
*/
for (var i = 0; i < len; i++)
{
var presBase = i * dirs;
var pairBase = i * DeltaPressurePairCount;
for (var j = 0; j < DeltaPressurePairCount; j++)
{
var pressurePairBase = DeltaPressurePairCount * j;
groupA[pairBase + j] = pressures[presBase + pressurePairBase];
groupB[pairBase + j] = pressures[presBase + pressurePairBase + 1];
}
}
// Time to get crankin
TensorPrimitives.Max(groupA, groupB, groupMax);
TensorPrimitives.Subtract(groupA, groupB, groupA);
TensorPrimitives.Abs(groupA, groupA);
// Now go through each entity and determine their max pressure & delta pressure.
// Queue for damage if necessary.
for (var i = 0; i < len; i++)
{
var ent = entList[start + i];
// It is genuinely a massive pain in the ass to handle skipping in the beginning than it is to get that
// microboost from skipping work. As such, just skip at the very end.
if (!_random.Prob(ent.Comp.RandomDamageChance))
{
SetIsTakingDamageState(ent, false);
continue;
}
var pairBase = i * DeltaPressurePairCount;
var maxPressure = 0f;
var maxDelta = 0f;
for (var j = 0; j < DeltaPressurePairCount; j++)
{
// I actually did write a HorizontalMax SIMD method but benchmarking showed that
// it was only superior when n > 4. Since we can only compute the max on 4 elements
// we can't take advantage of our array being big right here.
maxPressure = MathF.Max(maxPressure, groupMax[pairBase + j]);
maxDelta = MathF.Max(maxDelta, groupA[pairBase + j]);
}
EnqueueDeltaPressureDamage(ent, gridAtmosComp, maxPressure, maxDelta);
}
}
finally
{
ArrayPool<TileAtmosphere?>.Shared.Return(tilesArr);
ArrayPool<AirtightComponent>.Shared.Return(airtightCompsArr);
ArrayPool<float>.Shared.Return(groupAArr);
ArrayPool<float>.Shared.Return(groupBArr);
ArrayPool<float>.Shared.Return(groupMaxArr);
ArrayPool<float>.Shared.Return(pressuresArr);
}
}
/// <summary>
/// Packs data into a <see cref="DeltaPressureDamageResult"/> data struct and enqueues it
/// into the <see cref="GridAtmosphereComponent.DeltaPressureDamageResults"/> queue for
/// later processing.
/// </summary>
/// <param name="ent">The entity to enqueue if necessary.</param>
/// <param name="gridAtmosComp">The <see cref="GridAtmosphereComponent"/>
/// containing the queue.</param>
/// <param name="pressure">The current absolute pressure being experienced by the entity.</param>
/// <param name="delta">The current delta pressure being experienced by the entity.</param>
private void EnqueueDeltaPressureDamage(Entity<DeltaPressureComponent> ent,
GridAtmosphereComponent gridAtmosComp,
float pressure,
float delta)
{
var aboveMinPressure = pressure > ent.Comp.MinPressure;
var aboveMinDeltaPressure = delta > ent.Comp.MinPressureDelta;
if (!aboveMinPressure && !aboveMinDeltaPressure)
{
SetIsTakingDamageState(ent, false);
return;
}
gridAtmosComp.DeltaPressureDamageResults.Enqueue(new DeltaPressureDamageResult(ent,
pressure,
delta));
}
/// <summary>
/// Job for solving DeltaPressure entities in parallel.
/// Batches are given some index to start from, so each thread can simply just start at that index
/// and process the next n entities in the list.
/// </summary>
/// <param name="system">The AtmosphereSystem instance.</param>
/// <param name="atmosphere">The GridAtmosphereComponent to work with.</param>
/// <param name="startIndex">The index in the DeltaPressureEntities list to start from.</param>
/// <param name="cvarBatchSize">The batch size to use for this job.</param>
private sealed class DeltaPressureParallelBulkJob(
AtmosphereSystem system,
GridAtmosphereComponent atmosphere,
int startIndex,
int cvarBatchSize)
: IParallelBulkRobustJob
{
public int BatchSize => cvarBatchSize;
public void ExecuteRange(int start, int end)
{
system.ProcessDeltaPressureEntityBulk(atmosphere, start + startIndex, end + startIndex);
}
}
/// <summary>
/// Does damage to an entity depending on the pressure experienced by it, based on the
/// entity's <see cref="DeltaPressureComponent"/>.
/// </summary>
/// <param name="ent">The entity to apply damage to.</param>
/// <param name="pressure">The absolute pressure being exerted on the entity.</param>
/// <param name="deltaPressure">The delta pressure being exerted on the entity.</param>
private void PerformDamage(Entity<DeltaPressureComponent> ent, float pressure, float deltaPressure)
{
var maxPressure = Math.Max(pressure - ent.Comp.MinPressure, deltaPressure - ent.Comp.MinPressureDelta);
var maxPressureCapped = Math.Min(maxPressure, ent.Comp.MaxEffectivePressure);
var appliedDamage = ScaleDamage(ent, ent.Comp.BaseDamage, maxPressureCapped);
_damage.ChangeDamage(ent.Owner, appliedDamage, ignoreResistances: true, interruptsDoAfters: false);
SetIsTakingDamageState(ent, true);
}
/// <summary>
/// Helper function to prevent spamming clients with dirty events when the damage state hasn't changed.
/// </summary>
/// <param name="ent">The entity to check.</param>
/// <param name="toSet">The value to set.</param>
private void SetIsTakingDamageState(Entity<DeltaPressureComponent> ent, bool toSet)
{
if (ent.Comp.IsTakingDamage == toSet)
return;
ent.Comp.IsTakingDamage = toSet;
Dirty(ent);
}
/// <summary>
/// Returns a new DamageSpecifier scaled based on values on an entity with a DeltaPressureComponent.
/// </summary>
/// <param name="ent">The entity to base the manipulations off of (pull scaling type)</param>
/// <param name="damage">The base damage specifier to scale.</param>
/// <param name="pressure">The pressure being exerted on the entity.</param>
/// <returns>A scaled DamageSpecifier.</returns>
private static DamageSpecifier ScaleDamage(Entity<DeltaPressureComponent> ent, DamageSpecifier damage, float pressure)
{
var factor = ent.Comp.ScalingType switch
{
DeltaPressureDamageScalingType.Threshold => 1f,
DeltaPressureDamageScalingType.Linear => pressure * ent.Comp.ScalingPower,
DeltaPressureDamageScalingType.Log =>
(float) Math.Log(pressure, ent.Comp.ScalingPower),
_ => throw new ArgumentOutOfRangeException(nameof(ent), "Invalid damage scaling type!"),
};
return damage * factor;
}
}