diff --git a/Directory.Packages.props b/Directory.Packages.props
index de08a5d..df0aa04 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -4,11 +4,7 @@
-
- runtime; build; native; contentfiles; analyzers; buildtransitive
- all
-
-
+
@@ -21,9 +17,13 @@
-
-
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
@@ -33,11 +33,11 @@
-
-
+
+
-
\ No newline at end of file
+
diff --git a/ImGuiApp.Test/FontMemoryGuardTests.cs b/ImGuiApp.Test/FontMemoryGuardTests.cs
new file mode 100644
index 0000000..64c18ca
--- /dev/null
+++ b/ImGuiApp.Test/FontMemoryGuardTests.cs
@@ -0,0 +1,450 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp.Test;
+
+using System;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+///
+/// Tests for FontMemoryGuard functionality including memory estimation, GPU detection,
+/// and Intel/AMD integrated GPU support.
+///
+[TestClass]
+public class FontMemoryGuardTests
+{
+ [TestInitialize]
+ public void Setup()
+ {
+ ImGuiApp.Reset();
+ // Reset FontMemoryGuard configuration to defaults
+ FontMemoryGuard.CurrentConfig = new FontMemoryGuard.FontMemoryConfig();
+ }
+
+ #region FontMemoryEstimate Tests
+
+ [TestMethod]
+ public void FontMemoryEstimate_Equality_WorksCorrectly()
+ {
+ FontMemoryGuard.FontMemoryEstimate estimate1 = new()
+ {
+ EstimatedBytes = 1024,
+ EstimatedGlyphCount = 100,
+ ExceedsLimits = false,
+ RecommendedMaxSizes = 5,
+ ShouldDisableEmojis = false,
+ ShouldReduceUnicodeRanges = false
+ };
+
+ FontMemoryGuard.FontMemoryEstimate estimate2 = new()
+ {
+ EstimatedBytes = 1024,
+ EstimatedGlyphCount = 100,
+ ExceedsLimits = false,
+ RecommendedMaxSizes = 5,
+ ShouldDisableEmojis = false,
+ ShouldReduceUnicodeRanges = false
+ };
+
+ FontMemoryGuard.FontMemoryEstimate estimate3 = new()
+ {
+ EstimatedBytes = 2048,
+ EstimatedGlyphCount = 100,
+ ExceedsLimits = false,
+ RecommendedMaxSizes = 5,
+ ShouldDisableEmojis = false,
+ ShouldReduceUnicodeRanges = false
+ };
+
+ // Test equality
+ Assert.AreEqual(estimate1, estimate2);
+ Assert.AreNotEqual(estimate1, estimate3);
+
+ // Test operators
+ Assert.IsTrue(estimate1 == estimate2);
+ Assert.IsFalse(estimate1 == estimate3);
+ Assert.IsFalse(estimate1 != estimate2);
+ Assert.IsTrue(estimate1 != estimate3);
+
+ // Test Equals with object
+ Assert.IsTrue(estimate1.Equals((object)estimate2));
+ Assert.IsFalse(estimate1.Equals((object)estimate3));
+ Assert.IsFalse(estimate1.Equals(null));
+ Assert.IsFalse(estimate1.Equals("not an estimate"));
+ }
+
+ [TestMethod]
+ public void FontMemoryEstimate_GetHashCode_ConsistentForEqualObjects()
+ {
+ FontMemoryGuard.FontMemoryEstimate estimate1 = new()
+ {
+ EstimatedBytes = 1024,
+ EstimatedGlyphCount = 100,
+ ExceedsLimits = true,
+ RecommendedMaxSizes = 3,
+ ShouldDisableEmojis = true,
+ ShouldReduceUnicodeRanges = false
+ };
+
+ FontMemoryGuard.FontMemoryEstimate estimate2 = new()
+ {
+ EstimatedBytes = 1024,
+ EstimatedGlyphCount = 100,
+ ExceedsLimits = true,
+ RecommendedMaxSizes = 3,
+ ShouldDisableEmojis = true,
+ ShouldReduceUnicodeRanges = false
+ };
+
+ Assert.AreEqual(estimate1.GetHashCode(), estimate2.GetHashCode());
+ }
+
+ #endregion
+
+ #region EstimateMemoryUsage Tests
+
+ [TestMethod]
+ public void EstimateMemoryUsage_ValidParameters_ReturnsValidEstimate()
+ {
+ int[] fontSizes = [12, 14, 16, 18, 20];
+
+ FontMemoryGuard.FontMemoryEstimate estimate = FontMemoryGuard.EstimateMemoryUsage(
+ fontCount: 2,
+ fontSizes: fontSizes,
+ includeEmojis: true,
+ includeExtendedUnicode: true,
+ scaleFactor: 1.0f);
+
+ Assert.IsTrue(estimate.EstimatedBytes > 0);
+ Assert.IsTrue(estimate.EstimatedGlyphCount > 0);
+ Assert.IsTrue(estimate.RecommendedMaxSizes > 0);
+ }
+
+ [TestMethod]
+ public void EstimateMemoryUsage_HighScaleFactor_IncreasesMemoryEstimate()
+ {
+ int[] fontSizes = [14, 16, 18];
+
+ FontMemoryGuard.FontMemoryEstimate lowScaleEstimate = FontMemoryGuard.EstimateMemoryUsage(1, fontSizes, false, false, 1.0f);
+ FontMemoryGuard.FontMemoryEstimate highScaleEstimate = FontMemoryGuard.EstimateMemoryUsage(1, fontSizes, false, false, 2.0f);
+
+ Assert.IsTrue(highScaleEstimate.EstimatedBytes > lowScaleEstimate.EstimatedBytes);
+ }
+
+ [TestMethod]
+ public void EstimateMemoryUsage_WithEmojisAndUnicode_IncreasesGlyphCount()
+ {
+ int[] fontSizes = [14];
+
+ FontMemoryGuard.FontMemoryEstimate basicEstimate = FontMemoryGuard.EstimateMemoryUsage(1, fontSizes, false, false, 1.0f);
+ FontMemoryGuard.FontMemoryEstimate unicodeEstimate = FontMemoryGuard.EstimateMemoryUsage(1, fontSizes, false, true, 1.0f);
+ FontMemoryGuard.FontMemoryEstimate emojiEstimate = FontMemoryGuard.EstimateMemoryUsage(1, fontSizes, true, false, 1.0f);
+ FontMemoryGuard.FontMemoryEstimate fullEstimate = FontMemoryGuard.EstimateMemoryUsage(1, fontSizes, true, true, 1.0f);
+
+ Assert.IsTrue(unicodeEstimate.EstimatedGlyphCount > basicEstimate.EstimatedGlyphCount);
+ Assert.IsTrue(emojiEstimate.EstimatedGlyphCount > basicEstimate.EstimatedGlyphCount);
+ Assert.IsTrue(fullEstimate.EstimatedGlyphCount > unicodeEstimate.EstimatedGlyphCount);
+ Assert.IsTrue(fullEstimate.EstimatedGlyphCount > emojiEstimate.EstimatedGlyphCount);
+ }
+
+ [TestMethod]
+ public void EstimateMemoryUsage_NullFontSizes_ThrowsArgumentNullException()
+ {
+ Assert.ThrowsExactly(() =>
+ FontMemoryGuard.EstimateMemoryUsage(1, null!, false, false, 1.0f));
+ }
+
+ [TestMethod]
+ public void EstimateMemoryUsage_ExceedsLimits_SetsCorrectFlags()
+ {
+ // Set a very low memory limit to trigger limit exceeded
+ FontMemoryGuard.CurrentConfig.MaxAtlasMemoryBytes = 1024; // 1KB - very small
+
+ int[] largeFontSizes = [12, 14, 16, 18, 20, 24, 32, 48]; // Many sizes
+
+ FontMemoryGuard.FontMemoryEstimate estimate = FontMemoryGuard.EstimateMemoryUsage(
+ fontCount: 3, // Multiple fonts
+ fontSizes: largeFontSizes,
+ includeEmojis: true,
+ includeExtendedUnicode: true,
+ scaleFactor: 2.0f); // High DPI
+
+ Assert.IsTrue(estimate.ExceedsLimits, "Should exceed the very low memory limit");
+ }
+
+ #endregion
+
+ #region GetReducedFontSizes Tests
+
+ [TestMethod]
+ public void GetReducedFontSizes_FewerThanMax_ReturnsOriginal()
+ {
+ int[] originalSizes = [12, 14, 16];
+ int[] result = FontMemoryGuard.GetReducedFontSizes(originalSizes, 5, 14);
+
+ CollectionAssert.AreEqual(originalSizes, result);
+ }
+
+ [TestMethod]
+ public void GetReducedFontSizes_MoreThanMax_ReducesToMax()
+ {
+ int[] originalSizes = [10, 12, 14, 16, 18, 20, 24, 32, 48];
+ int[] result = FontMemoryGuard.GetReducedFontSizes(originalSizes, 3, 14);
+
+ Assert.AreEqual(3, result.Length);
+ Assert.IsTrue(result.Contains(14), "Should always include preferred size");
+ }
+
+ [TestMethod]
+ public void GetReducedFontSizes_PrioritizesPreferredSize()
+ {
+ int[] originalSizes = [10, 12, 14, 16, 18, 20, 24, 32, 48];
+ int[] result = FontMemoryGuard.GetReducedFontSizes(originalSizes, 4, 16);
+
+ Assert.IsTrue(result.Contains(16), "Should include preferred size 16");
+ Assert.AreEqual(4, result.Length);
+ }
+
+ [TestMethod]
+ public void GetReducedFontSizes_RespectsMinimumSizes()
+ {
+ FontMemoryGuard.CurrentConfig.MinFontSizesToLoad = 2;
+ int[] originalSizes = [12, 14, 16, 18, 20];
+ int[] result = FontMemoryGuard.GetReducedFontSizes(originalSizes, 1, 14); // Request only 1, but min is 2
+
+ Assert.IsTrue(result.Length >= 2, "Should respect minimum font sizes setting");
+ }
+
+ [TestMethod]
+ public void GetReducedFontSizes_NullOriginalSizes_ThrowsArgumentNullException()
+ {
+ Assert.ThrowsExactly(() =>
+ FontMemoryGuard.GetReducedFontSizes(null!, 3, 14));
+ }
+
+ [TestMethod]
+ public void GetReducedFontSizes_ResultIsSorted()
+ {
+ int[] originalSizes = [48, 12, 20, 14, 16]; // Unsorted input
+ int[] result = FontMemoryGuard.GetReducedFontSizes(originalSizes, 3, 14);
+
+ for (int i = 1; i < result.Length; i++)
+ {
+ Assert.IsTrue(result[i] > result[i - 1], "Result should be sorted in ascending order");
+ }
+ }
+
+ #endregion
+
+ #region DetermineFallbackStrategy Tests
+
+ [TestMethod]
+ public void DetermineFallbackStrategy_NoLimitsExceeded_ReturnsNone()
+ {
+ FontMemoryGuard.FontMemoryEstimate estimate = new()
+ {
+ ExceedsLimits = false,
+ EstimatedBytes = 1024
+ };
+
+ FontMemoryGuard.FallbackStrategy strategy = FontMemoryGuard.DetermineFallbackStrategy(estimate);
+ Assert.AreEqual(FontMemoryGuard.FallbackStrategy.None, strategy);
+ }
+
+ [TestMethod]
+ public void DetermineFallbackStrategy_SlightOverage_ReturnsReduceFontSizes()
+ {
+ FontMemoryGuard.CurrentConfig.MaxAtlasMemoryBytes = 1024;
+ FontMemoryGuard.FontMemoryEstimate estimate = new()
+ {
+ ExceedsLimits = true,
+ EstimatedBytes = 1200 // 1.17x over limit
+ };
+
+ FontMemoryGuard.FallbackStrategy strategy = FontMemoryGuard.DetermineFallbackStrategy(estimate);
+ Assert.AreEqual(FontMemoryGuard.FallbackStrategy.ReduceFontSizes, strategy);
+ }
+
+ [TestMethod]
+ public void DetermineFallbackStrategy_ModerateOverage_ReturnsDisableEmojis()
+ {
+ FontMemoryGuard.CurrentConfig.MaxAtlasMemoryBytes = 1024;
+ FontMemoryGuard.FontMemoryEstimate estimate = new()
+ {
+ ExceedsLimits = true,
+ EstimatedBytes = 1700 // 1.66x over limit
+ };
+
+ FontMemoryGuard.FallbackStrategy strategy = FontMemoryGuard.DetermineFallbackStrategy(estimate);
+ Assert.AreEqual(FontMemoryGuard.FallbackStrategy.DisableEmojis, strategy);
+ }
+
+ [TestMethod]
+ public void DetermineFallbackStrategy_HighOverage_ReturnsReduceUnicodeRanges()
+ {
+ FontMemoryGuard.CurrentConfig.MaxAtlasMemoryBytes = 1024;
+ FontMemoryGuard.FontMemoryEstimate estimate = new()
+ {
+ ExceedsLimits = true,
+ EstimatedBytes = 2500 // 2.44x over limit
+ };
+
+ FontMemoryGuard.FallbackStrategy strategy = FontMemoryGuard.DetermineFallbackStrategy(estimate);
+ Assert.AreEqual(FontMemoryGuard.FallbackStrategy.ReduceUnicodeRanges, strategy);
+ }
+
+ [TestMethod]
+ public void DetermineFallbackStrategy_ExtremeOverage_ReturnsMinimalFonts()
+ {
+ FontMemoryGuard.CurrentConfig.MaxAtlasMemoryBytes = 1024;
+ FontMemoryGuard.FontMemoryEstimate estimate = new()
+ {
+ ExceedsLimits = true,
+ EstimatedBytes = 5000 // 4.88x over limit
+ };
+
+ FontMemoryGuard.FallbackStrategy strategy = FontMemoryGuard.DetermineFallbackStrategy(estimate);
+ Assert.AreEqual(FontMemoryGuard.FallbackStrategy.MinimalFonts, strategy);
+ }
+
+ #endregion
+
+ #region GPU Detection Tests
+
+ [TestMethod]
+ public void TryDetectAndConfigureGpuMemory_NullGL_ReturnsFalse()
+ {
+ bool result = FontMemoryGuard.TryDetectAndConfigureGpuMemory(null!);
+ Assert.IsFalse(result);
+ }
+
+ [TestMethod]
+ public void TryDetectAndConfigureGpuMemory_DetectionDisabled_ReturnsFalse()
+ {
+ FontMemoryGuard.CurrentConfig.EnableGpuMemoryDetection = false;
+
+ // Since detection is disabled, the method should return false regardless of GL
+ bool result = FontMemoryGuard.TryDetectAndConfigureGpuMemory(null!);
+ Assert.IsFalse(result);
+ }
+
+ #endregion
+
+ #region GPU Heuristics Unit Tests (without actual GL calls)
+
+ [TestMethod]
+ public void FontMemoryGuard_IsIntegratedGpu_DetectsIntelCorrectly()
+ {
+ // Test Intel GPU detection patterns
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("Intel(R) HD Graphics 530"));
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("Intel(R) UHD Graphics 620"));
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("Intel(R) Xe Graphics"));
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("Intel(R) Iris(R) Xe Graphics"));
+
+ // Should not detect discrete GPUs as integrated
+ Assert.IsFalse(FontMemoryGuard.IsIntegratedGpu("NVIDIA GeForce RTX 3060"));
+ Assert.IsFalse(FontMemoryGuard.IsIntegratedGpu("AMD Radeon RX 6600 XT"));
+ }
+
+ [TestMethod]
+ public void FontMemoryGuard_IsIntegratedGpu_DetectsAmdApuCorrectly()
+ {
+ // Test AMD APU detection patterns
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("AMD Radeon(TM) Vega 8 Graphics"));
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("AMD Radeon(TM) 680M"));
+ Assert.IsTrue(FontMemoryGuard.IsIntegratedGpu("AMD Radeon(TM) R7 Graphics"));
+
+ // Should not detect discrete GPUs as integrated
+ Assert.IsFalse(FontMemoryGuard.IsIntegratedGpu("AMD Radeon RX 6600 XT"));
+ Assert.IsFalse(FontMemoryGuard.IsIntegratedGpu("AMD Radeon RX 7900 XTX"));
+ }
+
+ [TestMethod]
+ public void FontMemoryGuard_GetIntelGpuMemoryLimit_ReturnsCorrectLimits()
+ {
+ // Test Intel GPU generation-based memory limits
+ Assert.AreEqual(96 * 1024 * 1024, FontMemoryGuard.GetIntelGpuMemoryLimit("Intel(R) Xe Graphics"));
+ Assert.AreEqual(80 * 1024 * 1024, FontMemoryGuard.GetIntelGpuMemoryLimit("Intel(R) Iris(R) Xe Graphics"));
+ Assert.AreEqual(64 * 1024 * 1024, FontMemoryGuard.GetIntelGpuMemoryLimit("Intel(R) UHD Graphics 620"));
+ Assert.AreEqual(32 * 1024 * 1024, FontMemoryGuard.GetIntelGpuMemoryLimit("Intel(R) HD Graphics 530"));
+
+ // Default for unrecognized Intel GPUs
+ Assert.AreEqual(32 * 1024 * 1024, FontMemoryGuard.GetIntelGpuMemoryLimit("Intel(R) Unknown Graphics"));
+ }
+
+ [TestMethod]
+ public void FontMemoryGuard_GetAmdApuMemoryLimit_ReturnsCorrectLimits()
+ {
+ // Test AMD APU generation-based memory limits
+ Assert.AreEqual(128 * 1024 * 1024, FontMemoryGuard.GetAmdApuMemoryLimit("AMD Radeon(TM) 680M"));
+ Assert.AreEqual(96 * 1024 * 1024, FontMemoryGuard.GetAmdApuMemoryLimit("AMD Radeon(TM) Vega 8 Graphics"));
+ Assert.AreEqual(48 * 1024 * 1024, FontMemoryGuard.GetAmdApuMemoryLimit("AMD Radeon(TM) R7 Graphics"));
+
+ // Default for unrecognized AMD APUs
+ Assert.AreEqual(64 * 1024 * 1024, FontMemoryGuard.GetAmdApuMemoryLimit("AMD Radeon(TM) Unknown APU"));
+ }
+
+ #endregion
+
+ #region Configuration Tests
+
+ [TestMethod]
+ public void FontMemoryConfig_DefaultValues_AreReasonable()
+ {
+ FontMemoryGuard.FontMemoryConfig config = new();
+
+ Assert.AreEqual(FontMemoryGuard.DefaultMaxAtlasMemoryBytes, config.MaxAtlasMemoryBytes);
+ Assert.IsTrue(config.EnableGpuMemoryDetection);
+ Assert.AreEqual(0.1f, config.MaxGpuMemoryPercentage);
+ Assert.IsTrue(config.EnableFallbackStrategies);
+ Assert.AreEqual(3, config.MinFontSizesToLoad);
+ Assert.IsTrue(config.DisableEmojisOnLowMemory);
+ Assert.IsTrue(config.ReduceUnicodeRangesOnLowMemory);
+ Assert.IsTrue(config.EnableIntelGpuHeuristics);
+ Assert.IsTrue(config.EnableAmdApuHeuristics);
+ }
+
+ [TestMethod]
+ public void FontMemoryGuard_Constants_HaveExpectedValues()
+ {
+ Assert.AreEqual(64 * 1024 * 1024, FontMemoryGuard.DefaultMaxAtlasMemoryBytes);
+ Assert.AreEqual(8 * 1024 * 1024, FontMemoryGuard.MinAtlasMemoryBytes);
+ Assert.AreEqual(4096, FontMemoryGuard.MaxAtlasTextureDimension);
+ Assert.AreEqual(128, FontMemoryGuard.EstimatedBytesPerGlyph);
+ }
+
+ #endregion
+
+ #region Error Handling Tests
+
+ [TestMethod]
+ public void LogMemoryUsage_DoesNotThrow()
+ {
+ FontMemoryGuard.FontMemoryEstimate estimate = new()
+ {
+ EstimatedBytes = 1024,
+ EstimatedGlyphCount = 100,
+ ExceedsLimits = true
+ };
+
+ try
+ {
+ FontMemoryGuard.LogMemoryUsage(estimate, FontMemoryGuard.FallbackStrategy.ReduceFontSizes);
+ }
+ catch (ArgumentException ex)
+ {
+ Assert.Fail($"LogMemoryUsage should not throw ArgumentException, but got: {ex.Message}");
+ }
+ catch (InvalidOperationException ex)
+ {
+ Assert.Fail($"LogMemoryUsage should not throw InvalidOperationException, but got: {ex.Message}");
+ }
+ catch (System.ComponentModel.Win32Exception ex)
+ {
+ Assert.Fail($"LogMemoryUsage should not throw Win32Exception, but got: {ex.Message}");
+ }
+ }
+
+ #endregion
+}
diff --git a/ImGuiApp.Test/ImGuiApp.Test.csproj b/ImGuiApp.Test/ImGuiApp.Test.csproj
index 88a9665..65fc55d 100644
--- a/ImGuiApp.Test/ImGuiApp.Test.csproj
+++ b/ImGuiApp.Test/ImGuiApp.Test.csproj
@@ -1,6 +1,7 @@
+ net9.0;
true
diff --git a/ImGuiApp.Test/ImGuiAppTests.cs b/ImGuiApp.Test/ImGuiAppTests.cs
index a0b3363..8ed4bf1 100644
--- a/ImGuiApp.Test/ImGuiAppTests.cs
+++ b/ImGuiApp.Test/ImGuiAppTests.cs
@@ -2,8 +2,6 @@
// All rights reserved.
// Licensed under the MIT license.
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-
[assembly: DoNotParallelize]
namespace ktsu.ImGuiApp.Test;
diff --git a/ImGuiApp/FontMemoryGuard.cs b/ImGuiApp/FontMemoryGuard.cs
new file mode 100644
index 0000000..67d0b50
--- /dev/null
+++ b/ImGuiApp/FontMemoryGuard.cs
@@ -0,0 +1,762 @@
+// Copyright (c) ktsu.dev
+// All rights reserved.
+// Licensed under the MIT license.
+
+namespace ktsu.ImGuiApp;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Hexa.NET.ImGui;
+using Silk.NET.OpenGL;
+
+///
+/// Provides memory guards and limits for font loading to prevent excessive texture memory allocation
+/// on small GPUs or high-resolution displays.
+///
+/// Intel & AMD Integrated GPU Support:
+/// Special handling for integrated graphics which are the primary targets for memory constraints:
+///
+/// - Intel Graphics: HD Graphics, UHD Graphics, Iris, Xe Graphics - Often don't expose memory query extensions
+/// - AMD APUs: Vega, RDNA integrated graphics - Share system RAM with limited bandwidth
+/// - Automatic Detection: Uses renderer string analysis when OpenGL memory extensions aren't available
+/// - Conservative Limits: 16-96MB font atlas limits for integrated GPUs vs 64-128MB for discrete
+/// - Generation-Aware: Newer integrated GPUs (Xe, RDNA2+) get higher limits than older ones
+///
+///
+/// Why This Matters:
+/// Integrated GPUs share system RAM and have limited memory bandwidth. A 4K display with full Unicode
+/// font support can easily create 200MB+ font atlases, which can cause:
+///
+/// - Application crashes due to GPU memory exhaustion
+/// - Severe performance degradation from memory pressure
+/// - System-wide slowdowns as integrated GPU competes with CPU for RAM bandwidth
+///
+///
+public static class FontMemoryGuard
+{
+ ///
+ /// Default maximum font atlas texture size in bytes (64MB).
+ /// This is conservative for integrated GPUs and high-DPI displays.
+ ///
+ public const long DefaultMaxAtlasMemoryBytes = 64 * 1024 * 1024; // 64MB
+
+ ///
+ /// Minimum font atlas texture size in bytes (8MB).
+ /// Below this threshold, basic functionality may be compromised.
+ ///
+ public const long MinAtlasMemoryBytes = 8 * 1024 * 1024; // 8MB
+
+ ///
+ /// Maximum font atlas texture dimension (e.g., 4096x4096).
+ /// Most GPUs support at least this size.
+ ///
+ public const int MaxAtlasTextureDimension = 4096;
+
+ ///
+ /// Estimated bytes per glyph for memory calculations.
+ /// This is a rough estimate based on typical font rasterization.
+ ///
+ public const int EstimatedBytesPerGlyph = 128;
+
+ ///
+ /// Configuration for font memory limits.
+ ///
+ public class FontMemoryConfig
+ {
+ ///
+ /// Maximum memory to allocate for font atlas textures in bytes.
+ ///
+ public long MaxAtlasMemoryBytes { get; set; } = DefaultMaxAtlasMemoryBytes;
+
+ ///
+ /// Whether to enable automatic GPU memory detection.
+ ///
+ public bool EnableGpuMemoryDetection { get; set; } = true;
+
+ ///
+ /// Maximum percentage of available GPU memory to use for font textures.
+ /// Note: Integrated GPUs automatically use lower percentages (3-5%) regardless of this setting.
+ ///
+ public float MaxGpuMemoryPercentage { get; set; } = 0.1f; // 10% for discrete GPUs
+
+ ///
+ /// Whether to enable special handling for Intel integrated GPUs.
+ /// Intel GPUs often don't expose memory query extensions, so we use heuristics.
+ ///
+ public bool EnableIntelGpuHeuristics { get; set; } = true;
+
+ ///
+ /// Whether to enable special handling for AMD integrated GPUs (APUs).
+ ///
+ public bool EnableAmdApuHeuristics { get; set; } = true;
+
+ ///
+ /// Whether to enable fallback strategies when memory limits are exceeded.
+ ///
+ public bool EnableFallbackStrategies { get; set; } = true;
+
+ ///
+ /// Minimum number of font sizes to load even under memory constraints.
+ ///
+ public int MinFontSizesToLoad { get; set; } = 3; // e.g., 12, 16, 20
+
+ ///
+ /// Whether to disable emoji fonts under memory constraints.
+ ///
+ public bool DisableEmojisOnLowMemory { get; set; } = true;
+
+ ///
+ /// Whether to reduce Unicode glyph ranges under memory constraints.
+ ///
+ public bool ReduceUnicodeRangesOnLowMemory { get; set; } = true;
+ }
+
+ ///
+ /// Result of font memory estimation.
+ ///
+ public struct FontMemoryEstimate : IEquatable
+ {
+ ///
+ /// Estimated memory usage in bytes.
+ ///
+ public long EstimatedBytes { get; set; }
+
+ ///
+ /// Number of glyphs estimated.
+ ///
+ public int EstimatedGlyphCount { get; set; }
+
+ ///
+ /// Whether the estimate exceeds memory limits.
+ ///
+ public bool ExceedsLimits { get; set; }
+
+ ///
+ /// Recommended maximum number of font sizes to load.
+ ///
+ public int RecommendedMaxSizes { get; set; }
+
+ ///
+ /// Whether emoji fonts should be disabled.
+ ///
+ public bool ShouldDisableEmojis { get; set; }
+
+ ///
+ /// Whether Unicode ranges should be reduced.
+ ///
+ public bool ShouldReduceUnicodeRanges { get; set; }
+
+ ///
+ /// Determines whether the specified object is equal to the current object.
+ ///
+ /// The object to compare with the current object.
+ /// true if the specified object is equal to the current object; otherwise, false.
+ public override readonly bool Equals(object? obj) => obj is FontMemoryEstimate estimate && Equals(estimate);
+
+ ///
+ /// Serves as the default hash function.
+ ///
+ /// A hash code for the current object.
+ public override readonly int GetHashCode() => HashCode.Combine(EstimatedBytes, EstimatedGlyphCount, ExceedsLimits, RecommendedMaxSizes, ShouldDisableEmojis, ShouldReduceUnicodeRanges);
+
+ ///
+ /// Indicates whether the current object is equal to another object of the same type.
+ ///
+ /// An object to compare with this object.
+ /// true if the current object is equal to the other parameter; otherwise, false.
+ public readonly bool Equals(FontMemoryEstimate other) =>
+ EstimatedBytes == other.EstimatedBytes &&
+ EstimatedGlyphCount == other.EstimatedGlyphCount &&
+ ExceedsLimits == other.ExceedsLimits &&
+ RecommendedMaxSizes == other.RecommendedMaxSizes &&
+ ShouldDisableEmojis == other.ShouldDisableEmojis &&
+ ShouldReduceUnicodeRanges == other.ShouldReduceUnicodeRanges;
+
+ ///
+ /// Determines whether two specified instances of FontMemoryEstimate are equal.
+ ///
+ /// The first FontMemoryEstimate to compare.
+ /// The second FontMemoryEstimate to compare.
+ /// true if the two FontMemoryEstimate instances are equal; otherwise, false.
+ public static bool operator ==(FontMemoryEstimate left, FontMemoryEstimate right) => left.Equals(right);
+
+ ///
+ /// Determines whether two specified instances of FontMemoryEstimate are not equal.
+ ///
+ /// The first FontMemoryEstimate to compare.
+ /// The second FontMemoryEstimate to compare.
+ /// true if the two FontMemoryEstimate instances are not equal; otherwise, false.
+ public static bool operator !=(FontMemoryEstimate left, FontMemoryEstimate right) => !(left == right);
+ }
+
+ ///
+ /// Fallback strategy for font loading under memory constraints.
+ ///
+ public enum FallbackStrategy
+ {
+ ///
+ /// No fallback needed.
+ ///
+ None,
+
+ ///
+ /// Reduce number of font sizes.
+ ///
+ ReduceFontSizes,
+
+ ///
+ /// Disable emoji fonts.
+ ///
+ DisableEmojis,
+
+ ///
+ /// Reduce Unicode glyph ranges.
+ ///
+ ReduceUnicodeRanges,
+
+ ///
+ /// Aggressive fallback: minimal fonts only.
+ ///
+ MinimalFonts
+ }
+
+ ///
+ /// Gets the current font memory configuration.
+ ///
+ public static FontMemoryConfig CurrentConfig { get; set; } = new();
+
+ ///
+ /// Estimates the memory usage for font loading based on the provided parameters.
+ ///
+ /// Number of font files to load.
+ /// Array of font sizes to load.
+ /// Whether emoji fonts will be included.
+ /// Whether extended Unicode ranges will be included.
+ /// Current DPI scale factor.
+ /// Font memory estimate.
+ public static FontMemoryEstimate EstimateMemoryUsage(
+ int fontCount,
+ int[] fontSizes,
+ bool includeEmojis,
+ bool includeExtendedUnicode,
+ float scaleFactor)
+ {
+ ArgumentNullException.ThrowIfNull(fontSizes);
+
+ // Estimate glyph count based on configuration
+ int baseGlyphCount = 128; // ASCII
+ if (includeExtendedUnicode)
+ {
+ baseGlyphCount += GetExtendedUnicodeGlyphCount();
+ }
+ if (includeEmojis)
+ {
+ baseGlyphCount += GetEmojiGlyphCount();
+ }
+
+ // Calculate total glyph count across all fonts and sizes
+ int totalGlyphCount = fontCount * fontSizes.Length * baseGlyphCount;
+
+ // Apply scale factor impact (higher DPI = larger textures)
+ float scaleImpact = scaleFactor * scaleFactor; // Quadratic impact on texture size
+ long estimatedBytes = (long)(totalGlyphCount * EstimatedBytesPerGlyph * scaleImpact);
+
+ // Determine if limits are exceeded and recommend fallbacks
+ bool exceedsLimits = estimatedBytes > CurrentConfig.MaxAtlasMemoryBytes;
+ int recommendedMaxSizes = CalculateRecommendedMaxSizes(estimatedBytes, fontCount, baseGlyphCount, scaleFactor);
+ bool shouldDisableEmojis = exceedsLimits && CurrentConfig.DisableEmojisOnLowMemory && includeEmojis;
+ bool shouldReduceUnicode = exceedsLimits && CurrentConfig.ReduceUnicodeRangesOnLowMemory && includeExtendedUnicode;
+
+ return new FontMemoryEstimate
+ {
+ EstimatedBytes = estimatedBytes,
+ EstimatedGlyphCount = totalGlyphCount,
+ ExceedsLimits = exceedsLimits,
+ RecommendedMaxSizes = recommendedMaxSizes,
+ ShouldDisableEmojis = shouldDisableEmojis,
+ ShouldReduceUnicodeRanges = shouldReduceUnicode
+ };
+ }
+
+ ///
+ /// Attempts to detect available GPU memory and update configuration accordingly.
+ /// Special handling for Intel and AMD integrated GPUs which are primary targets for memory constraints.
+ ///
+ /// OpenGL context for querying GPU information.
+ /// True if GPU memory was successfully detected and configuration updated.
+ public static unsafe bool TryDetectAndConfigureGpuMemory(GL gl)
+ {
+ if (!CurrentConfig.EnableGpuMemoryDetection || gl == null)
+ {
+ return false;
+ }
+
+ try
+ {
+ // Get GPU vendor and renderer information
+ string vendor = gl.GetStringS(GLEnum.Vendor) ?? "";
+ string renderer = gl.GetStringS(GLEnum.Renderer) ?? "";
+ bool isIntelGpu = vendor.Contains("Intel", StringComparison.OrdinalIgnoreCase);
+ bool isAmdGpu = vendor.Contains("AMD", StringComparison.OrdinalIgnoreCase) || vendor.Contains("ATI", StringComparison.OrdinalIgnoreCase);
+ bool isNvidiaGpu = vendor.Contains("NVIDIA", StringComparison.OrdinalIgnoreCase);
+ bool isIntegratedGpu = IsIntegratedGpu(renderer);
+
+ DebugLogger.Log($"FontMemoryGuard: Detected GPU - Vendor: {vendor}, Renderer: {renderer}");
+ DebugLogger.Log($"FontMemoryGuard: GPU Classification - Intel: {isIntelGpu}, AMD: {isAmdGpu}, NVIDIA: {isNvidiaGpu}, Integrated: {isIntegratedGpu}");
+
+ // Try to get GPU memory information using OpenGL extensions
+ bool memoryDetected = false;
+ long availableMemoryKB = 0;
+
+ // Try NVIDIA extension first (works on discrete NVIDIA GPUs)
+ // Note: NVIDIA memory extensions typically aren't available as TryGetExtension in Silk.NET
+ // We'll use direct OpenGL calls instead
+ if (isNvidiaGpu && gl.IsExtensionPresent("GL_NVX_gpu_memory_info"))
+ {
+ gl.GetInteger((GLEnum)0x9048, out int totalMemKB); // GL_GPU_MEMORY_TOTAL_AVAILABLE_MEMORY_NVX
+ gl.GetInteger((GLEnum)0x9049, out int currentMemKB); // GL_GPU_MEMORY_CURRENT_AVAILABLE_MEMORY_NVX
+ availableMemoryKB = currentMemKB;
+ memoryDetected = true;
+ DebugLogger.Log($"FontMemoryGuard: NVIDIA GPU memory: {totalMemKB}KB total, {currentMemKB}KB available");
+ }
+ // Try ATI extension (works on AMD discrete and some integrated GPUs)
+ else if (isAmdGpu && gl.IsExtensionPresent("GL_ATI_meminfo"))
+ {
+ Span memInfo = stackalloc int[4];
+ gl.GetInteger((GLEnum)0x87FC, memInfo); // GL_TEXTURE_FREE_MEMORY_ATI
+ availableMemoryKB = memInfo[0];
+ memoryDetected = true;
+ DebugLogger.Log($"FontMemoryGuard: AMD GPU texture memory: {availableMemoryKB}KB available");
+ }
+
+ // Apply memory-based configuration if detected
+ if (memoryDetected && availableMemoryKB > 0)
+ {
+ long recommendedFontMemory = CalculateRecommendedMemoryFromDetection(availableMemoryKB, isIntegratedGpu);
+ CurrentConfig.MaxAtlasMemoryBytes = recommendedFontMemory;
+ DebugLogger.Log($"FontMemoryGuard: Set font memory limit to {recommendedFontMemory / (1024 * 1024)}MB based on GPU memory detection");
+ return true;
+ }
+
+ // Fallback to heuristic-based configuration for Intel and undetected GPUs
+ if (ApplyIntegratedGpuHeuristics(isIntelGpu, isAmdGpu, isIntegratedGpu, renderer))
+ {
+ return true;
+ }
+ }
+ catch (InvalidOperationException ex)
+ {
+ DebugLogger.Log($"FontMemoryGuard: GPU memory detection failed with InvalidOperationException: {ex.Message}");
+ }
+ catch (ArgumentException ex)
+ {
+ DebugLogger.Log($"FontMemoryGuard: GPU memory detection failed with ArgumentException: {ex.Message}");
+ }
+ catch (System.ComponentModel.Win32Exception ex)
+ {
+ DebugLogger.Log($"FontMemoryGuard: GPU memory detection failed with Win32Exception: {ex.Message}");
+ }
+
+ return false;
+ }
+
+ ///
+ /// Determines if a GPU is integrated based on renderer string analysis.
+ ///
+ /// GPU renderer string from OpenGL.
+ /// True if the GPU appears to be integrated.
+ public static bool IsIntegratedGpu(string renderer)
+ {
+ if (string.IsNullOrEmpty(renderer))
+ {
+ return false;
+ }
+
+ string rendererLower = renderer.ToLowerInvariant();
+
+ // Intel integrated GPU patterns
+ string[] intelIntegratedPatterns = [
+ "intel", "hd graphics", "uhd graphics", "iris", "xe graphics"
+ ];
+
+ // AMD integrated GPU patterns
+ string[] amdIntegratedPatterns = [
+ "radeon(tm)", "vega", "rdna", "apu", "r5 graphics", "r7 graphics"
+ ];
+
+ // General integrated GPU indicators
+ string[] integratedPatterns = [
+ "integrated", "onboard", "shared"
+ ];
+
+ return intelIntegratedPatterns.Any(rendererLower.Contains) ||
+ amdIntegratedPatterns.Any(rendererLower.Contains) ||
+ integratedPatterns.Any(rendererLower.Contains);
+ }
+
+ ///
+ /// Applies memory configuration heuristics for integrated GPUs when direct memory detection fails.
+ /// This is especially important for Intel integrated GPUs which often don't expose memory extensions.
+ ///
+ /// Whether this is an Intel GPU.
+ /// Whether this is an AMD GPU.
+ /// Whether this appears to be integrated graphics.
+ /// GPU renderer string for additional analysis.
+ /// True if heuristics were applied and configuration updated.
+ private static bool ApplyIntegratedGpuHeuristics(bool isIntelGpu, bool isAmdGpu, bool isIntegratedGpu, string renderer)
+ {
+ if (!isIntegratedGpu && !isIntelGpu)
+ {
+ return false; // Only apply heuristics for suspected integrated GPUs
+ }
+
+ long recommendedMemory = DefaultMaxAtlasMemoryBytes; // Default 64MB
+
+ if (isIntelGpu)
+ {
+ // Intel integrated GPU memory recommendations
+ recommendedMemory = GetIntelGpuMemoryLimit(renderer);
+ DebugLogger.Log($"FontMemoryGuard: Applied Intel integrated GPU heuristics: {recommendedMemory / (1024 * 1024)}MB limit");
+ }
+ else if (isAmdGpu && isIntegratedGpu)
+ {
+ // AMD integrated GPU (APU) memory recommendations
+ recommendedMemory = GetAmdApuMemoryLimit(renderer);
+ DebugLogger.Log($"FontMemoryGuard: Applied AMD integrated GPU heuristics: {recommendedMemory / (1024 * 1024)}MB limit");
+ }
+ else if (isIntegratedGpu)
+ {
+ // Generic integrated GPU - be conservative
+ recommendedMemory = 32 * 1024 * 1024; // 32MB
+ DebugLogger.Log($"FontMemoryGuard: Applied generic integrated GPU heuristics: {recommendedMemory / (1024 * 1024)}MB limit");
+ }
+
+ CurrentConfig.MaxAtlasMemoryBytes = recommendedMemory;
+
+ // Enable more aggressive fallbacks for integrated GPUs
+ CurrentConfig.DisableEmojisOnLowMemory = true;
+ CurrentConfig.ReduceUnicodeRangesOnLowMemory = true;
+ CurrentConfig.MaxGpuMemoryPercentage = 0.05f; // Use only 5% for integrated GPUs
+
+ return true;
+ }
+
+ ///
+ /// Gets the recommended memory limit for Intel integrated graphics based on the renderer string.
+ ///
+ /// The Intel GPU renderer string.
+ /// Recommended memory limit in bytes.
+ public static long GetIntelGpuMemoryLimit(string renderer)
+ {
+ if (string.IsNullOrEmpty(renderer))
+ {
+ return 32 * 1024 * 1024; // Conservative 32MB for unknown Intel GPU
+ }
+
+ string rendererLower = renderer.ToLowerInvariant();
+
+ // Iris graphics (higher end integrated) - check first before Xe to get correct priority
+ if (rendererLower.Contains("iris"))
+ {
+ return 80 * 1024 * 1024; // 80MB - Iris has better performance
+ }
+
+ // Modern Intel GPUs (12th gen+, Xe Graphics)
+ if (rendererLower.Contains("xe") || rendererLower.Contains("arc"))
+ {
+ return 96 * 1024 * 1024; // 96MB - Modern Intel has better memory bandwidth
+ }
+
+ // Recent Intel GPUs (UHD Graphics series)
+ if (rendererLower.Contains("uhd"))
+ {
+ return 64 * 1024 * 1024; // 64MB - Standard limit
+ }
+
+ // Older Intel GPUs (HD Graphics series)
+ if (rendererLower.Contains("hd graphics"))
+ {
+ // Extract generation number if possible
+ if (rendererLower.Contains("630") || rendererLower.Contains("620") || rendererLower.Contains("610"))
+ {
+ return 48 * 1024 * 1024; // 48MB - 7th/8th gen
+ }
+ else if (rendererLower.Contains("530") || rendererLower.Contains("520") || rendererLower.Contains("510"))
+ {
+ return 32 * 1024 * 1024; // 32MB - 6th gen and older
+ }
+ else
+ {
+ return 24 * 1024 * 1024; // 24MB - Very old Intel GPUs
+ }
+ }
+
+ // Default for unknown Intel GPU
+ return 32 * 1024 * 1024; // 32MB conservative
+ }
+
+ ///
+ /// Gets the recommended memory limit for AMD APU graphics based on the renderer string.
+ ///
+ /// The AMD APU renderer string.
+ /// Recommended memory limit in bytes.
+ public static long GetAmdApuMemoryLimit(string renderer)
+ {
+ if (string.IsNullOrEmpty(renderer))
+ {
+ return 48 * 1024 * 1024; // Conservative 48MB for unknown AMD APU
+ }
+
+ string rendererLower = renderer.ToLowerInvariant();
+
+ // Modern RDNA2/3 APUs (6000+ series, Steam Deck)
+ if (rendererLower.Contains("rdna") || rendererLower.Contains("6600") || rendererLower.Contains("6800") || rendererLower.Contains("7600") || rendererLower.Contains("680m"))
+ {
+ return 128 * 1024 * 1024; // 128MB - Modern AMD APUs are quite capable
+ }
+
+ // Vega-based APUs (good performance)
+ if (rendererLower.Contains("vega"))
+ {
+ return 96 * 1024 * 1024; // 96MB - Vega integrated graphics are decent
+ }
+
+ // Older GCN-based APUs
+ if (rendererLower.Contains("radeon") && (rendererLower.Contains("r5") || rendererLower.Contains("r7")))
+ {
+ return 48 * 1024 * 1024; // 48MB - Older AMD APUs
+ }
+
+ // Default for unknown AMD APU
+ return 64 * 1024 * 1024; // 64MB standard
+ }
+
+ ///
+ /// Calculates recommended memory limit based on detected GPU memory and type.
+ ///
+ /// Available memory in KB from GPU detection.
+ /// Whether this is an integrated GPU.
+ /// Recommended font memory limit in bytes.
+ private static long CalculateRecommendedMemoryFromDetection(long availableMemoryKB, bool isIntegratedGpu)
+ {
+ // Convert KB to bytes
+ long availableMemoryBytes = availableMemoryKB * 1024;
+
+ // Use different percentages for integrated vs discrete GPUs
+ float percentage = isIntegratedGpu ? 0.03f : CurrentConfig.MaxGpuMemoryPercentage; // 3% for integrated, configurable for discrete
+ long recommendedFontMemory = (long)(availableMemoryBytes * percentage);
+
+ // Apply bounds based on GPU type
+ long minMemory = isIntegratedGpu ? 16 * 1024 * 1024 : MinAtlasMemoryBytes; // 16MB min for integrated
+ long maxMemory = isIntegratedGpu ? 96 * 1024 * 1024 : DefaultMaxAtlasMemoryBytes * 2; // 96MB max for integrated
+
+ return Math.Clamp(recommendedFontMemory, minMemory, maxMemory);
+ }
+
+ ///
+ /// Determines the best fallback strategy based on memory constraints.
+ ///
+ /// Current memory estimate.
+ /// Recommended fallback strategy.
+ public static FallbackStrategy DetermineFallbackStrategy(FontMemoryEstimate estimate)
+ {
+ if (!estimate.ExceedsLimits)
+ {
+ return FallbackStrategy.None;
+ }
+
+ // Calculate how much we're over the limit
+ double overageRatio = (double)estimate.EstimatedBytes / CurrentConfig.MaxAtlasMemoryBytes;
+
+ if (overageRatio > 4.0) // More than 4x over limit
+ {
+ return FallbackStrategy.MinimalFonts;
+ }
+ else if (overageRatio > 2.0) // More than 2x over limit
+ {
+ return FallbackStrategy.ReduceUnicodeRanges;
+ }
+ else if (overageRatio > 1.5) // More than 1.5x over limit
+ {
+ return FallbackStrategy.DisableEmojis;
+ }
+ else // Less than 1.5x over limit
+ {
+ return FallbackStrategy.ReduceFontSizes;
+ }
+ }
+
+ ///
+ /// Gets a reduced set of font sizes based on memory constraints.
+ ///
+ /// Original font sizes to load.
+ /// Maximum number of sizes to include.
+ /// Preferred size to always include (e.g., default font size).
+ /// Reduced array of font sizes.
+ public static int[] GetReducedFontSizes(int[] originalSizes, int maxSizes, int preferredSize = 14)
+ {
+ ArgumentNullException.ThrowIfNull(originalSizes);
+
+ if (originalSizes.Length <= maxSizes)
+ {
+ return originalSizes;
+ }
+
+ maxSizes = Math.Max(CurrentConfig.MinFontSizesToLoad, maxSizes);
+ List selectedSizes = [];
+
+ // Always include the preferred size
+ if (originalSizes.Contains(preferredSize))
+ {
+ selectedSizes.Add(preferredSize);
+ }
+
+ // Add other sizes, prioritizing medium sizes over very large or very small ones
+ IEnumerable remainingSizes = originalSizes.Where(s => s != preferredSize)
+ .OrderBy(s => Math.Abs(s - preferredSize)) // Sort by distance from preferred size
+ .Take(maxSizes - selectedSizes.Count);
+
+ selectedSizes.AddRange(remainingSizes);
+
+ return [.. selectedSizes.OrderBy(s => s)];
+ }
+
+ ///
+ /// Gets reduced Unicode glyph ranges for memory-constrained scenarios.
+ ///
+ /// Font atlas for building ranges.
+ /// Pointer to reduced glyph ranges.
+ public static unsafe uint* GetReducedUnicodeRanges(ImFontAtlasPtr fontAtlasPtr)
+ {
+ // Create a minimal set of ranges that still provides good functionality
+ ImFontGlyphRangesBuilderPtr builder = new(ImGui.ImFontGlyphRangesBuilder());
+
+ // Add default ranges (ASCII) - always needed
+ builder.AddRanges(fontAtlasPtr.GetGlyphRangesDefault());
+
+ // Add only the most essential extended ranges
+ AddEssentialLatinExtended(builder);
+ AddEssentialSymbols(builder);
+ AddEssentialNerdFontRanges(builder);
+
+ // Build and cache the ranges
+ ImVector reducedRanges = new();
+ builder.BuildRanges(ref reducedRanges);
+
+ return reducedRanges.Data;
+ }
+
+ ///
+ /// Logs memory usage information for debugging.
+ ///
+ /// Memory estimate to log.
+ /// Applied fallback strategy.
+ public static void LogMemoryUsage(FontMemoryEstimate estimate, FallbackStrategy strategy)
+ {
+ DebugLogger.Log($"FontMemoryGuard: Estimated font memory usage: {estimate.EstimatedBytes / (1024 * 1024)}MB");
+ DebugLogger.Log($"FontMemoryGuard: Memory limit: {CurrentConfig.MaxAtlasMemoryBytes / (1024 * 1024)}MB");
+ DebugLogger.Log($"FontMemoryGuard: Estimated glyph count: {estimate.EstimatedGlyphCount:N0}");
+
+ if (estimate.ExceedsLimits)
+ {
+ DebugLogger.Log($"FontMemoryGuard: Memory limit exceeded, applying fallback strategy: {strategy}");
+ if (estimate.ShouldDisableEmojis)
+ {
+ DebugLogger.Log("FontMemoryGuard: Disabling emoji fonts to reduce memory usage");
+ }
+ if (estimate.ShouldReduceUnicodeRanges)
+ {
+ DebugLogger.Log("FontMemoryGuard: Reducing Unicode ranges to reduce memory usage");
+ }
+ if (estimate.RecommendedMaxSizes > 0)
+ {
+ DebugLogger.Log($"FontMemoryGuard: Recommending maximum {estimate.RecommendedMaxSizes} font sizes");
+ }
+ }
+ else
+ {
+ DebugLogger.Log("FontMemoryGuard: Memory usage within limits, no restrictions applied");
+ }
+ }
+
+ #region Private Helper Methods
+
+ private static int GetExtendedUnicodeGlyphCount() =>
+ // Estimate glyph count for extended Unicode ranges
+ // This is based on the ranges defined in FontHelper.AddSymbolRanges and AddNerdFontRanges
+ 2000; // Conservative estimate
+
+ private static int GetEmojiGlyphCount() =>
+ // Estimate glyph count for emoji ranges
+ // This is based on the ranges defined in FontHelper.AddEmojiRanges
+ 3000; // Conservative estimate
+
+ private static int CalculateRecommendedMaxSizes(long estimatedBytes, int fontCount, int baseGlyphCount, float scaleFactor)
+ {
+ if (estimatedBytes <= CurrentConfig.MaxAtlasMemoryBytes)
+ {
+ return int.MaxValue; // No limit needed
+ }
+
+ // Calculate how many sizes we can fit within the memory limit
+ long bytesPerFontPerSize = (long)(baseGlyphCount * EstimatedBytesPerGlyph * scaleFactor * scaleFactor);
+ long availableBytesPerFont = CurrentConfig.MaxAtlasMemoryBytes / fontCount;
+ int maxSizesPerFont = (int)(availableBytesPerFont / bytesPerFontPerSize);
+
+ return Math.Max(CurrentConfig.MinFontSizesToLoad, maxSizesPerFont);
+ }
+
+ private static void AddEssentialLatinExtended(ImFontGlyphRangesBuilderPtr builder)
+ {
+ // Add only the most commonly used Latin Extended characters
+ // Latin Extended-A (U+0100–U+017F) - partial
+ for (uint c = 0x00C0; c <= 0x00FF; c++) // Latin-1 Supplement (most common)
+ {
+ builder.AddChar(c);
+ }
+ for (uint c = 0x0100; c <= 0x017F; c += 2) // Every other character in Latin Extended-A
+ {
+ builder.AddChar(c);
+ }
+ }
+
+ private static void AddEssentialSymbols(ImFontGlyphRangesBuilderPtr builder)
+ {
+ // Add only the most essential symbols
+ (uint start, uint end)[] essentialRanges = [
+ (0x2000, 0x206F), // General Punctuation
+ (0x20A0, 0x20CF), // Currency Symbols
+ (0x2190, 0x21FF), // Arrows
+ (0x2500, 0x257F), // Box Drawing
+ ];
+
+ foreach ((uint start, uint end) in essentialRanges)
+ {
+ for (uint c = start; c <= end; c += 2) // Every other character to reduce count
+ {
+ builder.AddChar(c);
+ }
+ }
+ }
+
+ private static void AddEssentialNerdFontRanges(ImFontGlyphRangesBuilderPtr builder)
+ {
+ // Add only the most commonly used Nerd Font icons
+ (uint start, uint end)[] essentialNerdRanges = [
+ (0xE0A0, 0xE0A2), // Powerline symbols
+ (0xE0B0, 0xE0B3), // Powerline symbols
+ (0xF000, 0xF0FF), // First 256 Font Awesome icons (most common)
+ ];
+
+ foreach ((uint start, uint end) in essentialNerdRanges)
+ {
+ for (uint c = start; c <= end; c++)
+ {
+ builder.AddChar(c);
+ }
+ }
+ }
+
+ #endregion
+}
diff --git a/ImGuiApp/ImGuiApp.cs b/ImGuiApp/ImGuiApp.cs
index d6527a7..c50b075 100644
--- a/ImGuiApp/ImGuiApp.cs
+++ b/ImGuiApp/ImGuiApp.cs
@@ -936,11 +936,11 @@ internal static void UpdateDpiScale()
}
}
- // Using the new ImGui 1.92.0 dynamic font system
- // Fonts can now be rendered at any size dynamically - no need to preload multiple sizes!
+ // Using the new ImGui 1.92.0 dynamic font system with memory guards
+ // Fonts can now be rendered at any size dynamically, but we limit memory usage for small GPUs
internal static unsafe void InitFonts()
{
- DebugLogger.Log("InitFonts: Starting font initialization");
+ DebugLogger.Log("InitFonts: Starting font initialization with memory guards");
// Only load fonts if they haven't been loaded or if scale factor has changed
if (controller?.FontsConfigured == true && Math.Abs(lastFontScaleFactor - ScaleFactor) < 0.01f)
@@ -954,22 +954,54 @@ internal static unsafe void InitFonts()
ImGuiIOPtr io = ImGui.GetIO();
ImFontAtlasPtr fontAtlasPtr = io.Fonts;
+ // Initialize font memory configuration
+ FontMemoryGuard.CurrentConfig = Config.FontMemoryConfig;
+
+ // Try to detect GPU memory and adjust limits
+ if (gl != null)
+ {
+ FontMemoryGuard.TryDetectAndConfigureGpuMemory(gl);
+ }
+
// Clear existing font data and indices
fontAtlasPtr.Clear();
FontIndices.Clear();
+ // Load fonts from configuration
+ IEnumerable> fontsToLoad = Config.Fonts.Concat(Config.DefaultFonts);
+ int fontCount = fontsToLoad.Count();
+
+ // Get emoji font data
+ byte[]? emojiBytes = ImGuiAppConfig.EmojiFont;
+ bool hasEmojiFont = emojiBytes != null;
+
+ // Estimate memory usage and determine constraints
+ FontMemoryGuard.FontMemoryEstimate memoryEstimate = FontMemoryGuard.EstimateMemoryUsage(
+ fontCount,
+ CommonFontSizes,
+ hasEmojiFont,
+ Config.EnableUnicodeSupport,
+ ScaleFactor);
+
+ // Determine fallback strategy
+ FontMemoryGuard.FallbackStrategy fallbackStrategy = FontMemoryGuard.DetermineFallbackStrategy(memoryEstimate);
+
+ // Log memory usage information
+ FontMemoryGuard.LogMemoryUsage(memoryEstimate, fallbackStrategy);
+
+ // Apply fallback constraints
+ int[] fontSizesToLoad = ApplyFallbackConstraints(fallbackStrategy, memoryEstimate);
+ bool shouldLoadEmojis = ShouldLoadEmojis(fallbackStrategy, hasEmojiFont);
+ bool shouldUseReducedUnicode = ShouldUseReducedUnicode(fallbackStrategy);
+
// Track fonts that need disposal after rebuilding the atlas
List fontPinnedData = [];
-
- // Load fonts from configuration at multiple sizes
- IEnumerable> fontsToLoad = Config.Fonts.Concat(Config.DefaultFonts);
int defaultFontIndex = -1;
- // Pre-allocate emoji font memory outside the loops for reuse
+ // Pre-allocate emoji font memory if needed
nint emojiHandle = IntPtr.Zero;
int emojiLength = 0;
- byte[]? emojiBytes = ImGuiAppConfig.EmojiFont;
- if (emojiBytes != null)
+ if (shouldLoadEmojis && emojiBytes != null)
{
emojiHandle = Marshal.AllocHGlobal(emojiBytes.Length);
currentFontMemoryHandles.Add(emojiHandle);
@@ -977,6 +1009,7 @@ internal static unsafe void InitFonts()
emojiLength = emojiBytes.Length;
}
+ // Load fonts with memory constraints applied
foreach ((string name, byte[] fontBytes) in fontsToLoad)
{
// Pre-allocate main font memory outside the size loop for reuse
@@ -984,12 +1017,15 @@ internal static unsafe void InitFonts()
currentFontMemoryHandles.Add(fontHandle);
Marshal.Copy(fontBytes, 0, fontHandle, fontBytes.Length);
- foreach (int size in CommonFontSizes)
+ foreach (int size in fontSizesToLoad)
{
- LoadFontFromMemory($"{name}_{size}", fontHandle, fontBytes.Length, fontAtlasPtr, size);
+ // Determine glyph ranges based on constraints
+ uint* glyphRanges = GetConstrainedGlyphRanges(fontAtlasPtr, shouldUseReducedUnicode);
+
+ LoadFontFromMemory($"{name}_{size}", fontHandle, fontBytes.Length, fontAtlasPtr, size, glyphRanges);
// Load and merge emoji font immediately after main font for proper merging
- if (emojiHandle != IntPtr.Zero)
+ if (shouldLoadEmojis && emojiHandle != IntPtr.Zero)
{
LoadEmojiFontFromMemory(emojiHandle, emojiLength, fontAtlasPtr, size);
}
@@ -1032,6 +1068,9 @@ internal static unsafe void InitFonts()
// Build the font atlas to generate the texture
ImGuiP.ImFontAtlasBuildMain(fontAtlasPtr);
+ // Log final atlas information
+ LogFontAtlasInfo(fontAtlasPtr, fallbackStrategy);
+
// Set the default font for ImGui rendering
if (defaultFontIndex != -1 && defaultFontIndex < fontAtlasPtr.Fonts.Size)
{
@@ -1040,7 +1079,116 @@ internal static unsafe void InitFonts()
// Store the pinned font data for later cleanup
StorePinnedFontData(fontPinnedData);
- DebugLogger.Log("InitFonts: Font initialization completed");
+ DebugLogger.Log("InitFonts: Font initialization completed with memory guards applied");
+ }
+
+ ///
+ /// Applies fallback constraints to font loading based on memory limits.
+ ///
+ /// The determined fallback strategy.
+ /// The memory estimate for font loading.
+ /// Array of font sizes to load based on constraints.
+ internal static int[] ApplyFallbackConstraints(FontMemoryGuard.FallbackStrategy fallbackStrategy, FontMemoryGuard.FontMemoryEstimate memoryEstimate)
+ {
+ return fallbackStrategy switch
+ {
+ FontMemoryGuard.FallbackStrategy.None => CommonFontSizes,
+ FontMemoryGuard.FallbackStrategy.ReduceFontSizes => FontMemoryGuard.GetReducedFontSizes(CommonFontSizes, memoryEstimate.RecommendedMaxSizes, FontAppearance.DefaultFontPointSize),
+ FontMemoryGuard.FallbackStrategy.DisableEmojis => CommonFontSizes,
+ FontMemoryGuard.FallbackStrategy.ReduceUnicodeRanges => CommonFontSizes,
+ FontMemoryGuard.FallbackStrategy.MinimalFonts => FontMemoryGuard.GetReducedFontSizes(CommonFontSizes, FontMemoryGuard.CurrentConfig.MinFontSizesToLoad, FontAppearance.DefaultFontPointSize),
+ _ => CommonFontSizes
+ };
+ }
+
+ ///
+ /// Determines whether emoji fonts should be loaded based on fallback strategy.
+ ///
+ /// The determined fallback strategy.
+ /// Whether emoji font data is available.
+ /// True if emoji fonts should be loaded.
+ internal static bool ShouldLoadEmojis(FontMemoryGuard.FallbackStrategy fallbackStrategy, bool hasEmojiFont)
+ {
+ if (!hasEmojiFont)
+ {
+ return false;
+ }
+
+ return fallbackStrategy switch
+ {
+ FontMemoryGuard.FallbackStrategy.DisableEmojis => false,
+ FontMemoryGuard.FallbackStrategy.ReduceUnicodeRanges => false,
+ FontMemoryGuard.FallbackStrategy.MinimalFonts => false,
+ _ => true
+ };
+ }
+
+ ///
+ /// Determines whether reduced Unicode ranges should be used based on fallback strategy.
+ ///
+ /// The determined fallback strategy.
+ /// True if reduced Unicode ranges should be used.
+ internal static bool ShouldUseReducedUnicode(FontMemoryGuard.FallbackStrategy fallbackStrategy)
+ {
+ return fallbackStrategy switch
+ {
+ FontMemoryGuard.FallbackStrategy.ReduceUnicodeRanges => true,
+ FontMemoryGuard.FallbackStrategy.MinimalFonts => true,
+ _ => false
+ };
+ }
+
+ ///
+ /// Gets the appropriate glyph ranges based on memory constraints.
+ ///
+ /// The font atlas for building ranges.
+ /// Whether to use reduced Unicode ranges.
+ /// Pointer to the appropriate glyph ranges.
+ internal static unsafe uint* GetConstrainedGlyphRanges(ImFontAtlasPtr fontAtlasPtr, bool useReducedUnicode)
+ {
+ if (useReducedUnicode)
+ {
+ return FontMemoryGuard.GetReducedUnicodeRanges(fontAtlasPtr);
+ }
+ else if (Config.EnableUnicodeSupport)
+ {
+ return FontHelper.GetExtendedUnicodeRanges(fontAtlasPtr);
+ }
+ else
+ {
+ return fontAtlasPtr.GetGlyphRangesDefault();
+ }
+ }
+
+ ///
+ /// Logs information about the final font atlas after building.
+ ///
+ /// The built font atlas.
+ /// The applied fallback strategy.
+ internal static unsafe void LogFontAtlasInfo(ImFontAtlasPtr fontAtlasPtr, FontMemoryGuard.FallbackStrategy fallbackStrategy)
+ {
+ // Get atlas texture information using the correct API for Hexa.NET.ImGui
+ ImTextureDataPtr texData = fontAtlasPtr.TexData;
+ int width = texData.Width;
+ int height = texData.Height;
+ int bytesPerPixel = 4; // RGBA32 = 4 bytes per pixel
+ long atlasMemoryBytes = (long)width * height * bytesPerPixel;
+
+ DebugLogger.Log($"FontMemoryGuard: Final font atlas size: {width}x{height} pixels");
+ DebugLogger.Log($"FontMemoryGuard: Final font atlas memory: {atlasMemoryBytes / (1024 * 1024)}MB");
+ DebugLogger.Log($"FontMemoryGuard: Applied fallback strategy: {fallbackStrategy}");
+ DebugLogger.Log($"FontMemoryGuard: Loaded {fontAtlasPtr.Fonts.Size} font variants");
+
+ // Warn if atlas is getting close to common GPU limits
+ if (width > 2048 || height > 2048)
+ {
+ DebugLogger.Log("FontMemoryGuard: Warning - Large font atlas may cause issues on older GPUs");
+ }
+
+ if (atlasMemoryBytes > FontMemoryGuard.CurrentConfig.MaxAtlasMemoryBytes)
+ {
+ DebugLogger.Log("FontMemoryGuard: Warning - Font atlas exceeds configured memory limit");
+ }
}
///
diff --git a/ImGuiApp/ImGuiApp.csproj b/ImGuiApp/ImGuiApp.csproj
index 2ac3da5..e1608bf 100644
--- a/ImGuiApp/ImGuiApp.csproj
+++ b/ImGuiApp/ImGuiApp.csproj
@@ -1,6 +1,7 @@
+ net9.0;net8.0
true
$(NoWarn);CA5392;
diff --git a/ImGuiApp/ImGuiAppConfig.cs b/ImGuiApp/ImGuiAppConfig.cs
index 6de715f..f65837f 100644
--- a/ImGuiApp/ImGuiAppConfig.cs
+++ b/ImGuiApp/ImGuiAppConfig.cs
@@ -98,6 +98,12 @@ public class ImGuiAppConfig
///
public ImGuiAppPerformanceSettings PerformanceSettings { get; init; } = new();
+ ///
+ /// Gets or sets the font memory configuration for limiting texture memory allocation.
+ /// This helps prevent excessive memory usage on small GPUs or high-resolution displays.
+ ///
+ public FontMemoryGuard.FontMemoryConfig FontMemoryConfig { get; init; } = new();
+
internal Dictionary DefaultFonts { get; init; } = new Dictionary
{
{ "default", Resources.Resources.NerdFont}
diff --git a/ImGuiAppDemo/ImGuiAppDemo.csproj b/ImGuiAppDemo/ImGuiAppDemo.csproj
index d1dd612..12d985f 100644
--- a/ImGuiAppDemo/ImGuiAppDemo.csproj
+++ b/ImGuiAppDemo/ImGuiAppDemo.csproj
@@ -1,5 +1,6 @@
+ net9.0;net8.0
true
diff --git a/global.json b/global.json
index af0efba..e44410d 100644
--- a/global.json
+++ b/global.json
@@ -4,12 +4,12 @@
"rollForward": "latestFeature"
},
"msbuild-sdks": {
- "ktsu.Sdk": "1.48.0",
- "ktsu.Sdk.Lib": "1.48.0",
- "ktsu.Sdk.ConsoleApp": "1.48.0",
- "ktsu.Sdk.Test": "1.48.0",
- "ktsu.Sdk.ImGuiApp": "1.48.0",
- "ktsu.Sdk.WinApp": "1.48.0",
- "ktsu.Sdk.WinTest": "1.48.0"
+ "ktsu.Sdk": "1.56.0",
+ "ktsu.Sdk.Lib": "1.56.0",
+ "ktsu.Sdk.ConsoleApp": "1.56.0",
+ "ktsu.Sdk.Test": "1.56.0",
+ "ktsu.Sdk.ImGuiApp": "1.56.0",
+ "ktsu.Sdk.WinApp": "1.56.0",
+ "ktsu.Sdk.WinTest": "1.56.0"
}
}