diff --git a/StarterNG.Tests/GraphicsSettingsTests.cs b/StarterNG.Tests/GraphicsSettingsTests.cs new file mode 100644 index 0000000..98f496d --- /dev/null +++ b/StarterNG.Tests/GraphicsSettingsTests.cs @@ -0,0 +1,405 @@ +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); + } + + [Fact] + public void The_renderer_is_written_as_a_name_every_simulator_reads() + { + Assert.Equal("gfxrenderer full", Line(Write(Read("gfxrenderer legacy\n")), "gfxrenderer")); + Assert.Equal("gfxrenderer vulkan", Line(Write(Read("gfxrenderer vulkan14\n")), "gfxrenderer")); + } + + [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 Gfx_antialiasing_is_moved_below_multisampling_so_it_takes_effect() + { + var s = Read("gfx.antialiasing msaax4\nmultisampling 2\n"); + s.AntiAliasing = AntiAliasingMode.Taa; + + string text = Write(s, "gfx.antialiasing msaax4\nmultisampling 2\n"); + var lines = Lines(text).ToList(); + + Assert.True(lines.IndexOf("gfx.antialiasing taa") > lines.IndexOf("multisampling 0")); + Assert.Equal(AntiAliasingMode.Taa, Read(text).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 Keys_the_simulator_dropped_are_commented_out() + { + const string old = "usevbo yes\ngfx.angleplatform vulkan\ngfx.usegles no\ngfx.framebuffer.width 1920\ngfx.framebuffer.height 1080\n"; + + string text = Write(Read(old), old); + + foreach (string key in new[] { "usevbo", "gfx.angleplatform", "gfx.usegles", "gfx.framebuffer.width", "gfx.framebuffer.height" }) + { + Assert.Null(Line(text, key)); + Assert.Contains(Lines(text), l => l.StartsWith("// " + key, StringComparison.Ordinal)); + } + } + + [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); + } + + [Theory] + [InlineData(GraphicsPreset.Lowest)] + [InlineData(GraphicsPreset.Low)] + [InlineData(GraphicsPreset.Medium)] + [InlineData(GraphicsPreset.High)] + [InlineData(GraphicsPreset.Ultra)] + public void A_preset_is_recognised_after_it_is_applied_and_survives_a_round_trip(GraphicsPreset preset) + { + foreach (var backend in new[] { GraphicsBackend.OpenGL, GraphicsBackend.Vulkan }) + { + var s = new SimulatorSettings { Backend = backend }; + GraphicsPresets.Apply(s, preset, true); + + Assert.Equal(preset, GraphicsPresets.Detect(s, true)); + Assert.Equal(preset, GraphicsPresets.Detect(Read(Write(s)), true)); + } + } + + [Fact] + public void A_change_after_a_preset_makes_the_settings_custom() + { + var s = new SimulatorSettings(); + GraphicsPresets.Apply(s, GraphicsPreset.High, true); + s.Clouds = 4; + + Assert.Equal(GraphicsPreset.Custom, GraphicsPresets.Detect(s, true)); + } + + [Fact] + public void Presets_leave_alone_what_is_not_a_matter_of_quality() + { + var s = new SimulatorSettings { Width = 2560, Height = 1440, BufferScalePercent = 67, VSync = true, FieldOfView = 60, MotionBlur = false }; + + GraphicsPresets.Apply(s, GraphicsPreset.Lowest, true); + + Assert.Equal((2560, 1440, 67, true, 60, false), (s.Width, s.Height, s.BufferScalePercent, s.VSync, s.FieldOfView, s.MotionBlur)); + } + + [Fact] + public void Ray_tracing_comes_with_ultra_on_vulkan_only() + { + var vulkan = new SimulatorSettings { Backend = GraphicsBackend.Vulkan }; + var opengl = new SimulatorSettings { Backend = GraphicsBackend.OpenGL }; + + GraphicsPresets.Apply(vulkan, GraphicsPreset.Ultra, true); + GraphicsPresets.Apply(opengl, GraphicsPreset.Ultra, true); + + Assert.True(vulkan.RayTracing); + Assert.False(opengl.RayTracing); + } + + [Fact] + public void A_preset_keeps_dlss_on_vulkan() + { + var s = new SimulatorSettings { Backend = GraphicsBackend.Vulkan, AntiAliasing = AntiAliasingMode.Dlss }; + + GraphicsPresets.Apply(s, GraphicsPreset.Ultra, true); + + Assert.Equal(AntiAliasingMode.Dlss, s.AntiAliasing); + Assert.Equal(GraphicsPreset.Ultra, GraphicsPresets.Detect(s, true)); + } + + [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 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")); + } +} diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs index 744912b..5ca8820 100644 --- a/StarterNG/Application/AppServices.cs +++ b/StarterNG/Application/AppServices.cs @@ -50,7 +50,7 @@ public sealed class AppServices State = new AppState(); 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()); StartSimulation = new StartSimulation(State, SettingsStore, Library.Vehicles, Executables, files, paths, diff --git a/StarterNG/Application/SettingsStore.cs b/StarterNG/Application/SettingsStore.cs index bb56352..718852f 100644 --- a/StarterNG/Application/SettingsStore.cs +++ b/StarterNG/Application/SettingsStore.cs @@ -75,6 +75,68 @@ public sealed class SettingsStore TouchConfigAge(); } + /// 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, 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; + string lastScenery = s.LastScenery; + bool maximized = s.WindowMaximized; + + _serializer.Read(s, imported); + + s.SelectExeAutomatically = selectAutomatically; + s.ExecutablePath = executable; + s.LastScenery = lastScenery; + s.WindowMaximized = maximized; + 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()); + _serializer.Write(Settings, copy); + + try + { + string? directory = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(directory)) + _files.CreateDirectory(directory); + + _files.WriteAllText(path, copy.ToText(), _encoding); + return true; + } + catch (Exception ex) + { + _log.Log($"exporting {path}", ex); + return false; + } + } + public void CaptureAndSave() { _capture?.CaptureInto(Settings); diff --git a/StarterNG/Classes/ConfigFile.cs b/StarterNG/Classes/ConfigFile.cs index e551b2c..8ec2c3a 100644 --- a/StarterNG/Classes/ConfigFile.cs +++ b/StarterNG/Classes/ConfigFile.cs @@ -32,6 +32,28 @@ 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; + + /// + /// Moves the line of right below that of + /// when it is above it, for keys the simulator lets override one another. + /// + public void MoveAfter(string key, string anchor) + { + if (!_index.TryGetValue(key, out int from) || !_index.TryGetValue(anchor, out int to) || from > to) + return; + + string line = _lines[from]; + _lines.RemoveAt(from); + _lines.Insert(to, line); + + _index.Clear(); + for (int i = 0; i < _lines.Count; i++) + if (TryParseKey(_lines[i], out string k)) + _index[k] = i; + } + public string? GetRaw(string key) { if (!_index.TryGetValue(key, out int i)) diff --git a/StarterNG/Domain/Settings/GraphicsPresets.cs b/StarterNG/Domain/Settings/GraphicsPresets.cs new file mode 100644 index 0000000..cded183 --- /dev/null +++ b/StarterNG/Domain/Settings/GraphicsPresets.cs @@ -0,0 +1,151 @@ +using System; + +namespace StarterNG.Domain.Settings; + +public enum GraphicsPreset { Lowest, Low, Medium, High, Ultra, Custom } + +/// +/// Quality presets of the starter: one set of graphics settings each. A preset +/// leaves alone what is a matter of taste or of the machine rather than of quality +/// (resolution, render scale, vsync, field of view, motion blur, chromatic +/// aberration) and keeps DLSS when it is on, since that is a choice of the +/// anti-aliasing the presets cannot make better. Ray tracing is only for Vulkan. +/// +public static class GraphicsPresets +{ + public static readonly GraphicsPreset[] All = + { + GraphicsPreset.Lowest, GraphicsPreset.Low, GraphicsPreset.Medium, GraphicsPreset.High, GraphicsPreset.Ultra + }; + + /// The settings a preset governs. Doubles are kept in hundredths so equality is exact. + private sealed record Quality( + int Texture, int CabTexture, int Anisotropy, AntiAliasingMode AntiAliasing, + int DrawRange, int Spline, int DynamicLights, + bool Shadows, int ShadowMap, int ShadowRange, int ShadowRank, int LightShadows, int ContactShadows, + int Ao, int AoSlices, int AoSteps, + int ReflectionsFidelity, int ReflectionsFramerate, int Ssr, bool RayTracing, + bool VolumetricFog, int FogQuality, int Clouds, bool CloudShadows, + bool Grass, int GrassDistance, int GrassDensityPercent, + int SmokeFidelity, int Precipitation, bool Bloom, bool ExtraEffects); + + private static Quality Table(GraphicsPreset preset) => preset switch + { + GraphicsPreset.Lowest => new Quality( + 1024, 1024, 1, AntiAliasingMode.Off, + 1, 1, 3, + false, 1024, 75, 1, 0, 0, + 0, 3, 4, + 0, 5, 0, false, + false, 1, 0, false, + false, 60, 100, + 1, 16384, false, false), + GraphicsPreset.Low => new Quality( + 2048, 2048, 4, AntiAliasingMode.Msaa2, + 2, 2, 5, + true, 1024, 150, 2, 1, 0, + 1, 3, 4, + 0, 10, 1, false, + true, 1, 1, false, + true, 40, 50, + 1, 32768, true, true), + GraphicsPreset.Medium => new Quality( + 4096, 4096, 8, AntiAliasingMode.Msaa2, + 3, 3, 7, + true, 2048, 250, 3, 2, 0, + 2, 3, 4, + 1, 30, 2, false, + true, 2, 2, true, + true, 60, 100, + 2, 65536, true, true), + GraphicsPreset.High => new Quality( + 4096, 4096, 16, AntiAliasingMode.Msaa4, + 3, 4, 7, + true, 4096, 300, 3, 3, 1, + 2, 4, 6, + 2, 30, 2, false, + true, 3, 3, true, + true, 90, 100, + 3, 65536, true, true), + GraphicsPreset.Ultra => new Quality( + 8192, 8192, 16, AntiAliasingMode.Msaa8, + 3, 4, 7, + true, 8192, 400, 3, 3, 3, + 2, 6, 8, + 2, 60, 3, true, + true, 4, 4, true, + true, 120, 150, + 4, 131072, true, true), + _ => throw new ArgumentOutOfRangeException(nameof(preset), preset, null) + }; + + /// A preset as it lands on these settings: DLSS kept where it runs, ray tracing on Vulkan only. + private static Quality Resolved(GraphicsPreset preset, SimulatorSettings s, bool dlssPlatform) + { + var q = Table(preset); + if (GraphicsRules.DlssActive(s, dlssPlatform)) + q = q with { AntiAliasing = s.AntiAliasing }; + if (!GraphicsRules.RayTracingAllowed(s)) + q = q with { RayTracing = false }; + return q; + } + + private static Quality Of(SimulatorSettings s) => new( + s.MaxTextureSize, s.MaxCabTextureSize, s.TextureFiltering, s.AntiAliasing, + (int)Math.Round(s.DrawRangeFactor), s.SplineFidelity, s.DynamicLights, + s.RenderShadows, s.ShadowMapResolution, s.ShadowProjectionRange, s.ShadowRankCutoff, s.LightShadows, s.ContactShadows, + s.AmbientOcclusion, s.AoSlices, s.AoSteps, + s.ReflectionsFidelity, s.ReflectionsFramerate, s.Ssr, s.RayTracing, + s.VolumetricFog, s.FogQuality, s.Clouds, s.CloudShadows, + s.Grass, s.GrassDistance, (int)Math.Round(s.GrassDensity * 100), + s.SmokeFidelity, s.PrecipitationParticles, s.Bloom, s.ExtraEffects); + + public static void Apply(SimulatorSettings s, GraphicsPreset preset, bool dlssPlatform) + { + if (preset == GraphicsPreset.Custom) + return; + + var q = Resolved(preset, s, dlssPlatform); + s.MaxTextureSize = q.Texture; + s.MaxCabTextureSize = q.CabTexture; + s.TextureFiltering = q.Anisotropy; + s.AntiAliasing = q.AntiAliasing; + s.DrawRangeFactor = q.DrawRange; + s.SplineFidelity = q.Spline; + s.DynamicLights = q.DynamicLights; + s.RenderShadows = q.Shadows; + s.ShadowMapResolution = q.ShadowMap; + s.ShadowProjectionRange = q.ShadowRange; + s.ShadowRankCutoff = q.ShadowRank; + s.LightShadows = q.LightShadows; + s.ContactShadows = q.ContactShadows; + s.AmbientOcclusion = q.Ao; + s.AoSlices = q.AoSlices; + s.AoSteps = q.AoSteps; + s.ReflectionsFidelity = q.ReflectionsFidelity; + s.ReflectionsFramerate = q.ReflectionsFramerate; + s.Ssr = q.Ssr; + s.RayTracing = q.RayTracing; + s.VolumetricFog = q.VolumetricFog; + s.FogQuality = q.FogQuality; + s.Clouds = q.Clouds; + s.CloudShadows = q.CloudShadows; + s.Grass = q.Grass; + s.GrassDistance = q.GrassDistance; + s.GrassDensity = q.GrassDensityPercent / 100.0; + s.SmokeFidelity = q.SmokeFidelity; + s.PrecipitationParticles = q.Precipitation; + s.Bloom = q.Bloom; + s.ExtraEffects = q.ExtraEffects; + } + + /// The preset these settings are exactly, or Custom. + public static GraphicsPreset Detect(SimulatorSettings s, bool dlssPlatform) + { + var current = Of(s); + foreach (var preset in All) + if (Resolved(preset, s, dlssPlatform) == current) + return preset; + return GraphicsPreset.Custom; + } +} diff --git a/StarterNG/Domain/Settings/GraphicsRules.cs b/StarterNG/Domain/Settings/GraphicsRules.cs new file mode 100644 index 0000000..b4830e5 --- /dev/null +++ b/StarterNG/Domain/Settings/GraphicsRules.cs @@ -0,0 +1,105 @@ +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; + + /// + /// 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.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/SimulatorSettings.cs b/StarterNG/Domain/Settings/SimulatorSettings.cs index e1dea3c..90d8918 100644 --- a/StarterNG/Domain/Settings/SimulatorSettings.cs +++ b/StarterNG/Domain/Settings/SimulatorSettings.cs @@ -7,6 +7,12 @@ public enum ConsistReadyOverride { Scenery = 0, AlwaysCold = 1, AlwaysReady = 2 public enum ExeProblem { None, NotFound, NotExecutable, WrongPlatform } +// gfxrenderer: the opengl 4.6 renderer ("full", which the simulator reads as "default") 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; } @@ -37,14 +43,18 @@ public sealed class SimulatorSettings public bool LogMissingVehicleFiles; public bool DeveloperTools; - public int RenderEngine; + public GraphicsBackend Backend = 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 int 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; @@ -54,8 +64,27 @@ 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 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; public int ReflectionsFramerate = 60; public int ShadowMapResolution = 2048; public int ShadowProjectionRange = 250; @@ -69,7 +98,6 @@ public sealed class SimulatorSettings public int FpsLimit = 30; public double ShadowAngleLimit = 0.2; public bool FullscreenWindowed; - public bool RenderAngleVulkan; public int SplineFidelity = 1; public bool FullPhysics = true; @@ -97,7 +125,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; @@ -120,8 +147,8 @@ public sealed class SimulatorSettings public bool WindowMaximized; - public static readonly string[] RenderEngines = - { "full", "legacy", "simpleshader", "simple", "off", "experimental" }; + // "full" rather than "default": every simulator build reads it, the new ones as the opengl 4.6 renderer. + public static readonly string[] RenderEngines = { "full", "vulkan" }; public static readonly int[] AnisotropySteps = { 1, 2, 4, 8, 16 }; diff --git a/StarterNG/Infrastructure/Settings/ExecutableLocator.cs b/StarterNG/Infrastructure/Settings/ExecutableLocator.cs index cf580d2..3c6bd4b 100644 --- a/StarterNG/Infrastructure/Settings/ExecutableLocator.cs +++ b/StarterNG/Infrastructure/Settings/ExecutableLocator.cs @@ -246,6 +246,37 @@ public sealed class ExecutableLocator return extension.Length == 0 || extension is ".exe" or ".x86_64" or ".run" or ".appimage"; } + /// + /// Whether the NVIDIA libraries of DLSS and of its frame generation sit next to + /// the simulator, where it loads them from; without them it runs with TAA. + /// The names follow the build: a Windows one wants the DLLs, a Linux one the .so files. + /// + 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; + private 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 79260d8..e33cbd9 100644 --- a/StarterNG/Infrastructure/Settings/SettingsSerializer.cs +++ b/StarterNG/Infrastructure/Settings/SettingsSerializer.cs @@ -10,6 +10,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"); @@ -58,14 +68,18 @@ public sealed class SettingsSerializer s.LogMissingVehicleFiles = c.GetBool("starter.logmissingvehicles", false); s.DeveloperTools = c.GetBool("starter.developertools", false); - s.RenderEngine = IndexOf(SimulatorSettings.RenderEngines, c.GetString("gfxrenderer", "full"), 0); + s.Backend = ParseBackend(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 = Clamp(c.GetInt("starter.bufferscale", 100), 50, 200); + s.BufferScalePercent = ReadBufferScale(c); s.MaxTextureSize = c.GetInt("maxtexturesize", 4096); s.MaxCabTextureSize = c.GetInt("maxcabtexturesize", 4096); s.TextureFiltering = c.GetInt("anisotropicfiltering", 8); - s.Multisampling = Clamp(c.GetInt("multisampling", 2), 0, 3); + 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); @@ -75,8 +89,28 @@ public sealed class SettingsSerializer s.MotionBlur = c.GetBool("gfx.postfx.motionblur.enabled", true); s.ExtraEffects = c.GetBool("gfx.extraeffects", true); 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.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); s.ReflectionsFramerate = c.GetInt("gfx.reflections.framerate", 60); var shadowTune = c.GetTokens("shadowtune"); @@ -94,7 +128,6 @@ public sealed class SettingsSerializer s.FpsLimit = Clamp(c.GetInt("fpslimit", 30), 1, 100); s.ShadowAngleLimit = c.GetDouble("gfx.shadow.angle.min", 0.2); s.FullscreenWindowed = c.GetBool("fullscreenwindowed", false); - s.RenderAngleVulkan = string.Equals(c.GetString("gfx.angleplatform", ""), "vulkan", StringComparison.OrdinalIgnoreCase); s.SplineFidelity = Clamp(c.GetInt("splinefidelity", 1), 1, 4); s.FullPhysics = c.GetBool("fullphysics", true); @@ -125,7 +158,6 @@ public sealed class SettingsSerializer s.CompressTextures = c.GetBool("compresstex", true); s.ScaleSpeculars = c.GetBool("scalespeculars", true); - s.UseGLES = c.GetBool("gfx.usegles", false); s.ShaderGamma = c.GetBool("gfx.shadergamma", false); s.ScreenMipmaps = c.GetBool("python.mipmaps", true); s.ExtendedModelConversion = c.GetInt("convertmodels", 0) != 0; @@ -151,15 +183,25 @@ public sealed class SettingsSerializer s.HideArchivalVehicles = c.GetBool("starter.hide.archivalvehicles", true); s.LastScenery = c.GetString("starter.last.scenery", ""); s.WindowMaximized = c.GetBool("starter.window.maximized", false); + + GraphicsRules.Normalize(s, _dlssPlatform); } + // Keys the simulator no longer reads (it logs each of them on every start); commented out on save. private static readonly string[] ObsoleteKeys = { "gfx.postfx.tonemapping", + "usevbo", + "gfx.angleplatform", + "gfx.usegles", + "gfx.framebuffer.width", + "gfx.framebuffer.height", }; public void Write(SimulatorSettings s, ConfigFile c) { + GraphicsRules.Normalize(s, _dlssPlatform); + foreach (string key in ObsoleteKeys) c.Remove(key); @@ -198,18 +240,24 @@ public sealed class SettingsSerializer c.SetBool("starter.logmissingvehicles", s.LogMissingVehicleFiles); c.SetBool("starter.developertools", s.DeveloperTools); - c.Set("gfxrenderer", SimulatorSettings.RenderEngines[Clamp(s.RenderEngine, 0, SimulatorSettings.RenderEngines.Length - 1)]); + c.Set("gfxrenderer", SimulatorSettings.RenderEngines[(int)s.Backend]); + if (!string.IsNullOrWhiteSpace(s.VulkanDevice)) c.Set("gfx.vulkan.device", s.VulkanDevice.Trim()); else c.Remove("gfx.vulkan.device"); c.SetInt("width", s.Width); c.SetInt("height", s.Height); c.SetInt("starter.bufferscale", s.BufferScalePercent); - int fbw = (int)Math.Round(s.Width * s.BufferScalePercent / 100.0); - int fbh = (int)Math.Round(s.Height * s.BufferScalePercent / 100.0); - c.SetInt("gfx.framebuffer.width", fbw); - c.SetInt("gfx.framebuffer.height", fbh); + // Over 4 the simulator takes it as percents. + c.SetInt("gfx.buffer.percent", s.BufferScalePercent); c.SetInt("maxtexturesize", s.MaxTextureSize); c.SetInt("maxcabtexturesize", s.MaxCabTextureSize); c.SetInt("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.MoveAfter("gfx.antialiasing", "multisampling"); + 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); @@ -219,9 +267,29 @@ public sealed class SettingsSerializer c.SetBool("gfx.postfx.motionblur.enabled", s.MotionBlur); c.SetBool("gfx.extraeffects", s.ExtraEffects); c.SetBool("gfx.envmap.enabled", s.EnvMap); - 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.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.SetInt("gfx.reflections.framerate", s.ReflectionsFramerate); c.Set("shadowtune", $"{s.ShadowMapResolution} {s.ShadowProjectionRange} " + @@ -235,7 +303,6 @@ public sealed class SettingsSerializer if (s.FpsLimitEnabled) c.SetInt("fpslimit", s.FpsLimit); else c.Remove("fpslimit"); c.SetDouble("gfx.shadow.angle.min", s.ShadowAngleLimit); c.SetBool("fullscreenwindowed", s.FullscreenWindowed); - if (s.RenderAngleVulkan) c.Set("gfx.angleplatform", "vulkan"); else c.Remove("gfx.angleplatform"); c.SetInt("splinefidelity", s.SplineFidelity); c.SetBool("fullphysics", s.FullPhysics); @@ -262,7 +329,6 @@ public sealed class SettingsSerializer c.SetBool("compresstex", s.CompressTextures); c.SetBool("scalespeculars", s.ScaleSpeculars); - c.SetBool("gfx.usegles", s.UseGLES); c.SetBool("gfx.shadergamma", s.ShaderGamma); c.SetBool("python.mipmaps", s.ScreenMipmaps); c.Set("convertmodels", s.ExtendedModelConversion ? "143" : "0"); @@ -291,14 +357,134 @@ public sealed class SettingsSerializer private static int ParseInt(string s, int fallback) => int.TryParse(s, NumberStyles.Integer, CultureInfo.InvariantCulture, out int n) ? n : fallback; - private static int IndexOf(string[] values, string token, int fallback) + private static string FirstToken(ConfigFile c, string key) { - for (int i = 0; i < values.Length; i++) - if (string.Equals(values[i], token, StringComparison.OrdinalIgnoreCase)) - return i; - return fallback; + var tokens = c.GetTokens(key); + return tokens.Length > 0 ? tokens[0].ToLowerInvariant() : string.Empty; } + // The aliases the simulator accepts; the removed renderers ("legacy", "simple", "experimental", ...) run as opengl. + private static GraphicsBackend ParseBackend(string value) => + value.Trim().ToLowerInvariant() is "vulkan" or "vulkan14" ? GraphicsBackend.Vulkan : GraphicsBackend.OpenGL; + + private static int ReadBufferScale(ConfigFile c) + { + if (c.Has("gfx.buffer.percent")) + { + double value = c.GetDouble("gfx.buffer.percent", 100); + // As the simulator reads it: up to 4 a part of the window, over it percents. + int percent = (int)Math.Round(value > 4 ? value : value * 100); + return Clamp(percent, 25, 200); + } + return Clamp(c.GetInt("starter.bufferscale", 100), 25, 200); + } + + // 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" + }; + + 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 MapOldPyScreenPriority(string priority) => priority.ToLowerInvariant() switch { diff --git a/StarterNG/Services/LocalizationService.cs b/StarterNG/Services/LocalizationService.cs index 8357d95..1e7cb6c 100644 --- a/StarterNG/Services/LocalizationService.cs +++ b/StarterNG/Services/LocalizationService.cs @@ -140,6 +140,35 @@ public class LocalizationService : INotifyPropertyChanged, ILocalizedStrings { var dict = new Dictionary(StringComparer.Ordinal); + // A translation which has not caught up with new settings shows them in English + // rather than as their raw keys. Descriptions stay out: a missing one collapses. + string english = Path.Combine(Path.GetDirectoryName(path) ?? LangDirectory, "en.xml"); + if (!string.Equals(code, "en", StringComparison.OrdinalIgnoreCase) && File.Exists(english)) + { + try + { + ReadStrings(english, dict); + foreach (var key in new List(dict.Keys)) + if (key.EndsWith(OptionalSuffix, StringComparison.Ordinal)) + dict.Remove(key); + } + catch (Exception ex) + { + _log.Log($"lang/{Path.GetFileName(english)}", ex); + } + } + + ReadStrings(path, dict); + + _strings = dict; + lock (_reportedMisses) + _reportedMisses.Clear(); + CurrentLangCode = code; + CurrentLanguage = name; + } + + private static void ReadStrings(string path, Dictionary dict) + { var settings = new XmlReaderSettings { IgnoreComments = true, IgnoreWhitespace = true }; using var reader = XmlReader.Create(path, settings); @@ -157,12 +186,6 @@ public class LocalizationService : INotifyPropertyChanged, ILocalizedStrings reader.Read(); } } - - _strings = dict; - lock (_reportedMisses) - _reportedMisses.Clear(); - CurrentLangCode = code; - CurrentLanguage = name; } private static (string code, string name) ReadHeader(string path) diff --git a/StarterNG/Styles/Settings.axaml b/StarterNG/Styles/Settings.axaml index c5a8ca4..882ed32 100644 --- a/StarterNG/Styles/Settings.axaml +++ b/StarterNG/Styles/Settings.axaml @@ -197,6 +197,21 @@ + + + + +