Features: - ConsistLOD: fast-path Bezier position update for distant cars (PositionAccuracy matched, LocationF/R synced, OnPosition fired, culler sphere kept current) - ConsistFreezer: per-consist auto-freeze replacing stock all-or-nothing optimizer - PhysicsTimer: stacked ring buffers (render, FixedUpdate, Tick, PosCars) + report - PhysicsOverlayGUI: draggable stacked-area chart, reference lines always visible, ON/OFF toggles with persistence, opacity control - Settings: JSON persistence, full UMM settings panel with auto-save - ConsoleCommands: /rpf with freeze/unfreeze/dump/timing/overlay/forceactive/lod
226 lines
8.4 KiB
C#
226 lines
8.4 KiB
C#
using System.Diagnostics;
|
||
using System.Reflection;
|
||
using System.Text;
|
||
using HarmonyLib;
|
||
using Model.Physics;
|
||
using UnityModManagerNet;
|
||
|
||
namespace RailroaderPhysicsOverhaul;
|
||
|
||
public static class PhysicsTimer
|
||
{
|
||
// --- Config ---
|
||
public const int RingSize = 300; // ~5 seconds at 60fps
|
||
const int SampleFrames = 60;
|
||
|
||
// --- ForceActive: bypass AllCarsAtRest() so we always get solver data ---
|
||
public static bool ForceActive = false;
|
||
|
||
// --- PositionCars sub-timing (dynamically patched — may not be present) ---
|
||
public static bool HasPosCarsData { get; private set; } = false;
|
||
|
||
// --- Rolling average accumulators ---
|
||
static long _tickElapsed;
|
||
static long _frameElapsed;
|
||
static long _posCarsElapsed;
|
||
static int _tickCarSum;
|
||
static int _frames;
|
||
|
||
static long _lastTickElapsed;
|
||
static long _lastFrameElapsed;
|
||
static long _lastPosCarsElapsed;
|
||
static int _lastTickCarSum;
|
||
static int _lastFrames;
|
||
|
||
// --- Render frame rolling average (recorded from LateUpdate, not FixedUpdate) ---
|
||
static float _renderMsAccum;
|
||
static int _renderFrameCount;
|
||
static float _lastAvgRenderMs;
|
||
public static float LastAvgRenderMs => _lastAvgRenderMs;
|
||
|
||
// --- Per-physics-frame ring buffers (graph) ---
|
||
public static readonly float[] RingFrame = new float[RingSize]; // total FixedUpdate ms
|
||
public static readonly float[] RingTick = new float[RingSize]; // Tick() only ms
|
||
public static readonly float[] RingPosCars = new float[RingSize]; // PositionCars ms (0 if not patched)
|
||
public static readonly float[] RingRender = new float[RingSize]; // render frame ms at time of physics tick
|
||
public static int RingWrite { get; private set; }
|
||
|
||
// Most recent render-frame time — updated each LateUpdate, sampled into RingRender each FixedUpdate.
|
||
static float _lastInstantRenderMs;
|
||
|
||
// Intra-frame accumulators — reset each FixedUpdate
|
||
static long _frameTickElapsed;
|
||
static long _framePosCarsElapsed;
|
||
static int _frameCarCount;
|
||
|
||
static readonly double TicksPerMs = 1000.0 / Stopwatch.Frequency;
|
||
|
||
// --- Recording ---
|
||
|
||
public static void RecordTick(long ticks, int cars)
|
||
{
|
||
_tickElapsed += ticks;
|
||
_frameTickElapsed += ticks;
|
||
_tickCarSum += cars;
|
||
_frameCarCount += cars;
|
||
}
|
||
|
||
public static void RecordPosCars(long ticks)
|
||
{
|
||
_posCarsElapsed += ticks;
|
||
_framePosCarsElapsed += ticks;
|
||
}
|
||
|
||
public static void RecordFrame(long ticks)
|
||
{
|
||
// Write ring entry
|
||
RingFrame[RingWrite] = (float)(ticks * TicksPerMs);
|
||
RingTick[RingWrite] = (float)(_frameTickElapsed * TicksPerMs);
|
||
RingPosCars[RingWrite] = (float)(_framePosCarsElapsed * TicksPerMs);
|
||
RingRender[RingWrite] = _lastInstantRenderMs;
|
||
RingWrite = (RingWrite + 1) % RingSize;
|
||
|
||
_frameTickElapsed = 0;
|
||
_framePosCarsElapsed = 0;
|
||
_frameCarCount = 0;
|
||
|
||
// Rolling average
|
||
_frameElapsed += ticks;
|
||
_frames++;
|
||
if (_frames < SampleFrames) return;
|
||
|
||
_lastTickElapsed = _tickElapsed;
|
||
_lastFrameElapsed = _frameElapsed;
|
||
_lastPosCarsElapsed = _posCarsElapsed;
|
||
_lastTickCarSum = _tickCarSum;
|
||
_lastFrames = _frames;
|
||
|
||
_tickElapsed = 0;
|
||
_frameElapsed = 0;
|
||
_posCarsElapsed = 0;
|
||
_tickCarSum = 0;
|
||
_frames = 0;
|
||
}
|
||
|
||
// Called from PhysicsOverlayGUI.LateUpdate() to capture render frame time.
|
||
public static void RecordRenderFrame(float deltaMs)
|
||
{
|
||
_lastInstantRenderMs = deltaMs; // sampled into RingRender on the next RecordFrame call
|
||
_renderMsAccum += deltaMs;
|
||
_renderFrameCount++;
|
||
if (_renderFrameCount < SampleFrames) return;
|
||
_lastAvgRenderMs = _renderMsAccum / _renderFrameCount;
|
||
_renderMsAccum = 0;
|
||
_renderFrameCount = 0;
|
||
}
|
||
|
||
// --- Graph scaling ---
|
||
// Returns the max stacked total (render + FixedUpdate) so the chart scales to show all layers.
|
||
public static float GetRingMax()
|
||
{
|
||
float max = 0f;
|
||
for (int i = 0; i < RingSize; i++)
|
||
{
|
||
float total = RingRender[i] + RingFrame[i];
|
||
if (total > max) max = total;
|
||
}
|
||
return max * 1.15f;
|
||
}
|
||
|
||
// --- Text report ---
|
||
public static string GetReport()
|
||
{
|
||
if (_lastFrames == 0)
|
||
return "Collecting samples...";
|
||
|
||
double frameMs = _lastFrameElapsed * TicksPerMs / _lastFrames;
|
||
double tickMs = _lastTickElapsed * TicksPerMs / _lastFrames;
|
||
double posCarsMs = _lastPosCarsElapsed * TicksPerMs / _lastFrames;
|
||
double otherMs = frameMs - tickMs;
|
||
double pct = frameMs > 0 ? tickMs / frameMs * 100.0 : 0;
|
||
|
||
var sb = new StringBuilder();
|
||
sb.AppendLine($"FixedUpdate: {frameMs:F3}ms/frame");
|
||
sb.AppendLine($" Tick(): {tickMs:F3}ms/frame ({pct:F0}% of FixedUpdate)");
|
||
|
||
int avgCars = _lastFrames > 0 ? _lastTickCarSum / _lastFrames : 0;
|
||
if (avgCars > 0)
|
||
sb.AppendLine($" per car: {tickMs / avgCars * 1000.0:F1}µs × {avgCars} cars");
|
||
|
||
if (HasPosCarsData)
|
||
{
|
||
double integrateMs = tickMs - posCarsMs;
|
||
sb.AppendLine($" PosCars: {posCarsMs:F3}ms (3D update)");
|
||
sb.AppendLine($" Integrate: {integrateMs:F3}ms (Verlet+constraints)");
|
||
}
|
||
|
||
sb.AppendLine($" Air/other: {otherMs:F3}ms/frame");
|
||
|
||
if (_lastAvgRenderMs > 0)
|
||
{
|
||
float fps = 1000f / _lastAvgRenderMs;
|
||
sb.Append($"Render: {_lastAvgRenderMs:F1}ms/frame ({fps:F0}fps)");
|
||
}
|
||
|
||
return sb.ToString();
|
||
}
|
||
|
||
// --- Dynamic patch for PositionCars ---
|
||
// Called from Main.Load() after PatchAll(). Gracefully skips if the method
|
||
// doesn't exist under this name (just logs a message and leaves RingPosCars empty).
|
||
public static void TryPatchPositionCars(Harmony harmony, UnityModManager.ModEntry.ModLogger log)
|
||
{
|
||
string[] candidates = {
|
||
"PositionCars", "UpdateCarPositions", "UpdatePositions",
|
||
"PositionAllCars", "MoveAllCars", "UpdateElementPositions"
|
||
};
|
||
foreach (string name in candidates)
|
||
{
|
||
var m = typeof(IntegrationSet).GetMethod(name,
|
||
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
||
if (m == null) continue;
|
||
|
||
harmony.Patch(m,
|
||
new HarmonyMethod(typeof(PosCarsTimerPatch), nameof(PosCarsTimerPatch.Prefix)),
|
||
new HarmonyMethod(typeof(PosCarsTimerPatch), nameof(PosCarsTimerPatch.Postfix)));
|
||
HasPosCarsData = true;
|
||
log.Log($"[RPF] Patched IntegrationSet.{name} for PositionCars timing.");
|
||
return;
|
||
}
|
||
|
||
// None of the guesses matched — dump all non-property methods so we know what to use.
|
||
log.Log("[RPF] PositionCars not found. IntegrationSet methods:");
|
||
foreach (var m in typeof(IntegrationSet).GetMethods(
|
||
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static))
|
||
{
|
||
if (m.IsSpecialName) continue; // skip property accessors
|
||
string parms = string.Join(", ",
|
||
System.Array.ConvertAll(m.GetParameters(), p => p.ParameterType.Name + " " + p.Name));
|
||
log.Log($"[RPF] {m.ReturnType.Name} {m.Name}({parms})");
|
||
}
|
||
}
|
||
}
|
||
|
||
// Static patch class used by TryPatchPositionCars — must be visible to Harmony's IL.
|
||
static class PosCarsTimerPatch
|
||
{
|
||
public static void Prefix(out long __state) => __state = Stopwatch.GetTimestamp();
|
||
public static void Postfix(long __state) =>
|
||
PhysicsTimer.RecordPosCars(Stopwatch.GetTimestamp() - __state);
|
||
}
|
||
|
||
[HarmonyPatch(typeof(IntegrationSet), "Tick")]
|
||
static class TickTimerPatch
|
||
{
|
||
static void Prefix(out long __state) => __state = Stopwatch.GetTimestamp();
|
||
static void Postfix(IntegrationSet __instance, long __state) =>
|
||
PhysicsTimer.RecordTick(Stopwatch.GetTimestamp() - __state, __instance.NumberOfCars);
|
||
}
|
||
|
||
[HarmonyPatch(typeof(TrainController), "FixedUpdate")]
|
||
static class FixedUpdateTimerPatch
|
||
{
|
||
static void Prefix(out long __state) => __state = Stopwatch.GetTimestamp();
|
||
static void Postfix(long __state) =>
|
||
PhysicsTimer.RecordFrame(Stopwatch.GetTimestamp() - __state);
|
||
}
|