using AcDream.App.Plugins; using AcDream.App.Rendering.Gpu; using AcDream.App.Rendering.Packs; using AcDream.Plugin.Abstractions.Rendering; using AcDream.UI.Abstractions.Panels.Settings; namespace AcDream.App.Tests.Rendering.Packs; public sealed class RenderPackAutoRuntimeTests { [Fact] public void AutoStartsMediumAndAtomicallyPublishesPreparedLowCandidateAtBoundary() { using var fixture = new Fixture(); fixture.Activate("auto"); FakeRuntime medium = fixture.Active; Assert.Equal("medium", medium.Preset.Id); Assert.Equal("auto", fixture.Controller.Snapshot.Selection.PresetId); fixture.ObserveOverBudget(AtmosphericAutoQualityController.DowngradeHysteresisFrames); Assert.Same(medium, fixture.Controller.ActiveRuntime); Assert.False(medium.Disposed); Assert.Equal(1, fixture.Factory.BuildCount); RenderPackActivationSnapshot changed = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1920, 1080, 2)); FakeRuntime low = fixture.Active; Assert.Equal(RenderPackActivationState.Active, changed.State); Assert.Equal("auto", changed.Selection.PresetId); Assert.Equal("low", low.Preset.Id); Assert.True(low.Prepared); Assert.True(medium.Disposed); Assert.Equal( ["build:medium", "prepare:medium:1280x720x4", "build:low", "prepare:low:1920x1080x2", "dispose:medium"], fixture.Events); Assert.Equal(0, fixture.Controller.Performance.CpuSampleCount); Assert.Equal(AtmosphericQualityLevel.Low, fixture.Controller.AutoQuality!.Value.Current); RenderPackDiagnosticsSnapshot diagnostics = fixture.Controller.CaptureDiagnostics(); Assert.Equal("auto", diagnostics.PresetId); Assert.Equal("low", diagnostics.EffectiveQuality); } [Fact] public void Auto_keeps_current_quality_live_while_replacement_prepares_off_side() { var scheduler = new ControlledPreparationScheduler(); using var fixture = new Fixture(preparationScheduler: scheduler); fixture.Controller.Request(new RenderPackSelectionSettings( "auto.test", "1.0.0", "auto")); fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); scheduler.CompleteNext(); fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); FakeRuntime medium = fixture.Active; fixture.ObserveOverBudget(AtmosphericAutoQualityController.DowngradeHysteresisFrames); RenderPackActivationSnapshot pending = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal(RenderPackActivationState.CandidatePending, pending.State); Assert.Same(medium, fixture.Controller.ActiveRuntime); Assert.False(medium.Disposed); scheduler.CompleteNext(); Assert.Same(medium, fixture.Controller.ActiveRuntime); fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal("low", fixture.Active.Preset.Id); Assert.True(medium.Disposed); } [Fact] public void WeakHostStartsAutoAtLowAndCannotPromoteIntoUnavailablePresets() { using var fixture = new Fixture(new RenderPackHostCapabilities( Enum.GetValues().ToHashSet(), 4096, 4, 64L * 1024 * 1024)); fixture.Activate("auto"); Assert.Equal("low", fixture.Active.Preset.Id); Assert.Equal(AtmosphericQualityLevel.Low, fixture.Controller.AutoQuality!.Value.Current); fixture.Active.ResolvedGpuMilliseconds = 0.01; for (int i = 0; i < AtmosphericAutoQualityController.UpgradeHysteresisFrames + 1; i++) fixture.Observe(0.01, stable: true); Assert.Equal(AtmosphericQualityLevel.Low, fixture.Controller.AutoQuality!.Value.Current); Assert.Equal(1, fixture.Factory.BuildCount); } [Fact] public void AutoFailsSafelyToRetailWhenLowPersistentlyExceedsItsDeclaredBudget() { using var fixture = new Fixture(); fixture.Activate("auto"); fixture.ObserveOverBudget(AtmosphericAutoQualityController.DowngradeHysteresisFrames); fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); FakeRuntime low = fixture.Active; Assert.Equal("low", low.Preset.Id); fixture.ObserveOverBudget( 1 + AtmosphericAutoQualityController.ChangeCooldownFrames + AtmosphericAutoQualityController.DowngradeHysteresisFrames); Assert.Same(low, fixture.Controller.ActiveRuntime); Assert.True(fixture.Controller.AutoQuality!.Value.SafeFallbackToRetailRequested); RenderPackActivationSnapshot fallback = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal(RenderPackActivationState.FailedToRetail, fallback.State); Assert.Equal(RenderPackSelectionSettings.Retail, fallback.Selection); Assert.Null(fixture.Controller.ActiveRuntime); Assert.True(low.Disposed); Assert.Contains( "Low remained over its declared performance budget for 180 stable samples", fallback.Reason, StringComparison.Ordinal); Assert.Contains("GPU p99 20.000 ms (budget 12.000 ms)", fallback.Reason); Assert.Contains("CPU p99 20.000 ms (budget 3.000 ms)", fallback.Reason); Assert.Contains("resident GPU bytes 536870912 (budget 67108864)", fallback.Reason); Assert.Null(fixture.Controller.AutoQuality); Assert.Equal(0, fixture.Controller.Performance.CpuSampleCount); } [Fact] public void HostThatCannotSupportLowFailsAutoPreciselyToRetail() { using var fixture = new Fixture(new RenderPackHostCapabilities( Enum.GetValues().ToHashSet(), 4096, 4, 32L * 1024 * 1024, MemoryPolicyDescription: "test weak-host policy")); fixture.Controller.Request(new RenderPackSelectionSettings( "auto.test", "1.0.0", "auto")); RenderPackActivationSnapshot snapshot = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal(RenderPackActivationState.FailedToRetail, snapshot.State); Assert.Null(fixture.Controller.ActiveRuntime); Assert.Contains("cannot support Low", snapshot.Reason, StringComparison.Ordinal); Assert.Contains("33554432", snapshot.Reason, StringComparison.Ordinal); Assert.Equal(0, fixture.Factory.BuildCount); } [Fact] public void AutoCandidateResourceFailureDisposesBothCandidatesAndFailsSafelyToRetail() { using var fixture = new Fixture(); fixture.Activate("auto"); FakeRuntime medium = fixture.Active; fixture.ObserveOverBudget(AtmosphericAutoQualityController.DowngradeHysteresisFrames); fixture.Factory.FailPreparePreset = "low"; RenderPackActivationSnapshot failed = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal(RenderPackActivationState.FailedToRetail, failed.State); Assert.Null(fixture.Controller.ActiveRuntime); Assert.True(medium.Disposed); FakeRuntime candidate = fixture.Factory.Runtimes[^1]; Assert.Equal("low", candidate.Preset.Id); Assert.True(candidate.Disposed); Assert.Contains("injected low resource failure", failed.Reason, StringComparison.Ordinal); Assert.Equal(0, fixture.Controller.Performance.CpuSampleCount); } [Fact] public void UnstableFramesAndExplicitPresetsNeverDriveAutomaticChanges() { using var auto = new Fixture(); auto.Activate("auto"); auto.ObserveOverBudget(1000, stable: false); Assert.Equal(AtmosphericQualityLevel.Medium, auto.Controller.AutoQuality!.Value.Current); Assert.Equal(0, auto.Controller.Performance.CpuSampleCount); Assert.Equal(1, auto.Factory.BuildCount); using var explicitHigh = new Fixture(); explicitHigh.Activate("high"); explicitHigh.ObserveOverBudget(1000); explicitHigh.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal("high", explicitHigh.Active.Preset.Id); Assert.Null(explicitHigh.Controller.AutoQuality); Assert.Equal(1, explicitHigh.Factory.BuildCount); } [Fact] public void AutomaticQualityBooleanEnablesAutoFromAnExplicitPreset() { using var fixture = new Fixture(descriptor: DescriptorWithAutomaticSetting()); fixture.Controller.Request(new RenderPackSelectionSettings( "auto.test", "1.0.0", "high") { SettingOverrides = new RenderPackSettingOverrides( new Dictionary { ["automatic-quality"] = "true" }), }); RenderPackActivationSnapshot snapshot = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal(RenderPackActivationState.Active, snapshot.State); Assert.Equal("high", fixture.Active.Preset.Id); Assert.Equal(AtmosphericQualityLevel.High, fixture.Controller.AutoQuality!.Value.Current); } [Fact] public void AutomaticQualityBooleanCanDisableTheAutomaticSelector() { using var fixture = new Fixture(descriptor: DescriptorWithAutomaticSetting()); fixture.Controller.Request(new RenderPackSelectionSettings( "auto.test", "1.0.0", "auto") { SettingOverrides = new RenderPackSettingOverrides( new Dictionary { ["automatic-quality"] = "false" }), }); RenderPackActivationSnapshot snapshot = fixture.Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.Equal(RenderPackActivationState.Active, snapshot.State); Assert.Equal("medium", fixture.Active.Preset.Id); Assert.Null(fixture.Controller.AutoQuality); } [Fact] public void ResourceGenerationChangeResetsWindowBeforeAcceptingNewLayoutSamples() { using var fixture = new Fixture(); fixture.Activate("auto"); fixture.ObserveOverBudget(5); Assert.Equal(5, fixture.Controller.Performance.CpuSampleCount); fixture.Active.ResourceGeneration++; fixture.ObserveOverBudget(1); Assert.Equal(0, fixture.Controller.Performance.CpuSampleCount); fixture.ObserveOverBudget(1); Assert.Equal(1, fixture.Controller.Performance.CpuSampleCount); } [Fact] public void DiagnosticResetStartsACompleteFreshPerformanceWindow() { using var fixture = new Fixture(); fixture.Activate("high"); fixture.ObserveOverBudget(4); Assert.Equal(4, fixture.Controller.MinimumPerformanceSampleCount); Assert.True( fixture.Controller.TryResetPerformanceEvidence(out string error), error); Assert.Equal(0, fixture.Controller.MinimumPerformanceSampleCount); fixture.ObserveOverBudget(1); Assert.Equal(1, fixture.Controller.MinimumPerformanceSampleCount); } [Fact] public void DiagnosticResetCannotPerturbAutomaticQualityEvidence() { using var fixture = new Fixture(); fixture.Activate("auto"); fixture.ObserveOverBudget(1); Assert.False( fixture.Controller.TryResetPerformanceEvidence(out string error)); Assert.Contains("explicit quality preset", error); Assert.Equal(1, fixture.Controller.MinimumPerformanceSampleCount); } [Fact] public void DiagnosticsSeparatePackAddedCpuAbsoluteReceiverCpuAndInclusiveGpu() { using var fixture = new Fixture(); fixture.Activate("high"); double[] cpu = [1, 2, 3, 4]; double[] gpu = [4, 6, 8, 10]; for (int i = 0; i < cpu.Length; i++) { fixture.Active.ResolvedGpuMilliseconds = gpu[i]; fixture.Observe(cpu[i], stable: true); } RenderPackDiagnosticsSnapshot diagnostics = fixture.Controller.CaptureDiagnostics(); Assert.Equal(4, diagnostics.Performance.CpuSampleCount); Assert.Equal(4, diagnostics.Performance.AbsoluteReceiverCpuSampleCount); Assert.Equal(4, diagnostics.Performance.GpuSampleCount); Assert.Equal(2, diagnostics.Performance.IncrementalCpuMillisecondsP50); Assert.Equal(4, diagnostics.Performance.IncrementalCpuMillisecondsP95); Assert.Equal(4, diagnostics.Performance.IncrementalCpuMillisecondsP99); Assert.Equal(0, diagnostics.Performance.AbsoluteReceiverCpuMillisecondsP50); Assert.Equal(6, diagnostics.Performance.InclusiveGpuMillisecondsP50); Assert.Equal(10, diagnostics.Performance.InclusiveGpuMillisecondsP95); Assert.Equal(10, diagnostics.Performance.InclusiveGpuMillisecondsP99); Assert.Contains( "perf=cpu-added:2.000/4.000/4.000ms,receiver-cpu-absolute:0.000/0.000/0.000ms,gpu-inclusive:6.000/10.000/10.000ms", RenderPackDiagnosticsFormatter.Format(diagnostics), StringComparison.Ordinal); } [Fact] public void AbsoluteReceiverCpuIsDiagnosticOnlyWhileGpuRemainsInclusive() { using var fixture = new Fixture(); fixture.Activate("high"); fixture.Active.ResolvedGpuMilliseconds = 4.5; fixture.Observe( cpuMilliseconds: 1.25, stable: true, receiverCpuMilliseconds: 0.75); RenderPackPerformanceSnapshot performance = fixture.Controller.Performance; Assert.Equal(1, performance.CpuSampleCount); Assert.Equal(1.25, performance.IncrementalCpuMillisecondsP50); Assert.Equal(0.75, performance.AbsoluteReceiverCpuMillisecondsP50); Assert.Equal(4.5, performance.InclusiveGpuMillisecondsP50); } [Fact] public void LargeAbsoluteReceiverCpuCannotForceAutoDownButPackAddedCpuCan() { using var receiverHeavy = new Fixture(); receiverHeavy.Activate("auto"); receiverHeavy.Active.ResolvedGpuMilliseconds = 0.1; for (int i = 0; i < AtmosphericAutoQualityController.DowngradeHysteresisFrames; i++) { receiverHeavy.Observe( cpuMilliseconds: 0.1, stable: true, receiverCpuMilliseconds: 100); } Assert.Equal( AtmosphericQualityLevel.Medium, receiverHeavy.Controller.AutoQuality!.Value.Current); Assert.Equal( 100, receiverHeavy.Controller.Performance.AbsoluteReceiverCpuMillisecondsP99); Assert.Equal( 0.1, receiverHeavy.Controller.Performance.IncrementalCpuMillisecondsP99); using var packHeavy = new Fixture(); packHeavy.Activate("auto"); packHeavy.Active.ResolvedGpuMilliseconds = 0.1; for (int i = 0; i < AtmosphericAutoQualityController.DowngradeHysteresisFrames; i++) { packHeavy.Observe( cpuMilliseconds: 10, stable: true, receiverCpuMilliseconds: 0.1); } Assert.Equal( AtmosphericQualityLevel.Low, packHeavy.Controller.AutoQuality!.Value.Current); } [Fact] public void StablePerformanceObservationAllocatesNothingAfterWarmup() { using var fixture = new Fixture(); fixture.Activate("high"); for (int i = 0; i < 128; i++) fixture.Observe(1, stable: true); long before = GC.GetAllocatedBytesForCurrentThread(); for (int i = 0; i < 128; i++) fixture.Observe(1, stable: true); long after = GC.GetAllocatedBytesForCurrentThread(); Assert.Equal(0, after - before); } private sealed class Fixture : IDisposable { private readonly BufferedRenderPackRegistry _registry = new(); private readonly IDisposable _registration; internal Fixture( RenderPackHostCapabilities? capabilities = null, RenderPackDescriptor? descriptor = null, IRenderPackPreparationScheduler? preparationScheduler = null) { Factory = new FakeFactory(Events); _registration = _registry.Register(descriptor ?? Descriptor(), new EmptyAssets()); Controller = new RenderPackController( () => RenderPackCatalog.Build( _registry.Snapshot(), capabilities ?? RenderPackHostCapabilities.Conformance), Factory, preparationScheduler: preparationScheduler ?? InlineRenderPackPreparationScheduler.Instance); } internal List Events { get; } = []; internal FakeFactory Factory { get; } internal RenderPackController Controller { get; } internal FakeRuntime Active => Assert.IsType(Controller.ActiveRuntime); internal void Activate(string preset) { Controller.Request(new RenderPackSelectionSettings("auto.test", "1.0.0", preset)); RenderPackActivationSnapshot snapshot = Controller.ApplyAtFrameBoundary( new RenderPackActivationExtent(1280, 720, 4)); Assert.True( snapshot.State == RenderPackActivationState.Active, snapshot.Reason); } internal void ObserveOverBudget(int count, bool stable = true) { Active.ResolvedGpuMilliseconds = 20; Active.RetainedGpuBytes = 512L * 1024 * 1024; for (int i = 0; i < count; i++) Observe(20, stable); } internal void Observe( double cpuMilliseconds, bool stable, double receiverCpuMilliseconds = 0d) { var observation = new RenderPackFramePerformanceObservation( cpuMilliseconds, stable, 1280, 720, 4, receiverCpuMilliseconds); Controller.ObserveActiveFrame(in observation); } public void Dispose() { Controller.Dispose(); _registration.Dispose(); _registry.Dispose(); } } private sealed class ControlledPreparationScheduler : IRenderPackPreparationScheduler { private readonly Queue<(Action Work, TaskCompletionSource Completion)> _pending = []; public Task Schedule(Action preparation) { var completion = new TaskCompletionSource( TaskCreationOptions.RunContinuationsAsynchronously); _pending.Enqueue((preparation, completion)); return completion.Task; } internal void CompleteNext() { (Action work, TaskCompletionSource completion) = _pending.Dequeue(); try { work(); completion.SetResult(); } catch (Exception error) { completion.SetException(error); } } } private sealed class FakeFactory(List events) : IRenderPackRuntimeFactory { internal int BuildCount { get; private set; } internal string? FailPreparePreset { get; set; } internal List Runtimes { get; } = []; public IRenderPackRuntime Build( RenderPackDescriptor descriptor, ValidatedRenderPackShaderAssets assets, RenderQualityPreset preset, IReadOnlyDictionary userSettingOverrides) { BuildCount++; events.Add("build:" + preset.Id); var runtime = new FakeRuntime( descriptor, preset, events, string.Equals(FailPreparePreset, preset.Id, StringComparison.Ordinal)); Runtimes.Add(runtime); return runtime; } } private sealed class FakeRuntime( RenderPackDescriptor descriptor, RenderQualityPreset preset, List events, bool failPrepare) : IAtmosphericWorldGraphRuntime, IRenderPackRuntimePerformanceSource, IRenderPackRuntimeDiagnosticsSource { public RenderPackDescriptor Descriptor { get; } = descriptor; public RenderQualityPreset Preset { get; } = preset; internal long ResourceGeneration { get; set; } internal double ResolvedGpuMilliseconds { get; set; } = 1; internal long RetainedGpuBytes { get; set; } = 32L * 1024 * 1024; internal bool Prepared { get; private set; } internal bool Disposed { get; private set; } public IGpuRenderTarget PrepareWorldTarget(int width, int height, int sampleCount) { events.Add($"prepare:{Preset.Id}:{width}x{height}x{sampleCount}"); if (failPrepare) throw new InvalidOperationException($"injected {Preset.Id} resource failure"); Prepared = true; ResourceGeneration++; return null!; } public void RenderPostProcess(IGpuFrame frame, in AtmosphericFrameInputs inputs) { } public RenderPackRuntimePerformanceMetrics CapturePerformanceMetrics() => new( ResourceGeneration, HasResolvedGpuMeasurement: true, ResolvedGpuMilliseconds, RetainedGpuBytes, TransientGpuBytes: 4L * 1024 * 1024); public RenderPackRuntimeDiagnostics CaptureDiagnostics() => RenderPackRuntimeDiagnostics.Empty(Preset.Id); public void Dispose() { if (Disposed) return; Disposed = true; events.Add("dispose:" + Preset.Id); } } private sealed class EmptyAssets : IRenderPackAssets { public Stream OpenRead(string assetKey) => Stream.Null; } private static RenderPackDescriptor Descriptor() => new( "auto.test", "Auto Test", new Version(1, 0, 0), RenderPackApi.Current, RenderPackTier.Tier1, [], [], [], [], [], [], [ Preset("low"), Preset("medium"), Preset("high"), Preset("auto") with { AutoEligible = false }, ], [], null) { FeatureSummary = "Automatic-quality test render pack.", }; private static RenderPackDescriptor DescriptorWithAutomaticSetting() { RenderPackDescriptor descriptor = Descriptor(); return descriptor with { QualityPresets = descriptor.QualityPresets.Select(preset => preset.Semantic == RenderQualitySemantic.Automatic ? preset with { SettingOverrides = [ new RenderQualitySettingOverride("automatic-quality", "true"), ], } : preset).ToArray(), Settings = [ new RenderSettingDeclaration( "automatic-quality", "Automatic quality", RenderSettingKind.Boolean, "false", null, null, null, []) { Semantic = RenderSettingSemantic.AutomaticQuality, }, ], }; } private static RenderQualityPreset Preset(string id) => new RenderQualityPreset( id, char.ToUpperInvariant(id[0]) + id[1..], [], [], [], (id switch { "low" => 64L, "high" => 256L, _ => 128L, }) * 1024 * 1024, 10, 12, 2, 3) { Semantic = id switch { "low" => RenderQualitySemantic.Low, "medium" => RenderQualitySemantic.Medium, "high" => RenderQualitySemantic.High, "auto" => RenderQualitySemantic.Automatic, _ => RenderQualitySemantic.Custom, }, }; }