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 @@
+
+
+
+
+