diff --git a/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs b/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs index 22e756e6ce..68551f05a0 100644 --- a/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs +++ b/Intersect.Client.Core/Interface/Debugging/DebugWindow.cs @@ -17,6 +17,7 @@ using Intersect.Client.Interface.Debugging.Providers; using Intersect.Client.Localization; using Intersect.Client.Maps; +using Intersect.Client.MonoGame.NativeInterop.OpenGL; using Intersect.Framework.Reflection; using static Intersect.Client.Framework.File_Management.GameContentManager; @@ -405,7 +406,13 @@ private Table CreateInfoTableDebugStats(Base parent) table.AddRow(Strings.Debug.LightsDrawn, name: "LightsDrawnRow").Listen(1, new DelegateDataProvider(() => Graphics.LightsDrawn), NoValue); table.AddRow(Strings.Debug.InterfaceObjects, name: "InterfaceObjectsRow").Listen(1, new DelegateDataProvider(() => Interface.CurrentInterface?.NodeCount, delayMilliseconds: 1000), NoValue); - var titleRow = table.AddRow(Strings.Debug.ControlUnderCursor, columnCount: 2, name: "ControlUnderCursorRow", columnIndex: 1); + _ = table.AddRow(Strings.Debug.SectionGPUStatistics, columnCount: 2, name: "SectionGPU", columnIndex: 1); + + table.AddRow(Strings.Debug.RenderBufferVRAMFree, name: "GPUVRAMRenderBuffers").Listen(1, new DelegateDataProvider(() => Strings.FormatBytes(GL.AvailableRenderBufferMemory)), NoValue); + table.AddRow(Strings.Debug.TextureVRAMFree, name: "GPUVRAMTextures").Listen(1, new DelegateDataProvider(() => Strings.FormatBytes(GL.AvailableTextureMemory)), NoValue); + table.AddRow(Strings.Debug.VBOVRAMFree, name: "GPUVRAMVBOs").Listen(1, new DelegateDataProvider(() => Strings.FormatBytes(GL.AvailableVBOMemory)), NoValue); + + _ = table.AddRow(Strings.Debug.ControlUnderCursor, columnCount: 2, name: "SectionUI", columnIndex: 1); table.AddRow(Strings.Internals.Type, name: "TypeRow").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.GetType().GetName(), Strings.Internals.NotApplicable); table.AddRow(Strings.Internals.Name, name: "NameRow").Listen(1, _nodeUnderCursorProvider, (node, _) => node?.ParentQualifiedName, NoValue); @@ -435,7 +442,7 @@ private Table CreateInfoTableDebugStats(Base parent) var rows = table.Children.OfType().ToArray(); foreach (var row in rows) { - if (row == titleRow && row.GetCellContents(1) is Label titleLabel) + if (row.Name.StartsWith("Section") && row.GetCellContents(1) is Label titleLabel) { titleLabel.Padding = titleLabel.Padding with { Top = 8 }; } diff --git a/Intersect.Client.Core/Localization/Strings.cs b/Intersect.Client.Core/Localization/Strings.cs index ab50629cf1..3932467236 100644 --- a/Intersect.Client.Core/Localization/Strings.cs +++ b/Intersect.Client.Core/Localization/Strings.cs @@ -20,6 +20,31 @@ public static partial class Strings private const string StringsFileName = "client_strings.json"; private static char[] mQuantityTrimChars = new char[] { '.', '0' }; + private static string[] _unitsBits = [string.Empty, "Ki", "Mi", "Gi", "Ti"]; + private static string[] _unitsBytes = [string.Empty, "K", "M", "G", "T"]; + + public static string FormatBits(long quantity) + { + var log = quantity < 2 ? 0 : Math.Log2(quantity); + var offsetLog = Math.Max(0, Math.Floor(log - 3.3)); + var unitIndex = (int)Math.Clamp(Math.Floor(offsetLog / 10), 0, _unitsBits.Length - 1); + var divisor = Math.Pow(2, unitIndex * 10); + var quotient = quantity / divisor; + var unitPrefix = _unitsBits[unitIndex]; + return $"{quotient:0.##}{unitPrefix}B"; + } + + public static string FormatBytes(long quantity) + { + var log = quantity < 10 ? 0 : Math.Floor(Math.Log10(quantity)); + var offsetLog = Math.Max(0, log - 1); + var unitIndex = (int)Math.Clamp(Math.Floor(offsetLog / 3), 0, _unitsBytes.Length - 1); + var divisor = Math.Pow(10, unitIndex * 3); + var quotient = quantity / divisor; + var unitPrefix = _unitsBytes[unitIndex]; + return $"{quotient:0.##}{unitPrefix}B"; + } + public static string FormatQuantityAbbreviated(long value) { if (value == 0) @@ -930,6 +955,18 @@ public partial struct Credits public partial struct Debug { + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public static LocalizedString SectionGPUStatistics = @"GPU Statistics"; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public static LocalizedString RenderBufferVRAMFree = @"Render Buffer VRAM Free"; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public static LocalizedString TextureVRAMFree = @"Texture VRAM Free"; + + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] + public static LocalizedString VBOVRAMFree = @"VBO VRAM Free"; + [JsonProperty(NullValueHandling = NullValueHandling.Ignore)] public static LocalizedString ControlUnderCursor = @"Control Under Cursor"; diff --git a/Intersect.Client.Core/MonoGame/Graphics/MonoRenderer.cs b/Intersect.Client.Core/MonoGame/Graphics/MonoRenderer.cs index 7f42e75023..c11e4a98f4 100644 --- a/Intersect.Client.Core/MonoGame/Graphics/MonoRenderer.cs +++ b/Intersect.Client.Core/MonoGame/Graphics/MonoRenderer.cs @@ -9,6 +9,7 @@ using Intersect.Client.Interface.Shared; using Intersect.Client.Localization; using Intersect.Client.MonoGame.NativeInterop; +using Intersect.Client.MonoGame.NativeInterop.OpenGL; using Intersect.Client.ThirdParty; using Intersect.Configuration; using Intersect.Extensions; diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.ATI_meminfo.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.ATI_meminfo.cs new file mode 100644 index 0000000000..177f945b97 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.ATI_meminfo.cs @@ -0,0 +1,32 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static partial class GL +{ + private static bool IsATI_meminfoSupported => IsExtensionSupported("GL_ATI_meminfo"); + + [StructLayout(LayoutKind.Sequential)] + private struct ATI_meminfo_Tuple + { + public int FreeInPool; + public int LargestFreeBlockInPool; + public int FreeInAuxiliaryPool; + public int LargestFreeBlockInAuxiliaryPool; + } + + private static unsafe ATI_meminfo_Tuple glGetATIMemInfo(GLenum name) + { + ATI_meminfo_Tuple data = default; + int* p_data = &data.FreeInPool; + glGetIntegerv_f(name, p_data); + return data; + } +} + +/* + VBO_FREE_MEMORY_ATI 0x87FB + TEXTURE_FREE_MEMORY_ATI 0x87FC + RENDERBUFFER_FREE_MEMORY_ATI 0x87FD*/ \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.Extensions.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.Extensions.cs new file mode 100644 index 0000000000..dccc256380 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.Extensions.cs @@ -0,0 +1,21 @@ +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +public static partial class GL +{ + private static HashSet? _cachedExtensions; + + public static HashSet glGetExtensions() + { + // ReSharper disable once InvertIf + if (_cachedExtensions is not { Count: > 0 }) + { + var numExtensions = glGetIntegerv(GLenum.GL_NUM_EXTENSIONS); + var extensions = glGetStrings(GLenum.GL_EXTENSIONS, numExtensions); + _cachedExtensions = extensions.OfType().ToHashSet(); + } + + return _cachedExtensions; + } + + public static bool IsExtensionSupported(string extensionName) => glGetExtensions().Contains(extensionName); +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.Memory.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.Memory.cs new file mode 100644 index 0000000000..fe5f9d2749 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.Memory.cs @@ -0,0 +1,70 @@ +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +public static partial class GL +{ + /// + /// Available amount of memory for render buffers in bytes + /// + public static long AvailableRenderBufferMemory + { + get + { + if (IsATI_meminfoSupported) + { + var info = glGetATIMemInfo(GLenum.RENDERBUFFER_FREE_MEMORY_ATI); + return info.FreeInPool * 1000L; + } + + if (IsNVX_gpu_memory_infoSupported) + { + return glGetNVXGPUMemoryInfo(GLenum.GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX) * 1000L; + } + + return -1; + } + } + + /// + /// Available amount of memory for textures in bytes + /// + public static long AvailableTextureMemory + { + get + { + if (IsATI_meminfoSupported) + { + var info = glGetATIMemInfo(GLenum.TEXTURE_FREE_MEMORY_ATI); + return info.FreeInPool * 1000L; + } + + if (IsNVX_gpu_memory_infoSupported) + { + return glGetNVXGPUMemoryInfo(GLenum.GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX) * 1000L; + } + + return -1; + } + } + + /// + /// Available amount of memory for VBOs in bytes + /// + public static long AvailableVBOMemory + { + get + { + if (IsATI_meminfoSupported) + { + var info = glGetATIMemInfo(GLenum.VBO_FREE_MEMORY_ATI); + return info.FreeInPool * 1000L; + } + + if (IsNVX_gpu_memory_infoSupported) + { + return glGetNVXGPUMemoryInfo(GLenum.GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX) * 1000L; + } + + return -1; + } + } +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.NVX_gpu_memory_info.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.NVX_gpu_memory_info.cs new file mode 100644 index 0000000000..8d51add1ba --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.NVX_gpu_memory_info.cs @@ -0,0 +1,11 @@ +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +public static partial class GL +{ + public static bool IsNVX_gpu_memory_infoSupported => IsExtensionSupported("GL_NVX_gpu_memory_info"); + + private static long glGetNVXGPUMemoryInfo(GLenum name) + { + return glGetIntegerv(name); + } +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.cs new file mode 100644 index 0000000000..1b14a9aae4 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GL.cs @@ -0,0 +1,38 @@ +using System.Runtime.InteropServices; + +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +public static partial class GL +{ + private static TDelegate LoadFunction(string function) + { + return LoadFunction(function, false)!; + } + + private static T? LoadFunction(string function, bool throwIfNotFound) + { + var ret = Sdl2.SDL_GL_GetProcAddress(function); + + if (ret != IntPtr.Zero) + { + return Marshal.GetDelegateForFunctionPointer(ret); + } + + if (throwIfNotFound) + { + throw new EntryPointNotFoundException(function); + } + + return default; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate GLenum glGetError_d(); + + private static readonly glGetError_d glGetError_f = LoadFunction(nameof(glGetError)); + + public static GLenum glGetError() + { + return glGetError_f(); + } +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GLenum.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GLenum.cs new file mode 100644 index 0000000000..4ab814ce1c --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/GLenum.cs @@ -0,0 +1,45 @@ +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +public enum GLenum +{ + #region Utility + + GL_VENDOR = 0x1F00, + GL_RENDERER = 0x1F01, + GL_VERSION = 0x1F02, + GL_EXTENSIONS = 0x1F03, + + #endregion Utility + + #region Errors + + GL_NO_ERROR = 0, + GL_INVALID_ENUM = 0x0500, + GL_INVALID_VALUE = 0x0501, + GL_INVALID_OPERATION = 0x0502, + GL_STACK_OVERFLOW = 0x0503, + GL_STACK_UNDERFLOW = 0x0504, + GL_OUT_OF_MEMORY = 0x0505, + + #endregion Errors + + GL_NUM_EXTENSIONS = 0x821D, + + #region ATI_meminfo + + VBO_FREE_MEMORY_ATI = 0x87FB, + TEXTURE_FREE_MEMORY_ATI = 0x87FC, + RENDERBUFFER_FREE_MEMORY_ATI = 0x87FD, + + #endregion ATI_meminfo + + #region NVX_gpu_memory_info + + GPU_MEMORY_INFO_DEDICATED_VIDMEM_NVX = 0x9047, + GPU_MEMORY_INFO_TOTAL_AVAILABLE_MEMORY_NVX = 0x9048, + GPU_MEMORY_INFO_CURRENT_AVAILABLE_VIDMEM_NVX = 0x9049, + GPU_MEMORY_INFO_EVICTION_COUNT_NVX = 0x904A, + GPU_MEMORY_INFO_EVICTED_MEMORY_NVX = 0x904B, + + #endregion NVX_gpu_memory_info +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/Gl.Helpers.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/Gl.Helpers.cs new file mode 100644 index 0000000000..ff61270032 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/Gl.Helpers.cs @@ -0,0 +1,24 @@ +using System.Text; + +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +public static partial class GL +{ + private static unsafe string? PointerToUTF8(byte* ptr) + { + if (ptr == default) + { + return null; + } + + var end = ptr; + while (*end != 0) + { + ++end; + } + + var length = (int)(end - ptr); + var str = Encoding.UTF8.GetString(ptr, length); + return str; + } +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/Gl.glGet.cs b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/Gl.glGet.cs new file mode 100644 index 0000000000..7338035b24 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/OpenGL/Gl.glGet.cs @@ -0,0 +1,97 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; + +namespace Intersect.Client.MonoGame.NativeInterop.OpenGL; + +[SuppressMessage("ReSharper", "InconsistentNaming")] +public static partial class GL +{ + private static readonly glGetIntegerv_d glGetIntegerv_f = + LoadFunction(nameof(glGetIntegerv)); + + private static readonly glGetString_d glGetString_f = + LoadFunction(nameof(glGetString)); + + private static readonly glGetStringi_d glGetStringi_f = + LoadFunction(nameof(glGetStringi)); + + public static unsafe int glGetIntegerv(GLenum property) + { + int value; + glGetIntegerv_f(property, &value); + return value; + } + + public static unsafe int[] glGetIntegerv(GLenum property, uint count) + { + var values = new int[count]; + fixed (int* p_values = values) + { + glGetIntegerv_f(property, p_values); + } + return values; + } + + public static unsafe string? glGetString(GLenum name) + { + var ptr = glGetString_f(name); + if (ptr != default) + { + return PointerToUTF8(ptr); + } + + var error = glGetError(); + if (error == GLenum.GL_INVALID_ENUM) + { + throw new ArgumentException($"Invalid glGetString() name '{name}'", nameof(name)); + } + + throw new InvalidOperationException($"Unexpected error {error} when executing glGetString({name})"); + } + + public static unsafe string? glGetStringi(GLenum name, uint index) + { + var ptr = glGetStringi_f(name, index); + if (ptr != default) + { + return PointerToUTF8(ptr); + } + + var error = glGetError(); + if (error == GLenum.GL_INVALID_ENUM) + { + throw new ArgumentException($"Invalid glGetString() name '{name}'", nameof(name)); + } + + if (error == GLenum.GL_INVALID_VALUE) + { + throw new ArgumentOutOfRangeException( + nameof(index), + index, + $"Index out of range for glGetString enum {name}" + ); + } + + throw new InvalidOperationException($"Unexpected error {error} when executing glGetStringi({name}, {index})"); + } + + public static string?[] glGetStrings(GLenum name, int count) + { + var buffer = new string?[count]; + for (uint index = 0; index < count; ++index) + { + buffer[index] = glGetStringi(name, index); + } + + return buffer; + } + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate void glGetIntegerv_d(GLenum property, int* value); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate byte* glGetString_d(GLenum name); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private unsafe delegate byte* glGetStringi_d(GLenum name, uint index); +} \ No newline at end of file diff --git a/Intersect.Client.Core/MonoGame/NativeInterop/Sdl2.GL.cs b/Intersect.Client.Core/MonoGame/NativeInterop/Sdl2.GL.cs new file mode 100644 index 0000000000..b9c8e10fb2 --- /dev/null +++ b/Intersect.Client.Core/MonoGame/NativeInterop/Sdl2.GL.cs @@ -0,0 +1,11 @@ +using System.Runtime.InteropServices; + +namespace Intersect.Client.MonoGame.NativeInterop; + +public partial class Sdl2 +{ + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + private delegate IntPtr SDL_GL_GetProcAddress_d(string proc); + private static SDL_GL_GetProcAddress_d SDL_GL_GetProcAddress_f = Loader.Functions.LoadFunction(nameof(SDL_GL_GetProcAddress)); + public static nint SDL_GL_GetProcAddress(string proc) => SDL_GL_GetProcAddress_f(proc); +} \ No newline at end of file diff --git a/Intersect.Tests.Client/Intersect.Tests.Client.csproj b/Intersect.Tests.Client/Intersect.Tests.Client.csproj index 195a502bed..0b9677e1b6 100644 --- a/Intersect.Tests.Client/Intersect.Tests.Client.csproj +++ b/Intersect.Tests.Client/Intersect.Tests.Client.csproj @@ -16,13 +16,17 @@ - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + \ No newline at end of file diff --git a/Intersect.Tests.Client/Localization/StringsTests.cs b/Intersect.Tests.Client/Localization/StringsTests.cs new file mode 100644 index 0000000000..6408c0e265 --- /dev/null +++ b/Intersect.Tests.Client/Localization/StringsTests.cs @@ -0,0 +1,39 @@ +using System.Globalization; +using Intersect.Client.Localization; +using NUnit.Framework; + +namespace Intersect.Tests.Client.Localization; + +[TestFixture] +public class StringsTests +{ + [SetUp] + public void SetUp() + { + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + } + + [TestCase(1, "1B")] + [TestCase(999, "999B")] + [TestCase(1001, "1001B")] + [TestCase(1234, "1234B")] + [TestCase(12345, "12.35KB")] + public void TestFormatBytes(long bytes, string expectedFormattedString) + { + var actualFormattedString = Strings.FormatBytes(bytes); + Assert.That(actualFormattedString, Is.EqualTo(expectedFormattedString)); + } + + [TestCase(1, "1B")] + [TestCase(999, "999B")] + [TestCase(1001, "1001B")] + [TestCase(1234, "1234B")] + [TestCase(12641, "12.34KiB")] + [TestCase(12944670, "12.34MiB")] + [TestCase(13255342817, "12.34GiB")] + public void TestFormatBits(long bytes, string expectedFormattedString) + { + var actualFormattedString = Strings.FormatBits(bytes); + Assert.That(actualFormattedString, Is.EqualTo(expectedFormattedString)); + } +} \ No newline at end of file diff --git a/Intersect.Tests.Client/Stub.cs b/Intersect.Tests.Client/Stub.cs index 463dfa4ddb..f8422a497b 100644 --- a/Intersect.Tests.Client/Stub.cs +++ b/Intersect.Tests.Client/Stub.cs @@ -12,7 +12,7 @@ public partial class Stub public void TestStub() { // Needed so NUnit doesn't return -2 - Assert.AreEqual(0, MathHelper.Lerp(0, 0, 0)); + Assert.That(0, Is.EqualTo(MathHelper.Lerp(0, 0, 0))); } } diff --git a/Intersect.sln.DotSettings b/Intersect.sln.DotSettings index 40adf0d319..d08674b0f0 100644 --- a/Intersect.sln.DotSettings +++ b/Intersect.sln.DotSettings @@ -1,13 +1,20 @@  IP UI + UTF UV + VBO + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Functions"><ElementKinds><Kind Name="METHOD" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="gl" Suffix="" Style="AaBb_AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Delegates"><ElementKinds><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="gl" Suffix="_d" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Types"><ElementKinds><Kind Name="STRUCT" /><Kind Name="ENUM" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="GL" Suffix="" Style="aa_bb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private, Internal, Public" Description="OpenGL Macros"><ElementKinds><Kind Name="ENUM" /></ElementKinds></Descriptor><Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="GL_" Suffix="" Style="AA_BB"><ExtraRule Prefix="" Suffix="_NVX" Style="AA_BB" /><ExtraRule Prefix="" Suffix="_ATI" Style="AA_BB" /></Policy></Policy> True True True True True True + True True True True @@ -17,4 +24,5 @@ True True True - True + True + True