From a7c9cdb97f4afab87245644abc06655f8d623b62 Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 1 Nov 2026 20:54:01 +0100 Subject: [PATCH] Take the rest of feat/opengl-vulkan's settings What the merge left out, now on this branch's own six levels: - GraphicsRules, which say what a renderer or an anti-aliasing mode rules out, and the notes that show it - the settings split into Graphics and Effects tabs - the DLSS library check beside the exe - eu07.ini import and export - one list of renderers: the backend follows from the renderer chosen The post-processing part of the levels is gone, as three switches cannot tell six levels apart. Bloom goes with the whole level. Still a transitional variant. The old renderers, their keys and their rows stay on the page, as the exe a player runs may be an old one or may have no Vulkan. That branch's five-level list and its removal of the old renderers stay out. --- StarterNG.Tests/GraphicsPresetTests.cs | 115 ++-- StarterNG.Tests/GraphicsSettingsTests.cs | 593 ++++++++++++++++++ StarterNG.Tests/SettingsStoreTests.cs | 42 +- StarterNG/Application/AppServices.cs | 2 +- StarterNG/Application/SettingsStore.cs | 95 ++- StarterNG/Classes/ConfigFile.cs | 3 + StarterNG/Domain/Settings/GraphicsPreset.cs | 244 ++++--- StarterNG/Domain/Settings/GraphicsRules.cs | 126 ++++ StarterNG/Domain/Settings/SettingReach.cs | 2 +- StarterNG/Domain/Settings/SimulatorKeys.cs | 53 +- .../Domain/Settings/SimulatorSettings.cs | 56 +- StarterNG/Domain/Settings/Steps.cs | 7 - .../Settings/ExecutableLocator.cs | 27 + .../Settings/SettingsSerializer.cs | 207 +++++- StarterNG/Styles/Settings.axaml | 15 + StarterNG/Views/SettingTexts.cs | 47 +- StarterNG/Views/Settings.Graphics.cs | 395 ++++++++++++ StarterNG/Views/Settings.axaml | 393 +++++++++--- StarterNG/Views/Settings.axaml.cs | 26 +- StarterNG/Views/SimulatorKeyRows.cs | 23 - StarterNG/startercfg/lang/cz.xml | 158 ++--- StarterNG/startercfg/lang/en.xml | 203 +++--- StarterNG/startercfg/lang/hu.xml | 158 ++--- StarterNG/startercfg/lang/pl.xml | 203 +++--- StarterNG/startercfg/lang/ru.xml | 158 ++--- 25 files changed, 2513 insertions(+), 838 deletions(-) create mode 100644 StarterNG.Tests/GraphicsSettingsTests.cs create mode 100644 StarterNG/Domain/Settings/GraphicsRules.cs create mode 100644 StarterNG/Views/Settings.Graphics.cs diff --git a/StarterNG.Tests/GraphicsPresetTests.cs b/StarterNG.Tests/GraphicsPresetTests.cs index 4331268..fa55f64 100644 --- a/StarterNG.Tests/GraphicsPresetTests.cs +++ b/StarterNG.Tests/GraphicsPresetTests.cs @@ -26,6 +26,8 @@ public class GraphicsPresetTests var store = new TestInstallation().SettingsStore; var low = Defaults().Copy(); var top = Defaults().Copy(); + // Ray tracing is one of them, and only Vulkan has it. + low.RenderEngine = top.RenderEngine = SimulatorSettings.VulkanEngine; (GraphicsPreset.Minimal with { Extras = null }).ApplyTo(low); (GraphicsPreset.Ultra with { Extras = null, Top = null }).ApplyTo(top); @@ -33,7 +35,7 @@ public class GraphicsPresetTests var topKeys = store.Render(top); var differing = lowKeys.Keys.Union(topKeys.Keys) .Where(key => lowKeys.GetValueOrDefault(key) != topKeys.GetValueOrDefault(key)) - .Where(key => !key.StartsWith("gfx.framebuffer", StringComparison.Ordinal) && key != "gfx.buffer.percent"); + .Where(key => !key.StartsWith("gfx.framebuffer", StringComparison.Ordinal)); Assert.Equal(GraphicsPreset.Keys.OrderBy(k => k), differing.OrderBy(k => k)); } @@ -57,7 +59,7 @@ public class GraphicsPresetTests Climbs(p => p.Anisotropy, 1); // Four steps or three for six levels. - Climbs(p => p.Multisampling, 2); + Climbs(p => (int)p.AntiAliasing, 2); Climbs(p => p.SmokeFidelity, 2); Climbs(p => p.SplineFidelity, 2); Climbs(p => p.ReflectionsFidelity, 3); @@ -153,13 +155,7 @@ public class GraphicsPresetTests [Fact] public void The_new_renderers_scales_never_go_down() { - void WordsClimb(string name, Func word) - { - var words = SimulatorKeys.Named(name).Words!; - var places = GraphicsPreset.All.Select(p => Array.IndexOf(words, word(p.NewRenderer))).ToList(); - Assert.DoesNotContain(-1, places); - Assert.Equal(places.OrderBy(place => place), places); - } + void NeverDown(Func value) => Climbs(p => value(p.NewRenderer), 5); void TurnsOn(Func on) { @@ -167,27 +163,27 @@ public class GraphicsPresetTests Assert.Equal(values.OrderBy(value => value), values); } - WordsClimb("gfx.lights.shadows", r => r.LampShadows); - WordsClimb("gfx.contactshadows", r => r.ContactShadows); - WordsClimb("gfx.postfx.ao", r => r.Occlusion); - WordsClimb("gfx.postfx.ao.quality", r => r.OcclusionQuality); - WordsClimb("gfx.cabprobe", r => r.CabProbe); - WordsClimb("gfx.ssr", r => r.ScreenReflections); - WordsClimb("gfx.fog", r => r.Fog); - WordsClimb("gfx.fog.quality", r => r.FogQuality); - WordsClimb("gfx.clouds", r => r.Clouds); + NeverDown(r => r.LampShadows); + NeverDown(r => r.ContactShadows); + NeverDown(r => r.Occlusion); + NeverDown(r => r.OcclusionSlices); + NeverDown(r => r.OcclusionSteps); + NeverDown(r => r.CabProbe); + NeverDown(r => r.ScreenReflections); + NeverDown(r => r.FogQuality); + NeverDown(r => r.Clouds); TurnsOn(r => r.OcclusionLighting); TurnsOn(r => r.CabProbeReflections); TurnsOn(r => r.RayTracing); + TurnsOn(r => r.VolumetricFog); TurnsOn(r => r.CloudShadows); TurnsOn(r => r.Grass); TurnsOn(r => r.Bloom); Climbs(p => p.NewRenderer.GrassDistance, 0); - Climbs(p => p.NewRenderer.GrassFade, 0); Climbs(p => p.NewRenderer.GrassDensity, 0); - Climbs(p => p.NewRenderer.Precipitation, 0); + Climbs(p => p.NewRenderer.Precipitation, 1); // Frames between refreshes, so the dearer the fewer. Climbs(p => -p.NewRenderer.CabProbeInterval, 2); } @@ -196,28 +192,32 @@ public class GraphicsPresetTests public void Ultra_is_the_top_of_the_new_renderers_scales() { var ultra = GraphicsPreset.Ultra.NewRenderer; - double Top(string key) => SimulatorKeys.Named(key).Maximum; - string TopWord(string key) => SimulatorKeys.Named(key).Words!.Last(); - Assert.Equal(TopWord("gfx.lights.shadows"), ultra.LampShadows); - Assert.Equal(TopWord("gfx.contactshadows"), ultra.ContactShadows); - Assert.Equal(TopWord("gfx.postfx.ao"), ultra.Occlusion); - Assert.Equal(TopWord("gfx.postfx.ao.quality"), ultra.OcclusionQuality); - Assert.Equal(TopWord("gfx.cabprobe"), ultra.CabProbe); - Assert.Equal(TopWord("gfx.ssr"), ultra.ScreenReflections); - Assert.Equal(TopWord("gfx.fog"), ultra.Fog); - Assert.Equal(TopWord("gfx.fog.quality"), ultra.FogQuality); - Assert.Equal(TopWord("gfx.clouds"), ultra.Clouds); - Assert.Equal(Top("gfx.grass.distance"), ultra.GrassDistance); - Assert.Equal(Top("gfx.grass.fadedistance"), ultra.GrassFade); - Assert.Equal(Top("gfx.grass.density"), ultra.GrassDensity); - Assert.Equal(Top("gfx.precipitation.particles"), ultra.Precipitation); + Assert.Equal(3, ultra.LampShadows); + Assert.Equal(3, ultra.ContactShadows); + Assert.Equal(2, ultra.Occlusion); + Assert.Equal((6, 8), (ultra.OcclusionSlices, ultra.OcclusionSteps)); + Assert.Equal(3, ultra.CabProbe); + Assert.Equal(3, ultra.ScreenReflections); + Assert.True(ultra.RayTracing && ultra.VolumetricFog); + Assert.Equal(4, ultra.FogQuality); + Assert.Equal(4, ultra.Clouds); + Assert.Equal(300, ultra.GrassDistance); + Assert.Equal(2.0, ultra.GrassDensity); + Assert.Equal(262144, ultra.Precipitation); } [Fact] public void Grass_fades_out_beyond_where_it_is_drawn_in_full() { - Assert.All(GraphicsPreset.All, p => Assert.True(p.NewRenderer.GrassFade > p.NewRenderer.GrassDistance, p.NameKey)); + var store = new TestInstallation().SettingsStore; + foreach (var preset in GraphicsPreset.All) + { + var settings = Defaults().Copy(); + preset.ApplyTo(settings); + + Assert.True(int.Parse(store.Render(settings)["gfx.grass.fadedistance"]) > preset.NewRenderer.GrassDistance, preset.NameKey); + } } [Fact] @@ -227,7 +227,7 @@ public class GraphicsPresetTests GraphicsPreset.Medium.ApplyTo(settings); Assert.Same(GraphicsPreset.Medium, GraphicsPreset.Matching(settings)); - settings.Extra["gfx.fog.quality"] = "ultra"; + settings.FogQuality = 4; Assert.Null(GraphicsPreset.Matching(settings)); } @@ -236,18 +236,19 @@ public class GraphicsPresetTests public void What_is_left_to_the_player_stays_through_a_level() { var settings = Defaults().Copy(); - settings.Extra["gfx.antialiasing"] = "taa"; - settings.Extra["gfx.framegeneration"] = "x2"; - settings.Extra["gfx.vulkan.device"] = "RTX"; + settings.RenderEngine = SimulatorSettings.VulkanEngine; + settings.AntiAliasing = AntiAliasingMode.Dlss; + settings.FrameGeneration = 2; + settings.VulkanDevice = "RTX"; foreach (var preset in GraphicsPreset.All) { preset.ApplyTo(settings); Assert.Same(preset, GraphicsPreset.Matching(settings)); - Assert.Equal("taa", settings.Extra["gfx.antialiasing"]); - Assert.Equal("x2", settings.Extra["gfx.framegeneration"]); - Assert.Equal("RTX", settings.Extra["gfx.vulkan.device"]); + Assert.Equal(AntiAliasingMode.Dlss, settings.AntiAliasing); + Assert.Equal(2, settings.FrameGeneration); + Assert.Equal("RTX", settings.VulkanDevice); } } @@ -262,7 +263,7 @@ public class GraphicsPresetTests Assert.Equal(8192, ultra.ShadowMap); Assert.Equal(600, ultra.ShadowRange); Assert.Equal(Steps.Anisotropy.Max(), ultra.Anisotropy); - Assert.Equal(Steps.MultisamplingLevels.Max(), ultra.Multisampling); + Assert.Equal(AntiAliasingMode.Msaa8, ultra.AntiAliasing); Assert.Equal(Steps.ReflectionUpdates.Max(), ultra.ReflectionsRate); Assert.Equal(4, ultra.SmokeFidelity); Assert.Equal(4, ultra.SplineFidelity); @@ -610,13 +611,12 @@ public class GraphicsPresetTests OnSteps(preset.SleeperDistance, GraphicsPreset.SleeperKey); OnSteps(preset.ReflectedInstances, GraphicsPreset.ReflectedInstancesKey); OnSteps(preset.ReflectedVehicles, GraphicsPreset.ReflectedVehiclesKey); - OnSteps(preset.NewRenderer.CabProbeInterval, "gfx.cabprobe.interval"); - OnSteps(preset.NewRenderer.GrassDistance, "gfx.grass.distance"); - OnSteps(preset.NewRenderer.GrassFade, "gfx.grass.fadedistance"); - OnSteps(preset.NewRenderer.GrassDensity, "gfx.grass.density"); - OnSteps(preset.NewRenderer.Precipitation, "gfx.precipitation.particles"); + Assert.InRange(preset.NewRenderer.CabProbeInterval, 1, 120); + Assert.InRange(preset.NewRenderer.GrassDistance, 20, 300); + Assert.Equal(0, preset.NewRenderer.GrassDistance % 10); + Assert.Equal(0, preset.NewRenderer.GrassDensity % 0.25); + Assert.Contains(preset.NewRenderer.Precipitation, new[] { 16384, 32768, 65536, 131072, 262144 }); Assert.True(Steps.IndexOf(Steps.Anisotropy, preset.Anisotropy) >= 0, preset.NameKey); - Assert.True(Steps.IndexOf(Steps.MultisamplingLevels, preset.Multisampling) >= 0, preset.NameKey); Assert.True(Steps.IndexOf(Steps.ReflectionUpdates, preset.ReflectionsRate) >= 0, preset.NameKey); Assert.True(Steps.IndexOf(Steps.DrawRangeFactors, preset.DrawRangeFactor) >= 0, preset.NameKey); foreach (int size in new[] { preset.TextureSize, preset.CabTextureSize, preset.ShadowMap }) @@ -631,13 +631,14 @@ public class GraphicsPresetTests private static GraphicsGroupLevels Groups() => new(new TestInstallation().SettingsStore); [Fact] - public void Every_key_a_level_sets_but_the_buffer_belongs_to_one_part() + public void Every_key_a_level_sets_but_those_of_the_whole_belongs_to_one_part() { var inGroups = GraphicsGroup.All.SelectMany(group => group.Keys).ToList(); Assert.Equal(inGroups.Distinct(), inGroups); Assert.Empty(inGroups.Except(GraphicsPreset.Keys).Except(GraphicsExtras.Keys).Except(GraphicsTop.Keys)); - Assert.Equal(new[] { "starter.bufferscale" }, GraphicsPreset.Keys.Except(inGroups)); + Assert.Equal(new[] { "gfx.antialiasing", "gfx.buffer.percent", "gfx.extraeffects", "gfx.postfx.bloom", "multisampling", "starter.bufferscale" }, + GraphicsPreset.Keys.Except(inGroups).OrderBy(key => key, StringComparer.Ordinal)); } [Fact] @@ -683,7 +684,7 @@ public class GraphicsPresetTests var levels = Groups().LevelsOf(settings); - Assert.Null(levels[GraphicsGroup.Effects]); + Assert.Null(levels[GraphicsGroup.Lights]); Assert.Same(GraphicsPreset.High, levels[GraphicsGroup.Shadows]); } @@ -697,6 +698,7 @@ public class GraphicsPresetTests foreach (var group in GraphicsGroup.All) settings = groups.Apply(group, level, settings); settings.BufferScalePercent = level.BufferScale; + settings.AntiAliasing = level.AntiAliasing; Assert.Same(level, GraphicsPreset.Matching(settings)); } @@ -732,9 +734,10 @@ public class GraphicsPresetTests var settings = AtHigh(); settings.MotionBlur = false; - var moved = Groups().Apply(GraphicsGroup.Effects, GraphicsPreset.VeryHigh, settings); + settings.Smoke = false; - Assert.False(moved.MotionBlur); - Assert.Equal(GraphicsPreset.VeryHigh.SmokeFidelity, moved.SmokeFidelity); + var nature = Groups().Apply(GraphicsGroup.Nature, GraphicsPreset.VeryHigh, settings); + Assert.False(nature.Smoke); + Assert.Equal(GraphicsPreset.VeryHigh.SmokeFidelity, nature.SmokeFidelity); } } diff --git a/StarterNG.Tests/GraphicsSettingsTests.cs b/StarterNG.Tests/GraphicsSettingsTests.cs new file mode 100644 index 0000000..9ecfcf6 --- /dev/null +++ b/StarterNG.Tests/GraphicsSettingsTests.cs @@ -0,0 +1,593 @@ +using StarterNG.Classes; +using StarterNG.Domain.Settings; +using StarterNG.Infrastructure.Settings; +using StarterNG.Tests.Fakes; + +namespace StarterNG.Tests; + +public class GraphicsSettingsTests +{ + private static readonly string UserIni = Path.Combine("/home/kolejarz", ".config", "MaSzyna", "eu07.ini"); + + private static TestInstallation WithIni(string text) + { + var installation = new TestInstallation(new InMemoryFileSystem().WithFile(UserIni, text)); + installation.Environment.With("HOME", "/home/kolejarz"); + installation.SettingsStore.Load(); + return installation; + } + + private static SimulatorSettings Read(string text, bool dlssPlatform = true) + { + var s = new SimulatorSettings(); + new SettingsSerializer(dlssPlatform).Read(s, ConfigFile.Parse(text)); + return s; + } + + private static string Write(SimulatorSettings s, string text = "", bool dlssPlatform = true) + { + var c = ConfigFile.Parse(text); + new SettingsSerializer(dlssPlatform).Write(s, c); + return c.ToText(); + } + + private static string[] Lines(string text) => + text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None); + + private static string? Line(string text, string key) => + Lines(text).FirstOrDefault(l => l == key || l.StartsWith(key + " ", StringComparison.Ordinal)); + + [Theory] + [InlineData("full", GraphicsBackend.OpenGL)] + [InlineData("default", GraphicsBackend.OpenGL)] + [InlineData("gl46", GraphicsBackend.OpenGL)] + [InlineData("legacy", GraphicsBackend.OpenGL)] + [InlineData("experimental", GraphicsBackend.OpenGL)] + [InlineData("vulkan", GraphicsBackend.Vulkan)] + [InlineData("vulkan14", GraphicsBackend.Vulkan)] + public void The_renderer_is_read_with_the_aliases_the_simulator_takes(string value, GraphicsBackend expected) + { + Assert.Equal(expected, Read($"gfxrenderer {value}\n").Backend); + } + + [Theory] + [InlineData("full", "full")] + [InlineData("gl46", "full")] + [InlineData("legacy", "legacy")] + [InlineData("simpleshader", "legacy")] + [InlineData("simple", "simple")] + [InlineData("experimental", "experimental")] + [InlineData("vulkan14", "vulkan")] + public void A_renderer_is_written_by_its_name(string read, string written) + { + Assert.Equal($"gfxrenderer {written}", Line(Write(Read($"gfxrenderer {read}\n")), "gfxrenderer")); + } + + [Fact] + public void An_older_renderer_stays_chosen_and_can_be_chosen_again() + { + var installation = WithIni("gfxrenderer legacy\n"); + var store = installation.SettingsStore; + + store.Settings.Clouds = 3; + store.Save(); + Assert.Contains("gfxrenderer legacy", installation.Files.ReadAllText(UserIni)); + + store.Settings.RenderEngine = SimulatorSettings.VulkanEngine; + store.Save(); + Assert.Contains("gfxrenderer vulkan", installation.Files.ReadAllText(UserIni)); + Assert.Equal(GraphicsBackend.Vulkan, store.Settings.Backend); + + store.Settings.RenderEngine = SimulatorSettings.RenderEngineOf("simple"); + store.Save(); + Assert.Contains("gfxrenderer simple", installation.Files.ReadAllText(UserIni)); + Assert.Equal(GraphicsBackend.OpenGL, store.Settings.Backend); + } + + [Fact] + public void Keys_only_an_older_simulator_reads_stay_in_the_file() + { + string[] old = + { + "usevbo no", "gfx.angleplatform vulkan", "gfx.usegles yes", + "gfx.framebuffer.width 1920", "gfx.framebuffer.height 1080", "gfx.shadows.cab.range 20" + }; + var installation = WithIni(string.Join("\n", old) + "\n"); + installation.SettingsStore.Settings.Clouds = 3; + + installation.SettingsStore.Save(); + + string saved = installation.Files.ReadAllText(UserIni); + Assert.All(old, line => Assert.Contains(line, saved)); + } + + [Fact] + public void Levels_leave_the_switches_only_an_older_simulator_reads() + { + var s = new SimulatorSettings { UseVbo = false, UseGLES = true, RenderAngleVulkan = true }; + + GraphicsPreset.Ultra.ApplyTo(s); + + Assert.Equal((false, true, true), (s.UseVbo, s.UseGLES, s.RenderAngleVulkan)); + } + + [Fact] + public void The_new_graphics_keys_are_read() + { + var s = Read("gfxrenderer vulkan\n" + + "gfx.antialiasing dlss\ngfx.dlss.preset K\ngfx.framegeneration x3\ngfx.reflex boost\n" + + "gfx.raytracing yes\ngfx.raytracing.range 800\ngfx.ssr high\ngfx.postfx.ao ssao\ngfx.postfx.ao.quality 4 6\n" + + "gfx.lights.shadows low\ngfx.contactshadows 3\ngfx.fog analytic\ngfx.fog.quality ultra\n" + + "gfx.clouds ultra\ngfx.clouds.shadows no\ngfx.grass no\ngfx.grass.distance 120\ngfx.grass.density 1.5\n" + + "gfx.buffer.percent 0.67\ngfx.vulkan.device RTX\n"); + + Assert.Equal(GraphicsBackend.Vulkan, s.Backend); + Assert.Equal(AntiAliasingMode.Dlss, s.AntiAliasing); + Assert.Equal("k", s.DlssPreset); + Assert.Equal(3, s.FrameGeneration); + Assert.Equal(2, s.Reflex); + Assert.True(s.RayTracing); + Assert.Equal(800, s.RayTracingRange); + Assert.Equal(3, s.Ssr); + Assert.Equal(1, s.AmbientOcclusion); + Assert.Equal((4, 6), (s.AoSlices, s.AoSteps)); + Assert.Equal(1, s.LightShadows); + Assert.Equal(3, s.ContactShadows); + Assert.False(s.VolumetricFog); + Assert.Equal(4, s.FogQuality); + Assert.Equal(4, s.Clouds); + Assert.False(s.CloudShadows); + Assert.False(s.Grass); + Assert.Equal(120, s.GrassDistance); + Assert.Equal(1.5, s.GrassDensity); + Assert.Equal(67, s.BufferScalePercent); + Assert.Equal("RTX", s.VulkanDevice); + } + + [Fact] + public void Missing_keys_take_the_simulators_defaults() + { + var s = Read(""); + + Assert.Equal(GraphicsBackend.OpenGL, s.Backend); + Assert.Equal(AntiAliasingMode.Msaa4, s.AntiAliasing); + Assert.Equal(2, s.Ssr); + Assert.Equal(2, s.AmbientOcclusion); + Assert.Equal(2, s.LightShadows); + Assert.Equal(0, s.ContactShadows); + Assert.True(s.VolumetricFog); + Assert.Equal(2, s.FogQuality); + Assert.Equal(2, s.Clouds); + Assert.False(s.RayTracing); + Assert.Equal(1, s.FrameGeneration); + } + + [Fact] + public void The_tokens_written_are_the_ones_the_simulator_parses() + { + var s = Read("gfxrenderer vulkan\n"); + s.AntiAliasing = AntiAliasingMode.Dlaa; + s.FrameGeneration = 4; + s.Reflex = 1; + s.LightShadows = 3; + s.ContactShadows = 0; + s.Clouds = 4; + s.VolumetricFog = false; + s.FogQuality = 1; + s.AmbientOcclusion = 1; + s.Ssr = 2; + s.RayTracing = true; + + string text = Write(s); + + Assert.Equal("gfx.antialiasing dlaa", Line(text, "gfx.antialiasing")); + Assert.Equal("multisampling 0", Line(text, "multisampling")); + Assert.Equal("gfx.framegeneration x4", Line(text, "gfx.framegeneration")); + Assert.Equal("gfx.reflex on", Line(text, "gfx.reflex")); + Assert.Equal("gfx.lights.shadows high", Line(text, "gfx.lights.shadows")); + Assert.Equal("gfx.contactshadows no", Line(text, "gfx.contactshadows")); + Assert.Equal("gfx.clouds ultra", Line(text, "gfx.clouds")); + Assert.Equal("gfx.fog analytic", Line(text, "gfx.fog")); + Assert.Equal("gfx.fog.quality low", Line(text, "gfx.fog.quality")); + Assert.Equal("gfx.postfx.ao ssao", Line(text, "gfx.postfx.ao")); + Assert.Equal("gfx.ssr medium", Line(text, "gfx.ssr")); + Assert.Equal("gfx.raytracing yes", Line(text, "gfx.raytracing")); + Assert.Equal("gfx.buffer.percent 100", Line(text, "gfx.buffer.percent")); + } + + [Fact] + public void Msaa_is_written_to_both_keys_for_older_simulators() + { + var s = Read("multisampling 1\n"); + Assert.Equal(AntiAliasingMode.Msaa2, s.AntiAliasing); + + string text = Write(s); + + Assert.Equal("multisampling 1", Line(text, "multisampling")); + Assert.Equal("gfx.antialiasing msaax2", Line(text, "gfx.antialiasing")); + } + + [Fact] + public void Of_the_two_anti_aliasing_keys_the_later_one_wins_as_in_the_simulator() + { + Assert.Equal(AntiAliasingMode.Taa, Read("multisampling 2\ngfx.antialiasing taa\n").AntiAliasing); + Assert.Equal(AntiAliasingMode.Msaa4, Read("gfx.antialiasing taa\nmultisampling 2\n").AntiAliasing); + } + + [Fact] + public void On_opengl_the_vulkan_only_features_are_written_off() + { + var s = Read("gfxrenderer full\ngfx.antialiasing dlss\ngfx.framegeneration x2\ngfx.reflex on\ngfx.raytracing yes\n"); + + Assert.Equal(AntiAliasingMode.Taa, s.AntiAliasing); + Assert.Equal(1, s.FrameGeneration); + Assert.Equal(0, s.Reflex); + Assert.False(s.RayTracing); + + string text = Write(s); + Assert.Equal("gfx.antialiasing taa", Line(text, "gfx.antialiasing")); + Assert.Equal("gfx.framegeneration off", Line(text, "gfx.framegeneration")); + Assert.Equal("gfx.reflex off", Line(text, "gfx.reflex")); + Assert.Equal("gfx.raytracing no", Line(text, "gfx.raytracing")); + } + + [Fact] + public void Frame_generation_needs_dlss_even_on_vulkan() + { + var s = Read("gfxrenderer vulkan\ngfx.antialiasing taa\ngfx.framegeneration x2\ngfx.reflex on\n"); + + Assert.Equal(1, s.FrameGeneration); + Assert.Equal(1, s.Reflex); + Assert.False(GraphicsRules.ReflexForced(s, true)); + } + + [Fact] + public void Frame_generation_turns_reflex_on() + { + var s = Read("gfxrenderer vulkan\ngfx.antialiasing dlss\ngfx.framegeneration x2\n"); + + Assert.True(GraphicsRules.ReflexForced(s, true)); + } + + [Fact] + public void Without_a_dlss_platform_dlss_falls_back_to_taa() + { + var s = Read("gfxrenderer vulkan\ngfx.antialiasing dlss\n", dlssPlatform: false); + + Assert.Equal(AntiAliasingMode.Taa, s.AntiAliasing); + } + + [Fact] + public void Dlaa_ignores_the_render_scale() + { + var s = Read("gfxrenderer vulkan\ngfx.antialiasing dlaa\n"); + Assert.False(GraphicsRules.BufferScaleUsed(s, true)); + + s.AntiAliasing = AntiAliasingMode.Dlss; + Assert.True(GraphicsRules.BufferScaleUsed(s, true)); + } + + [Fact] + public void Sizes_are_rounded_down_to_what_the_simulator_uses() + { + var s = Read("maxtexturesize 16384\nmaxcabtexturesize 3000\nshadowtune 3000 0 250 0\n"); + + Assert.Equal(8192, s.MaxTextureSize); + Assert.Equal(2048, s.MaxCabTextureSize); + Assert.Equal(2048, s.ShadowMapResolution); + } + + [Fact] + public void The_render_scale_reads_the_old_starter_key_when_the_new_one_is_missing() + { + Assert.Equal(75, Read("starter.bufferscale 75\n").BufferScalePercent); + Assert.Equal(50, Read("starter.bufferscale 75\ngfx.buffer.percent 50\n").BufferScalePercent); + } + + [Fact] + public void A_level_is_recognised_after_it_is_applied_and_survives_a_save() + { + for (int engine = 0; engine < SimulatorSettings.RenderEngines.Length; engine++) + { + foreach (var level in GraphicsPreset.All) + { + var installation = WithIni(""); + var store = installation.SettingsStore; + store.Settings.RenderEngine = engine; + level.ApplyTo(store.Settings); + Assert.Same(level, GraphicsPreset.Matching(store.Settings)); + + store.Save(); + + var again = new TestInstallation(installation.Files); + again.Environment.With("HOME", "/home/kolejarz"); + again.SettingsStore.Load(); + Assert.Same(level, GraphicsPreset.Matching(again.SettingsStore.Settings)); + } + } + } + + [Fact] + public void A_change_after_a_level_leaves_the_level() + { + var s = new SimulatorSettings(); + GraphicsPreset.High.ApplyTo(s); + s.Clouds = 4; + + Assert.Null(GraphicsPreset.Matching(s)); + } + + [Fact] + public void Levels_leave_alone_what_is_not_a_matter_of_quality() + { + var s = new SimulatorSettings { Width = 2560, Height = 1440, VSync = true, FieldOfView = 60, MotionBlur = false }; + + GraphicsPreset.Low.ApplyTo(s); + + Assert.Equal((2560, 1440, true, 60, false), (s.Width, s.Height, s.VSync, s.FieldOfView, s.MotionBlur)); + } + + [Fact] + public void Ray_tracing_comes_with_ultra_on_vulkan_only() + { + var vulkan = new SimulatorSettings { RenderEngine = SimulatorSettings.VulkanEngine }; + var opengl = new SimulatorSettings(); + + GraphicsPreset.Ultra.ApplyTo(vulkan); + GraphicsPreset.Ultra.ApplyTo(opengl); + + Assert.True(vulkan.RayTracing); + Assert.False(opengl.RayTracing); + Assert.Same(GraphicsPreset.Ultra, GraphicsPreset.Matching(opengl)); + } + + [Fact] + public void A_level_keeps_dlss_on_vulkan() + { + var s = new SimulatorSettings { RenderEngine = SimulatorSettings.VulkanEngine, AntiAliasing = AntiAliasingMode.Dlss }; + + GraphicsPreset.Ultra.ApplyTo(s); + + Assert.Equal(AntiAliasingMode.Dlss, s.AntiAliasing); + Assert.Same(GraphicsPreset.Ultra, GraphicsPreset.Matching(s)); + } + + [Fact] + public void A_level_replaces_taa_with_its_own_multisampling() + { + var s = new SimulatorSettings { AntiAliasing = AntiAliasingMode.Taa }; + + GraphicsPreset.Medium.ApplyTo(s); + + Assert.Equal(AntiAliasingMode.Msaa2, s.AntiAliasing); + } + + [Fact] + public void An_import_takes_the_settings_but_keeps_this_installations_simulator_and_the_unmanaged_lines() + { + var installation = WithIni("starter.exe.auto no\nstarter.exe.path eu07_local.exe\ncalibratein 0 0 0 0\ngfx.clouds low\n"); + installation.Files.WithFile("/backup/eu07.ini", "starter.exe.auto no\nstarter.exe.path eu07_other.exe\ngfx.clouds ultra\ngfxrenderer vulkan\n"); + + Assert.True(installation.SettingsStore.ImportFrom("/backup/eu07.ini")); + var s = installation.SettingsStore.Settings; + Assert.Equal(4, s.Clouds); + Assert.Equal(GraphicsBackend.Vulkan, s.Backend); + Assert.Equal("eu07_local.exe", s.ExecutablePath); + + // Nothing is written until saved, and the save keeps the user's own lines. + Assert.Contains("gfx.clouds low", installation.Files.ReadAllText(UserIni)); + installation.SettingsStore.Save(); + string saved = installation.Files.ReadAllText(UserIni); + Assert.Contains("calibratein 0 0 0 0", saved); + Assert.Contains("gfx.clouds ultra", saved); + Assert.Contains("starter.exe.path eu07_local.exe", saved); + } + + [Fact] + public void An_import_of_a_missing_file_changes_nothing() + { + var installation = WithIni("gfx.clouds low\n"); + + Assert.False(installation.SettingsStore.ImportFrom("/nowhere/eu07.ini")); + Assert.Equal(1, installation.SettingsStore.Settings.Clouds); + } + + [Fact] + public void An_export_writes_a_complete_file_elsewhere_and_leaves_the_users_one_alone() + { + var installation = WithIni("calibratein 0 0 0 0\ngfx.clouds low\n"); + installation.SettingsStore.Settings.Clouds = 3; + + Assert.True(installation.SettingsStore.ExportTo("/export/eu07.ini")); + + string exported = installation.Files.ReadAllText("/export/eu07.ini"); + Assert.Contains("calibratein 0 0 0 0", exported); + Assert.Contains("gfx.clouds high", exported); + Assert.Contains("gfx.clouds low", installation.Files.ReadAllText(UserIni)); + } + + [Fact] + public void An_export_keeps_the_older_renderer_word_of_the_file() + { + var installation = WithIni("gfxrenderer legacy\n"); + + Assert.True(installation.SettingsStore.ExportTo("/export/eu07.ini")); + + Assert.Contains("gfxrenderer legacy", installation.Files.ReadAllText("/export/eu07.ini")); + } + + [Fact] + public void The_dlss_libraries_are_looked_for_next_to_a_windows_build() + { + var files = new InMemoryFileSystem() + .WithDirectory(TestInstallation.Root) + .WithExecutable(TestInstallation.At("eu07.exe"), "MZ binary") + .WithFile(TestInstallation.At("nvngx_dlss.dll"), "dll"); + var installation = new TestInstallation(files); + + Assert.Equal((true, false), installation.Executables.DlssLibraries(TestInstallation.At("eu07.exe"))); + + files.WithFile(TestInstallation.At("nvngx_dlssg.dll"), "dll"); + Assert.Equal((true, true), installation.Executables.DlssLibraries(TestInstallation.At("eu07.exe"))); + } + + [Fact] + public void The_dlss_libraries_of_a_linux_build_are_the_shared_objects() + { + var files = new InMemoryFileSystem() + .WithDirectory(TestInstallation.Root) + .WithExecutable(TestInstallation.At("eu07"), "\u007fELF binary") + .WithFile(TestInstallation.At("libnvidia-ngx-dlssg.so.310.4.0"), "so"); + var installation = new TestInstallation(files); + + Assert.Equal((false, true), installation.Executables.DlssLibraries(TestInstallation.At("eu07"))); + } + + [Fact] + public void A_later_line_moved_up_keeps_the_index_of_every_key() + { + var c = ConfigFile.Parse("a 1\nb 2\nc 3\n"); + + c.MoveAfter("a", "c"); + + Assert.Equal("b 2\nc 3\na 1\n", c.ToText().Replace("\r\n", "\n")); + Assert.True(c.LineOf("a") > c.LineOf("c")); + c.Set("b", "9"); + Assert.Equal("b 9\nc 3\na 1\n", c.ToText().Replace("\r\n", "\n")); + } + + [Fact] + public void The_ao_lighting_and_cab_probe_keys_survive_a_round_trip_in_the_simulators_form() + { + var s = Read("gfx.postfx.ao.lighting no\ngfx.cabprobe high\ngfx.cabprobe.interval 12\ngfx.cabprobe.bounce 2.5\ngfx.cabprobe.reflections no\n"); + + Assert.False(s.AoLighting); + Assert.Equal(3, s.CabProbe); + Assert.Equal(12, s.CabProbeInterval); + Assert.Equal(2.5, s.CabProbeBounce); + Assert.False(s.CabProbeReflections); + + string text = Write(s); + Assert.Equal("gfx.postfx.ao.lighting no", Line(text, "gfx.postfx.ao.lighting")); + Assert.Equal("gfx.cabprobe high", Line(text, "gfx.cabprobe")); + Assert.Equal("gfx.cabprobe.interval 12", Line(text, "gfx.cabprobe.interval")); + Assert.Equal("gfx.cabprobe.bounce 2.5", Line(text, "gfx.cabprobe.bounce")); + Assert.Equal("gfx.cabprobe.reflections no", Line(text, "gfx.cabprobe.reflections")); + + var again = Read(text); + Assert.Equal((s.AoLighting, s.CabProbe, s.CabProbeInterval, s.CabProbeBounce, s.CabProbeReflections), + (again.AoLighting, again.CabProbe, again.CabProbeInterval, again.CabProbeBounce, again.CabProbeReflections)); + } + + [Fact] + public void Missing_ao_lighting_and_cab_probe_keys_take_the_simulators_defaults_and_are_written_like_its_export() + { + var s = Read(""); + + Assert.Equal((true, 2, 4, 1.0, true), (s.AoLighting, s.CabProbe, s.CabProbeInterval, s.CabProbeBounce, s.CabProbeReflections)); + + string text = Write(s); + Assert.Equal("gfx.postfx.ao.lighting yes", Line(text, "gfx.postfx.ao.lighting")); + Assert.Equal("gfx.cabprobe medium", Line(text, "gfx.cabprobe")); + Assert.Equal("gfx.cabprobe.interval 4", Line(text, "gfx.cabprobe.interval")); + Assert.Equal("gfx.cabprobe.bounce 1", Line(text, "gfx.cabprobe.bounce")); + Assert.Equal("gfx.cabprobe.reflections yes", Line(text, "gfx.cabprobe.reflections")); + } + + [Fact] + public void Unknown_ao_lighting_and_cab_probe_values_fall_back_to_the_simulators_defaults() + { + var s = Read("gfx.postfx.ao.lighting maybe\ngfx.cabprobe bogus\ngfx.cabprobe.interval abc\ngfx.cabprobe.bounce x\ngfx.cabprobe.reflections ?\n"); + + Assert.Equal((true, 2, 4, 1.0, true), (s.AoLighting, s.CabProbe, s.CabProbeInterval, s.CabProbeBounce, s.CabProbeReflections)); + } + + [Theory] + [InlineData("no", 0)] + [InlineData("0", 0)] + [InlineData("low", 1)] + [InlineData("1", 1)] + [InlineData("medium", 2)] + [InlineData("yes", 2)] + [InlineData("2", 2)] + [InlineData("high", 3)] + [InlineData("3", 3)] + public void The_cab_probe_level_is_read_with_the_aliases_the_simulator_takes(string value, int expected) + { + Assert.Equal(expected, Read($"gfx.cabprobe {value}\n").CabProbe); + } + + [Fact] + public void The_cab_probe_numbers_are_kept_within_the_simulators_ranges() + { + var s = Read("gfx.cabprobe.interval 500\ngfx.cabprobe.bounce 9\n"); + Assert.Equal((120, 4.0), (s.CabProbeInterval, s.CabProbeBounce)); + + s = Read("gfx.cabprobe.interval 0\ngfx.cabprobe.bounce -1\n"); + Assert.Equal((1, 0.0), (s.CabProbeInterval, s.CabProbeBounce)); + } + + [Fact] + public void The_ao_lighting_counts_with_gtao_only() + { + Assert.True(GraphicsRules.AoLightingUsed(Read("gfx.postfx.ao gtao\n"))); + Assert.False(GraphicsRules.AoLightingUsed(Read("gfx.postfx.ao ssao\n"))); + Assert.False(GraphicsRules.AoLightingUsed(Read("gfx.postfx.ao no\n"))); + } + + [Theory] + [InlineData("gfx.ssr no\ngfx.contactshadows no\n", true)] + [InlineData("gfx.ssr low\ngfx.contactshadows no\n", false)] + [InlineData("gfx.ssr no\ngfx.contactshadows low\n", false)] + [InlineData("gfx.ssr no\ngfx.contactshadows no\ngfx.depthprepass.scene yes\n", false)] + [InlineData("gfx.ssr no\ngfx.contactshadows no\ngfx.postfx.ao.lighting no\n", false)] + [InlineData("gfx.ssr no\ngfx.contactshadows no\ngfx.postfx.ao ssao\n", false)] + public void The_ao_lighting_adds_the_depth_pass_only_when_nothing_else_draws_it(string text, bool expected) + { + Assert.Equal(expected, GraphicsRules.AoLightingAddsDepthPrepass(Read(text))); + } + + [Theory] + [InlineData("gfx.cabprobe medium\n", true, true)] + [InlineData("gfx.cabprobe no\n", false, false)] + [InlineData("gfx.cabprobe low\ngfx.lights.clustered no\n", true, false)] + [InlineData("gfx.cabprobe high\ngfx.lights.cab no\n", true, false)] + public void The_cab_probe_lights_the_cab_only_with_its_lamp_as_a_light(string text, bool used, bool lights) + { + var s = Read(text); + + Assert.Equal(used, GraphicsRules.CabProbeUsed(s)); + Assert.Equal(lights, GraphicsRules.CabProbeLights(s)); + } + + [Theory] + [InlineData(0, false, 0, 8, false)] + [InlineData(1, false, 0, 8, false)] + [InlineData(2, false, 1, 8, true)] + [InlineData(3, true, 2, 4, true)] + [InlineData(4, true, 3, 3, true)] + [InlineData(5, true, 3, 2, true)] + public void A_level_sets_the_ao_lighting_and_the_cab_probe(int place, bool aoLighting, int probe, int interval, bool reflections) + { + var level = GraphicsPreset.All[place]; + var s = new SimulatorSettings { CabProbeBounce = 2.5 }; + + level.ApplyTo(s); + + Assert.Equal((aoLighting, probe, interval, reflections), (s.AoLighting, s.CabProbe, s.CabProbeInterval, s.CabProbeReflections)); + Assert.Equal(2.5, s.CabProbeBounce); + Assert.Same(level, GraphicsPreset.Matching(s)); + } + + [Fact] + public void An_import_keeps_the_lamp_switch_of_the_users_own_file() + { + var installation = WithIni("gfx.lights.cab no\n"); + installation.Files.WithFile("/backup/eu07.ini", "gfx.lights.cab yes\ngfx.cabprobe high\n"); + + Assert.True(installation.SettingsStore.ImportFrom("/backup/eu07.ini")); + + Assert.False(installation.SettingsStore.Settings.CabLampLight); + Assert.Equal(3, installation.SettingsStore.Settings.CabProbe); + } +} diff --git a/StarterNG.Tests/SettingsStoreTests.cs b/StarterNG.Tests/SettingsStoreTests.cs index 2198dcb..fd13e4f 100644 --- a/StarterNG.Tests/SettingsStoreTests.cs +++ b/StarterNG.Tests/SettingsStoreTests.cs @@ -861,6 +861,9 @@ public class SettingsStoreTests [InlineData("opengl")] [InlineData("gl46")] [InlineData("vulkan14")] + [InlineData("legacy")] + [InlineData("simple")] + [InlineData("experimental")] public void A_renderer_word_the_simulator_reads_as_another_stays_until_changed(string word) { var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, $"gfxrenderer {word}\n")); @@ -936,46 +939,33 @@ public class SettingsStoreTests } [Fact] - public void Anti_aliasing_is_saved_below_multisampling_for_the_later_line_to_count() + public void The_two_anti_aliasing_keys_untouched_keep_their_order() { - var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "gfx.antialiasing taa\nwidth 800\nmultisampling 2\n")); + // Moving them would change which of the two the simulator goes by. + const string ini = "gfx.antialiasing taa\nwidth 800\nmultisampling 2\n"; + var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, ini)); installation.SettingsStore.Load(); installation.SettingsStore.Save(); string saved = SavedLines(installation); - Assert.True(saved.IndexOf("multisampling ", StringComparison.Ordinal) < saved.IndexOf("gfx.antialiasing taa", StringComparison.Ordinal), saved); + Assert.True(saved.IndexOf("gfx.antialiasing taa", StringComparison.Ordinal) < saved.IndexOf("multisampling 2", StringComparison.Ordinal), saved); } [Fact] - public void Anti_aliasing_left_to_multisampling_goes_from_the_file() + public void A_chosen_anti_aliasing_is_saved_below_multisampling_for_the_later_line_to_count() { - var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "multisampling 2\ngfx.antialiasing taa\n")); + var installation = WithHome(new InMemoryFileSystem().WithFile(UserIni, "gfx.antialiasing msaax4\nwidth 800\nmultisampling 2\n")); installation.SettingsStore.Load(); - installation.SettingsStore.Settings.Extra["gfx.antialiasing"] = ""; + installation.SettingsStore.Settings.AntiAliasing = AntiAliasingMode.Taa; installation.SettingsStore.Save(); - Assert.DoesNotContain("\ngfx.antialiasing", SavedLines(installation)); - } - - [Theory] - [InlineData("TAA", "taa")] - [InlineData(" dlaa ", "dlaa")] - [InlineData("", "")] - [InlineData("fxaa", "")] - public void A_choice_reads_the_words_it_offers_and_else_the_default(string raw, string word) - { - Assert.Equal(word, SimulatorKeys.Named("gfx.antialiasing").ParseChoice(raw)); - } - - [Theory] - [InlineData("4 6", "4 6")] - [InlineData("4 6", "4 6")] - [InlineData("5 5", "3 4")] - public void A_choice_of_two_numbers_is_one_word(string raw, string word) - { - Assert.Equal(word, SimulatorKeys.Named("gfx.postfx.ao.quality").ParseChoice(raw)); + string saved = SavedLines(installation); + Assert.True(saved.IndexOf("multisampling ", StringComparison.Ordinal) < saved.IndexOf("gfx.antialiasing taa", StringComparison.Ordinal), saved); + var again = WithHome(installation.Files); + again.SettingsStore.Load(); + Assert.Equal(AntiAliasingMode.Taa, again.SettingsStore.Settings.AntiAliasing); } [Fact] diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs index 2c91ba6..2961947 100644 --- a/StarterNG/Application/AppServices.cs +++ b/StarterNG/Application/AppServices.cs @@ -62,7 +62,7 @@ public sealed class AppServices SceneryFiles = new SceneryFileWatch(files, Sceneries, Library, State); Executables = new ExecutableLocator(files, paths, environment, log); - SettingsStore = new SettingsStore(files, clock, log, settingsPaths, new SettingsSerializer(), Executables); + SettingsStore = new SettingsStore(files, clock, log, settingsPaths, new SettingsSerializer(dlssPlatform: !environment.IsMacOS), Executables); MissingVehicleLog = new MissingVehicleLog(SettingsStore.Settings, missingAssets, Library, log); Favorites = new FavoriteSceneries(files, log, settingsPaths.FavoritesPath()); Tapes = new HaslerTapes(files, paths, log, clock, Timetables, diff --git a/StarterNG/Application/SettingsStore.cs b/StarterNG/Application/SettingsStore.cs index 6bcdbed..16a422a 100644 --- a/StarterNG/Application/SettingsStore.cs +++ b/StarterNG/Application/SettingsStore.cs @@ -157,15 +157,7 @@ public sealed class SettingsStore return; } - foreach (var change in Changes(Settings)) - { - if (change.After is null) - _config.Remove(change.Key); - else - _config.Set(change.Key, change.After); - } - // Each sets the mode as the simulator reads it, so this one has to come later to count. - _config.MoveAfter("gfx.antialiasing", "multisampling"); + ApplyChanges(_config, Changes(Settings)); _synced = Render(Settings); try @@ -184,6 +176,91 @@ public sealed class SettingsStore } } + private static void ApplyChanges(ConfigFile config, IReadOnlyList changes) + { + foreach (var change in changes) + { + if (change.After is null) + config.Remove(change.Key); + else + config.Set(change.Key, change.After); + } + + // Each sets the mode as the simulator reads it, so the new one has to come later to count. + // Untouched, the two keep their order: moving them would change which one decides. + if (changes.Any(change => change.Key is "gfx.antialiasing" or "multisampling")) + config.MoveAfter("gfx.antialiasing", "multisampling"); + } + + /// Whether the simulator can have DLSS on this platform at all. + public bool DlssPlatform => _serializer.DlssPlatform; + + /// + /// Takes the settings of another eu07.ini (a backup, a file from another machine) + /// into the current ones, keeping what belongs to this installation: the chosen + /// simulator, its card, the last scenery and the window state. The lines of this + /// user's file the starter does not manage stay as they are. Nothing is written until Save. + /// + public bool ImportFrom(string path) + { + ConfigFile imported; + try + { + imported = ConfigFile.Parse(_files.ReadAllText(path, _encoding)); + } + catch (Exception ex) + { + _log.Log($"importing {path}", ex); + return false; + } + + var s = Settings; + bool selectAutomatically = s.SelectExeAutomatically; + string executable = s.ExecutablePath; + var gpu = s.Gpu; + string lastScenery = s.LastScenery; + bool maximized = s.WindowMaximized; + // Only read for the dependencies and never written, so they stay as this user's file has them. + bool cabLampLight = s.CabLampLight; + bool depthPrepassScene = s.DepthPrepassScene; + + _serializer.Read(s, imported); + + s.SelectExeAutomatically = selectAutomatically; + s.ExecutablePath = executable; + s.Gpu = gpu; + s.LastScenery = lastScenery; + s.WindowMaximized = maximized; + s.CabLampLight = cabLampLight; + s.DepthPrepassScene = depthPrepassScene; + return true; + } + + /// + /// Writes the settings as a complete eu07.ini somewhere else - this user's file + /// with the settings applied - leaving the file itself and its state alone. + /// + public bool ExportTo(string path) + { + var copy = ConfigFile.Parse(_config.ToText()); + ApplyChanges(copy, Changes(Settings)); + + try + { + string? directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) + _files.CreateDirectory(directory); + + _files.WriteAllBytesAtomic(path, _encoding.GetBytes(copy.ToText())); + return true; + } + catch (Exception ex) + { + _log.Log($"exporting {path}", ex); + return false; + } + } + /// Every key as would put it, in the order they are written. public IReadOnlyDictionary Render(SimulatorSettings settings) { diff --git a/StarterNG/Classes/ConfigFile.cs b/StarterNG/Classes/ConfigFile.cs index 0769175..71f3459 100644 --- a/StarterNG/Classes/ConfigFile.cs +++ b/StarterNG/Classes/ConfigFile.cs @@ -58,6 +58,9 @@ public sealed class ConfigFile public bool Has(string key) => _index.ContainsKey(key); + /// The line the key is on, or -1. The simulator reads the file top down, so a later line wins. + public int LineOf(string key) => _index.TryGetValue(key, out int i) ? i : -1; + /// Whether the line setting the key is one the simulator never gets to. public bool IsPastEnd(string key) => _end >= 0 && _index.TryGetValue(key, out int line) && line > _end; diff --git a/StarterNG/Domain/Settings/GraphicsPreset.cs b/StarterNG/Domain/Settings/GraphicsPreset.cs index a1ae91c..2b14982 100644 --- a/StarterNG/Domain/Settings/GraphicsPreset.cs +++ b/StarterNG/Domain/Settings/GraphicsPreset.cs @@ -121,102 +121,75 @@ public sealed record GraphicsTop( /// /// What only the simulators with the OpenGL 4.6 and Vulkan 1.4 renderers read, the others pass it -/// over. High is what they do with nothing set. Left to the player: anti-aliasing by TAA or DLSS, -/// frame generation, Reflex, the choice of the card, automatic exposure and the fitted cascades. +/// over. High is what they do with nothing set. Left to the player: frame generation, Reflex, the +/// choice of the card, automatic exposure, the fitted cascades and the bounced light of the cab. /// /// gfx.cabprobe.interval, in frames: the lower the dearer. -/// gfx.raytracing: the Vulkan renderer's alone, OpenGL passes it over. +/// gfx.raytracing: the Vulkan renderer's alone, off on OpenGL. public sealed record GraphicsNewRenderer( - string LampShadows, - string ContactShadows, - string Occlusion, - string OcclusionQuality, + int LampShadows, + int ContactShadows, + int Occlusion, + int OcclusionSlices, + int OcclusionSteps, bool OcclusionLighting, - string CabProbe, - double CabProbeInterval, + int CabProbe, + int CabProbeInterval, bool CabProbeReflections, - string ScreenReflections, + int ScreenReflections, bool RayTracing, - string Fog, - string FogQuality, - string Clouds, + bool VolumetricFog, + int FogQuality, + int Clouds, bool CloudShadows, bool Grass, - double GrassDistance, - double GrassFade, + int GrassDistance, double GrassDensity, - double Precipitation, + int Precipitation, bool Bloom) { - private const string LampShadowsKey = "gfx.lights.shadows"; - private const string ContactShadowsKey = "gfx.contactshadows"; - private const string OcclusionKey = "gfx.postfx.ao"; - private const string OcclusionQualityKey = "gfx.postfx.ao.quality"; - private const string OcclusionLightingKey = "gfx.postfx.ao.lighting"; - private const string CabProbeKey = "gfx.cabprobe"; - private const string CabProbeIntervalKey = "gfx.cabprobe.interval"; - private const string CabProbeReflectionsKey = "gfx.cabprobe.reflections"; - private const string ScreenReflectionsKey = "gfx.ssr"; - private const string RayTracingKey = "gfx.raytracing"; - private const string FogKey = "gfx.fog"; - private const string FogQualityKey = "gfx.fog.quality"; - private const string CloudsKey = "gfx.clouds"; - private const string CloudShadowsKey = "gfx.clouds.shadows"; - private const string GrassKey = "gfx.grass"; - private const string GrassDistanceKey = "gfx.grass.distance"; - private const string GrassFadeKey = "gfx.grass.fadedistance"; - private const string GrassDensityKey = "gfx.grass.density"; - private const string PrecipitationKey = "gfx.precipitation.particles"; - private const string BloomKey = "gfx.postfx.bloom"; - - /// The eu07.ini keys these values live in, all of them rows of . + /// The eu07.ini keys these values live in; the fade of the grass follows its distance. public static readonly IReadOnlyList Keys = new[] { - LampShadowsKey, ContactShadowsKey, OcclusionKey, OcclusionQualityKey, OcclusionLightingKey, - CabProbeKey, CabProbeIntervalKey, CabProbeReflectionsKey, ScreenReflectionsKey, RayTracingKey, - FogKey, FogQualityKey, CloudsKey, CloudShadowsKey, - GrassKey, GrassDistanceKey, GrassFadeKey, GrassDensityKey, PrecipitationKey, BloomKey + "gfx.lights.shadows", "gfx.contactshadows", "gfx.postfx.ao", "gfx.postfx.ao.quality", "gfx.postfx.ao.lighting", + "gfx.cabprobe", "gfx.cabprobe.interval", "gfx.cabprobe.reflections", "gfx.ssr", "gfx.raytracing", + "gfx.fog", "gfx.fog.quality", "gfx.clouds", "gfx.clouds.shadows", + "gfx.grass", "gfx.grass.distance", "gfx.grass.fadedistance", "gfx.grass.density", + "gfx.precipitation.particles", "gfx.postfx.bloom" }; public static GraphicsNewRenderer Of(SimulatorSettings s) => - new(WordOf(s, LampShadowsKey), WordOf(s, ContactShadowsKey), WordOf(s, OcclusionKey), WordOf(s, OcclusionQualityKey), - GraphicsPreset.ToggleOf(s, OcclusionLightingKey), WordOf(s, CabProbeKey), GraphicsPreset.NumberOf(s, CabProbeIntervalKey), - GraphicsPreset.ToggleOf(s, CabProbeReflectionsKey), WordOf(s, ScreenReflectionsKey), GraphicsPreset.ToggleOf(s, RayTracingKey), - WordOf(s, FogKey), WordOf(s, FogQualityKey), WordOf(s, CloudsKey), GraphicsPreset.ToggleOf(s, CloudShadowsKey), - GraphicsPreset.ToggleOf(s, GrassKey), GraphicsPreset.NumberOf(s, GrassDistanceKey), GraphicsPreset.NumberOf(s, GrassFadeKey), - GraphicsPreset.NumberOf(s, GrassDensityKey), GraphicsPreset.NumberOf(s, PrecipitationKey), GraphicsPreset.ToggleOf(s, BloomKey)); + new(s.LightShadows, s.ContactShadows, s.AmbientOcclusion, s.AoSlices, s.AoSteps, s.AoLighting, + s.CabProbe, s.CabProbeInterval, s.CabProbeReflections, s.Ssr, s.RayTracing, + s.VolumetricFog, s.FogQuality, s.Clouds, s.CloudShadows, + s.Grass, s.GrassDistance, s.GrassDensity, s.PrecipitationParticles, s.Bloom); + + /// This level as it lands on : ray tracing on Vulkan only. + public GraphicsNewRenderer For(SimulatorSettings s) => + GraphicsRules.RayTracingAllowed(s) ? this : this with { RayTracing = false }; public void ApplyTo(SimulatorSettings s) { - void Number(string key, double value) => s.Extra[key] = value.ToString(CultureInfo.InvariantCulture); - void Toggle(string key, bool value) => s.Extra[key] = SimulatorKey.FormatToggle(value); - - s.Extra[LampShadowsKey] = LampShadows; - s.Extra[ContactShadowsKey] = ContactShadows; - s.Extra[OcclusionKey] = Occlusion; - s.Extra[OcclusionQualityKey] = OcclusionQuality; - Toggle(OcclusionLightingKey, OcclusionLighting); - s.Extra[CabProbeKey] = CabProbe; - Number(CabProbeIntervalKey, CabProbeInterval); - Toggle(CabProbeReflectionsKey, CabProbeReflections); - s.Extra[ScreenReflectionsKey] = ScreenReflections; - Toggle(RayTracingKey, RayTracing); - s.Extra[FogKey] = Fog; - s.Extra[FogQualityKey] = FogQuality; - s.Extra[CloudsKey] = Clouds; - Toggle(CloudShadowsKey, CloudShadows); - Toggle(GrassKey, Grass); - Number(GrassDistanceKey, GrassDistance); - Number(GrassFadeKey, GrassFade); - Number(GrassDensityKey, GrassDensity); - Number(PrecipitationKey, Precipitation); - Toggle(BloomKey, Bloom); - } - - private static string WordOf(SimulatorSettings s, string name) - { - var key = SimulatorKeys.Named(name); - return key.ParseChoice(key.RawIn(s)); + s.LightShadows = LampShadows; + s.ContactShadows = ContactShadows; + s.AmbientOcclusion = Occlusion; + s.AoSlices = OcclusionSlices; + s.AoSteps = OcclusionSteps; + s.AoLighting = OcclusionLighting; + s.CabProbe = CabProbe; + s.CabProbeInterval = CabProbeInterval; + s.CabProbeReflections = CabProbeReflections; + s.Ssr = ScreenReflections; + s.RayTracing = RayTracing && GraphicsRules.RayTracingAllowed(s); + s.VolumetricFog = VolumetricFog; + s.FogQuality = FogQuality; + s.Clouds = Clouds; + s.CloudShadows = CloudShadows; + s.Grass = Grass; + s.GrassDistance = GrassDistance; + s.GrassDensity = GrassDensity; + s.PrecipitationParticles = Precipitation; + s.Bloom = Bloom; } } @@ -228,6 +201,8 @@ public sealed record GraphicsNewRenderer( /// two ends of the scale, which have . /// /// Its name's translation key. +/// gfx.shadows.cab.range, in metres, read only by the legacy renderer. +/// Multisampling, as older simulators read it too. DLSS, once on, stays: no level does better. /// gfx.reflections.framerate, in updates a second. /// basedrawrange, in metres. Below 2000 it changes nothing on screen: the renderer /// keeps at least that for signals and lights (opengl33renderer.cpp). @@ -236,13 +211,12 @@ public sealed record GraphicsNewRenderer( /// starter.bufferscale, in percent. Above the screen over High: on a 4K one /// Ultra is a picture of 8K, multisampled on top. /// gfx.drawrange.factor.max: how far the regulator may stretch the draw range. -/// gfx.shadows.cab.range, in metres, read only by the legacy renderer. public sealed record GraphicsPreset( string NameKey, int TextureSize, int CabTextureSize, double Anisotropy, - int Multisampling, + AntiAliasingMode AntiAliasing, int DynamicLights, int SmokeFidelity, bool Shadows, @@ -276,52 +250,52 @@ public sealed record GraphicsPreset( public static readonly IReadOnlyList RowKeys = new[] { DrawRangeKey, DistanceFactorKey, SleeperKey, ReflectedInstancesKey, ReflectedVehiclesKey - }.Concat(GraphicsNewRenderer.Keys).ToList(); + }; /// The eu07.ini keys a level sets. public static readonly IReadOnlyList Keys = new[] { - "maxtexturesize", "maxcabtexturesize", "anisotropicfiltering", "multisampling", "dynamiclights", + "maxtexturesize", "maxcabtexturesize", "anisotropicfiltering", "multisampling", "gfx.antialiasing", "dynamiclights", "gfx.smoke.fidelity", "shadows", "gfx.shadowmap.enabled", "shadowtune", "gfx.shadow.rank.cutoff", "gfx.envmap.enabled", "gfx.reflections.fidelity", "gfx.reflections.framerate", "gfx.extraeffects", - "splinefidelity", DrawRangeKey, DistanceFactorKey, "starter.bufferscale", SleeperKey, + "splinefidelity", DrawRangeKey, DistanceFactorKey, "starter.bufferscale", "gfx.buffer.percent", SleeperKey, "gfx.drawrange.factor.max", ReflectedInstancesKey, ReflectedVehiclesKey, "gfx.shadows.cab.range" }.Concat(GraphicsNewRenderer.Keys).ToList(); /// The last resort: no shadows, no reflections, no effects, no smoke, a softer picture. public static readonly GraphicsPreset Minimal = new( - "PresetMinimal", TextureSize: 512, CabTextureSize: 512, Anisotropy: 1, Multisampling: 0, + "PresetMinimal", TextureSize: 512, CabTextureSize: 512, Anisotropy: 1, AntiAliasing: AntiAliasingMode.Off, DynamicLights: 1, SmokeFidelity: 1, Shadows: false, ShadowMap: 512, ShadowRange: 75, ShadowRank: 1, EnvMap: false, ReflectionsFidelity: 0, ReflectionsRate: 1 / 300.0, ExtraEffects: false, SplineFidelity: 1, DrawRange: 2000, DistanceFactor: 0.6, BufferScale: 70, SleeperDistance: 100, DrawRangeFactor: 1, ReflectedInstances: 250, ReflectedVehicles: 50, CabShadowsRange: 0, - NewRenderer: new GraphicsNewRenderer(LampShadows: "no", ContactShadows: "no", Occlusion: "no", OcclusionQuality: "3 4", - OcclusionLighting: false, CabProbe: "no", CabProbeInterval: 8, CabProbeReflections: false, ScreenReflections: "no", - RayTracing: false, Fog: "analytic", FogQuality: "low", Clouds: "no", CloudShadows: false, Grass: false, - GrassDistance: 30, GrassFade: 40, GrassDensity: 0.25, Precipitation: 16384, Bloom: false), + NewRenderer: new GraphicsNewRenderer(LampShadows: 0, ContactShadows: 0, Occlusion: 0, OcclusionSlices: 2, OcclusionSteps: 3, + OcclusionLighting: false, CabProbe: 0, CabProbeInterval: 8, CabProbeReflections: false, ScreenReflections: 0, + RayTracing: false, VolumetricFog: false, FogQuality: 1, Clouds: 0, CloudShadows: false, Grass: false, + GrassDistance: 30, GrassDensity: 0.25, Precipitation: 16384, Bloom: false), Extras: new GraphicsExtras(MotionBlur: false, ChromaticAberration: false, Smoke: false, PythonUpdateMs: 800)); public static readonly GraphicsPreset Low = new( - "PresetLow", TextureSize: 1024, CabTextureSize: 1024, Anisotropy: 2, Multisampling: 0, + "PresetLow", TextureSize: 1024, CabTextureSize: 1024, Anisotropy: 2, AntiAliasing: AntiAliasingMode.Off, DynamicLights: 3, SmokeFidelity: 1, Shadows: true, ShadowMap: 1024, ShadowRange: 125, ShadowRank: 2, EnvMap: true, ReflectionsFidelity: 0, ReflectionsRate: 1 / 60.0, ExtraEffects: true, SplineFidelity: 1, DrawRange: 2000, DistanceFactor: 0.7, BufferScale: 80, SleeperDistance: 150, DrawRangeFactor: 1.5, ReflectedInstances: 400, ReflectedVehicles: 100, CabShadowsRange: 0, - NewRenderer: new GraphicsNewRenderer(LampShadows: "low", ContactShadows: "no", Occlusion: "ssao", OcclusionQuality: "3 4", - OcclusionLighting: false, CabProbe: "no", CabProbeInterval: 8, CabProbeReflections: false, ScreenReflections: "low", - RayTracing: false, Fog: "volumetric", FogQuality: "low", Clouds: "low", CloudShadows: false, Grass: true, - GrassDistance: 40, GrassFade: 50, GrassDensity: 0.5, Precipitation: 32768, Bloom: true)); + NewRenderer: new GraphicsNewRenderer(LampShadows: 1, ContactShadows: 0, Occlusion: 1, OcclusionSlices: 2, OcclusionSteps: 3, + OcclusionLighting: false, CabProbe: 0, CabProbeInterval: 8, CabProbeReflections: false, ScreenReflections: 1, + RayTracing: false, VolumetricFog: true, FogQuality: 1, Clouds: 1, CloudShadows: false, Grass: true, + GrassDistance: 40, GrassDensity: 0.5, Precipitation: 32768, Bloom: true)); public static readonly GraphicsPreset Medium = new( - "PresetMedium", TextureSize: 2048, CabTextureSize: 2048, Anisotropy: 4, Multisampling: 1, + "PresetMedium", TextureSize: 2048, CabTextureSize: 2048, Anisotropy: 4, AntiAliasing: AntiAliasingMode.Msaa2, DynamicLights: 5, SmokeFidelity: 2, Shadows: true, ShadowMap: 1024, ShadowRange: 175, ShadowRank: 2, EnvMap: true, ReflectionsFidelity: 1, ReflectionsRate: 1 / 30.0, ExtraEffects: true, SplineFidelity: 2, DrawRange: 2250, DistanceFactor: 0.85, BufferScale: 90, SleeperDistance: 200, DrawRangeFactor: 2, ReflectedInstances: 550, ReflectedVehicles: 150, CabShadowsRange: 20, - NewRenderer: new GraphicsNewRenderer(LampShadows: "low", ContactShadows: "no", Occlusion: "ssao", OcclusionQuality: "3 4", - OcclusionLighting: false, CabProbe: "low", CabProbeInterval: 8, CabProbeReflections: true, ScreenReflections: "low", - RayTracing: false, Fog: "volumetric", FogQuality: "low", Clouds: "low", CloudShadows: true, Grass: true, - GrassDistance: 50, GrassFade: 60, GrassDensity: 0.75, Precipitation: 49152, Bloom: true)); + NewRenderer: new GraphicsNewRenderer(LampShadows: 1, ContactShadows: 0, Occlusion: 1, OcclusionSlices: 3, OcclusionSteps: 4, + OcclusionLighting: false, CabProbe: 1, CabProbeInterval: 8, CabProbeReflections: true, ScreenReflections: 1, + RayTracing: false, VolumetricFog: true, FogQuality: 1, Clouds: 1, CloudShadows: true, Grass: true, + GrassDistance: 50, GrassDensity: 0.75, Precipitation: 32768, Bloom: true)); /// /// Where a player with no settings of their own starts. The simulator's own values (Globals.h), @@ -329,27 +303,27 @@ public sealed record GraphicsPreset( /// with levels to go under this one. /// public static readonly GraphicsPreset High = new( - "PresetHigh", TextureSize: 4096, CabTextureSize: 4096, Anisotropy: 8, Multisampling: 2, + "PresetHigh", TextureSize: 4096, CabTextureSize: 4096, Anisotropy: 8, AntiAliasing: AntiAliasingMode.Msaa4, DynamicLights: 7, SmokeFidelity: 2, Shadows: true, ShadowMap: 2048, ShadowRange: 250, ShadowRank: 3, EnvMap: true, ReflectionsFidelity: 1, ReflectionsRate: 0.1, ExtraEffects: true, SplineFidelity: 2, DrawRange: 2500, DistanceFactor: 1, BufferScale: 100, SleeperDistance: 250, DrawRangeFactor: 3, ReflectedInstances: 750, ReflectedVehicles: 250, CabShadowsRange: 40, - NewRenderer: new GraphicsNewRenderer(LampShadows: "medium", ContactShadows: "no", Occlusion: "gtao", OcclusionQuality: "3 4", - OcclusionLighting: true, CabProbe: "medium", CabProbeInterval: 4, CabProbeReflections: true, ScreenReflections: "medium", - RayTracing: false, Fog: "volumetric", FogQuality: "medium", Clouds: "medium", CloudShadows: true, Grass: true, - GrassDistance: 60, GrassFade: 70, GrassDensity: 1, Precipitation: 65536, Bloom: true)); + NewRenderer: new GraphicsNewRenderer(LampShadows: 2, ContactShadows: 0, Occlusion: 2, OcclusionSlices: 3, OcclusionSteps: 4, + OcclusionLighting: true, CabProbe: 2, CabProbeInterval: 4, CabProbeReflections: true, ScreenReflections: 2, + RayTracing: false, VolumetricFog: true, FogQuality: 2, Clouds: 2, CloudShadows: true, Grass: true, + GrassDistance: 60, GrassDensity: 1, Precipitation: 65536, Bloom: true)); /// For a strong card. A map of 4096 doubles the cascades, so the range here reaches 800 m. public static readonly GraphicsPreset VeryHigh = new( - "PresetVeryHigh", TextureSize: 8192, CabTextureSize: 8192, Anisotropy: 16, Multisampling: 3, + "PresetVeryHigh", TextureSize: 8192, CabTextureSize: 8192, Anisotropy: 16, AntiAliasing: AntiAliasingMode.Msaa8, DynamicLights: 7, SmokeFidelity: 3, Shadows: true, ShadowMap: 4096, ShadowRange: 400, ShadowRank: 3, EnvMap: true, ReflectionsFidelity: 2, ReflectionsRate: 2, ExtraEffects: true, SplineFidelity: 3, DrawRange: 5000, DistanceFactor: 2, BufferScale: 150, SleeperDistance: 2000, DrawRangeFactor: 3, ReflectedInstances: 1500, ReflectedVehicles: 1000, CabShadowsRange: 70, - NewRenderer: new GraphicsNewRenderer(LampShadows: "high", ContactShadows: "medium", Occlusion: "gtao", OcclusionQuality: "4 6", - OcclusionLighting: true, CabProbe: "high", CabProbeInterval: 3, CabProbeReflections: true, ScreenReflections: "high", - RayTracing: false, Fog: "volumetric", FogQuality: "high", Clouds: "high", CloudShadows: true, Grass: true, - GrassDistance: 120, GrassFade: 140, GrassDensity: 1.5, Precipitation: 98304, Bloom: true)); + NewRenderer: new GraphicsNewRenderer(LampShadows: 3, ContactShadows: 2, Occlusion: 2, OcclusionSlices: 4, OcclusionSteps: 6, + OcclusionLighting: true, CabProbe: 3, CabProbeInterval: 3, CabProbeReflections: true, ScreenReflections: 3, + RayTracing: false, VolumetricFog: true, FogQuality: 3, Clouds: 3, CloudShadows: true, Grass: true, + GrassDistance: 120, GrassDensity: 1.5, Precipitation: 131072, Bloom: true)); /// /// Every value at the top of what the screen offers. Not the best picture throughout: a map @@ -357,15 +331,15 @@ public sealed record GraphicsPreset( /// than at . /// public static readonly GraphicsPreset Ultra = new( - "PresetUltra", TextureSize: 8192, CabTextureSize: 8192, Anisotropy: 16, Multisampling: 3, + "PresetUltra", TextureSize: 8192, CabTextureSize: 8192, Anisotropy: 16, AntiAliasing: AntiAliasingMode.Msaa8, DynamicLights: 7, SmokeFidelity: 4, Shadows: true, ShadowMap: 8192, ShadowRange: 600, ShadowRank: 3, EnvMap: true, ReflectionsFidelity: 2, ReflectionsRate: 60, ExtraEffects: true, SplineFidelity: 4, DrawRange: 10000, DistanceFactor: 3, BufferScale: 200, SleeperDistance: 5000, DrawRangeFactor: 3, ReflectedInstances: 3000, ReflectedVehicles: 2000, CabShadowsRange: 100, - NewRenderer: new GraphicsNewRenderer(LampShadows: "high", ContactShadows: "high", Occlusion: "gtao", OcclusionQuality: "6 8", - OcclusionLighting: true, CabProbe: "high", CabProbeInterval: 2, CabProbeReflections: true, ScreenReflections: "high", - RayTracing: true, Fog: "volumetric", FogQuality: "ultra", Clouds: "ultra", CloudShadows: true, Grass: true, - GrassDistance: 300, GrassFade: 350, GrassDensity: 2, Precipitation: 131072, Bloom: true), + NewRenderer: new GraphicsNewRenderer(LampShadows: 3, ContactShadows: 3, Occlusion: 2, OcclusionSlices: 6, OcclusionSteps: 8, + OcclusionLighting: true, CabProbe: 3, CabProbeInterval: 2, CabProbeReflections: true, ScreenReflections: 3, + RayTracing: true, VolumetricFog: true, FogQuality: 4, Clouds: 4, CloudShadows: true, Grass: true, + GrassDistance: 300, GrassDensity: 2, Precipitation: 262144, Bloom: true), Extras: new GraphicsExtras(MotionBlur: true, ChromaticAberration: true, Smoke: true, PythonUpdateMs: 200), Top: new GraphicsTop(ShadowAngleLimit: 0.2, TargetFps: 0, ScreenWindowFps: 200, TextureRelease: 0, Screens: true, ScreenMipmaps: true, CompressTextures: false, SkipDecor: false, @@ -378,7 +352,9 @@ public sealed record GraphicsPreset( s.MaxTextureSize = TextureSize; s.MaxCabTextureSize = CabTextureSize; s.TextureFiltering = Anisotropy; - s.Multisampling = Multisampling; + if (!GraphicsRules.IsDlss(s.AntiAliasing)) + s.AntiAliasing = AntiAliasing; + s.CabShadowsRange = CabShadowsRange; s.DynamicLights = DynamicLights; s.SmokeFidelity = SmokeFidelity; s.RenderShadows = Shadows; @@ -397,7 +373,6 @@ public sealed record GraphicsPreset( s.DrawRangeFactor = DrawRangeFactor; s.Extra[ReflectedInstancesKey] = ReflectedInstances.ToString(CultureInfo.InvariantCulture); s.Extra[ReflectedVehiclesKey] = ReflectedVehicles.ToString(CultureInfo.InvariantCulture); - s.CabShadowsRange = CabShadowsRange; NewRenderer.ApplyTo(s); Extras?.ApplyTo(s); Top?.ApplyTo(s); @@ -406,7 +381,7 @@ public sealed record GraphicsPreset( /// Whether is at this level throughout. public bool Matches(SimulatorSettings s) => s.MaxTextureSize == TextureSize && s.MaxCabTextureSize == CabTextureSize && - Near(s.TextureFiltering, Anisotropy) && s.Multisampling == Multisampling && + Near(s.TextureFiltering, Anisotropy) && (GraphicsRules.IsDlss(s.AntiAliasing) || s.AntiAliasing == AntiAliasing) && s.DynamicLights == DynamicLights && s.SmokeFidelity == SmokeFidelity && s.RenderShadows == Shadows && s.ShadowMapResolution == ShadowMap && s.ShadowProjectionRange == ShadowRange && s.ShadowRankCutoff == ShadowRank && @@ -419,7 +394,7 @@ public sealed record GraphicsPreset( Near(NumberOf(s, ReflectedInstancesKey), ReflectedInstances) && Near(NumberOf(s, ReflectedVehiclesKey), ReflectedVehicles) && s.CabShadowsRange == CabShadowsRange && - GraphicsNewRenderer.Of(s) == NewRenderer && + GraphicsNewRenderer.Of(s) == NewRenderer.For(s) && (Extras is null || GraphicsExtras.Of(s) == Extras) && (Top is null || GraphicsTop.Of(s) == Top); @@ -442,7 +417,7 @@ public sealed record GraphicsPreset( /// /// A part of the picture with a level of its own, as its section on the screen has it. The rest -/// of a level - the buffer, the frame pacing, the sky and the cab - goes only with the whole. +/// of a level - the buffer, the anti-aliasing, the post-processing switches, the frame pacing, the sky and the cab - goes only with the whole. /// /// The eu07.ini keys it sets. public sealed record GraphicsGroup(IReadOnlyList Keys) @@ -455,31 +430,42 @@ public sealed record GraphicsGroup(IReadOnlyList Keys) public static readonly GraphicsGroup Textures = new(new[] { - "maxtexturesize", "maxcabtexturesize", "anisotropicfiltering", "multisampling", + "maxtexturesize", "maxcabtexturesize", "anisotropicfiltering", "compresstex", GraphicsTop.TextureReleaseKey }); - public static readonly GraphicsGroup Effects = new(new[] - { - "dynamiclights", "gfx.smoke.fidelity", "gfx.extraeffects", "gfx.postfx.motionblur.enabled", - "gfx.postfx.chromaticaberration.enabled", "gfx.smoke" - }); - public static readonly GraphicsGroup Shadows = new(new[] { - "shadows", "gfx.shadowmap.enabled", "shadowtune", "gfx.shadow.rank.cutoff", - "gfx.shadow.angle.min", "gfx.shadows.cab.range" + "shadows", "gfx.shadowmap.enabled", "shadowtune", "gfx.shadow.rank.cutoff", "gfx.shadow.angle.min", "gfx.shadows.cab.range" + }); + + public static readonly GraphicsGroup Lights = new(new[] + { + "dynamiclights", "gfx.lights.shadows", "gfx.contactshadows", "gfx.postfx.ao", "gfx.postfx.ao.quality", + "gfx.postfx.ao.lighting", "gfx.cabprobe", "gfx.cabprobe.interval", "gfx.cabprobe.reflections" }); public static readonly GraphicsGroup Reflections = new(new[] { "gfx.envmap.enabled", "gfx.reflections.fidelity", "gfx.reflections.framerate", - GraphicsPreset.ReflectedInstancesKey, GraphicsPreset.ReflectedVehiclesKey + GraphicsPreset.ReflectedInstancesKey, GraphicsPreset.ReflectedVehiclesKey, "gfx.ssr", "gfx.raytracing" }); - public static readonly GraphicsGroup NewRenderer = new(GraphicsNewRenderer.Keys); + public static readonly GraphicsGroup Atmosphere = new(new[] + { + "gfx.fog", "gfx.fog.quality", "gfx.clouds", "gfx.clouds.shadows" + }); - public static readonly IReadOnlyList All = new[] { DrawRange, Textures, Effects, Shadows, Reflections, NewRenderer }; + public static readonly GraphicsGroup Nature = new(new[] + { + "gfx.grass", "gfx.grass.distance", "gfx.grass.fadedistance", "gfx.grass.density", + "gfx.smoke", "gfx.smoke.fidelity", "gfx.precipitation.particles" + }); + + public static readonly IReadOnlyList All = new[] + { + DrawRange, Textures, Shadows, Lights, Reflections, Atmosphere, Nature + }; } /// What applying a level touched. diff --git a/StarterNG/Domain/Settings/GraphicsRules.cs b/StarterNG/Domain/Settings/GraphicsRules.cs new file mode 100644 index 0000000..16ae082 --- /dev/null +++ b/StarterNG/Domain/Settings/GraphicsRules.cs @@ -0,0 +1,126 @@ +namespace StarterNG.Domain.Settings; + +/// +/// What the simulator does with combinations of graphics settings, mirrored here so +/// the starter only offers what takes effect and writes eu07.ini the way the +/// simulator will run it (utilities/Globals.cpp, rendering/vulkan14, rendering/opengl46). +/// +public static class GraphicsRules +{ + public static bool IsDlss(AntiAliasingMode mode) => mode is AntiAliasingMode.Dlss or AntiAliasingMode.Dlaa; + + public static bool IsTemporal(AntiAliasingMode mode) => mode == AntiAliasingMode.Taa || IsDlss(mode); + + public static bool IsVulkan(SimulatorSettings s) => s.Backend == GraphicsBackend.Vulkan; + + /// + /// DLSS is the Vulkan renderer's only (the OpenGL one takes TAA instead), and the + /// DLSS SDK has no macOS libraries. + /// + public static bool DlssAllowed(SimulatorSettings s, bool dlssPlatform) => dlssPlatform && IsVulkan(s); + + public static bool DlssActive(SimulatorSettings s, bool dlssPlatform) => + DlssAllowed(s, dlssPlatform) && IsDlss(s.AntiAliasing); + + /// The frame generation goes with gfx.antialiasing dlss or dlaa, nothing else. + public static bool FrameGenerationAllowed(SimulatorSettings s, bool dlssPlatform) => DlssActive(s, dlssPlatform); + + /// Reflex (VK_NV_low_latency2) is the Vulkan renderer's only. + public static bool ReflexAllowed(SimulatorSettings s) => IsVulkan(s); + + /// The frame generation turns Reflex on whatever gfx.reflex says. + public static bool ReflexForced(SimulatorSettings s, bool dlssPlatform) => + FrameGenerationAllowed(s, dlssPlatform) && s.FrameGeneration > 1; + + /// Ray traced reflections: the Vulkan renderer on a GPU with ray queries. + public static bool RayTracingAllowed(SimulatorSettings s) => IsVulkan(s); + + /// The render scale is ignored by DLAA, which always draws at the size of the window. + public static bool BufferScaleUsed(SimulatorSettings s, bool dlssPlatform) => + !(DlssActive(s, dlssPlatform) && s.AntiAliasing == AntiAliasingMode.Dlaa); + + /// TAA and DLSS run in the post-processing which gfx.skippipeline leaves out. + public static bool TemporalWithoutPipeline(SimulatorSettings s) => s.SkipPipeline && IsTemporal(s.AntiAliasing); + + /// With the clustered lights the headlights come from the clusters; dynamiclights only caps the old path. + public static bool DynamicLightsUsed(SimulatorSettings s) => !s.LightsClustered; + + /// The ambient occlusion goes into the lighting only with GTAO. + public static bool AoLightingUsed(SimulatorSettings s) => s.AmbientOcclusion == 2; + + /// + /// The AO in the lighting needs the depth of the whole scene drawn before its colour. SSR, contact + /// shadows and gfx.depthprepass.scene draw it anyway; without them it is an extra pass (CPU time on OpenGL). + /// + public static bool AoLightingAddsDepthPrepass(SimulatorSettings s) => + AoLightingUsed(s) && s.AoLighting && s.Ssr == 0 && s.ContactShadows == 0 && !s.DepthPrepassScene; + + public static bool CabProbeUsed(SimulatorSettings s) => s.CabProbe > 0; + + /// + /// The light bounced around the cab comes from the cab lamp as a light, which needs the clustered lights + /// and gfx.lights.cab; without them the probe gives the reflections only. + /// + public static bool CabProbeLights(SimulatorSettings s) => CabProbeUsed(s) && s.LightsClustered && s.CabLampLight; + + /// + /// Brings the settings to what the simulator would run with: the Vulkan-only + /// features off on OpenGL, DLSS falling back to TAA, values within the ranges + /// the simulator clamps them to. + /// + public static void Normalize(SimulatorSettings s, bool dlssPlatform) + { + if (IsDlss(s.AntiAliasing) && !DlssAllowed(s, dlssPlatform)) + s.AntiAliasing = AntiAliasingMode.Taa; + + s.FrameGeneration = FrameGenerationAllowed(s, dlssPlatform) ? Clamp(s.FrameGeneration, 1, 4) : 1; + s.Reflex = ReflexAllowed(s) ? Clamp(s.Reflex, 0, 2) : 0; + if (!RayTracingAllowed(s)) + s.RayTracing = false; + + if (!IsKnownDlssPreset(s.DlssPreset)) + s.DlssPreset = "default"; + + s.MaxTextureSize = PowerOfTwo(s.MaxTextureSize, 64, 8192); + s.MaxCabTextureSize = PowerOfTwo(s.MaxCabTextureSize, 512, 8192); + s.ShadowMapResolution = PowerOfTwo(s.ShadowMapResolution, 512, 8192); + s.BufferScalePercent = Clamp(s.BufferScalePercent, 25, 200); + s.LightShadows = Clamp(s.LightShadows, 0, 3); + s.ContactShadows = Clamp(s.ContactShadows, 0, 3); + s.AmbientOcclusion = Clamp(s.AmbientOcclusion, 0, 2); + s.AoSlices = Clamp(s.AoSlices, 1, 8); + s.AoSteps = Clamp(s.AoSteps, 1, 16); + s.CabProbe = Clamp(s.CabProbe, 0, 3); + s.CabProbeInterval = Clamp(s.CabProbeInterval, 1, 120); + s.CabProbeBounce = s.CabProbeBounce < 0 ? 0 : (s.CabProbeBounce > 4 ? 4 : s.CabProbeBounce); + s.Ssr = Clamp(s.Ssr, 0, 3); + s.RayTracingRange = Clamp(s.RayTracingRange, 10, 5000); + s.FogQuality = Clamp(s.FogQuality, 1, 4); + s.Clouds = Clamp(s.Clouds, 0, 4); + s.GrassDistance = Clamp(s.GrassDistance, 5, 500); + s.GrassDensity = s.GrassDensity < 0 ? 0 : (s.GrassDensity > 4 ? 4 : s.GrassDensity); + s.PrecipitationParticles = Clamp(s.PrecipitationParticles, 0, 1 << 20); + } + + public static readonly string[] DlssPresets = { "default", "j", "k", "l", "m" }; + + public static bool IsKnownDlssPreset(string? preset) + { + foreach (string known in DlssPresets) + if (string.Equals(known, preset, System.StringComparison.OrdinalIgnoreCase)) + return true; + return false; + } + + private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v); + + /// The power of two nearest below, as the simulator rounds texture and shadow map sizes. + private static int PowerOfTwo(int v, int lo, int hi) + { + v = Clamp(v, lo, hi); + int p = lo; + while (p * 2 <= v) + p *= 2; + return p; + } +} diff --git a/StarterNG/Domain/Settings/SettingReach.cs b/StarterNG/Domain/Settings/SettingReach.cs index 3bc5fc5..c7711dc 100644 --- a/StarterNG/Domain/Settings/SettingReach.cs +++ b/StarterNG/Domain/Settings/SettingReach.cs @@ -128,5 +128,5 @@ public static class SettingReach _ => RendererKind.Legacy }; - private static bool IsVulkan(string renderer) => renderer.ToLowerInvariant() is "vulkan" or "vulkan14"; + public static bool IsVulkan(string renderer) => renderer.ToLowerInvariant() is "vulkan" or "vulkan14"; } diff --git a/StarterNG/Domain/Settings/SimulatorKeys.cs b/StarterNG/Domain/Settings/SimulatorKeys.cs index 3193002..c391fe2 100644 --- a/StarterNG/Domain/Settings/SimulatorKeys.cs +++ b/StarterNG/Domain/Settings/SimulatorKeys.cs @@ -5,7 +5,7 @@ using System.Linq; namespace StarterNG.Domain.Settings; -public enum SimulatorKeyKind { Toggle, Number, Text, Choice } +public enum SimulatorKeyKind { Toggle, Number, Text } /// /// An eu07.ini key the simulator reads that needs no logic of the starter's own: @@ -24,25 +24,13 @@ public sealed record SimulatorKey( double Step = 1, string Unit = "", bool ZeroIsOff = false, - int Repeat = 1, - string[]? Words = null) + int Repeat = 1) { /// The label's translation key; the explanation's adds "Desc". public string LabelKey => "Ini." + Key; public string DescriptionKey => LabelKey + ".Desc"; - /// The translation key of a word of a choice; the empty one leaves the key out of the file. - public static string WordKey(string word) => - "IniWord." + (word.Length == 0 ? "same" : word.Replace(' ', '_')); - - /// The word of among those offered, or else the default: the row then shows that and leaves the file alone. - public string ParseChoice(string raw) - { - string word = string.Join(' ', raw.ToLowerInvariant().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries)); - return Words is not null && Words.Contains(word) ? word : Default; - } - public static bool ParseToggle(string raw) => FirstToken(raw).ToLowerInvariant() is "yes" or "true" or "1" or "tak"; @@ -81,8 +69,6 @@ public static class SimulatorKeys public const string Physics = "physics"; public const string Advanced = "advanced"; - private const string NewRenderer = "GfxColNewRenderer"; - private static SimulatorKey Toggle(string key, bool value, string category, string section) => new(key, SimulatorKeyKind.Toggle, SimulatorKey.FormatToggle(value), category, section); @@ -95,9 +81,6 @@ public static class SimulatorKeys private static SimulatorKey Text(string key, string category, string section) => new(key, SimulatorKeyKind.Text, string.Empty, category, section); - private static SimulatorKey Choice(string key, string value, string category, string section, params string[] words) => - new(key, SimulatorKeyKind.Choice, value, category, section, Words: words); - public static SimulatorKey Named(string key) => All.First(k => k.Key == key); public static IReadOnlyList All { get; } = new[] @@ -131,38 +114,6 @@ public static class SimulatorKeys Toggle("newaircouplers", true, Graphics, "IniSectionRanges"), Toggle("smoothtraction", true, Graphics, "IniSectionRanges"), - // Read by the OpenGL 4.6 and Vulkan 1.4 simulators; the older ones pass them over. - Choice("gfx.antialiasing", "", Graphics, NewRenderer, "", "taa", "dlss", "dlaa"), - Choice("gfx.dlss.preset", "default", Graphics, NewRenderer, "default", "k", "j", "m", "l"), - Choice("gfx.framegeneration", "off", Graphics, NewRenderer, "off", "x2", "x3", "x4"), - Choice("gfx.reflex", "off", Graphics, NewRenderer, "off", "on", "boost"), - Toggle("gfx.raytracing", false, Graphics, NewRenderer), - Number("gfx.raytracing.range", 500, Graphics, NewRenderer, 10, 5000, 10, " m"), - Text("gfx.vulkan.device", Graphics, NewRenderer), - Toggle("gfx.lights.clustered", true, Graphics, NewRenderer), - Choice("gfx.lights.shadows", "medium", Graphics, NewRenderer, "no", "low", "medium", "high"), - Choice("gfx.contactshadows", "no", Graphics, NewRenderer, "no", "low", "medium", "high"), - Toggle("gfx.shadow.sdsm", false, Graphics, NewRenderer), - Choice("gfx.postfx.ao", "gtao", Graphics, NewRenderer, "no", "ssao", "gtao"), - Choice("gfx.postfx.ao.quality", "3 4", Graphics, NewRenderer, "3 4", "4 6", "6 8"), - Toggle("gfx.postfx.ao.lighting", true, Graphics, NewRenderer), - Choice("gfx.ssr", "medium", Graphics, NewRenderer, "no", "low", "medium", "high"), - Choice("gfx.cabprobe", "medium", Graphics, NewRenderer, "no", "low", "medium", "high"), - Number("gfx.cabprobe.interval", 4, Graphics, NewRenderer, 1, 16, 1), - Toggle("gfx.cabprobe.reflections", true, Graphics, NewRenderer), - Number("gfx.cabprobe.bounce", 1, Graphics, NewRenderer, 0, 4, 0.25), - Choice("gfx.fog", "volumetric", Graphics, NewRenderer, "analytic", "volumetric"), - Choice("gfx.fog.quality", "medium", Graphics, NewRenderer, "low", "medium", "high", "ultra"), - Choice("gfx.clouds", "medium", Graphics, NewRenderer, "no", "low", "medium", "high", "ultra"), - Toggle("gfx.clouds.shadows", true, Graphics, NewRenderer), - Toggle("gfx.grass", true, Graphics, NewRenderer), - Number("gfx.grass.distance", 60, Graphics, NewRenderer, 5, 300, 5, " m"), - Number("gfx.grass.fadedistance", 70, Graphics, NewRenderer, 10, 350, 10, " m"), - Number("gfx.grass.density", 1, Graphics, NewRenderer, 0, 2, 0.25, "×"), - Number("gfx.precipitation.particles", 65536, Graphics, NewRenderer, 0, 131072, 16384), - Toggle("gfx.postfx.bloom", true, Graphics, NewRenderer), - Toggle("gfx.postfx.autoexposure", true, Graphics, NewRenderer), - Number("sound.maxsources", 30, Sound, "IniSectionAudio", 8, 256, 8), Toggle("sound.asyncload", false, Sound, "IniSectionAudio"), diff --git a/StarterNG/Domain/Settings/SimulatorSettings.cs b/StarterNG/Domain/Settings/SimulatorSettings.cs index 64183fc..d600399 100644 --- a/StarterNG/Domain/Settings/SimulatorSettings.cs +++ b/StarterNG/Domain/Settings/SimulatorSettings.cs @@ -8,6 +8,12 @@ public enum ConsistReadyOverride { Scenery = 0, AlwaysCold = 1, AlwaysReady = 2 public enum ExeProblem { None, NotFound, NotExecutable, WrongPlatform, NotAProgram } +// gfxrenderer: the OpenGL renderer ("full", read by every build) or the Vulkan 1.4 one. +public enum GraphicsBackend { OpenGL = 0, Vulkan = 1 } + +// gfx.antialiasing. Dlss and Dlaa are the Vulkan renderer's only; the simulator falls back to TAA elsewhere. +public enum AntiAliasingMode { Off, Msaa2, Msaa4, Msaa8, Taa, Dlss, Dlaa } + public sealed class SimulatorSettings { public bool LanguageWasSet { get; internal set; } @@ -49,13 +55,18 @@ public sealed class SimulatorSettings public bool LogMissingVehicleFiles; public int RenderEngine; + public GraphicsBackend Backend => RenderEngine == VulkanEngine ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGL; + public string VulkanDevice = ""; public int Width = 1280; public int Height = 720; public int BufferScalePercent = 100; public int MaxTextureSize = 4096; public int MaxCabTextureSize = 4096; public double TextureFiltering = 8; - public int Multisampling = 2; + public AntiAliasingMode AntiAliasing = AntiAliasingMode.Msaa4; + public string DlssPreset = "default"; + public int FrameGeneration = 1; + public int Reflex; public int DynamicLights = 7; public double DrawRangeFactor = 3.0; public bool VSync; @@ -65,14 +76,48 @@ public sealed class SimulatorSettings public bool MotionBlur = true; public bool ExtraEffects = true; public bool EnvMap = true; - public bool UseVbo = true; public bool RenderShadows = true; + public bool ShadowSdsm; + public int LightShadows = 2; + public int ContactShadows; + public bool LightsClustered = true; + public int AmbientOcclusion = 2; + public int AoSlices = 3; + public int AoSteps = 4; + public bool AoLighting = true; + public int CabProbe = 2; + public int CabProbeInterval = 4; + public double CabProbeBounce = 1.0; + public bool CabProbeReflections = true; + + // Read from eu07.ini only, for the dependencies shown: the starter offers no switch for them. + public bool CabLampLight { get; internal set; } = true; + public bool DepthPrepassScene { get; internal set; } + public int Ssr = 2; + public bool RayTracing; + public int RayTracingRange = 500; + public bool VolumetricFog = true; + public int FogQuality = 2; + public int Clouds = 2; + public bool CloudShadows = true; + public bool Grass = true; + public int GrassDistance = 60; + public double GrassDensity = 1.0; + public int PrecipitationParticles = 65536; + public bool Bloom = true; + public bool AutoExposure = true; + + // Only the older simulators read these. + public bool UseVbo = true; + public bool RenderAngleVulkan; + public bool UseGLES; + public int CabShadowsRange; + /// Updates of the reflections a second; the simulator's own is one in five minutes. public double ReflectionsFramerate = 1 / 300.0; public int ShadowMapResolution = 2048; /// The third number of shadowtune; the second and fourth go unread. public int ShadowProjectionRange = 250; - public int CabShadowsRange; public int ShadowRankCutoff = 3; public int ReflectionsFidelity; public int FieldOfView = 45; @@ -82,7 +127,6 @@ public sealed class SimulatorSettings public double FpsLimit = 30; public double ShadowAngleLimit = 0.2; public bool FullscreenWindowed; - public bool RenderAngleVulkan; public int SplineFidelity = 1; public bool FullPhysics = true; @@ -110,7 +154,6 @@ public sealed class SimulatorSettings public bool CompressTextures = true; public bool ScaleSpeculars = true; - public bool UseGLES; public bool ShaderGamma; public bool ScreenMipmaps = true; public bool ExtendedModelConversion; @@ -163,6 +206,9 @@ public sealed class SimulatorSettings /// public static readonly string[] RenderEngines = { "full", "legacy", "simple", "experimental", "vulkan" }; + /// Where "vulkan" stands among . + public const int VulkanEngine = 4; + /// Where stands among , as the simulator reads it. public static int RenderEngineOf(string word) => word.Trim().ToLowerInvariant() switch { diff --git a/StarterNG/Domain/Settings/Steps.cs b/StarterNG/Domain/Settings/Steps.cs index 8ce5c32..8e8cad0 100644 --- a/StarterNG/Domain/Settings/Steps.cs +++ b/StarterNG/Domain/Settings/Steps.cs @@ -26,13 +26,6 @@ public static class Steps /// anisotropicfiltering: 16 is the most any card offers. public static readonly double[] Anisotropy = { 1, 2, 4, 8, 16 }; - /// - /// multisampling, as the power of two of the samples: none to x8. The simulator - /// takes 4 (x16) too, which only some cards can do - on one that cannot, the - /// frame buffer fails to build - so it stays in the file but off the slider. - /// - public static readonly double[] MultisamplingLevels = { 0, 1, 2, 3 }; - /// /// with among them: one /// eu07.ini has that is none of the steps stays what it is. diff --git a/StarterNG/Infrastructure/Settings/ExecutableLocator.cs b/StarterNG/Infrastructure/Settings/ExecutableLocator.cs index dbb1a9b..81bbed7 100644 --- a/StarterNG/Infrastructure/Settings/ExecutableLocator.cs +++ b/StarterNG/Infrastructure/Settings/ExecutableLocator.cs @@ -268,6 +268,33 @@ public sealed class ExecutableLocator } /// A path as gives it, which may be relative to the installation. + /// Whether the DLSS and frame generation libraries lie next to the simulator: without them it cannot use either. + public (bool Dlss, bool FrameGeneration) DlssLibraries(string executable) + { + try + { + // Resolve hands back either a path of its own search or the bare name of the pick. + string full = _files.FileExists(executable) ? executable : FullPath(executable); + string? directory = Path.GetDirectoryName(full); + if (string.IsNullOrEmpty(directory) || !_files.DirectoryExists(directory)) + return (false, false); + + bool windowsBuild = _files.FileExists(full) + ? Format(full) == BinaryFormat.PortableExecutable + : _environment.IsWindows; + + return windowsBuild + ? (Has(directory, "nvngx_dlss.dll"), Has(directory, "nvngx_dlssg.dll")) + : (Has(directory, "libnvidia-ngx-dlss.so*"), Has(directory, "libnvidia-ngx-dlssg.so*")); + } + catch (Exception) + { + return (false, false); + } + } + + private bool Has(string directory, string pattern) => _files.GetFiles(directory, pattern).Count > 0; + public string FullPath(string path) => Path.IsPathRooted(path) ? path : Path.Combine(_paths.Root, path); diff --git a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs index 3b0fc96..2e87b81 100644 --- a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs +++ b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs @@ -9,6 +9,16 @@ namespace StarterNG.Infrastructure.Settings; public sealed class SettingsSerializer { + private readonly bool _dlssPlatform; + + /// Whether the simulator can have DLSS here at all (not on macOS). + public SettingsSerializer(bool dlssPlatform = true) + { + _dlssPlatform = dlssPlatform; + } + + public bool DlssPlatform => _dlssPlatform; + public void Read(SimulatorSettings s, ConfigFile c) { s.LanguageWasSet = c.Has("lang"); @@ -67,13 +77,17 @@ public sealed class SettingsSerializer s.LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false); s.RenderEngine = SimulatorSettings.RenderEngineOf(c.GetString("gfxrenderer", "full")); + s.VulkanDevice = c.GetString("gfx.vulkan.device", ""); s.Width = c.GetInt("width", 1280); s.Height = c.GetInt("height", 720); s.BufferScalePercent = ReadBufferScale(c); s.MaxTextureSize = c.GetInt("maxtexturesize", 4096); s.MaxCabTextureSize = c.GetInt("maxcabtexturesize", 4096); s.TextureFiltering = Math.Max(1, c.GetDouble("anisotropicfiltering", 8)); - s.Multisampling = Clamp(c.GetInt("multisampling", 2), 0, 4); + s.AntiAliasing = ReadAntiAliasing(c); + s.DlssPreset = c.GetString("gfx.dlss.preset", "default").ToLowerInvariant(); + s.FrameGeneration = ParseFrameGeneration(FirstToken(c, "gfx.framegeneration")); + s.Reflex = ParseReflex(FirstToken(c, "gfx.reflex")); s.DynamicLights = Clamp(c.GetInt("dynamiclights", 7), 0, 7); s.DrawRangeFactor = c.GetDouble("gfx.drawrange.factor.max", 3.0); s.VSync = c.GetBool("vsync", false); @@ -85,6 +99,34 @@ public sealed class SettingsSerializer s.EnvMap = c.GetBool("gfx.envmap.enabled", true); s.UseVbo = c.GetBool("usevbo", true); s.RenderShadows = c.GetBool("shadows", true); + s.ShadowSdsm = c.GetBool("gfx.shadow.sdsm", false); + s.LightShadows = ParseLevel(FirstToken(c, "gfx.lights.shadows"), 2, 3); + s.ContactShadows = ParseLevel(FirstToken(c, "gfx.contactshadows"), 0, 3); + s.LightsClustered = c.GetBool("gfx.lights.clustered", true); + s.AmbientOcclusion = ParseAmbientOcclusion(FirstToken(c, "gfx.postfx.ao")); + var aoQuality = c.GetTokens("gfx.postfx.ao.quality"); + s.AoSlices = aoQuality.Length >= 1 ? ParseInt(aoQuality[0], 3) : 3; + s.AoSteps = aoQuality.Length >= 2 ? ParseInt(aoQuality[1], 4) : 4; + s.AoLighting = c.GetBool("gfx.postfx.ao.lighting", true); + s.CabProbe = ParseCabProbe(FirstToken(c, "gfx.cabprobe")); + s.CabProbeInterval = c.GetInt("gfx.cabprobe.interval", 4); + s.CabProbeBounce = c.GetDouble("gfx.cabprobe.bounce", 1.0); + s.CabProbeReflections = c.GetBool("gfx.cabprobe.reflections", true); + s.CabLampLight = c.GetBool("gfx.lights.cab", true); + s.DepthPrepassScene = c.GetBool("gfx.depthprepass.scene", false); + s.Ssr = ParseLevel(FirstToken(c, "gfx.ssr"), 2, 3); + s.RayTracing = c.GetBool("gfx.raytracing", false); + s.RayTracingRange = (int)Math.Round(c.GetDouble("gfx.raytracing.range", 500)); + s.VolumetricFog = ParseVolumetricFog(FirstToken(c, "gfx.fog")); + s.FogQuality = ParseFogQuality(FirstToken(c, "gfx.fog.quality")); + s.Clouds = ParseLevel(FirstToken(c, "gfx.clouds"), 2, 4); + s.CloudShadows = c.GetBool("gfx.clouds.shadows", true); + s.Grass = c.GetBool("gfx.grass", true); + s.GrassDistance = (int)Math.Round(c.GetDouble("gfx.grass.distance", 60)); + s.GrassDensity = c.GetDouble("gfx.grass.density", 1.0); + s.PrecipitationParticles = c.GetInt("gfx.precipitation.particles", 65536); + s.Bloom = c.GetBool("gfx.postfx.bloom", true); + s.AutoExposure = c.GetBool("gfx.postfx.autoexposure", true); // A negative rate counts as its size; none at all, as never. double reflections = Math.Abs(c.GetDouble("gfx.reflections.framerate", 1 / 300.0)); s.ReflectionsFramerate = reflections > 0 ? reflections : 1 / 300.0; @@ -174,6 +216,8 @@ public sealed class SettingsSerializer foreach (var key in SimulatorKeys.All) if (c.Has(key.Key)) s.Extra[key.Key] = c.GetRaw(key.Key) ?? string.Empty; + + GraphicsRules.Normalize(s, _dlssPlatform); } /// @@ -184,7 +228,7 @@ public sealed class SettingsSerializer public static readonly string[] FixedLengthKeys = { "uart", "uarttune", "uartfeature" }; /// Keys written only when they say something, besides those of . - public static readonly string[] OptionalKeys = { "fpslimit", "gfx.angleplatform" }; + public static readonly string[] OptionalKeys = { "fpslimit", "gfx.angleplatform", "gfx.vulkan.device" }; /// Keys no simulator reads any more, taken out of the file on a save. public static readonly string[] ObsoleteKeys = @@ -195,6 +239,8 @@ public sealed class SettingsSerializer public void Write(SimulatorSettings s, ConfigFile c) { + GraphicsRules.Normalize(s, _dlssPlatform); + c.Set("lang", string.IsNullOrWhiteSpace(s.LanguageCode) ? "en" : s.LanguageCode.Trim().ToLowerInvariant()); @@ -241,6 +287,7 @@ public sealed class SettingsSerializer c.SetBool("starter.logmissingvehicles", s.LogMissingVehicleFiles); c.Set("gfxrenderer", SimulatorSettings.RenderEngines[Clamp(s.RenderEngine, 0, SimulatorSettings.RenderEngines.Length - 1)]); + if (!string.IsNullOrWhiteSpace(s.VulkanDevice)) c.Set("gfx.vulkan.device", s.VulkanDevice.Trim()); c.SetInt("width", s.Width); c.SetInt("height", s.Height); c.SetInt("starter.bufferscale", s.BufferScalePercent); @@ -253,7 +300,13 @@ public sealed class SettingsSerializer c.SetInt("maxtexturesize", s.MaxTextureSize); c.SetInt("maxcabtexturesize", s.MaxCabTextureSize); c.SetDouble("anisotropicfiltering", s.TextureFiltering); - c.SetInt("multisampling", s.Multisampling); + // multisampling is what older simulators read; gfx.antialiasing overrides it in the new ones, but only from + // a later line, since each key sets the mode as it is read. + c.SetInt("multisampling", MultisamplingLevel(s.AntiAliasing)); + c.Set("gfx.antialiasing", AntiAliasingToken(s.AntiAliasing)); + c.Set("gfx.dlss.preset", s.DlssPreset); + c.Set("gfx.framegeneration", s.FrameGeneration > 1 ? $"x{s.FrameGeneration}" : "off"); + c.Set("gfx.reflex", s.Reflex switch { 1 => "on", 2 => "boost", _ => "off" }); c.SetInt("dynamiclights", s.DynamicLights); c.SetDouble("gfx.drawrange.factor.max", s.DrawRangeFactor); c.SetBool("vsync", s.VSync); @@ -266,6 +319,32 @@ public sealed class SettingsSerializer c.SetBool("usevbo", s.UseVbo); c.SetBool("shadows", s.RenderShadows); c.SetBool("gfx.shadowmap.enabled", s.RenderShadows); + c.SetBool("gfx.shadow.sdsm", s.ShadowSdsm); + c.Set("gfx.lights.shadows", LevelToken(s.LightShadows)); + c.Set("gfx.contactshadows", LevelToken(s.ContactShadows)); + c.SetBool("gfx.lights.clustered", s.LightsClustered); + c.Set("gfx.postfx.ao", s.AmbientOcclusion switch { 1 => "ssao", 2 => "gtao", _ => "no" }); + c.Set("gfx.postfx.ao.quality", $"{s.AoSlices} {s.AoSteps}"); + c.SetBool("gfx.postfx.ao.lighting", s.AoLighting); + c.Set("gfx.cabprobe", LevelToken(s.CabProbe)); + c.SetInt("gfx.cabprobe.interval", s.CabProbeInterval); + c.SetDouble("gfx.cabprobe.bounce", s.CabProbeBounce); + c.SetBool("gfx.cabprobe.reflections", s.CabProbeReflections); + c.Set("gfx.ssr", LevelToken(s.Ssr)); + c.SetBool("gfx.raytracing", s.RayTracing); + c.SetInt("gfx.raytracing.range", s.RayTracingRange); + c.Set("gfx.fog", s.VolumetricFog ? "yes" : "analytic"); + c.Set("gfx.fog.quality", LevelToken(s.FogQuality)); + c.Set("gfx.clouds", LevelToken(s.Clouds)); + c.SetBool("gfx.clouds.shadows", s.CloudShadows); + c.SetBool("gfx.grass", s.Grass); + c.SetInt("gfx.grass.distance", s.GrassDistance); + // The grass thins out past its distance; the simulator's own default keeps a sixth of it for that. + c.SetInt("gfx.grass.fadedistance", Math.Min(600, s.GrassDistance + Math.Max(10, s.GrassDistance / 6))); + c.SetDouble("gfx.grass.density", s.GrassDensity); + c.SetInt("gfx.precipitation.particles", s.PrecipitationParticles); + c.SetBool("gfx.postfx.bloom", s.Bloom); + c.SetBool("gfx.postfx.autoexposure", s.AutoExposure); c.Set("gfx.reflections.framerate", s.ReflectionsFramerate.ToString("0.######", CultureInfo.InvariantCulture)); c.Set("shadowtune", $"{s.ShadowMapResolution} {s.ShadowTuneUnused[0]} " + @@ -346,6 +425,128 @@ public sealed class SettingsSerializer return Clamp((int)Math.Round(value > 4 ? value : value * 100), 25, 200); } + private static string FirstToken(ConfigFile c, string key) + { + var tokens = c.GetTokens(key); + return tokens.Length > 0 ? tokens[0].ToLowerInvariant() : string.Empty; + } + + // The names the simulator takes for Vulkan; every other word is OpenGL to the screen, and stays in the file until changed. + // Whichever of the two keys comes later in the file decides, as each sets the mode when the simulator reads it. + private static AntiAliasingMode ReadAntiAliasing(ConfigFile c) + { + var fromMultisampling = c.GetInt("multisampling", 2) switch + { + <= 0 => AntiAliasingMode.Off, + 1 => AntiAliasingMode.Msaa2, + 2 => AntiAliasingMode.Msaa4, + _ => AntiAliasingMode.Msaa8 + }; + + if (!c.Has("gfx.antialiasing") || + (c.Has("multisampling") && c.LineOf("multisampling") > c.LineOf("gfx.antialiasing"))) + return fromMultisampling; + + return FirstToken(c, "gfx.antialiasing") switch + { + "off" or "no" or "none" or "0" => AntiAliasingMode.Off, + "msaax2" => AntiAliasingMode.Msaa2, + "msaax4" => AntiAliasingMode.Msaa4, + "msaax8" or "msaax16" => AntiAliasingMode.Msaa8, + "taa" => AntiAliasingMode.Taa, + "dlss" => AntiAliasingMode.Dlss, + "dlaa" => AntiAliasingMode.Dlaa, + _ => fromMultisampling + }; + } + + private static string AntiAliasingToken(AntiAliasingMode mode) => mode switch + { + AntiAliasingMode.Msaa2 => "msaax2", + AntiAliasingMode.Msaa4 => "msaax4", + AntiAliasingMode.Msaa8 => "msaax8", + AntiAliasingMode.Taa => "taa", + AntiAliasingMode.Dlss => "dlss", + AntiAliasingMode.Dlaa => "dlaa", + _ => "off" + }; + + private static int MultisamplingLevel(AntiAliasingMode mode) => mode switch + { + AntiAliasingMode.Msaa2 => 1, + AntiAliasingMode.Msaa4 => 2, + AntiAliasingMode.Msaa8 => 3, + _ => 0 + }; + + private static int ParseFrameGeneration(string value) => value switch + { + "x2" or "on" or "yes" or "2" => 2, + "x3" or "3" => 3, + "x4" or "4" => 4, + _ => 1 + }; + + private static int ParseReflex(string value) => value switch + { + "on" or "yes" or "1" => 1, + "boost" or "2" => 2, + _ => 0 + }; + + // no/low/medium/high(/ultra) or the number of the level; an absent key keeps the simulator's default. + private static int ParseLevel(string value, int fallback, int max) + { + if (value.Length == 0) + return fallback; + int level = value switch + { + "low" or "1" => 1, + "medium" or "yes" or "true" or "2" => 2, + "high" or "3" => 3, + "ultra" or "4" => 4, + _ => 0 + }; + return Math.Min(level, max); + } + + private static string LevelToken(int level) => level switch + { + 1 => "low", + 2 => "medium", + 3 => "high", + 4 => "ultra", + _ => "no" + }; + + // As the simulator reads it, but a value it does not know keeps its default (medium) rather than turning the probe off. + private static int ParseCabProbe(string value) => value switch + { + "no" or "false" or "off" or "0" => 0, + "low" or "1" => 1, + "high" or "3" => 3, + _ => 2 + }; + + private static int ParseAmbientOcclusion(string value) => value switch + { + "" => 2, + "yes" or "true" or "1" or "gtao" => 2, + "ssao" => 1, + _ => 0 + }; + + private static bool ParseVolumetricFog(string value) => + value is not ("analytic" or "old" or "no" or "false" or "0"); + + private static int ParseFogQuality(string value) => value switch + { + "low" or "1" => 1, + "high" or "3" => 3, + "ultra" or "4" => 4, + _ => 2 + }; + private static int Clamp(int v, int lo, int hi) => v < lo ? lo : (v > hi ? hi : v); private static int SensitivityStep(double mouseScale) => diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index 1f0e1cd..dc938c0 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -253,6 +253,21 @@ + + + + +