diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs b/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs index 05438b0..3b5a2b6 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/DisplaySettings.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using AcDream.UI.Abstractions.Settings; namespace AcDream.UI.Abstractions.Panels.Settings; @@ -20,7 +21,8 @@ public sealed record DisplaySettings( bool VSync, float FieldOfView, float Gamma, - bool ShowFps) + bool ShowFps, + QualityPreset Quality) { /// Values used on first launch / when settings.json is absent. /// All defaults pinned to the pre-L.0 runtime state — Resolution @@ -35,7 +37,8 @@ public sealed record DisplaySettings( VSync: false, FieldOfView: 60f, Gamma: 1.0f, - ShowFps: true); + ShowFps: true, + Quality: QualityPreset.High); /// 16:9 resolution presets offered in the dropdown. public static IReadOnlyList AvailableResolutions { get; } = new[] diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs index 11264fc..5cb20e6 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using System.Text.Json; using System.Text.Json.Nodes; +using AcDream.UI.Abstractions.Settings; namespace AcDream.UI.Abstractions.Panels.Settings; @@ -62,12 +63,13 @@ public sealed class SettingsStore var d = DisplaySettings.Default; return new DisplaySettings( - Resolution: ReadString (disp, "resolution", d.Resolution), - Fullscreen: ReadBool (disp, "fullscreen", d.Fullscreen), - VSync: ReadBool (disp, "vsync", d.VSync), - FieldOfView: ReadFloat (disp, "fieldOfView", d.FieldOfView), - Gamma: ReadFloat (disp, "gamma", d.Gamma), - ShowFps: ReadBool (disp, "showFps", d.ShowFps)); + Resolution: ReadString (disp, "resolution", d.Resolution), + Fullscreen: ReadBool (disp, "fullscreen", d.Fullscreen), + VSync: ReadBool (disp, "vsync", d.VSync), + FieldOfView: ReadFloat (disp, "fieldOfView", d.FieldOfView), + Gamma: ReadFloat (disp, "gamma", d.Gamma), + ShowFps: ReadBool (disp, "showFps", d.ShowFps), + Quality: ReadQuality (disp, "quality", d.Quality)); } catch (Exception ex) { @@ -327,6 +329,7 @@ public sealed class SettingsStore ["fieldOfView"] = d.FieldOfView, ["fullscreen"] = d.Fullscreen, ["gamma"] = d.Gamma, + ["quality"] = d.Quality.ToString(), ["resolution"] = d.Resolution, ["showFps"] = d.ShowFps, ["vsync"] = d.VSync, @@ -405,4 +408,12 @@ public sealed class SettingsStore private static float ReadFloat(JsonElement obj, string name, float fallback) => obj.TryGetProperty(name, out var el) && el.ValueKind == JsonValueKind.Number ? el.GetSingle() : fallback; + + private static QualityPreset ReadQuality(JsonElement obj, string name, QualityPreset fallback) + { + if (!obj.TryGetProperty(name, out var el) || el.ValueKind != JsonValueKind.String) + return fallback; + var s = el.GetString(); + return Enum.TryParse(s, ignoreCase: true, out var v) ? v : fallback; + } } diff --git a/src/AcDream.UI.Abstractions/Settings/QualityPreset.cs b/src/AcDream.UI.Abstractions/Settings/QualityPreset.cs new file mode 100644 index 0000000..e215d66 --- /dev/null +++ b/src/AcDream.UI.Abstractions/Settings/QualityPreset.cs @@ -0,0 +1,67 @@ +namespace AcDream.UI.Abstractions.Settings; + +/// +/// A.5 T22.5: single user-facing quality knob that drives streaming radii, +/// MSAA samples, anisotropic level, alpha-to-coverage, and max completions +/// per frame in a single setting. Individual fields can still be overridden +/// by env vars (see ). +/// +public enum QualityPreset { Low, Medium, High, Ultra } + +/// +/// Resolved per-preset quality parameters. Constructed via +/// then optionally overridden with +/// before applying to the +/// renderer and streaming controller. +/// +public readonly record struct QualitySettings( + int NearRadius, + int FarRadius, + int MsaaSamples, // 0 = off, 2, 4, 8 + int AnisotropicLevel, // 1 = off, 4, 8, 16 + bool AlphaToCoverage, + int MaxCompletionsPerFrame) +{ + /// + /// Return the default for . + /// Unknown enum values fall back to . + /// + public static QualitySettings From(QualityPreset preset) => preset switch + { + QualityPreset.Low => new(NearRadius: 2, FarRadius: 5, MsaaSamples: 0, AnisotropicLevel: 4, AlphaToCoverage: false, MaxCompletionsPerFrame: 2), + QualityPreset.Medium => new(NearRadius: 3, FarRadius: 8, MsaaSamples: 2, AnisotropicLevel: 8, AlphaToCoverage: false, MaxCompletionsPerFrame: 3), + QualityPreset.High => new(NearRadius: 4, FarRadius: 12, MsaaSamples: 4, AnisotropicLevel: 16, AlphaToCoverage: true, MaxCompletionsPerFrame: 4), + QualityPreset.Ultra => new(NearRadius: 5, FarRadius: 15, MsaaSamples: 4, AnisotropicLevel: 16, AlphaToCoverage: true, MaxCompletionsPerFrame: 6), + _ => From(QualityPreset.High), + }; + + /// + /// Apply env-var overrides to a preset's resolved settings. Per-field + /// env vars beat the preset (so devs can spot-test a single dimension). + /// Unset or empty env vars leave the preset default unchanged. + /// + public static QualitySettings WithEnvOverrides(QualitySettings baseSettings) + { + int nearRadius = TryParseEnvInt("ACDREAM_NEAR_RADIUS", baseSettings.NearRadius); + int farRadius = TryParseEnvInt("ACDREAM_FAR_RADIUS", baseSettings.FarRadius); + int msaa = TryParseEnvInt("ACDREAM_MSAA_SAMPLES", baseSettings.MsaaSamples); + int aniso = TryParseEnvInt("ACDREAM_ANISOTROPIC", baseSettings.AnisotropicLevel); + // Bool override: any non-empty value other than "0"/"false" enables A2C. + // Empty / unset → keep preset default. + var a2cEnv = System.Environment.GetEnvironmentVariable("ACDREAM_A2C"); + bool a2c = a2cEnv switch + { + null or "" => baseSettings.AlphaToCoverage, + "0" or "false" or "False" or "FALSE" => false, + _ => true, + }; + int completions = TryParseEnvInt("ACDREAM_MAX_COMPLETIONS_PER_FRAME", baseSettings.MaxCompletionsPerFrame); + return new QualitySettings(nearRadius, farRadius, msaa, aniso, a2c, completions); + } + + private static int TryParseEnvInt(string name, int defaultValue) + { + var s = System.Environment.GetEnvironmentVariable(name); + return s is not null && int.TryParse(s, out var v) ? v : defaultValue; + } +} diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/QualityPresetTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/QualityPresetTests.cs new file mode 100644 index 0000000..754cba9 --- /dev/null +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/QualityPresetTests.cs @@ -0,0 +1,181 @@ +using AcDream.UI.Abstractions.Settings; +using Xunit; + +namespace AcDream.UI.Abstractions.Tests.Panels.Settings; + +/// +/// A.5 T22.5: preset table + env-var override +/// coverage. Env-var tests clear their variables in finally blocks so +/// parallel runners cannot bleed state between tests. +/// +public class QualityPresetTests +{ + [Theory] + [InlineData(QualityPreset.Low, 2, 5, 0)] + [InlineData(QualityPreset.Medium, 3, 8, 2)] + [InlineData(QualityPreset.High, 4, 12, 4)] + [InlineData(QualityPreset.Ultra, 5, 15, 4)] + public void From_Preset_ProducesExpectedRadiiAndMsaa( + QualityPreset preset, int n1, int n2, int msaa) + { + var s = QualitySettings.From(preset); + Assert.Equal(n1, s.NearRadius); + Assert.Equal(n2, s.FarRadius); + Assert.Equal(msaa, s.MsaaSamples); + } + + [Theory] + [InlineData(QualityPreset.Low, 4, false)] + [InlineData(QualityPreset.Medium, 8, false)] + [InlineData(QualityPreset.High, 16, true)] + [InlineData(QualityPreset.Ultra, 16, true)] + public void From_Preset_ProducesExpectedAnisoAndA2C( + QualityPreset preset, int aniso, bool a2c) + { + var s = QualitySettings.From(preset); + Assert.Equal(aniso, s.AnisotropicLevel); + Assert.Equal(a2c, s.AlphaToCoverage); + } + + [Theory] + [InlineData(QualityPreset.Low, 2)] + [InlineData(QualityPreset.Medium, 3)] + [InlineData(QualityPreset.High, 4)] + [InlineData(QualityPreset.Ultra, 6)] + public void From_Preset_ProducesExpectedMaxCompletions( + QualityPreset preset, int expected) + { + var s = QualitySettings.From(preset); + Assert.Equal(expected, s.MaxCompletionsPerFrame); + } + + [Fact] + public void EnvVar_NearRadius_OverridesPreset() + { + System.Environment.SetEnvironmentVariable("ACDREAM_NEAR_RADIUS", "2"); + try + { + var s = QualitySettings.From(QualityPreset.High); // High = NearRadius=4 normally + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.Equal(2, resolved.NearRadius); + Assert.Equal(12, resolved.FarRadius); // FarRadius unaffected + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_NEAR_RADIUS", null); } + } + + [Fact] + public void EnvVar_FarRadius_OverridesPreset() + { + System.Environment.SetEnvironmentVariable("ACDREAM_FAR_RADIUS", "20"); + try + { + var s = QualitySettings.From(QualityPreset.High); + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.Equal(4, resolved.NearRadius); // NearRadius unaffected + Assert.Equal(20, resolved.FarRadius); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_FAR_RADIUS", null); } + } + + [Fact] + public void EnvVar_AlphaToCoverage_BooleanParsing() + { + // Ensure "0" and "false" disable; other values enable. + System.Environment.SetEnvironmentVariable("ACDREAM_A2C", "0"); + try + { + var s = QualitySettings.From(QualityPreset.High); // High has A2C=true + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.False(resolved.AlphaToCoverage); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_A2C", null); } + } + + [Fact] + public void EnvVar_AlphaToCoverage_FalseString_Disables() + { + System.Environment.SetEnvironmentVariable("ACDREAM_A2C", "false"); + try + { + var s = QualitySettings.From(QualityPreset.High); + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.False(resolved.AlphaToCoverage); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_A2C", null); } + } + + [Fact] + public void EnvVar_AlphaToCoverage_NonZeroEnables() + { + System.Environment.SetEnvironmentVariable("ACDREAM_A2C", "1"); + try + { + var s = QualitySettings.From(QualityPreset.Low); // Low has A2C=false + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.True(resolved.AlphaToCoverage); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_A2C", null); } + } + + [Fact] + public void EnvVar_Unset_LeavesPresetDefault() + { + // Ensure no env vars are set for this test's fields. + System.Environment.SetEnvironmentVariable("ACDREAM_NEAR_RADIUS", null); + System.Environment.SetEnvironmentVariable("ACDREAM_FAR_RADIUS", null); + System.Environment.SetEnvironmentVariable("ACDREAM_A2C", null); + + var s = QualitySettings.From(QualityPreset.High); + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.Equal(s, resolved); + } + + [Fact] + public void From_UndefinedPreset_FallsBackToHigh() + { + var s = QualitySettings.From((QualityPreset)99); + Assert.Equal(4, s.NearRadius); // High default + Assert.Equal(12, s.FarRadius); + Assert.Equal(4, s.MsaaSamples); + Assert.True(s.AlphaToCoverage); + } + + [Fact] + public void EnvVar_MaxCompletionsPerFrame_OverridesPreset() + { + System.Environment.SetEnvironmentVariable("ACDREAM_MAX_COMPLETIONS_PER_FRAME", "8"); + try + { + var s = QualitySettings.From(QualityPreset.High); // High = 4 + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.Equal(8, resolved.MaxCompletionsPerFrame); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_MAX_COMPLETIONS_PER_FRAME", null); } + } + + [Fact] + public void EnvVar_MsaaSamples_OverridesPreset() + { + System.Environment.SetEnvironmentVariable("ACDREAM_MSAA_SAMPLES", "8"); + try + { + var s = QualitySettings.From(QualityPreset.High); // High = 4 + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.Equal(8, resolved.MsaaSamples); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_MSAA_SAMPLES", null); } + } + + [Fact] + public void EnvVar_Anisotropic_OverridesPreset() + { + System.Environment.SetEnvironmentVariable("ACDREAM_ANISOTROPIC", "4"); + try + { + var s = QualitySettings.From(QualityPreset.High); // High = 16 + var resolved = QualitySettings.WithEnvOverrides(s); + Assert.Equal(4, resolved.AnisotropicLevel); + } + finally { System.Environment.SetEnvironmentVariable("ACDREAM_ANISOTROPIC", null); } + } +} diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs index edc24b2..b54d0f0 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs @@ -44,7 +44,8 @@ public sealed class SettingsStoreTests : System.IDisposable VSync: false, FieldOfView: 100f, Gamma: 1.4f, - ShowFps: true); + ShowFps: true, + Quality: AcDream.UI.Abstractions.Settings.QualityPreset.Ultra); store.SaveDisplay(original); var loaded = store.LoadDisplay();