using System.Globalization; namespace AcDream.App.Streaming; /// /// Startup-time scheduling ceilings for update-thread streaming work. /// These values limit work per frame; they never reduce the amount or /// quality of content that eventually becomes resident. /// public sealed record StreamingWorkBudgetOptions( double MaxUpdateMilliseconds, int MaxCompletionAdmissions, long MaxAdoptedCpuBytes, int MaxEntityOperations, long MaxGpuUploadBytes, int MaxGlRetireOperations, float DestinationReserveFraction) { public const long MiB = 1024L * 1024L; public static StreamingWorkBudgetOptions Default { get; } = new( MaxUpdateMilliseconds: 2.0, MaxCompletionAdmissions: 64, MaxAdoptedCpuBytes: 8 * MiB, // Entity cursors are intentionally very small operations (often one // dictionary/index write). The elapsed-time ceiling remains the // authoritative CPU guard; 256 left more than 90% of that budget // unused and stretched destination publication past retail's // five-second wait-notice edge. MaxEntityOperations: 4_096, MaxGpuUploadBytes: 8 * MiB, MaxGlRetireOperations: 64, DestinationReserveFraction: 0.75f); internal static StreamingWorkBudgetOptions Parse( Func env) { ArgumentNullException.ThrowIfNull(env); StreamingWorkBudgetOptions defaults = Default; return new StreamingWorkBudgetOptions( MaxUpdateMilliseconds: ParsePositiveDouble( env("ACDREAM_STREAM_WORK_MS"), defaults.MaxUpdateMilliseconds), MaxCompletionAdmissions: ParsePositiveInt( env("ACDREAM_STREAM_WORK_COMPLETIONS"), defaults.MaxCompletionAdmissions), MaxAdoptedCpuBytes: ParseMiB( env("ACDREAM_STREAM_WORK_CPU_MIB"), defaults.MaxAdoptedCpuBytes), MaxEntityOperations: ParsePositiveInt( env("ACDREAM_STREAM_WORK_ENTITY_OPS"), defaults.MaxEntityOperations), MaxGpuUploadBytes: ParseMiB( env("ACDREAM_STREAM_WORK_GPU_MIB"), defaults.MaxGpuUploadBytes), MaxGlRetireOperations: ParsePositiveInt( env("ACDREAM_STREAM_WORK_GL_RETIRE_OPS"), defaults.MaxGlRetireOperations), DestinationReserveFraction: ParseReservePercent( env("ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT"), defaults.DestinationReserveFraction)); } public StreamingWorkBudget ToBudget() => new( TimeSpan.FromMilliseconds(MaxUpdateMilliseconds), MaxCompletionAdmissions, MaxAdoptedCpuBytes, MaxEntityOperations, MaxGpuUploadBytes, MaxGlRetireOperations, DestinationReserveFraction); /// /// Compatibility bridge for the pre-Slice-E quality setting. The former /// landblock count now selects a complete work profile instead of acting /// as a second hidden execution throttle. Four is the historical High /// profile and therefore preserves this configured profile exactly. /// public StreamingWorkBudgetOptions ScaleForLegacyCompletionCount(int count) { if (count <= 0) throw new ArgumentOutOfRangeException(nameof(count)); double scale = count / 4.0; return this with { MaxUpdateMilliseconds = Math.Max( 0.25, MaxUpdateMilliseconds * scale), MaxCompletionAdmissions = Scale(MaxCompletionAdmissions, scale), MaxAdoptedCpuBytes = Scale(MaxAdoptedCpuBytes, scale), MaxEntityOperations = Scale(MaxEntityOperations, scale), MaxGpuUploadBytes = Scale(MaxGpuUploadBytes, scale), MaxGlRetireOperations = Scale(MaxGlRetireOperations, scale), }; } private static double ParsePositiveDouble(string? value, double fallback) => double.TryParse( value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsed) && double.IsFinite(parsed) && parsed > 0 ? parsed : fallback; private static int ParsePositiveInt(string? value, int fallback) => int.TryParse( value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsed) && parsed > 0 ? parsed : fallback; private static long ParseMiB(string? value, long fallback) { if (!long.TryParse( value, NumberStyles.Integer, CultureInfo.InvariantCulture, out long mebibytes) || mebibytes <= 0 || mebibytes > long.MaxValue / MiB) { return fallback; } return checked(mebibytes * MiB); } private static float ParseReservePercent(string? value, float fallback) { if (!float.TryParse( value, NumberStyles.Float, CultureInfo.InvariantCulture, out float percent) || !float.IsFinite(percent) || percent <= 0f || percent >= 100f) { return fallback; } return percent / 100f; } private static int Scale(int value, double scale) { if (scale >= int.MaxValue / (double)value) return int.MaxValue; return Math.Max( 1, (int)Math.Round(value * scale, MidpointRounding.AwayFromZero)); } private static long Scale(long value, double scale) { if (scale >= long.MaxValue / (double)value) return long.MaxValue; return Math.Max( 1L, (long)Math.Round(value * scale, MidpointRounding.AwayFromZero)); } }