acdream/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs
Erik 823936ec31 fix(streaming): preserve portal destination ownership
Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
2026-07-25 08:35:12 +02:00

169 lines
5.9 KiB
C#

using System.Globalization;
namespace AcDream.App.Streaming;
/// <summary>
/// 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.
/// </summary>
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<string, string?> 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);
/// <summary>
/// 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.
/// </summary>
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));
}
}