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));
}
}