From 42b2259b7cc69b124f0160c860dd93cdd3c6203f Mon Sep 17 00:00:00 2001 From: maj00r Date: Sun, 1 Nov 2026 20:54:01 +0100 Subject: [PATCH] Show the graphics card beside the levels Its name and memory, with the other cards in the tooltip. Windows has them in the registry. On Linux the name comes from the driver or pci.ids and the memory from amdgpu or nvidia-smi; without either there is only the name. A card in a processor says shared memory, as what it reports is a share set in the firmware. No rating of the card against the settings: what a ride takes depends on the textures of its scenery and vehicles, which are not known before it starts. --- StarterNG.Tests/GraphicsCardsTests.cs | 156 ++++++++++++++ StarterNG/Application/AppServices.cs | 3 + StarterNG/Domain/Settings/GpuOffload.cs | 4 +- StarterNG/Domain/Settings/GraphicsCard.cs | 25 +++ StarterNG/Infrastructure/GraphicsCards.cs | 250 ++++++++++++++++++++++ StarterNG/Infrastructure/LinuxGpus.cs | 7 +- StarterNG/Views/Settings.axaml | 10 +- StarterNG/Views/Settings.axaml.cs | 28 +++ StarterNG/startercfg/lang/cz.xml | 2 + StarterNG/startercfg/lang/en.xml | 2 + StarterNG/startercfg/lang/hu.xml | 2 + StarterNG/startercfg/lang/pl.xml | 2 + StarterNG/startercfg/lang/ru.xml | 2 + 13 files changed, 489 insertions(+), 4 deletions(-) create mode 100644 StarterNG.Tests/GraphicsCardsTests.cs create mode 100644 StarterNG/Domain/Settings/GraphicsCard.cs create mode 100644 StarterNG/Infrastructure/GraphicsCards.cs diff --git a/StarterNG.Tests/GraphicsCardsTests.cs b/StarterNG.Tests/GraphicsCardsTests.cs new file mode 100644 index 0000000..f793054 --- /dev/null +++ b/StarterNG.Tests/GraphicsCardsTests.cs @@ -0,0 +1,156 @@ +using StarterNG.Domain.Settings; +using StarterNG.Infrastructure; +using StarterNG.Tests.Fakes; + +namespace StarterNG.Tests; + +public class GraphicsCardsTests +{ + private const long GiB = 1L << 30; + + private const string PciIds = + "# List of PCI ID's\n" + + "\n" + + "10de NVIDIA Corporation\n" + + "\t1000 GK110 [Nothing Here]\n" + + "\t2d19 GB206M [GeForce RTX 5060 Max-Q / Mobile]\n" + + "\t\t1043 1e0c A subsystem\n" + + "1002 Advanced Micro Devices, Inc. [AMD/ATI]\n" + + "\t1900 Phoenix1\n" + + "\t2d19 Not this vendor's card\n" + + "8086 Intel Corporation\n" + + "\t46a6 Alder Lake-P GT2 [Iris Xe Graphics]\n" + + "C 00 Unclassified device\n" + + "\t2d19 Not a card at all\n"; + + private static GraphicsCards Reader(InMemoryFileSystem files, FakeEnvironment environment, + params string[] nvidiaSmi) => + new(files, environment, new LinuxGpus(files, environment), () => nvidiaSmi); + + private static InMemoryFileSystem Card(InMemoryFileSystem files, string card, string driver, string id, string slot, + long memory = 0) + { + string device = $"/sys/class/drm/{card}/device"; + files.WithDirectory($"/sys/class/drm/{card}") + .WithFile($"{device}/uevent", $"DRIVER={driver}\nPCI_ID={id}\nPCI_SLOT_NAME={slot}\n"); + if (memory > 0) + files.WithFile($"{device}/mem_info_vram_total", memory + "\n"); + return files; + } + + [Theory] + [InlineData("10DE:2D19", "GeForce RTX 5060 Max-Q / Mobile")] + [InlineData("1002:1900", "Phoenix1")] + [InlineData("8086:46A6", "Iris Xe Graphics")] + [InlineData("10DE:FFFF", null)] + [InlineData("1234:2D19", null)] + [InlineData("nonsense", null)] + public void The_pci_list_names_a_card_by_its_vendor_and_device(string id, string? expected) + { + Assert.Equal(expected, GraphicsCards.ModelInPciList(PciIds.Split('\n'), id)); + } + + [Fact] + public void A_linux_card_takes_its_name_from_the_pci_list_and_its_memory_from_the_driver() + { + var files = Card(new InMemoryFileSystem(), "card0", "amdgpu", "1002:73DF", "0000:03:00.0", 12 * GiB) + .WithFile("/usr/share/hwdata/pci.ids", "1002 AMD\n\t73df Navi 22 [Radeon RX 6700 XT]\n"); + + var card = Assert.Single(Reader(files, new FakeEnvironment()).List()); + + Assert.Equal("AMD Radeon RX 6700 XT", card.Name); + Assert.Equal(12, card.MemoryGb); + } + + [Fact] + public void The_proprietary_driver_names_its_card_and_nvidia_smi_tells_its_memory() + { + var files = Card(new InMemoryFileSystem(), "card0", "nvidia", "10DE:2D19", "0000:01:00.0") + .WithFile("/proc/driver/nvidia/gpus/0000:01:00.0/information", + "Model: \t\t NVIDIA GeForce RTX 5060 Laptop GPU\nIRQ: \t\t 182\n"); + + var card = Assert.Single(Reader(files, new FakeEnvironment(), "00000000:01:00.0, 8188").List()); + + Assert.Equal("NVIDIA GeForce RTX 5060 Laptop GPU", card.Name); + Assert.Equal(8, card.MemoryGb); + } + + [Fact] + public void A_card_keeps_its_name_when_nothing_tells_its_memory() + { + var files = Card(new InMemoryFileSystem(), "card0", "nouveau", "10DE:2D19", "0000:01:00.0"); + + var card = Assert.Single(Reader(files, new FakeEnvironment()).List()); + + Assert.Equal("NVIDIA 10DE:2D19", card.Name); + Assert.Equal(0, card.MemoryBytes); + } + + [Fact] + public void The_discrete_card_comes_first_whatever_order_the_system_lists_them() + { + var files = new InMemoryFileSystem().WithFile( + "/usr/share/misc/pci.ids", "1002 AMD\n\t1900 Rembrandt [Radeon 780M]\n\t73df Navi 22 [Radeon RX 6700 XT]\n"); + Card(files, "card0", "amdgpu", "1002:1900", "0000:01:00.0", GiB / 2); + Card(files, "card1", "amdgpu", "1002:73DF", "0000:65:00.0", 12 * GiB); + + var cards = Reader(files, new FakeEnvironment()).List(); + + Assert.Equal(new[] { "AMD Radeon RX 6700 XT", "AMD Radeon 780M" }, cards.Select(card => card.Name)); + } + + [Fact] + public void There_are_no_cards_on_a_mac() + { + var files = Card(new InMemoryFileSystem(), "card0", "amdgpu", "1002:73DF", "0000:03:00.0", 12 * GiB); + + Assert.Empty(Reader(files, new FakeEnvironment { IsLinux = false, IsMacOS = true }).List()); + } + + [Theory] + [InlineData("00000000:01:00.0, 8188", "0000:01:00.0", 8188L << 20)] + [InlineData("0000:65:00.0, 16384", "0000:65:00.0", 16384L << 20)] + public void Nvidia_smi_is_read_by_the_address_sys_gives(string line, string slot, long bytes) + { + Assert.Equal(bytes, GraphicsCards.ParseNvidiaMemory(new[] { line })[slot]); + } + + [Theory] + [InlineData("Failed to initialize NVML")] + [InlineData("00000000:01:00.0, [N/A]")] + public void Nvidia_smi_saying_something_else_tells_nothing(string line) + { + Assert.Empty(GraphicsCards.ParseNvidiaMemory(new[] { line })); + } + + [Theory] + [InlineData("AMD Radeon 780M Graphics", true)] + [InlineData("AMD Radeon(TM) Graphics", true)] + [InlineData("AMD Radeon(TM) Vega 8 Graphics", true)] + [InlineData("AMD Radeon RX Vega 10 Graphics", true)] + [InlineData("AMD Radeon Vega Series / Radeon Vega Mobile Series", true)] + [InlineData("Intel(R) UHD Graphics 770", true)] + [InlineData("Intel(R) Iris(R) Xe Graphics", true)] + [InlineData("Intel(R) Arc(TM) Graphics", true)] + [InlineData("Intel(R) Arc(TM) A770 Graphics", false)] + [InlineData("Intel Arc B580", false)] + [InlineData("AMD Radeon RX 6800M", false)] + [InlineData("AMD Radeon RX Vega 56", false)] + [InlineData("NVIDIA GeForce RTX 5060 Laptop GPU", false)] + public void A_card_in_a_processor_is_told_by_its_name(string name, bool shared) + { + Assert.Equal(shared, new GraphicsCard(name, 8 * GiB).SharedMemory); + } + + [Fact] + public void A_card_reporting_under_a_gigabyte_counts_as_one_in_a_processor() + { + Assert.True(new GraphicsCard("AMD Phoenix1", GiB / 2).SharedMemory); + } + + [Fact] + public void The_reported_memory_is_rounded_the_way_a_box_says_it() + { + Assert.Equal(8, new GraphicsCard("NVIDIA GeForce RTX 5060 Laptop GPU", 8546942976).MemoryGb); + } +} diff --git a/StarterNG/Application/AppServices.cs b/StarterNG/Application/AppServices.cs index 2961947..ec064d9 100644 --- a/StarterNG/Application/AppServices.cs +++ b/StarterNG/Application/AppServices.cs @@ -68,6 +68,7 @@ public sealed class AppServices Tapes = new HaslerTapes(files, paths, log, clock, Timetables, () => Processes.IsRunning(Path.GetFileNameWithoutExtension(SettingsStore.ResolveExecutable()))); Gpus = new LinuxGpus(files, environment); + GraphicsCards = new GraphicsCards(files, environment, Gpus); Displays = new Displays(files, environment); StartSimulation = new StartSimulation(State, SettingsStore, Library.Vehicles, Executables, files, paths, Processes, random, log, Gpus, environment, @@ -133,6 +134,8 @@ public sealed class AppServices public LinuxGpus Gpus { get; } + public GraphicsCards GraphicsCards { get; } + public Displays Displays { get; } public SettingsPaths SettingsPaths { get; } diff --git a/StarterNG/Domain/Settings/GpuOffload.cs b/StarterNG/Domain/Settings/GpuOffload.cs index b27dd6e..a402019 100644 --- a/StarterNG/Domain/Settings/GpuOffload.cs +++ b/StarterNG/Domain/Settings/GpuOffload.cs @@ -36,7 +36,9 @@ public enum GpuNote /// The kernel driver: nvidia, amdgpu, i915... /// The card the firmware started the display on. /// The card has a laptop's built-in screen among its outputs (eDP, LVDS, DSI). -public sealed record GpuDevice(string Slot, string PciId, string Driver, bool BootVga, bool InternalPanel = false) +/// Its memory, where the driver tells: amdgpu does. +public sealed record GpuDevice(string Slot, string PciId, string Driver, bool BootVga, bool InternalPanel = false, + long MemoryBytes = 0) { public string Vendor => PciId.Split(':')[0].ToUpperInvariant() switch { diff --git a/StarterNG/Domain/Settings/GraphicsCard.cs b/StarterNG/Domain/Settings/GraphicsCard.cs new file mode 100644 index 0000000..948054b --- /dev/null +++ b/StarterNG/Domain/Settings/GraphicsCard.cs @@ -0,0 +1,25 @@ +using System; +using System.Text.RegularExpressions; + +namespace StarterNG.Domain.Settings; + +/// A graphics card as the system names it. +/// As the driver or the PCI list calls it. +/// The memory it reports; 0 when the system does not tell. +public sealed record GraphicsCard(string Name, long MemoryBytes) +{ + private const long GiB = 1L << 30; + + // Intel but for the discrete Arc A and B series; AMD's processor graphics, which + // are named by a number with an M, or not at all. + private static readonly Regex SharedMemoryName = new( + @"\bIntel\b(?!.*\bArc\b.*\b[AB]\d{3}\b)" + + @"|\bRadeon(\(TM\))? (\d{3}M\b|Graphics\b|Vega Series\b|(RX )?Vega (3|6|8|10|11)\b)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + + /// A processor's card, sharing the computer's memory. No discrete card reports under 1 GB any more. + public bool SharedMemory => SharedMemoryName.IsMatch(Name) || MemoryBytes is > 0 and < GiB; + + /// The memory as it is told apart on a box: 8546942976 bytes are 8 GB. + public int MemoryGb => (int)Math.Round(MemoryBytes / (double)GiB); +} diff --git a/StarterNG/Infrastructure/GraphicsCards.cs b/StarterNG/Infrastructure/GraphicsCards.cs new file mode 100644 index 0000000..a357586 --- /dev/null +++ b/StarterNG/Infrastructure/GraphicsCards.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.Versioning; +using System.Security; +using StarterNG.Application.Abstractions; +using StarterNG.Domain.Settings; + +namespace StarterNG.Infrastructure; + +/// +/// The names and memory of the machine's graphics cards, the one a simulator +/// runs on first. Nothing on macOS, where the simulator does not run natively. +/// +public sealed class GraphicsCards +{ + private static readonly string[] PciLists = + { + "/usr/share/hwdata/pci.ids", "/usr/share/misc/pci.ids", "/usr/share/pci.ids", + // The host's, from inside the Steam Linux Runtime container. + "/run/host/usr/share/hwdata/pci.ids", "/run/host/usr/share/misc/pci.ids" + }; + + private const string RegistryMachine = @"\Registry\Machine\"; + + private readonly IFileSystem _files; + private readonly IEnvironment _environment; + private readonly LinuxGpus _gpus; + private readonly Func> _nvidiaSmi; + + public GraphicsCards(IFileSystem files, IEnvironment environment, LinuxGpus gpus, + Func>? nvidiaSmi = null) + { + _files = files; + _environment = environment; + _gpus = gpus; + _nvidiaSmi = nvidiaSmi ?? AskNvidiaSmi; + } + + /// Takes a while on Linux, where the PCI list is read: not for the UI thread. + public IReadOnlyList List() + { + IEnumerable cards = Array.Empty(); + if (_environment.IsLinux) + cards = FromLinux(); + else if (_environment.IsWindows && OperatingSystem.IsWindows()) + cards = FromRegistry(); + + // A program goes to the discrete card, on Windows by its own request. + return cards.OrderBy(card => card.SharedMemory).ThenByDescending(card => card.MemoryBytes).ToList(); + } + + [SupportedOSPlatform("windows")] + private static IEnumerable FromRegistry() + { + try + { + using var video = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"HARDWARE\DEVICEMAP\VIDEO"); + if (video is null) + return Array.Empty(); + + var cards = new List(); + foreach (string name in video.GetValueNames()) + { + if (!name.StartsWith(@"\Device\Video", StringComparison.OrdinalIgnoreCase) || + video.GetValue(name) is not string path || + !path.StartsWith(RegistryMachine, StringComparison.OrdinalIgnoreCase)) + continue; + + using var key = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(path[RegistryMachine.Length..]); + // The system's basic display driver and remote desktops are no cards. + if (key?.GetValue("DriverDesc") is not string card || + key.GetValue("MatchingDeviceId") is not string device || + !device.StartsWith(@"pci\", StringComparison.OrdinalIgnoreCase)) + continue; + + long memory = MemoryIn(key.GetValue("HardwareInformation.qwMemorySize")); + if (memory == 0) + memory = MemoryIn(key.GetValue("HardwareInformation.MemorySize")); + cards.Add(new GraphicsCard(card, memory)); + } + + // A card is listed once for each of its outputs. + return cards.DistinctBy(card => card.Name).ToList(); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException or SecurityException) + { + return Array.Empty(); + } + } + + private static long MemoryIn(object? value) => value switch + { + long qword => qword, + int dword => (uint)dword, + byte[] { Length: 8 } bytes => BitConverter.ToInt64(bytes), + byte[] { Length: 4 } bytes => BitConverter.ToUInt32(bytes), + _ => 0 + }; + + private IEnumerable FromLinux() + { + var gpus = _gpus.List(); + if (gpus.Count == 0) + return Array.Empty(); + + IReadOnlyList? pciList = null; + var nvidiaMemory = gpus.Any(gpu => gpu.Driver == "nvidia") + ? ParseNvidiaMemory(_nvidiaSmi()) + : new Dictionary(StringComparer.OrdinalIgnoreCase); + + var cards = new List(); + foreach (var gpu in gpus) + { + string? model = NvidiaModel(gpu.Slot); + if (model is null) + { + pciList ??= ReadPciList(); + model = ModelInPciList(pciList, gpu.PciId); + } + + string name = model is null ? $"{gpu.Vendor} {gpu.PciId}" + : model.StartsWith(gpu.Vendor, StringComparison.OrdinalIgnoreCase) ? model + : $"{gpu.Vendor} {model}"; + long memory = gpu.MemoryBytes > 0 ? gpu.MemoryBytes : nvidiaMemory.GetValueOrDefault(gpu.Slot); + cards.Add(new GraphicsCard(name, memory)); + } + + return cards; + } + + private string? NvidiaModel(string slot) + { + string path = $"/proc/driver/nvidia/gpus/{slot}/information"; + try + { + if (!_files.FileExists(path)) + return null; + + foreach (string line in _files.ReadAllLines(path)) + if (line.StartsWith("Model:", StringComparison.Ordinal)) + return line["Model:".Length..].Trim(); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + } + + return null; + } + + private IReadOnlyList ReadPciList() + { + foreach (string path in PciLists) + { + try + { + if (_files.FileExists(path)) + return _files.ReadAllLines(path); + } + catch (Exception e) when (e is IOException or UnauthorizedAccessException) + { + } + } + + return Array.Empty(); + } + + /// What a pci.ids list calls a card: the marketing name in brackets, if it has one. + public static string? ModelInPciList(IEnumerable lines, string pciId) + { + string[] ids = pciId.ToLowerInvariant().Split(':'); + if (ids.Length != 2) + return null; + + string vendorLine = ids[0] + " "; + string deviceLine = "\t" + ids[1] + " "; + bool inVendor = false; + foreach (string line in lines) + { + if (line.Length == 0 || line[0] == '#') + continue; + + if (line[0] != '\t') + { + if (inVendor) + return null; + inVendor = line.StartsWith(vendorLine, StringComparison.Ordinal); + } + else if (inVendor && line.StartsWith(deviceLine, StringComparison.Ordinal)) + { + string model = line[deviceLine.Length..].Trim(); + int bracket = model.LastIndexOf('['); + return bracket >= 0 && model.EndsWith(']') ? model[(bracket + 1)..^1] : model; + } + } + + return null; + } + + /// Lines of "00000000:01:00.0, 8188": the PCI address, as /sys gives it with a shorter domain, and MiB. + public static IReadOnlyDictionary ParseNvidiaMemory(IEnumerable lines) + { + var memory = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (string line in lines) + { + string[] fields = line.Split(',', StringSplitOptions.TrimEntries); + if (fields.Length == 2 && fields[0].Length >= 12 && long.TryParse(fields[1], out long mib)) + memory[fields[0][^12..]] = mib << 20; + } + + return memory; + } + + private static IReadOnlyList AskNvidiaSmi() + { + var start = new ProcessStartInfo("nvidia-smi", "--query-gpu=pci.bus_id,memory.total --format=csv,noheader,nounits") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + try + { + using var process = Process.Start(start); + if (process is null) + return Array.Empty(); + + process.ErrorDataReceived += (_, _) => { }; + process.BeginErrorReadLine(); + var output = process.StandardOutput.ReadToEndAsync(); + if (!process.WaitForExit(3000)) + { + process.Kill(); + return Array.Empty(); + } + + return output.GetAwaiter().GetResult().Split('\n', StringSplitOptions.RemoveEmptyEntries); + } + // Not installed, as it often is not inside the Steam Linux Runtime container. + catch (Exception e) when (e is Win32Exception or InvalidOperationException or IOException) + { + return Array.Empty(); + } + } +} diff --git a/StarterNG/Infrastructure/LinuxGpus.cs b/StarterNG/Infrastructure/LinuxGpus.cs index d42a29d..c52411c 100644 --- a/StarterNG/Infrastructure/LinuxGpus.cs +++ b/StarterNG/Infrastructure/LinuxGpus.cs @@ -61,7 +61,12 @@ public sealed class LinuxGpus string bootVga = Path.Combine(device, "boot_vga"); bool boot = _files.FileExists(bootVga) && _files.ReadAllText(bootVga).Trim() == "1"; - gpus.Add(new GpuDevice(slot, id, fields.GetValueOrDefault("DRIVER", ""), boot, withPanel.Contains(name))); + string vram = Path.Combine(device, "mem_info_vram_total"); + long memory = _files.FileExists(vram) && long.TryParse(_files.ReadAllText(vram).Trim(), out long bytes) + ? bytes + : 0; + gpus.Add(new GpuDevice(slot, id, fields.GetValueOrDefault("DRIVER", ""), boot, withPanel.Contains(name), + memory)); } return gpus.OrderBy(g => g.Slot, StringComparer.Ordinal).ToList(); diff --git a/StarterNG/Views/Settings.axaml b/StarterNG/Views/Settings.axaml index 7b74ab0..7e62af8 100644 --- a/StarterNG/Views/Settings.axaml +++ b/StarterNG/Views/Settings.axaml @@ -96,8 +96,14 @@ - + + + + diff --git a/StarterNG/Views/Settings.axaml.cs b/StarterNG/Views/Settings.axaml.cs index b34504a..ee250be 100644 --- a/StarterNG/Views/Settings.axaml.cs +++ b/StarterNG/Views/Settings.axaml.cs @@ -99,6 +99,8 @@ public partial class Settings : UserControl, ISettingsCapture _keyRows.Changed += QueuePresetMarks; BuildPresetButtons(); BuildMonitorRates(); + ReadGraphicsCards(); + App.Loc.LanguageChanged += UpdateCardText; App.Loc.LanguageChanged += _keyRows.UpdateTexts; FeedbackCb.SelectionChanged += FeedbackCb_OnSelectionChanged; @@ -625,6 +627,32 @@ public partial class Settings : UserControl, ISettingsCapture _markingLevels = false; } + // ── The player's graphics card, beside the levels ──────────────────────── + + private IReadOnlyList _cards = Array.Empty(); + + private async void ReadGraphicsCards() + { + var reader = AppServices.Current.GraphicsCards; + _cards = await Task.Run(() => reader.List()); + UpdateCardText(); + } + + private static string CardLine(GraphicsCard card) => + card.SharedMemory ? string.Format(App.Loc["GpuCardShared"], card.Name) + : card.MemoryBytes > 0 ? string.Format(App.Loc["GpuCard"], card.Name, card.MemoryGb) + : card.Name; + + private void UpdateCardText() + { + CardText.IsVisible = _cards.Count > 0; + if (_cards.Count == 0) + return; + + CardText.Text = CardLine(_cards[0]); + ToolTip.SetTip(CardText, string.Join(Environment.NewLine, _cards.Select(CardLine))); + } + // ── The screens' refresh rates, for the frame rate limit ───────────────── private static readonly IBrush FgDimBrush = new SolidColorBrush(Color.Parse("#9098A0")); diff --git a/StarterNG/startercfg/lang/cz.xml b/StarterNG/startercfg/lang/cz.xml index c73f0c5..2d17b72 100644 --- a/StarterNG/startercfg/lang/cz.xml +++ b/StarterNG/startercfg/lang/cz.xml @@ -941,4 +941,6 @@ Dosah zdrojů stínů v kabině Použít OpenGL ES OpenGL ES místo OpenGL. Nefunguje s rendererem legacy. + {0}, {1} GB + {0}, sdílená paměť diff --git a/StarterNG/startercfg/lang/en.xml b/StarterNG/startercfg/lang/en.xml index 0421f4c..94e8c0e 100644 --- a/StarterNG/startercfg/lang/en.xml +++ b/StarterNG/startercfg/lang/en.xml @@ -1193,4 +1193,6 @@ Distance at which light sources still cast shadows inside the cab. Zero turns cab shadows off. Use OpenGL ES OpenGL ES instead of OpenGL. Does not work with the legacy renderer. + {0}, {1} GB + {0}, shared memory diff --git a/StarterNG/startercfg/lang/hu.xml b/StarterNG/startercfg/lang/hu.xml index 314ed43..5efacc4 100644 --- a/StarterNG/startercfg/lang/hu.xml +++ b/StarterNG/startercfg/lang/hu.xml @@ -941,4 +941,6 @@ Vezetőállás árnyékforrásainak hatótávolsága OpenGL ES használata OpenGL ES az OpenGL helyett. A legacy megjelenítővel nem működik. + {0}, {1} GB + {0}, megosztott memória diff --git a/StarterNG/startercfg/lang/pl.xml b/StarterNG/startercfg/lang/pl.xml index 27ec6bc..d06007b 100644 --- a/StarterNG/startercfg/lang/pl.xml +++ b/StarterNG/startercfg/lang/pl.xml @@ -1015,4 +1015,6 @@ Odległość, z jakiej źródła światła nadal rzucają cień w kabinie. Zero wyłącza cienie w kabinie. Używaj OpenGL ES OpenGL ES zamiast OpenGL. Nie działa z rendererem legacy. + {0}, {1} GB + {0}, pamięć wspólna diff --git a/StarterNG/startercfg/lang/ru.xml b/StarterNG/startercfg/lang/ru.xml index e6aefbd..78a7208 100644 --- a/StarterNG/startercfg/lang/ru.xml +++ b/StarterNG/startercfg/lang/ru.xml @@ -941,4 +941,6 @@ Дальность источников теней в кабине Использовать OpenGL ES OpenGL ES вместо OpenGL. Не работает с рендерером legacy. + {0}, {1} ГБ + {0}, общая память