namespace AcDream.App.Rendering.Packs; using AcDream.Plugin.Abstractions.Rendering; internal enum AtmosphericQualityLevel : byte { Low, Medium, High, } internal readonly record struct AtmosphericQualityMeasurement( double InclusivePackGpuMillisecondsP99, double IncrementalCpuMillisecondsP99, long ResidentGpuBytes, bool StableFrameBoundary); internal readonly record struct AtmosphericAutoQualitySnapshot( AtmosphericQualityLevel Current, int ConsecutiveOverBudgetFrames, int ConsecutiveHeadroomFrames, int CooldownFramesRemaining, long ChangeGeneration, bool SafeFallbackToRetailRequested); internal readonly record struct AtmosphericQualityBudget( double GpuMillisecondsP99, double CpuMillisecondsP99, long ResidentGpuBytes) { internal static AtmosphericQualityBudget FromPreset(RenderQualityPreset preset) => new( preset.MaxIncrementalGpuMillisecondsP99, preset.MaxIncrementalCpuMillisecondsP99, preset.MaxResidentGpuBytes); } /// /// Long-hysteresis automatic quality policy. It changes only resolution, /// cascade count/reach, and post-process sampling through one stable preset /// swap. If even Low remains over its declared budget, it requests an atomic /// whole-pack fallback to retail instead of silently dropping caster classes. /// internal sealed class AtmosphericAutoQualityController { internal const int DowngradeHysteresisFrames = 180; internal const int UpgradeHysteresisFrames = 900; internal const int ChangeCooldownFrames = 300; private AtmosphericQualityLevel _current; private readonly AtmosphericQualityLevel _minimum; private readonly AtmosphericQualityLevel _maximum; private readonly AtmosphericQualityBudget[] _budgets; private int _overBudget; private int _headroom; private int _cooldown; private long _generation; private bool _safeFallbackToRetailRequested; internal AtmosphericAutoQualityController( AtmosphericQualityLevel initial = AtmosphericQualityLevel.Medium, AtmosphericQualityLevel minimum = AtmosphericQualityLevel.Low, AtmosphericQualityLevel maximum = AtmosphericQualityLevel.High) : this(DefaultBudgets(), initial, minimum, maximum) { } internal AtmosphericAutoQualityController( IReadOnlyList budgets, AtmosphericQualityLevel initial = AtmosphericQualityLevel.Medium, AtmosphericQualityLevel minimum = AtmosphericQualityLevel.Low, AtmosphericQualityLevel maximum = AtmosphericQualityLevel.High) { ArgumentNullException.ThrowIfNull(budgets); if (budgets.Count != 3) throw new ArgumentException("Auto quality requires Low, Medium, and High budgets.", nameof(budgets)); if (minimum > initial || initial > maximum) throw new ArgumentOutOfRangeException(nameof(initial)); _budgets = budgets.ToArray(); foreach (AtmosphericQualityBudget budget in _budgets) { if (!double.IsFinite(budget.GpuMillisecondsP99) || budget.GpuMillisecondsP99 < 0d || !double.IsFinite(budget.CpuMillisecondsP99) || budget.CpuMillisecondsP99 < 0d || budget.ResidentGpuBytes < 0) { throw new ArgumentOutOfRangeException( nameof(budgets), "Automatic-quality budgets must be finite and non-negative."); } } _minimum = minimum; _maximum = maximum; _current = initial; } internal AtmosphericAutoQualitySnapshot Snapshot => new( _current, _overBudget, _headroom, _cooldown, _generation, _safeFallbackToRetailRequested); internal AtmosphericQualityBudget CurrentBudget => _budgets[(int)_current]; internal AtmosphericAutoQualitySnapshot Observe( in AtmosphericQualityMeasurement measurement) { Validate(in measurement); if (!measurement.StableFrameBoundary) return Snapshot; if (_safeFallbackToRetailRequested) return Snapshot; if (_cooldown > 0) { _cooldown--; _overBudget = 0; _headroom = 0; return Snapshot; } AtmosphericQualityBudget budget = _budgets[(int)_current]; bool over = measurement.InclusivePackGpuMillisecondsP99 > budget.GpuMillisecondsP99 || measurement.IncrementalCpuMillisecondsP99 > budget.CpuMillisecondsP99 || measurement.ResidentGpuBytes > budget.ResidentGpuBytes; if (over) { _overBudget++; _headroom = 0; if (_overBudget >= DowngradeHysteresisFrames) { if (_current != _minimum) Change((AtmosphericQualityLevel)((int)_current - 1)); else RequestSafeFallback(); } return Snapshot; } _overBudget = 0; if (_current == _maximum) { _headroom = 0; return Snapshot; } AtmosphericQualityLevel next = (AtmosphericQualityLevel)((int)_current + 1); AtmosphericQualityBudget nextBudget = _budgets[(int)next]; bool hasHeadroom = measurement.InclusivePackGpuMillisecondsP99 <= nextBudget.GpuMillisecondsP99 * 0.70 && measurement.IncrementalCpuMillisecondsP99 <= nextBudget.CpuMillisecondsP99 * 0.70 && measurement.ResidentGpuBytes <= (long)(nextBudget.ResidentGpuBytes * 0.70); if (!hasHeadroom) { _headroom = 0; return Snapshot; } _headroom++; if (_headroom >= UpgradeHysteresisFrames) Change(next); return Snapshot; } internal void Reset(AtmosphericQualityLevel level) { _current = level; _overBudget = 0; _headroom = 0; _cooldown = 0; _safeFallbackToRetailRequested = false; _generation = checked(_generation + 1); } private void Change(AtmosphericQualityLevel value) { _current = value; _overBudget = 0; _headroom = 0; _cooldown = ChangeCooldownFrames; _generation = checked(_generation + 1); } private void RequestSafeFallback() { _overBudget = DowngradeHysteresisFrames; _headroom = 0; _cooldown = 0; _safeFallbackToRetailRequested = true; _generation = checked(_generation + 1); } private static AtmosphericQualityBudget[] DefaultBudgets() => [ From(DirectionalShadowPreset.Low), From(DirectionalShadowPreset.Medium), From(DirectionalShadowPreset.High), ]; private static AtmosphericQualityBudget From(DirectionalShadowPreset preset) { DirectionalShadowQuality quality = DirectionalShadowQuality.For(preset); return new AtmosphericQualityBudget( quality.IncrementalGpuP99BudgetMilliseconds, quality.IncrementalCpuP99BudgetMilliseconds, quality.PackResidentGpuByteBudget); } private static void Validate(in AtmosphericQualityMeasurement value) { if (!double.IsFinite(value.InclusivePackGpuMillisecondsP99) || value.InclusivePackGpuMillisecondsP99 < 0 || !double.IsFinite(value.IncrementalCpuMillisecondsP99) || value.IncrementalCpuMillisecondsP99 < 0 || value.ResidentGpuBytes < 0) { throw new ArgumentOutOfRangeException( nameof(value), "Atmospheric quality measurements must be finite and non-negative."); } } }