diff --git a/FerramAerospaceResearch.Base/FerramAerospaceResearch.Base.csproj b/FerramAerospaceResearch.Base/FerramAerospaceResearch.Base.csproj index 28ce2c707..455104051 100644 --- a/FerramAerospaceResearch.Base/FerramAerospaceResearch.Base.csproj +++ b/FerramAerospaceResearch.Base/FerramAerospaceResearch.Base.csproj @@ -36,6 +36,22 @@ + + False + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\Unity.Burst.dll + + + False + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\Unity.Burst.Unsafe.dll + + + False + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\\Unity.Collections.dll + + + False + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\Unity.Mathematics.dll + $(KSP_DIR_BUILD)KSP_x64_Data\Managed\UnityEngine.dll @@ -119,10 +135,12 @@ + + diff --git a/FerramAerospaceResearch.Base/UnityJobs/OcclusionJobs.cs b/FerramAerospaceResearch.Base/UnityJobs/OcclusionJobs.cs new file mode 100644 index 000000000..14f9b4f44 --- /dev/null +++ b/FerramAerospaceResearch.Base/UnityJobs/OcclusionJobs.cs @@ -0,0 +1,387 @@ +using System; +using Unity.Burst; +using Unity.Collections; +using Unity.Collections.LowLevel.Unsafe; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; + +namespace FerramAerospaceResearch.UnityJobs +{ + [BurstCompile] + public struct SphereDistanceInfo : IComparable, IEquatable + { + public quaternion q; + public float distance; + + public int CompareTo(SphereDistanceInfo obj) + { + return distance.CompareTo(obj.distance); + } + + public bool Equals(SphereDistanceInfo other) + { + return q.Equals(other.q) && distance.Equals(other.distance); + } + } + + public struct EMPTY_STRUCT { } + + //http://extremelearning.com.au/how-to-evenly-distribute-points-on-a-sphere-more-effectively-than-the-canonical-fibonacci-lattice/ + [BurstCompile] + public struct SpherePointsJob : IJobParallelFor + { + [ReadOnly] public int points; + [ReadOnly] public float epsilon; + [WriteOnly] public NativeArray results; + private const float GoldenRatio = 1.61803398875f; //(1 + math.sqrt(5)) / 2; + private const float ThetaConstantTerm = 2 * math.PI / GoldenRatio; + private readonly float denomRecip; + + public SpherePointsJob(int points, float epsilon) : this() + { + this.points = points; + this.epsilon = epsilon; + denomRecip = 1f / (points - 1 + (2 * epsilon)); + } + + public void Execute(int index) + { + if (Unity.Burst.CompilerServices.Hint.Unlikely(index == 0)) + { + results[index] = quaternion.identity; + } + else if (Unity.Burst.CompilerServices.Hint.Unlikely(index == points - 1)) + { + results[index] = quaternion.Euler(180, 0, 0); + } + else + { + float theta = index * ThetaConstantTerm; + float num = 2 * (index + epsilon); + float cosPhi = 1 - num * denomRecip; + float sinPhi = math.sqrt(1 - cosPhi * cosPhi); + math.sincos(theta, out float sinTheta, out float cosTheta); + var res = new float3(cosTheta * sinPhi, + sinTheta * sinPhi, + cosPhi); + results[index] = Quaternion.FromToRotation(new float3(0,0,1), res); + } + } + } + + [BurstCompile] + public struct SetBoundsJob : IJobParallelFor + { + [ReadOnly] public float3 boundsCenter; + [ReadOnly] public float3 extents; + [ReadOnly] public NativeArray quaternions; + [WriteOnly] public NativeArray bounds; + + // We now have center, extents, and the raycaster plane (normal = rotation * vessel.forward) + // Project each of the 8 corners of the bounding box onto the plane. + // https://stackoverflow.com/questions/9605556/how-to-project-a-point-onto-a-plane-in-3d/41897378#41897378 + public void Execute(int index) + { + float3 normal = math.mul(quaternions[index], new float3(0, 0, 1)); + float3 origin = float3.zero + math.length(extents) * normal; + float2 min = new(float.PositiveInfinity, float.PositiveInfinity); + float2 max = new(float.NegativeInfinity, float.NegativeInfinity); + float3 ext = extents / 2; + float3 center = boundsCenter; + unsafe + { + float3* arr = stackalloc[] + { + center - ext, + new float3(center.x - ext.x, center.y - ext.y, center.z + ext.z), + new float3(center.x - ext.x, center.y + ext.y, center.z - ext.z), + new float3(center.x - ext.x, center.y + ext.y, center.z + ext.z), + new float3(center.x + ext.x, center.y - ext.y, center.z - ext.z), + new float3(center.x + ext.x, center.y - ext.y, center.z + ext.z), + new float3(center.x + ext.x, center.y + ext.y, center.z - ext.z), + center + ext, + }; + var quaternionInverse = math.inverse(quaternions[index]); + for (int i = 0; i < 8; i++) + { + // Project the bounding box onto the plane + float dist = math.dot(arr[i] - origin, normal); + float3 projection = arr[i] - dist * normal; + // Rotate the projection back towards the vessel alignment + projection = math.mul(quaternionInverse, projection); + + // The x,y coords of projection are again aligned to the vessel axes. + min = math.min(min, projection.xy); + max = math.max(max, projection.xy); + } + } + + bounds[index] = math.abs(max - min); + } + } + + [BurstCompile] + public struct SetIntervalAndDimensionsJob : IJobParallelFor + { + [ReadOnly] public NativeArray bounds; + [ReadOnly] public int maxDim; + [ReadOnly] public float resolution; + [WriteOnly] public NativeArray interval; + [WriteOnly] public NativeArray dims; + + public void Execute(int index) + { + var minSize = new int2(math.ceil(bounds[index] / resolution)); + dims[index] = math.min(minSize, maxDim); + interval[index] = math.max(bounds[index] / maxDim, resolution); + } + } + + [BurstCompile] + public struct SetVectorsJob : IJobParallelFor + { + [ReadOnly] public NativeArray quaternions; + [ReadOnly] public float4x4 localToWorldMatrix; + [WriteOnly] public NativeArray forward; + [WriteOnly] public NativeArray up; + [WriteOnly] public NativeArray right; + + public void Execute(int index) + { + float3 dir = math.mul(quaternions[index], new float3(0, 0, 1)); + float4 fwd = math.mul(localToWorldMatrix, new float4(dir.x, dir.y, dir.z, 0)); + forward[index] = fwd.xyz; + + dir = math.mul(quaternions[index], new float3(0, 1, 0)); + var upLoc = math.mul(localToWorldMatrix, new float4(dir.x, dir.y, dir.z, 0)); + up[index] = upLoc.xyz; + + right[index] = math.cross(fwd.xyz, upLoc.xyz); + } + } + + [BurstCompile] + public struct AngleBetween : IJobParallelFor + { + [ReadOnly] public float3 dir; + [ReadOnly] public NativeArray rotations; + [WriteOnly] public NativeArray angles; + + public void Execute(int index) + { + float3 rot_dir = math.mul(rotations[index], new float3(0, 0, 1)); + angles[index] = math.degrees(math.acos(math.clamp(math.dot(dir, rot_dir), -1, 1))); + } + } + + [BurstCompile] + public struct GeneralDistanceInfo : IJob + { + [ReadOnly] public NativeArray distances; + [WriteOnly] public NativeArray output; + + public void Execute() + { + float min = float.PositiveInfinity; + float max = 0; + float total = 0; + for (int i = 0; i < distances.Length; i++) + { + total += distances[i]; + max = math.max(max, distances[i]); + min = math.min(min, distances[i]); + } + output[0] = new float3(min, max, total / distances.Length); + } + } + + [BurstCompile] + public struct DotProductJob : IJobParallelFor + { + [ReadOnly] public NativeArray A; + [ReadOnly] public float3 B; + [WriteOnly] public NativeArray result; + + public void Execute(int index) + { + result[index] = math.dot(A[index], B); + } + } + + [BurstCompile] + public struct SortedFilterAndMapByDistanceJob : IJob + { + [ReadOnly] public NativeArray quaternions; + [ReadOnly] public NativeArray distances; + [ReadOnly] public NativeArray cutoffDistance; + public NativeList sdiList; + [WriteOnly] public NativeList orderedQuaternions; + + public void Execute() + { + for (int i = 0; i < quaternions.Length; i++) + { + if (distances[i] < cutoffDistance[0]) + { + SphereDistanceInfo sdi = new() + { + q = quaternions[i], + distance = distances[i] + }; + sdiList.Add(sdi); + } + } + NativeSortExtension.Sort(sdiList); + for (int i = 0; i < sdiList.Length; i++) + orderedQuaternions.Add(sdiList[i].q); + } + } + + [BurstCompile] + public struct MakeQuaternionIndexMapJob : IJob + { + [ReadOnly] public NativeArray arr; + [WriteOnly] public NativeHashMap map; + + public void Execute() + { + for (int index=0; index sorted; + public NativeHashMap map; + public void Execute() + { + for (int index=0; index < sorted.Length; index++) + { + quaternion q = sorted[index]; + if (map.TryGetValue(q, out int i) && index < i) + { + map[q] = index; + } + else + map.TryAdd(q, index); + } + } + } + + [BurstCompile] + public struct BucketSortByPriority : IJobParallelFor + { + [ReadOnly] public NativeArray quaternions; + [ReadOnly] public NativeHashMap priorityMap; + [ReadOnly] public NativeHashMap completed; + [ReadOnly] public int maxIndexForPriority0; + [WriteOnly] public NativeMultiHashMap.ParallelWriter map; + + public void Execute(int index) + { + if (!completed.ContainsKey(quaternions[index])) + { + int pri = 2; + if (priorityMap.TryGetValue(quaternions[index], out int i)) + pri = math.min(pri, (i <= maxIndexForPriority0) ? 0 : 1); + map.Add(pri, index); + } + } + } + + [BurstCompile] + public struct ClearNativeHashMap : IJob where T1:struct, IEquatable where T2:struct + { + public NativeHashMap map; + public void Execute() => map.Clear(); + } + + + [BurstCompile] + public struct ClearNativeMultiHashMap : IJob where T1 : struct, IEquatable where T2 : struct + { + public NativeMultiHashMap map; + public void Execute() => map.Clear(); + } + + // hitsMap is the mapping from collider to all of the indicies in the hitsIn array. + [BurstCompile] + public struct RaycastPrepareJob : IJobParallelFor + { + [ReadOnly] public NativeArray hitsIn; + [WriteOnly] public NativeMultiHashMap.ParallelWriter hitsMap; + + public void Execute(int index) + { + unsafe + { + RaycastHit* array_ptr = (RaycastHit*)hitsIn.GetUnsafeReadOnlyPtr(); + int collider_id = *(int*)((byte*)&array_ptr[index] + 40); + if (collider_id != 0) + hitsMap.Add(collider_id, index); + } + } + } + + // hits is the array of all Raycasts + // hitMap is the mapping from collider to all of the indicies in the hits array. + [BurstCompile] + public struct RaycastProcessorJob : IJobNativeMultiHashMapMergedSharedKeyIndices + { + [ReadOnly] public NativeArray hits; + [ReadOnly] public NativeMultiHashMap hitMap; + [ReadOnly] public float area; + [WriteOnly] public NativeHashMap.ParallelWriter hitsOut; + public NativeArray areaSum; + + // index is the *value* in the MultiHashMap hitMap + // The key generating this is a RaycastHitSummary = Tuple + public void ExecuteFirst(int index) + { + unsafe + { + areaSum[index] = area; + hitsOut.TryAdd(index, area); + } + } + + public void ExecuteNext(int firstIndex, int index) + { + float t = areaSum[firstIndex]; + areaSum[firstIndex] = t + area; + } + } + + [BurstCompile] + public struct OcclusionRaycastBuilder : IJobParallelFor + { + [ReadOnly] public float3 forwardDir; + [ReadOnly] public float3 rightDir; + [ReadOnly] public float3 upDir; + [ReadOnly] public float3 startPosition; + [ReadOnly] public float offset; + [ReadOnly] public int2 dimensions; + [ReadOnly] public float2 interval; + [WriteOnly] public NativeArray commands; + + public void Execute(int index) + { + // Everything is already rotated by the quaternion rotation and transformed to worldspace + // Iterate raycasts in X, Y + float3 origin = startPosition + offset * forwardDir; + int row = index / dimensions.x; + int col = index % dimensions.x; + float row_eff = row - (dimensions.y / 2); + float col_eff = col - (dimensions.x / 2); + + // All raycasts are in the same direction. + float3 pos = origin + (row_eff * interval.y * upDir) + (col_eff * interval.x * rightDir); + commands[index] = new RaycastCommand(pos, -forwardDir, distance: 1e5f, layerMask: 19, maxHits: 1); + } + } +} diff --git a/FerramAerospaceResearch.Base/Utils/Metrics.cs b/FerramAerospaceResearch.Base/Utils/Metrics.cs new file mode 100644 index 000000000..8e430cef4 --- /dev/null +++ b/FerramAerospaceResearch.Base/Utils/Metrics.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; + +namespace FerramAerospaceResearch.Utils +{ + public class Metrics + { + public Dictionary data = new(); + const int HysteresisFactor = 20; + public Metrics() { } + public void Reset() => data.Clear(); + public void AddMeasurement(string name, double t) + { + if (!data.TryGetValue(name, out MetricsElement m)) + { + m = new MetricsElement(); + data.Add(name, m); + } + m.iterations++; + m.hysteresisTime = (m.hysteresisTime * (HysteresisFactor - 1) + t) / HysteresisFactor; + } + } + + public class MetricsElement + { + public int iterations = 0; + public double hysteresisTime = 0; + + public MetricsElement() { } + public override string ToString() => $"iter: {iterations} TimePerRun: {hysteresisTime:F2} ms"; + } +} diff --git a/FerramAerospaceResearch/FARAeroComponents/FARAeroPartModule.cs b/FerramAerospaceResearch/FARAeroComponents/FARAeroPartModule.cs index 8e6775ca3..3d1324359 100644 --- a/FerramAerospaceResearch/FARAeroComponents/FARAeroPartModule.cs +++ b/FerramAerospaceResearch/FARAeroComponents/FARAeroPartModule.cs @@ -90,14 +90,20 @@ public class FARAeroPartModule : PartModule, ILiftProvider private DummyAirstreamShield shield; + public const string GroupName = "FARAeroGroup"; + public const string GroupDisplayName = "FAR Debug"; private bool fieldsVisible; + private bool thermalFieldsVisible; + public readonly List ThermalFields = new List(); // ReSharper disable once NotAccessedField.Global -> unity [KSPField(isPersistant = false, guiActive = false, guiActiveEditor = false, guiFormat = "F3", - guiUnits = "FARUnitKN")] + guiUnits = "FARUnitKN", + groupName = GroupName, + groupDisplayName = GroupDisplayName)] public float dragForce; // ReSharper disable once NotAccessedField.Global -> unity @@ -105,9 +111,58 @@ public class FARAeroPartModule : PartModule, ILiftProvider guiActive = false, guiActiveEditor = false, guiFormat = "F3", - guiUnits = "FARUnitKN")] + guiUnits = "FARUnitKN", + groupName = GroupName)] public float liftForce; + [KSPField(guiFormat = "F3", guiUnits = "FARUnitMSq", groupName = GroupName, groupDisplayName = GroupDisplayName)] + public double radiativeArea; + [KSPField(guiFormat = "F3", guiUnits = "FARUnitMSq", groupName = GroupName)] + public double convectionArea; + [KSPField(guiFormat = "F1", guiUnits = "K", groupName = GroupName)] + public double exposedSkinTemp; + [KSPField(guiFormat = "F1", guiUnits = "K", groupName = GroupName)] + public double unexposedSkinTemp; + [KSPField(guiFormat = "F1", guiUnits = "K", groupName = GroupName)] + public double partTemp; + [KSPField(guiFormat = "F1", guiUnits = "K", groupName = GroupName)] + public double atmosphereTemp; + [KSPField(guiFormat = "F1", guiUnits = "K", groupName = GroupName)] + public double externalTemp; + [KSPField(guiFormat = "F1", guiUnits = "K", groupName = GroupName)] + public double exposedBackgroundTemp; + + [KSPField(guiFormat = "F3", groupName = GroupName)] + public double convergenceFactor; + [KSPField(guiFormat = "F3", groupName = GroupName)] + public double skinSkinConductionMult; + [KSPField(guiFormat = "F3", groupName = GroupName)] + public double skinSkinConductionFactor; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double convectionFlux; + [KSPField(guiFormat = "F3", groupName = GroupName)] + public double finalConvCoeff; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double intConductionFlux; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double skinConductionFlux; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double skinInternalConductionFlux; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double unexpSkinInternalConductionFlux; + [KSPField(guiFormat = "P1", groupName = GroupName)] + public double skinExposedAreaFrac; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double radiationFlux; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double unexpRadiationFlux; + [KSPField(guiFormat = "F1", guiUnits = "kW", groupName = GroupName)] + public double skinSkinConductionFlux; + [KSPField(guiFormat = "F3", guiUnits = "ms", groupName = GroupName)] + public double fixedUpdateStall; + [KSPField(guiFormat = "F3", guiUnits = "ms", groupName = GroupName)] + public double updateStall; + private Transform partTransform; private MaterialColorUpdater materialColorUpdater; @@ -202,12 +257,6 @@ public void SetProjectedArea(ProjectedArea areas, Matrix4x4 vesselToWorldMatrix) { part.ShieldedFromAirstream = true; part.AddShield(shield); - if (fieldsVisible) - { - Fields["dragForce"].guiActive = false; - Fields["liftForce"].guiActive = false; - fieldsVisible = false; - } if (!(liftArrow is null)) { @@ -312,6 +361,74 @@ private void Start() stockAeroSurfaceModule = part.Modules.Contains() ? part.Modules.GetModule() : null; + AddThermalDebugFields(); + } + + private void AddThermalDebugFields() + { + ThermalFields.Clear(); + ThermalFields.Add(Fields[nameof(convergenceFactor)]); + ThermalFields.Add(Fields[nameof(skinSkinConductionMult)]); + ThermalFields.Add(Fields[nameof(skinSkinConductionFactor)]); + ThermalFields.Add(Fields[nameof(convectionArea)]); + ThermalFields.Add(Fields[nameof(radiativeArea)]); + ThermalFields.Add(Fields[nameof(skinExposedAreaFrac)]); + ThermalFields.Add(Fields[nameof(exposedSkinTemp)]); + ThermalFields.Add(Fields[nameof(unexposedSkinTemp)]); + ThermalFields.Add(Fields[nameof(partTemp)]); + ThermalFields.Add(Fields[nameof(externalTemp)]); + ThermalFields.Add(Fields[nameof(atmosphereTemp)]); + ThermalFields.Add(Fields[nameof(exposedBackgroundTemp)]); + ThermalFields.Add(Fields[nameof(convectionFlux)]); + ThermalFields.Add(Fields[nameof(finalConvCoeff)]); + ThermalFields.Add(Fields[nameof(intConductionFlux)]); + ThermalFields.Add(Fields[nameof(skinConductionFlux)]); + ThermalFields.Add(Fields[nameof(skinInternalConductionFlux)]); + ThermalFields.Add(Fields[nameof(skinSkinConductionFlux)]); + ThermalFields.Add(Fields[nameof(unexpSkinInternalConductionFlux)]); + ThermalFields.Add(Fields[nameof(radiationFlux)]); + ThermalFields.Add(Fields[nameof(unexpRadiationFlux)]); + ThermalFields.Add(Fields[nameof(fixedUpdateStall)]); + ThermalFields.Add(Fields[nameof(updateStall)]); + } + private void SetThermalFieldsVisibility(bool enabled) + { + thermalFieldsVisible = enabled; + foreach (BaseField f in ThermalFields) + f.guiActive = enabled; + } + private void UpdateThermalDebugFields() + { + convergenceFactor = PhysicsGlobals.ThermalConvergenceFactor; + skinSkinConductionMult = part.skinSkinConductionMult; + skinSkinConductionFactor = PhysicsGlobals.SkinSkinConductionFactor; + convectionArea = part.ptd.convectionArea; + radiativeArea = 1f / part.ptd.radAreaRecip; + skinExposedAreaFrac = part.skinExposedAreaFrac; + exposedSkinTemp = part.skinTemperature; + unexposedSkinTemp = part.skinUnexposedTemperature; + partTemp = part.temperature; + externalTemp = FlightIntegrator.ActiveVesselFI.externalTemperature; + atmosphereTemp = FlightIntegrator.ActiveVesselFI.atmosphericTemperature; + exposedBackgroundTemp = part.ptd.brtExposed; + //bodyArea = FlightIntegrator.ActiveVesselFI.GetBodyArea(part.ptd); + //sunArea = FlightIntegrator.ActiveVesselFI.GetSunArea(part.ptd); + convectionFlux = part.ptd.convectionFlux; + finalConvCoeff = part.ptd.finalCoeff; + intConductionFlux = part.ptd.intConductionFlux; + skinConductionFlux = part.ptd.skinConductionFlux; + skinInternalConductionFlux = part.ptd.skinInteralConductionFlux; + skinSkinConductionFlux = part.ptd.skinSkinConductionFlux; + unexpSkinInternalConductionFlux = part.ptd.unexpSkinInternalConductionFlux; + //expFlux = part.ptd.expFlux; + //unexpFlux = part.ptd.unexpFlux; + radiationFlux = part.ptd.radiationFlux; + unexpRadiationFlux = part.ptd.unexpRadiationFlux; + if (vessel.GetComponent() is VehicleOcclusion vo) + { + fixedUpdateStall = vo.metrics.data.TryGetValue(VehicleOcclusion.FixedUpdateMetric, out Utils.MetricsElement e) ? e.hysteresisTime : 0; + updateStall = vo.metrics.data.TryGetValue(VehicleOcclusion.UpdateMetric, out Utils.MetricsElement e2) ? e2.hysteresisTime : 0; + } } public double ProjectedAreaWorld(Vector3 normalizedDirectionVector) @@ -701,13 +818,12 @@ private void UpdateAeroDisplay() momentArrow = null; } } - if (PhysicsGlobals.AeroDataDisplay && !part.ShieldedFromAirstream) { if (!fieldsVisible) { - Fields["dragForce"].guiActive = true; - Fields["liftForce"].guiActive = true; + Fields[nameof(dragForce)].guiActive = true; + Fields[nameof(liftForce)].guiActive = true; fieldsVisible = true; } @@ -716,12 +832,20 @@ private void UpdateAeroDisplay() } else if (fieldsVisible) { - Fields["dragForce"].guiActive = false; - Fields["liftForce"].guiActive = false; + Fields[nameof(dragForce)].guiActive = false; + Fields[nameof(liftForce)].guiActive = false; fieldsVisible = false; } } + public override void OnUpdate() + { + if (PhysicsGlobals.ThermalDataDisplay != thermalFieldsVisible) + SetThermalFieldsVisibility(PhysicsGlobals.ThermalDataDisplay); + if (PhysicsGlobals.ThermalDataDisplay) + UpdateThermalDebugFields(); + } + public override void OnLoad(ConfigNode node) { base.OnLoad(node); diff --git a/FerramAerospaceResearch/FARAeroComponents/FARVesselAero.cs b/FerramAerospaceResearch/FARAeroComponents/FARVesselAero.cs index 3dad65340..f269c13e3 100644 --- a/FerramAerospaceResearch/FARAeroComponents/FARVesselAero.cs +++ b/FerramAerospaceResearch/FARAeroComponents/FARVesselAero.cs @@ -77,6 +77,8 @@ internal VehicleAerodynamics VehicleAero get { return _vehicleAero; } } + public VehicleOcclusion vehicleOcclusion { get; private set; } + public double Length { get { return _vehicleAero.Length; } @@ -131,6 +133,11 @@ protected override void OnStart() p.AddModule("FARAeroPartModule").OnStart(StartState()); _currentGeoModules.Add(g); } + if (Settings.OcclusionSettings.UseRaycaster) + { + vehicleOcclusion = Vessel.gameObject.AddComponent(); + vehicleOcclusion.Setup(this); + } RequestUpdateVoxel(false); @@ -184,6 +191,17 @@ private void FixedUpdate() out _currentAeroSections, out _legacyWingModels); + if (Settings.OcclusionSettings.UseRaycaster) + { + if (vehicleOcclusion is null) + { + vehicleOcclusion = Vessel.gameObject.AddComponent(); + vehicleOcclusion.Setup(this); + } + vehicleOcclusion.RequestReset = true; + vehicleOcclusion.SetVehicleBounds(_vehicleAero.VoxelCenter, _vehicleAero.VoxelMeshExtents); + } + if (_flightGUI is null) _flightGUI = vessel.GetComponent(); diff --git a/FerramAerospaceResearch/FARAeroComponents/ModularFlightIntegratorRegisterer.cs b/FerramAerospaceResearch/FARAeroComponents/ModularFlightIntegratorRegisterer.cs index 75601e14e..5b830e00a 100644 --- a/FerramAerospaceResearch/FARAeroComponents/ModularFlightIntegratorRegisterer.cs +++ b/FerramAerospaceResearch/FARAeroComponents/ModularFlightIntegratorRegisterer.cs @@ -52,6 +52,7 @@ namespace FerramAerospaceResearch.FARAeroComponents [KSPAddon(KSPAddon.Startup.SpaceCentre, true)] public class ModularFlightIntegratorRegisterer : MonoBehaviour { + public static float MIN_AREA_FRACTION = 0.01f; private void Start() { FARLogger.Info("Modular Flight Integrator function registration started"); @@ -61,10 +62,153 @@ private void Start() ModularFlightIntegrator.RegisterCalculateAreaRadiativeOverride(CalculateAreaRadiative); ModularFlightIntegrator.RegisterGetSunAreaOverride(CalculateSunArea); ModularFlightIntegrator.RegisterGetBodyAreaOverride(CalculateBodyArea); + ModularFlightIntegrator.RegisterUpdateOcclusionOverride(UpdateOcclusion); + ModularFlightIntegrator.RegisterSetSkinProperties(SetSkinProperties); + ModularFlightIntegrator.RegisterUpdateConductionOverride(UpdateConduction); FARLogger.Info("Modular Flight Integrator function registration complete"); Destroy(this); } + private void UpdateConduction(ModularFlightIntegrator fi) + { + // Improve the skinSkinTransfer calc: move the exposed fraction into the sqrt. + foreach (PartThermalData ptd in fi.partThermalDataList) + { + double frac = Math.Min(ptd.part.skinExposedAreaFrac, 1.0 - ptd.part.skinExposedAreaFrac); + double exposedArea = frac * ptd.part.radiativeArea; + ptd.skinSkinTransfer = ptd.part.skinSkinConductionMult * PhysicsGlobals.SkinSkinConductionFactor * 2.0 * Math.Sqrt(exposedArea); + } + fi.BaseFIUpdateConduction(); + } + + // This is the primary consumer of DragCube.ExposedArea, so handling this ourselves may + // remove the need for any calculation gymnastics we were doing to not interfere with stock handling. + private static void SetSkinProperties(ModularFlightIntegrator fi, PartThermalData ptd) + { + Part part = ptd.part; + + //if (occlusion == null || occlusion.state == VehicleOcclusion.State.Invalid || aeroModule == null) + if (!(fi.Vessel.GetComponent() is VehicleOcclusion occlusion && + part.Modules.GetModule() is FARAeroPartModule aeroModule && + occlusion.state != VehicleOcclusion.State.Invalid)) + { + fi.BaseFIetSkinPropertie(ptd); + return; + } + if (part.skinUnexposedTemperature < PhysicsGlobals.SpaceTemperature) + part.skinUnexposedTemperature = part.skinTemperature; + + double a = Math.Max(part.radiativeArea, 0.001); + ptd.radAreaRecip = 1.0 / Math.Max(part.radiativeArea, 0.001); + +// Vector3 localForward = (part.vessel.transform.worldToLocalMatrix * part.vessel.transform.forward); +// Vector3 localVel = part.vessel.transform.worldToLocalMatrix * fi.Vel; + double areaFraction = 0; + if (!part.ShieldedFromAirstream && part.atmDensity > 0) + { + // Stock calculation: + //ptd.convectionArea = UtilMath.Lerp(a, part.exposedArea, -PhysicsGlobals.FullConvectionAreaMin + (fi.mach - PhysicsGlobals.FullToCrossSectionLerpStart) / (PhysicsGlobals.FullToCrossSectionLerpEnd - PhysicsGlobals.FullToCrossSectionLerpStart)) * ptd.convectionAreaMultiplier; + // Note that part.exposedArea has some scaling based on the mach factor: + //float dot = Vector3.Dot(direction, faceDirection); + //float num2 = PhysicsGlobals.DragCurveValue(PhysicsGlobals.SurfaceCurves, ((dot + 1.0)/2), machNumber); + //retData.exposedArea += this.areaOccluded[index] * num2 / PhysicsGlobals.DragCurveMultiplier.Evaluate(machNumber) + // PhysicsGlobals.DragCurveValue scales its transsonic calcs by *= PhysicsGlobals.DragCubeMultiplier.Evaluate(machNumber) + // So thermal convection undoes -that- part of the scaling, but leaves: + /* + DRAG_TIP + { + key = 0 1 0 0 + key = 0.85 1.19 0.6960422 0.6960422 + key = 1.1 2.83 0.730473 0.730473 + key = 5 4 0 0 + } + Mach 1.1 - 5 has a multiplier from 2.83 to 4. Mach 5+ has a multiplier of 4. + Does this make sense for convection heating? + */ + + // VehicleOcclusion's handler might need to be more careful about how it averages values. + // Otherwise parts behind same-size shields can tend towards very small convectionArea when it should probably be 0. + ptd.convectionArea = occlusion.ConvectionArea(part, fi.Vel); + ptd.convectionCoeffMultiplier = PhysicsGlobals.SurfaceCurves.dragCurveTip.Evaluate(Convert.ToSingle(fi.mach)); + + double d = ptd.convectionArea * ptd.radAreaRecip; + areaFraction = (!double.IsNaN(d) && d > 0.001) ? d : 0; + areaFraction = Math.Min(areaFraction, 1); + if (areaFraction < MIN_AREA_FRACTION) + areaFraction = 0; + } + if (areaFraction > 0) + { + StockSkinTemperatureHandling(ptd, areaFraction); + ptd.exposed = true; + part.skinExposedAreaFrac = areaFraction; + part.skinExposedArea = areaFraction * a; // == ptd.convectionArea + } + else + { + if (ptd.exposed) + fi.UnifySkinTemp(ptd); + ptd.exposed = false; + ptd.convectionArea = part.skinExposedArea = part.skinUnexposedMassMult = 0.0; + part.skinExposedAreaFrac = part.skinExposedMassMult = 1.0; + } + ptd.convectionTempMultiplier = ptd.exposed ? 1 : 0; + } + + private static void StockSkinTemperatureHandling(PartThermalData ptd, double areaFraction) + { + if (areaFraction <= 0) + return; + Part part = ptd.part; + if (!ptd.exposed || areaFraction == 1.0) + part.skinUnexposedTemperature = part.skinTemperature; + ptd.exposed = true; + part.skinExposedMassMult = 1.0 / areaFraction; + if (areaFraction < 1.0) + part.skinUnexposedMassMult = 1.0 / (1.0 - areaFraction); + else + part.skinUnexposedMassMult = 0.0; + + // If the area fraction has changed since last calculation + if (part.skinExposedAreaFrac != areaFraction) + { + if (part.skinUnexposedTemperature != part.skinTemperature && + part.skinExposedAreaFrac > 0.0 && + part.skinExposedAreaFrac < 1.0 && + areaFraction < 1.0) + { + double unexposedAreaFrac = 1.0 - part.skinExposedAreaFrac; + double thermalEnergyInExposedSkin = part.skinTemperature * part.skinExposedAreaFrac * part.skinThermalMass; + double thermalEnergyInUnexposedSkin = part.skinUnexposedTemperature * unexposedAreaFrac * part.skinThermalMass; + double dAreaFrac = areaFraction - part.skinExposedAreaFrac; + double dThermalEnergy; + if (dAreaFrac > 0.0) + dThermalEnergy = dAreaFrac / unexposedAreaFrac * thermalEnergyInUnexposedSkin; + else + dThermalEnergy = dAreaFrac / part.skinExposedAreaFrac * thermalEnergyInExposedSkin; + part.skinTemperature = (thermalEnergyInExposedSkin + dThermalEnergy) * part.skinExposedMassMult * part.skinThermalMassRecip; + part.skinUnexposedTemperature = (thermalEnergyInUnexposedSkin - dThermalEnergy) * part.skinUnexposedMassMult * part.skinThermalMassRecip; + } + } + } + + private static void UpdateOcclusion(ModularFlightIntegrator fi, bool all) + { + if (fi.Vessel.GetComponent() is VehicleOcclusion occlusion + && occlusion.state != VehicleOcclusion.State.Invalid) + { + foreach (Part p in fi.Vessel.Parts) + { + p.ptd.bodyAreaMultiplier = 1; + p.ptd.sunAreaMultiplier = 1; + p.ptd.convectionAreaMultiplier = 1; + p.ptd.convectionTempMultiplier = 1; + } + } + else + fi.BaseFIUpdateOcclusion(all); + } + private static void UpdateThermodynamicsPre(ModularFlightIntegrator fi) { bool voxelizationCompleted = @@ -75,8 +219,7 @@ private static void UpdateThermodynamicsPre(ModularFlightIntegrator fi) { PartThermalData ptd = fi.partThermalDataList[i]; Part part = ptd.part; - FARAeroPartModule aeroModule = part.Modules.GetModule(); - if (aeroModule is null) + if (!(part.Modules.GetModule() is FARAeroPartModule aeroModule)) continue; // make sure drag cube areas are correct based on voxelization @@ -193,7 +336,7 @@ private static double CalculateAreaRadiative( FARAeroPartModule aeroModule ) { - if (aeroModule is null) + if (aeroModule is null || !FARAPI.VesselVoxelizationCompletedEver(part.vessel)) return fi.BaseFICalculateAreaRadiative(part); double radArea = aeroModule.ProjectedAreas.totalArea; @@ -208,7 +351,7 @@ private static double CalculateAreaExposed(ModularFlightIntegrator fi, Part part private static double CalculateAreaExposed(ModularFlightIntegrator fi, Part part, FARAeroPartModule aeroModule) { - if (aeroModule is null) + if (aeroModule is null || !FARAPI.VesselVoxelizationCompletedEver(part.vessel)) return fi.BaseFICalculateAreaExposed(part); // Apparently stock exposed area is actually weighted by some function of mach number... @@ -222,23 +365,30 @@ private static double CalculateAreaExposed(ModularFlightIntegrator fi, Part part private static double CalculateSunArea(ModularFlightIntegrator fi, PartThermalData ptd) { - FARAeroPartModule module = ptd.part.Modules.GetModule(); - - if (module is null) - return fi.BaseFIGetSunArea(ptd); - double sunArea = module.ProjectedAreaWorld(fi.sunVector) * ptd.sunAreaMultiplier; - + double sunArea = 0; + if (fi.Vessel.GetComponent() is VehicleOcclusion occlusion) + { + sunArea = occlusion.SunArea(ptd.part, fi.sunVector); + } + else if (ptd.part.Modules.GetModule() is FARAeroPartModule module && FARAPI.VesselVoxelizationCompletedEver(ptd.part.vessel)) + { + sunArea = module.ProjectedAreaWorld(fi.sunVector) * ptd.sunAreaMultiplier; + } return sunArea > 0 ? sunArea : fi.BaseFIGetSunArea(ptd); } private static double CalculateBodyArea(ModularFlightIntegrator fi, PartThermalData ptd) { - FARAeroPartModule module = ptd.part.Modules.GetModule(); - - if (module is null) - return fi.BaseFIBodyArea(ptd); - double bodyArea = module.ProjectedAreaWorld(-fi.Vessel.upAxis) * ptd.bodyAreaMultiplier; - + double bodyArea = 0; + Vector3 bodyVec = fi.Vessel.transform.worldToLocalMatrix * (Vector3)(fi.Vessel.mainBody.position - fi.Vessel.transform.position); + if (fi.Vessel.GetComponent() is VehicleOcclusion occlusion && occlusion.state != VehicleOcclusion.State.Invalid) + { + bodyArea = occlusion.BodyArea(ptd.part, bodyVec.normalized); + } + else if (ptd.part.Modules.GetModule() is FARAeroPartModule module && FARAPI.VesselVoxelizationCompletedEver(ptd.part.vessel)) + { + bodyArea = module.ProjectedAreaWorld(bodyVec.normalized) * ptd.bodyAreaMultiplier; + } return bodyArea > 0 ? bodyArea : fi.BaseFIBodyArea(ptd); } } diff --git a/FerramAerospaceResearch/FARAeroComponents/VehicleAerodynamics.cs b/FerramAerospaceResearch/FARAeroComponents/VehicleAerodynamics.cs index 9e7458832..39864dcac 100644 --- a/FerramAerospaceResearch/FARAeroComponents/VehicleAerodynamics.cs +++ b/FerramAerospaceResearch/FARAeroComponents/VehicleAerodynamics.cs @@ -83,6 +83,8 @@ internal class VehicleAerodynamics private Vector3d _voxelLowerRightCorner; private double _voxelElementSize; private double _sectionThickness; + public Vector3d VoxelCenter { get; private set; } + public Vector3d VoxelMeshExtents { get; private set; } private Vector3 _vehicleMainAxis; private List _vehiclePartList; @@ -399,6 +401,8 @@ private void CreateVoxel(Vessel vessel = null) _voxelLowerRightCorner = _voxel.LocalLowerRightCorner; _voxelElementSize = _voxel.ElementSize; + VoxelCenter = _voxel.Center; + VoxelMeshExtents = _voxel.MeshExtents; CalculateVesselAeroProperties(); CalculationCompleted = true; diff --git a/FerramAerospaceResearch/FARAeroComponents/VehicleOcclusion.cs b/FerramAerospaceResearch/FARAeroComponents/VehicleOcclusion.cs new file mode 100644 index 000000000..815405c5f --- /dev/null +++ b/FerramAerospaceResearch/FARAeroComponents/VehicleOcclusion.cs @@ -0,0 +1,823 @@ +using System.Collections; +using System.Collections.Generic; +using FerramAerospaceResearch.UnityJobs; +using Unity.Collections; +using Unity.Jobs; +using Unity.Mathematics; +using UnityEngine; +using UnityEngine.Jobs; +using UnityEngine.Profiling; +using FerramAerospaceResearch.Settings; + +/* Theory of Job Control: + * At beginning of FixedUpdate cycle, pre-calculate the occlusion orientations we need. + * Convection occlusion calcuation and sun occlusion calculation are separate orientations. + * Weighted-average / lerp the 3 closest orientations; this can be 6 raycasts on first execution if we missed all caches. + * On exemplar system (4-core I5-6500 3.2GHz 16GB RAM): + * <2ms at FixedUpdate.ObscenelyEarly to gather the raycast data + * Expensive raycasts can cost ~4ms (10,000 rays), cheaper ones 0.5ms + * + * */ +namespace FerramAerospaceResearch.FARAeroComponents +{ + public class VehicleOcclusion : MonoBehaviour + { + public enum State { Invalid, Initialized, Running, Completed } + public State state = State.Invalid; + public static bool PassStarted { get; private set; } = false; + public static int JobsInCurrentPass { get; private set; } = 0; + private IEnumerator resetWaitCoroutine = null; + + private FARVesselAero farVesselAero; + //private Vessel Vessel => farVesselAero?.Vessel; + public Vessel Vessel { get; private set; } + bool OnValidPhysicsVessel => farVesselAero && Vessel && (!Vessel.packed || Vessel == FlightIntegrator.ActiveVesselFI?.Vessel); + + private readonly Dictionary partsByTransform = new Dictionary(); + private readonly Dictionary partOcclusionInfo = new Dictionary(); + + // Quaternions is the full list of orientations for measurement + // processedQuaternionsMap is the map of elements in Quaternions that have been processed + // quaternionWorkList is the list of quaternions to process on this specific pass. + private NativeHashMap processedQuaternionsMap; + private NativeArray Quaternions; + + // Closest 3 convection and sun quaternions for each FixedUpdate + private readonly SphereDistanceInfo[] convectionPoints = new SphereDistanceInfo[3]; + private readonly SphereDistanceInfo[] sunPoints = new SphereDistanceInfo[3]; + private readonly SphereDistanceInfo[] bodyPoints = new SphereDistanceInfo[3]; + + private readonly DirectionPreprocessInfo[] directionPreprocessInfos = new DirectionPreprocessInfo[3]; + private readonly List directionList = new List(OcclusionSettings.MaxJobs); + private readonly List occlusionPointsList = new List(3); + public float3 DefaultAngleWeights => new float3(0.85f, 0, 0.15f); + + // VehicleVoxel center and extents + private Vector3 center = Vector3.zero; + private Vector3 extents = new Vector3(10, 10, 10); + + private int jobsInProgress; + private float averageMissOnBestAngle = 0; + + // Large long-term allocations, memory re-used in each + // Cleaned and Allocated in ResetCalculations(). Disposed in OnDestroy() + private NativeMultiHashMap indexedPriorityMap; + + private readonly NativeArray[] allCasts = new NativeArray[OcclusionSettings.MaxJobs]; + private readonly NativeArray[] allHits = new NativeArray[OcclusionSettings.MaxJobs]; + private readonly NativeMultiHashMap[] allHitsMaps = new NativeMultiHashMap[OcclusionSettings.MaxJobs]; + private readonly NativeHashMap[] allHitSizeMaps = new NativeHashMap[OcclusionSettings.MaxJobs]; + private readonly NativeArray[] allSummedHitAreas = new NativeArray[OcclusionSettings.MaxJobs]; + + // allCasts => allHits + // allHits => allHitMaps + // allHitMaps => allHitSizeMaps, + // allSummedHitAreas, + + private readonly RaycastJobInfo[] raycastJobTracker = new RaycastJobInfo[OcclusionSettings.MaxJobs]; + public bool RequestReset = false; + public Utils.Metrics metrics = new Utils.Metrics(); + public const string FixedUpdateMetric = "FixedUpdateStall"; + public const string UpdateMetric = "UpdateStall"; + private System.Diagnostics.Stopwatch JobCompleteWatch = new System.Diagnostics.Stopwatch(); + + public void Setup(FARVesselAero farVesselAero) + { + this.farVesselAero = farVesselAero; + Vessel = farVesselAero.Vessel; + } + + //Extents = axis-aligned bounding box dimensions of combined vehicle meshes + public void SetVehicleBounds(Vector3d center, Vector3d extents) + { + this.center = center; + this.extents = extents; + FARLogger.Info($"[VehicleOcclusion] {Vessel?.name} Learned Center {center} and Extents {extents}"); + } + + private void DisposeLongTermAllocations() + { + if (state != State.Invalid) + { + Quaternions.Dispose(); + processedQuaternionsMap.Dispose(); + indexedPriorityMap.Dispose(); + + foreach (var x in allCasts) + x.Dispose(); + foreach (var x in allHits) + x.Dispose(); + foreach (var x in allHitsMaps) + x.Dispose(); + foreach (var x in allSummedHitAreas) + x.Dispose(); + foreach (var x in allHitSizeMaps) + x.Dispose(); + } + } + + private void ResetCalculations() + { + FARLogger.Info($"[VehicleOcclusion] {Vessel?.name} Resetting occlusion calculation data."); + if (resetWaitCoroutine != null) + { + StopCoroutine(resetWaitCoroutine); + resetWaitCoroutine = null; + } + + partsByTransform.Clear(); + partOcclusionInfo.Clear(); + DisposeLongTermAllocations(); + + indexedPriorityMap = new NativeMultiHashMap(OcclusionSettings.FibonacciLatticeSize, Allocator.Persistent); + Quaternions = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.Persistent); + var handle = new SpherePointsJob(OcclusionSettings.FibonacciLatticeSize, Lattice_epsilon) + { + results = Quaternions, + }.Schedule(OcclusionSettings.FibonacciLatticeSize, 16); + JobHandle.ScheduleBatchedJobs(); + + processedQuaternionsMap = new NativeHashMap(OcclusionSettings.FibonacciLatticeSize, Allocator.Persistent); + int sz = OcclusionSettings.MaxRaycastDimension * OcclusionSettings.MaxRaycastDimension; + for (int i=0; i(sz, Allocator.Persistent); + allHits[i] = new NativeArray(sz, Allocator.Persistent); + allHitsMaps[i] = new NativeMultiHashMap(sz, Allocator.Persistent); + allSummedHitAreas[i] = new NativeArray(sz, Allocator.Persistent); + allHitSizeMaps[i] = new NativeHashMap(sz, Allocator.Persistent); + } + foreach (Part p in Vessel.Parts) + { + partsByTransform.Add(p.transform, p); + } + handle.Complete(); + RequestReset = false; + state = State.Initialized; + } + + public void Start() + { + FARLogger.Info($"VehicleOcclusion on {Vessel?.name} reporting startup"); + state = State.Invalid; + occlusionPointsList.Clear(); + occlusionPointsList.Add(convectionPoints); + occlusionPointsList.Add(sunPoints); + occlusionPointsList.Add(bodyPoints); + TimingManager.FixedUpdateAdd(TimingManager.TimingStage.ObscenelyEarly, FixedUpdateEarly); + TimingManager.UpdateAdd(TimingManager.TimingStage.ObscenelyEarly, UpdateEarly); + TimingManager.LateUpdateAdd(TimingManager.TimingStage.Late, LateUpdateComplete); + } + + public void OnDestroy() + { + DisposeLongTermAllocations(); + + TimingManager.FixedUpdateRemove(TimingManager.TimingStage.ObscenelyEarly, FixedUpdateEarly); + TimingManager.UpdateRemove(TimingManager.TimingStage.ObscenelyEarly, UpdateEarly); + TimingManager.LateUpdateRemove(TimingManager.TimingStage.Late, LateUpdateComplete); + } + + private void LaunchAngleJobs(ref DirectionPreprocessInfo info, in NativeArray quaternions) + { + var angleJob = new AngleBetween + { + dir = info.dir, + rotations = quaternions, + angles = info.angles, + }.Schedule(OcclusionSettings.FibonacciLatticeSize, 16); + + var statsJob = new GeneralDistanceInfo + { + distances = info.angles, + output = info.angleStats, + }.Schedule(angleJob); + + var statsCombineJob = new DotProductJob + { + A = info.angleStats, + B = info.weights, + result = info.distanceCutoff, + }.Schedule(info.angleStats.Length, 16, statsJob); + + info.sortJob = new SortedFilterAndMapByDistanceJob + { + quaternions = quaternions, + distances = info.angles, + cutoffDistance = info.distanceCutoff, + sdiList = info.sortedSDIList, + orderedQuaternions = info.orderedQuaternions, + }.Schedule(statsCombineJob); + JobHandle.ScheduleBatchedJobs(); + } + + private void LaunchRaycastJobs( + float4x4 localToWorldMatrix, + in NativeArray quaternions, + in NativeHashMap fullQuaternionIndexMap, + in NativeArray boundsArr, + in NativeArray intervalArr, + in NativeArray dimensionsArr, + in NativeArray forwardArr, + in NativeArray rightArr, + in NativeArray upArr, + out JobHandle indexMapJob, + out JobHandle vectorJob, + out JobHandle intervalJob) + { + indexMapJob = new MakeQuaternionIndexMapJob + { + arr = quaternions, + map = fullQuaternionIndexMap, + }.Schedule(); + + vectorJob = new SetVectorsJob + { + quaternions = quaternions, + localToWorldMatrix = localToWorldMatrix, + forward = forwardArr, + up = upArr, + right = rightArr, + }.Schedule(OcclusionSettings.FibonacciLatticeSize, 16); + + var setBounds = new SetBoundsJob + { + boundsCenter = center, + extents = extents, + quaternions = quaternions, + bounds = boundsArr, + }.Schedule(OcclusionSettings.FibonacciLatticeSize, 16); + + intervalJob = new SetIntervalAndDimensionsJob + { + bounds = boundsArr, + maxDim = OcclusionSettings.MaxRaycastDimension, + resolution = OcclusionSettings.RaycastResolution, + interval = intervalArr, + dims = dimensionsArr, + }.Schedule(OcclusionSettings.FibonacciLatticeSize, 16, setBounds); + + JobHandle.ScheduleBatchedJobs(); + } + + private void BuildPreprocessInfo(DirectionPreprocessInfo[] infos, List directions) + { + for (int i=0; i(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob), + angleStats = new NativeArray(1, Allocator.TempJob), + sortedSDIList = new NativeList(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob), + orderedQuaternions = new NativeList(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob), + distanceCutoff = new NativeArray(1, Allocator.TempJob), + dir = v.normalized, + weights = DefaultAngleWeights, + }; + } + } + + private int BuildAndLaunchRaycastJobs( + Vessel v, + DirectionPreprocessInfo[] dirInfos, + RaycastJobInfo[] tracker, + in NativeArray quaternions, + in NativeHashMap processedMap, + in NativeMultiHashMap priorityMap, + int suggestedJobs, + float3 startPosition, + float offset, + NativeArray[] casts, + NativeArray[] hits, + NativeMultiHashMap[] hitsMaps, + NativeHashMap[] hitSizeMaps, + NativeArray[] summedHitAreas) + { + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.Allocation_CastPlanes"); + var mapClearJob = new ClearNativeMultiHashMap { map = priorityMap }.Schedule(); + using var workList = new NativeList(OcclusionSettings.MaxJobs, Allocator.TempJob); + using var indexMap = new NativeHashMap(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob); + JobHandle lastJob = default; + for (int j = 0; j < dirInfos.Length; j++) + { + var x = new ListToIndexedMap + { + sorted = dirInfos[j].orderedQuaternions.AsDeferredJobArray(), + map = indexMap, + }.Schedule(lastJob); + lastJob = x; + } + + var bucketSortPriority = new BucketSortByPriority + { + quaternions = quaternions, + priorityMap = indexMap, + completed = processedMap, + maxIndexForPriority0 = 2, + map = priorityMap.AsParallelWriter(), + }.Schedule(OcclusionSettings.FibonacciLatticeSize, 16, JobHandle.CombineDependencies(lastJob, mapClearJob)); + JobHandle.ScheduleBatchedJobs(); + + using var fullQuaternionIndexMap = new NativeHashMap(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob); + using var boundsArr = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob); + using var intervalArr = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + using var dimensionsArr = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + using var forwardArr = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + using var rightArr = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + using var upArr = new NativeArray(OcclusionSettings.FibonacciLatticeSize, Allocator.TempJob, NativeArrayOptions.UninitializedMemory); + Profiler.EndSample(); + + // Calculate raycast plane orientation/offset, dimensions, spacing, area/ray, element count + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.JobSetup_CastPlanes"); + LaunchRaycastJobs( + v.transform.localToWorldMatrix, + in quaternions, + in fullQuaternionIndexMap, + in boundsArr, + in intervalArr, + in dimensionsArr, + in forwardArr, + in rightArr, + in upArr, + out var makeFullIndexMap, + out var setVectorsJob, + out var intervalJob); + Profiler.EndSample(); + + bucketSortPriority.Complete(); + SelectQuaternions(workList, priorityMap, OcclusionSettings.MaxJobs, 0); // Fill priority 0 (required) + SelectQuaternions(workList, priorityMap, suggestedJobs, 1); // Append priority 1 (desired) until size + SelectQuaternions(workList, priorityMap, suggestedJobs, 2); // Append priority 2 (anything) until size + + JobHandle.CompleteAll(ref setVectorsJob, ref intervalJob, ref makeFullIndexMap); + + int i = 0; + foreach (quaternion q in workList) + { + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.SingleRaycastJobSetup"); + var clearMap1 = new ClearNativeMultiHashMap { map = hitsMaps[i], }.Schedule(); + var clearMap2 = new ClearNativeHashMap { map = hitSizeMaps[i], }.Schedule(); + JobHandle.ScheduleBatchedJobs(); + + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.SingleRaycastJobSetup.Prep"); + int index = fullQuaternionIndexMap[q]; + int elements = dimensionsArr[index].x * dimensionsArr[index].y; + tracker[i] = new RaycastJobInfo + { + q = q, + index = index, + area = intervalArr[index].x * intervalArr[index].y, + }; + Profiler.EndSample(); + + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.SingleRaycastJobSetup.BuilderJob"); + tracker[i].builderJob = new OcclusionRaycastBuilder + { + startPosition = startPosition, + offset = offset, + forwardDir = forwardArr[index], + rightDir = rightArr[index], + upDir = upArr[index], + dimensions = dimensionsArr[index], + interval = intervalArr[index], + commands = casts[i], + }.Schedule(elements, 8); + Profiler.EndSample(); + + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.SingleRaycastJobSetup.RaycastJob"); + tracker[i].raycastJob = RaycastCommand.ScheduleBatch(casts[i], hits[i], 1, tracker[i].builderJob); + Profiler.EndSample(); + + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.SingleRaycastJobSetup.PreparerJob"); + tracker[i].preparerJob = new RaycastPrepareJob + { + hitsIn = hits[i], + hitsMap = hitsMaps[i].AsParallelWriter(), + }.Schedule(elements, 8, JobHandle.CombineDependencies(clearMap1, clearMap2, tracker[i].raycastJob)); + Profiler.EndSample(); + + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.SingleRaycastJobSetup.ProcessorJob"); + tracker[i].processorJob = new RaycastProcessorJob + { + hits = hits[i], + hitMap = hitsMaps[i], + hitsOut = hitSizeMaps[i].AsParallelWriter(), + area = tracker[i].area, + areaSum = summedHitAreas[i], + }.Schedule(hitsMaps[i], 16, tracker[i].preparerJob); + Profiler.EndSample(); + + JobHandle.ScheduleBatchedJobs(); + Profiler.EndSample(); + i++; + + } + return i; + } + + + private void LaunchJobs(Vessel v, DirectionPreprocessInfo[] dirInfos, List dirs, List sdiArrays, int suggestedJobs = 1) + { + Profiler.BeginSample("VehicleOcclusion-LaunchJobs"); + + float offset = extents.magnitude * 2; + float4 tmp = math.mul(v.transform.localToWorldMatrix, new float4(center.x, center.y, center.z, 1)); + float3 startPosition = new float3(tmp.x, tmp.y, tmp.z); + + Profiler.BeginSample("VehicleOcclusion-LaunchJobs.FilterJob_Setup"); + + BuildPreprocessInfo(dirInfos, dirs); + + for (int i=0; i< dirs.Count; i++) + LaunchAngleJobs(ref dirInfos[i], Quaternions); + for (int i = 0; i < dirs.Count; i++) + dirInfos[i].sortJob.Complete(); + + using NativeList requiredQuaternions = new NativeList(sdiArrays.Count * dirs.Count, Allocator.Temp); + + for (int i = 0; i < sdiArrays.Count; i++) + { + for (int j=0; j < dirs.Count; j++) + { + sdiArrays[j][i] = dirInfos[j].sortedSDIList[i]; + if (!processedQuaternionsMap.ContainsKey(dirInfos[j].sortedSDIList[i].q)) + requiredQuaternions.Add(dirInfos[j].sortedSDIList[i].q); + + } + } + + jobsInProgress = 0; + + Profiler.EndSample(); + // Build raycast jobs only if we are processing and require or will accept additional work + if (state == State.Running && (requiredQuaternions.Length > 0 || suggestedJobs > 0)) + { + jobsInProgress = BuildAndLaunchRaycastJobs( + Vessel, + dirInfos, + raycastJobTracker, + Quaternions, + processedQuaternionsMap, + indexedPriorityMap, + suggestedJobs, + startPosition, + offset, + allCasts, + allHits, + allHitsMaps, + allHitSizeMaps, + allSummedHitAreas); + } + + for (int i=0; i< dirs.Count; i++) + { + dirInfos[i].DisposeAll(); + } + Profiler.EndSample(); + } + + // On demand, return the unoccluded area of Part p in + private float AreaOnDemand(Part p, Vector3 dir, bool worldSpace = false) + { + float area = 0; + if (p && p.vessel == Vessel) + { + if (worldSpace) + dir = Vessel.transform.worldToLocalMatrix * dir; + + float offset = extents.magnitude * 2; + float4 tmp = math.mul(p.vessel.transform.localToWorldMatrix, new float4(center.x, center.y, center.z, 1)); + float3 startPosition = new float3(tmp.x, tmp.y, tmp.z); + + var infos = new DirectionPreprocessInfo[1]; + BuildPreprocessInfo(infos, new List(1) { dir.normalized }); + LaunchAngleJobs(ref infos[0], Quaternions); + infos[0].sortJob.Complete(); + + NativeList requiredQuaternions = new NativeList(3, Allocator.Temp); + + for (int i=0; i<3; i++) + { + if (!processedQuaternionsMap.ContainsKey(infos[0].sortedSDIList[i].q)) + requiredQuaternions.Add(infos[0].sortedSDIList[i].q); + } + // Raycast if necessary + if (requiredQuaternions.Length > 0) + { + using var requiredArr = new NativeArray(requiredQuaternions, Allocator.Temp); + var tracker = new RaycastJobInfo[3]; + + var casts = new NativeArray[3]; + var hits = new NativeArray[3]; + var hitsMaps = new NativeMultiHashMap[3]; + var summedHitAreas = new NativeArray[3]; + var hitSizeMaps = new NativeHashMap[3]; + using var emptyPriMap = new NativeMultiHashMap(1, Allocator.TempJob); + + jobsInProgress = BuildAndLaunchRaycastJobs( + p.vessel, + infos, + tracker, + requiredArr, + processedQuaternionsMap, + emptyPriMap, + OcclusionSettings.MaxJobs, + startPosition, + offset, + casts, + hits, + hitsMaps, + hitSizeMaps, + summedHitAreas); + + int i = 0; + foreach (quaternion q in requiredQuaternions) + { + casts[i].Dispose(); + hits[i].Dispose(); + hitsMaps[i].Dispose(); + summedHitAreas[i].Dispose(); + hitSizeMaps[i].Dispose(); + i++; + } + } + + // Raycasts are complete, we have saved the angle data. + if (partOcclusionInfo.TryGetValue(p, out DirectionalOcclusionInfo info)) + area = Area(p, convectionPoints, info.convectionArea); + + infos[0].DisposeAll(); + requiredQuaternions.Dispose(); + } + return area; + } + + public struct RaycastJobInfo + { + public quaternion q; + public int index; + public float area; + public JobHandle builderJob; + public JobHandle raycastJob; + public JobHandle preparerJob; + public JobHandle processorJob; + } + + public struct DirectionPreprocessInfo + { + public float3 dir; + public NativeArray angles; + public NativeArray angleStats; + public NativeList sortedSDIList; + public NativeList orderedQuaternions; + public NativeArray distanceCutoff; + public float3 weights; + public JobHandle sortJob; + + public void DisposeAll() + { + angles.Dispose(); + angleStats.Dispose(); + sortedSDIList.Dispose(); + orderedQuaternions.Dispose(); + distanceCutoff.Dispose(); + } + } + + private void SelectQuaternions(in NativeList q, in NativeMultiHashMap map, int maxSize, int key=0) + { + if (q.Length < maxSize && map.TryGetFirstValue(key, out int index, out NativeMultiHashMapIterator iter)) + { + q.Add(Quaternions[index]); + while (q.Length< maxSize && map.TryGetNextValue(out index, ref iter)) + { + q.Add(Quaternions[index]); + } + } + } + + public void ProcessRaycastResults(ref RaycastJobInfo jobInfo, in NativeHashMap sizeMap, in NativeArray hits, in NativeArray summedHitAreas) + { + Profiler.BeginSample("VehicleOcclusion-ProcessRaycastResults"); + quaternion rotation = jobInfo.q; + processedQuaternionsMap.TryAdd(rotation, new EMPTY_STRUCT { }); + if (sizeMap.Length > 0) + { + // The value in sizeMap is the size of a single element [duplicative of jobInfo.area] + // The key is the index into summedHitAreas + NativeArray sizeIndices = sizeMap.GetKeyArray(Allocator.Temp); + for (int key = 0; key < sizeIndices.Length; key++) + { + int index = sizeIndices[key]; + float size = summedHitAreas[index]; + if (hits[index].transform is Transform t) + { + while (t.parent != null) + t = t.parent; + if (partsByTransform.TryGetValue(t, out Part p)) + { + if (!partOcclusionInfo.TryGetValue(p, out DirectionalOcclusionInfo occlInfo)) + { + occlInfo = new DirectionalOcclusionInfo(); + partOcclusionInfo.Add(p, occlInfo); + } + if (!occlInfo.convectionArea.ContainsKey(rotation)) + { + occlInfo.convectionArea.Add(rotation, 0); + } + occlInfo.convectionArea[rotation] += size; + } + else if (t.name.Equals("localspace") || (t.gameObject.GetComponent() is Part part && Vessel != part.vessel)) + { + } + else + { + Debug.LogWarning($"[VehicleOcclusion.ProcessRaycastResults] {Vessel?.name}: {hits[index].transform} ancestor {t} not associated with any part!"); + } + } + } + sizeIndices.Dispose(); + } + Profiler.EndSample(); + } + + private void SetupDefaultDirs(List dirs, Vessel v) + { + dirs.Clear(); + Vector3 localVelocity = v.velocityD.magnitude < OcclusionSettings.VelocityThreshold ? + Vector3.forward : + (Vector3)(v.transform.worldToLocalMatrix * (Vector3)v.velocityD); + Vector3 localSunVec = v.transform.worldToLocalMatrix * (Planetarium.fetch.Sun.transform.position - v.transform.position); + Vector3 bodyVec = v.transform.worldToLocalMatrix * (Vector3)(Vessel.mainBody.position - v.transform.position); + dirs.Add(localVelocity.normalized); + dirs.Add(localSunVec.normalized); + dirs.Add(bodyVec.normalized); + } + + public void FixedUpdateEarly() + { + if (!OnValidPhysicsVessel) + return; + + // We must call this to pre-calculate the nearest quaternions for sun and convection. + // It may return an empty work queue if we are not otherwise running, which is fine. + if (state == State.Initialized) + state = State.Running; + if (state == State.Running || state == State.Completed) + { + SetupDefaultDirs(directionList, Vessel); + LaunchJobs(Vessel, directionPreprocessInfos, directionList, occlusionPointsList, 0); + if (!PassStarted) + { + PassStarted = true; + JobsInCurrentPass = 0; + } + JobsInCurrentPass += jobsInProgress; + } + } + + public void FixedUpdate() + { + PassStarted = false; + if (!OnValidPhysicsVessel) + return; + if (state == State.Running) + HandleJobCompletion(raycastJobTracker, FixedUpdateMetric); + } + + public void UpdateEarly() + { + if (!OnValidPhysicsVessel) + return; + if (state == State.Running) + { + int threadCount = System.Environment.ProcessorCount - 1; + int availableJobs = math.max(OcclusionSettings.MaxJobs - JobsInCurrentPass, 0); + int updateJobs = math.min(availableJobs, math.max(1, Settings.OcclusionSettings.JobsPerThread * threadCount)); + SetupDefaultDirs(directionList, Vessel); + LaunchJobs(Vessel, directionPreprocessInfos, directionList, occlusionPointsList, updateJobs); + if (!PassStarted) + { + PassStarted = true; + JobsInCurrentPass = 0; + } + JobsInCurrentPass += jobsInProgress; + } + } + + public void LateUpdateComplete() + { + PassStarted = false; + if (!OnValidPhysicsVessel) + return; + if (state == State.Running && jobsInProgress == 0 && processedQuaternionsMap.Length == Quaternions.Length) + state = State.Completed; + else if (state == State.Running) + HandleJobCompletion(raycastJobTracker, UpdateMetric); + else if (state == State.Completed) + { + if (resetWaitCoroutine == null) + StartCoroutine(resetWaitCoroutine = WaitForResetCR(OcclusionSettings.ResetInterval)); + } + if (RequestReset) + ResetCalculations(); + } + + IEnumerator WaitForResetCR(float interval) + { + yield return new WaitForSeconds(interval); + ResetCalculations(); + } + + public void HandleJobCompletion(RaycastJobInfo[] tracker, string metric) + { + JobCompleteWatch.Reset(); + for (int i=0; i areaInfos) + { + float area = 0; + float distanceSum = 0; + foreach (SphereDistanceInfo sdi in sdiArray) + { + if (areaInfos.TryGetValue(sdi.q, out float newArea)) + { + if (sdi.distance < float.Epsilon) + return newArea; + area += newArea / sdi.distance; + distanceSum += (1 / sdi.distance); + } + else if (processedQuaternionsMap.ContainsKey(sdi.q)) + { + // Processed quaternion but no per-part data means 0 area [no raycasters hit] + distanceSum += (1 / sdi.distance); + } + else + { + Debug.LogWarning($"[VehicleOcclusion.Area] {Vessel?.name} Quaternion {sdi.q} was in top-3 but had not been processed!"); + } + } + return area / distanceSum; + } + + private class DirectionalOcclusionInfo + { + public Dictionary convectionArea; + public Dictionary area; + + public DirectionalOcclusionInfo() + { + convectionArea = new Dictionary(); + area = new Dictionary(); + } + } + } +} diff --git a/FerramAerospaceResearch/FARPartGeometry/VehicleVoxel.cs b/FerramAerospaceResearch/FARPartGeometry/VehicleVoxel.cs index d05edf496..a3e459388 100644 --- a/FerramAerospaceResearch/FARPartGeometry/VehicleVoxel.cs +++ b/FerramAerospaceResearch/FARPartGeometry/VehicleVoxel.cs @@ -91,6 +91,8 @@ private VehicleVoxel(Vessel vessel) public double ElementSize { get; private set; } public Vector3d LocalLowerRightCorner { get; private set; } + public Vector3d Center { get; private set; } + public Vector3d MeshExtents { get; private set; } public VoxelCrossSection[] EmptyCrossSectionArray { @@ -210,6 +212,7 @@ bool solidify } Vector3d size = max - min; + MeshExtents = size; Volume = size.x * size.y * size.z; //from bounds, get voxel volume @@ -245,17 +248,17 @@ bool solidify zCellLength = zLength * 8; //this will be the distance from the center to the edges of the voxel object - var extents = new Vector3d + Vector3d extents = new Vector3d { x = xLength * 4 * ElementSize, y = yLength * 4 * ElementSize, z = zLength * 4 * ElementSize }; - Vector3d center = (max + min) * 0.5f; //Center of the vessel + Center = (max + min) * 0.5f; //Center of the vessel //This places the center of the voxel at the center of the vehicle to achieve maximum symmetry - LocalLowerRightCorner = center - extents; + LocalLowerRightCorner = Center - extents; voxelChunks = new VoxelChunk[xLength, yLength, zLength]; diff --git a/FerramAerospaceResearch/FerramAerospaceResearch.csproj b/FerramAerospaceResearch/FerramAerospaceResearch.csproj index 6ff36f8ce..6d940fe0c 100644 --- a/FerramAerospaceResearch/FerramAerospaceResearch.csproj +++ b/FerramAerospaceResearch/FerramAerospaceResearch.csproj @@ -1,4 +1,4 @@ - + Debug @@ -21,7 +21,18 @@ portable false $(SolutionDir)bin\$(Configuration)\ - DEBUG;TRACE;ASSERT;LOG_TRACE + TRACE;DEBUG;ASSERT;LOG_TRACE + prompt + 4 + true + false + + + true + portable + false + $(SolutionDir)bin\$(Configuration)\ + TRACE;DEBUG;ASSERT;LOG_TRACE;ENABLE_PROFILER prompt 4 true @@ -54,7 +65,18 @@ False ..\GameData\FerramAerospaceResearch\Plugins\Scale_Redist.dll - + + + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\System.Runtime.CompilerServices.Unsafe.dll + + + False + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\Unity.Collections.dll + + + False + $(KSP_DIR_BUILD)GameData\000_KSPBurst\Plugins\Unity.Mathematics.dll + False $(KSP_DIR_BUILD)KSP_x64_Data\Managed\UnityEngine.dll @@ -90,6 +112,7 @@ + @@ -178,6 +201,7 @@ + @@ -215,4 +239,4 @@ --> - + \ No newline at end of file diff --git a/FerramAerospaceResearch/Settings/OcclusionSettings.cs b/FerramAerospaceResearch/Settings/OcclusionSettings.cs new file mode 100644 index 000000000..c4b0c926c --- /dev/null +++ b/FerramAerospaceResearch/Settings/OcclusionSettings.cs @@ -0,0 +1,18 @@ +using FerramAerospaceResearch.Reflection; + +namespace FerramAerospaceResearch.Settings +{ + [ConfigNode("Occlusion", shouldSave: false, parent: typeof(FARConfig))] + public static class OcclusionSettings + { + [ConfigValue("useRaycaster")] public static bool UseRaycaster { get; set; } = false; + [ConfigValue("jobsPerThread")] public static int JobsPerThread { get; set; } = 3; + [ConfigValue("maxJobs")] public static int MaxJobs { get; set; } = 10; + [ConfigValue("fibonacciLatticeSize")] public static int FibonacciLatticeSize { get; set; } = 2000; + // 1000 points produces typical misses 2-3 degrees when optimized for average miss distance. + [ConfigValue("maxRaycastDimension")] public static int MaxRaycastDimension { get; set; } = 100; + [ConfigValue("raycastResolution")] public static float RaycastResolution { get; set; } = 0.1f; + [ConfigValue("resetInterval")] public static float ResetInterval { get; set; } = 180; + [ConfigValue("velocityThreshold")] public static float VelocityThreshold { get; set; } = 0.01f; + } +} diff --git a/GameData/FerramAerospaceResearch/FARConfig.cfg b/GameData/FerramAerospaceResearch/FARConfig.cfg index e869cbf1a..30c9a4910 100644 --- a/GameData/FerramAerospaceResearch/FARConfig.cfg +++ b/GameData/FerramAerospaceResearch/FARConfig.cfg @@ -392,4 +392,38 @@ FARConfig color = 0.764706, 0.760784, 0.364706 } } + + Occlusion { + // Enable this raycast-based Occlusion system + useRaycaster = false + + // Target number of occlusion jobs to schedule per CPU thread + // Lower this value to trade performance for overall time to fill the cache + // If jobs are not causing stalls (reported in the PAW), raise it to fill the cache faster + jobsPerThread = 3 + + // Global [desired] limit on number of jobs to run per Update cycle + // Lower this if jobs are causing stalls reported in the PAW + // Note this is not strict and can be exceeded if many vessels all require calculations for the current + // physics tick in flight. For instance, immediately after staging many individual fairings + // during atmospheric flight + maxJobs = 10 + + // Size of the lattice of angles to precompute + // 1000 points produces typical misses 2-3 degrees when optimized for average miss distance + fibonacciLatticeSize = 2000 + + // Limit number of raycasts per linear dimension (ie max 100 raycasts in each of X and Y) + maxRaycastDimension = 100 + + // Minimum distance between raycasts, ie maximum linear resolution + // Resolution will be sacrificed to meet maxRaycastDimension covering the entire vessel + raycastResolution = 0.1 + + // Interval in seconds after completing the cache to invalidate it and start again + resetInterval = 180 + + // Minimum velocity to determine direction of travel to prioritize computations + velocityThreshold = 0.01 + } }