From 3f3f5f4b7203c9a253b8ce277f59b5f36b12edf5 Mon Sep 17 00:00:00 2001 From: goat Date: Thu, 26 Dec 2024 22:10:10 +0100 Subject: [PATCH] hide assert logs that spam every frame automatically, add "verbose" mode --- .../Internal/Asserts/AssertHandler.cs | 57 +++++++++++++++---- .../Interface/Internal/DalamudInterface.cs | 48 +++++++++------- .../Interface/Internal/InterfaceManager.cs | 19 +++++-- 3 files changed, 87 insertions(+), 37 deletions(-) diff --git a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs index b0cfb462c..c1dc12206 100644 --- a/Dalamud/Interface/Internal/Asserts/AssertHandler.cs +++ b/Dalamud/Interface/Internal/Asserts/AssertHandler.cs @@ -15,7 +15,11 @@ namespace Dalamud.Interface.Internal.Asserts; /// internal class AssertHandler : IDisposable { + private const int HideThreshold = 20; + private const int HidePrintEvery = 500; + private readonly HashSet ignoredAsserts = []; + private readonly Dictionary assertCounts = new(); // Store callback to avoid it from being GC'd private readonly AssertCallbackDelegate callback; @@ -37,7 +41,13 @@ private delegate void AssertCallbackDelegate( /// Gets or sets a value indicating whether ImGui asserts should be shown to the user. /// public bool ShowAsserts { get; set; } - + + /// + /// Gets or sets a value indicating whether we want to hide asserts that occur frequently (= every update) + /// and whether we want to log callstacks. + /// + public bool EnableVerboseLogging { get; set; } + /// /// Register the cimgui assert handler with the native library. /// @@ -62,18 +72,43 @@ public void Dispose() private void OnImGuiAssert(string expr, string file, int line) { - Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}", expr, file, line); - - if (!this.ShowAsserts) - return; - var key = $"{file}:{line}"; if (this.ignoredAsserts.Contains(key)) return; - // TODO: It would be nice to get unmanaged stack frames here, seems hard though without pulling that - // entire code in from the crash handler - var originalStackTrace = new StackTrace(2).ToString(); + Lazy stackTrace = new(() => new StackTrace(3).ToString()); + + if (!this.EnableVerboseLogging) + { + if (this.assertCounts.TryGetValue(key, out var count)) + { + this.assertCounts[key] = count + 1; + + if (count <= HideThreshold || count % HidePrintEvery == 0) + { + Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line} (repeated {Count} times)", + expr, + file, + line, + count); + } + } + else + { + this.assertCounts[key] = 1; + } + } + else + { + Log.Warning("ImGui assertion failed: {Expr} at {File}:{Line}\n{StackTrace:l}", + expr, + file, + line, + stackTrace.Value); + } + + if (!this.ShowAsserts) + return; string? GetRepoUrl() { @@ -108,7 +143,7 @@ private void OnImGuiAssert(string expr, string file, int line) Text = "Break", AllowCloseDialog = true, }; - + var disableButton = new TaskDialogButton { Text = "Disable for this session", @@ -132,7 +167,7 @@ void DialogThreadStart() { CollapsedButtonText = "Show stack trace", ExpandedButtonText = "Hide stack trace", - Text = originalStackTrace, + Text = stackTrace.Value, }, Text = $"Some code in a plugin or Dalamud itself has caused an internal assertion in ImGui to fail. The game will most likely crash now.\n\n{expr}\nAt: {file}:{line}", Icon = TaskDialogIcon.Warning, diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index d432d9d4c..0381164c9 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -91,7 +91,7 @@ internal class DalamudInterface : IInternalDisposableService private bool isImPlotDrawDemoWindow = false; private bool isImGuiTestWindowsInMonospace = false; private bool isImGuiDrawMetricsWindow = false; - + [ServiceManager.ServiceConstructor] private DalamudInterface( Dalamud dalamud, @@ -112,7 +112,7 @@ private DalamudInterface( this.interfaceManager = interfaceManager; this.WindowSystem = new WindowSystem("DalamudCore"); - + this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; this.componentDemoWindow = new ComponentDemoWindow() { IsOpen = false }; this.dataWindow = new DataWindow() { IsOpen = false }; @@ -193,7 +193,7 @@ private DalamudInterface( this.creditsDarkeningAnimation.Point1 = Vector2.Zero; this.creditsDarkeningAnimation.Point2 = new Vector2(CreditsDarkeningMaxAlpha); - + // This is temporary, until we know the repercussions of vtable hooking mode consoleManager.AddCommand( "dalamud.interface.swapchain_mode", @@ -212,14 +212,14 @@ private DalamudInterface( Log.Error("Unknown swapchain mode: {Mode}", mode); return false; } - + this.configuration.QueueSave(); return true; }); } - + private delegate nint CrashDebugDelegate(nint self); - + /// /// Gets the number of frames since Dalamud has loaded. /// @@ -319,7 +319,7 @@ public void OpenPluginStats() this.pluginStatWindow.IsOpen = true; this.pluginStatWindow.BringToFront(); } - + /// /// Opens the on the plugin installed. /// @@ -384,7 +384,7 @@ public void OpenProfiler() this.profilerWindow.IsOpen = true; this.profilerWindow.BringToFront(); } - + /// /// Opens the . /// @@ -696,7 +696,7 @@ private void DrawDevMenu() ImGui.EndMenu(); } - + var logSynchronously = this.configuration.LogSynchronously; if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) { @@ -788,14 +788,14 @@ private void DrawDevMenu() } ImGui.Separator(); - + if (ImGui.BeginMenu("Crash game")) { if (ImGui.MenuItem("Access Violation")) { Marshal.ReadByte(IntPtr.Zero); - } - + } + if (ImGui.MenuItem("Set UiModule to NULL")) { unsafe @@ -804,7 +804,7 @@ private void DrawDevMenu() framework->UIModule = (UIModule*)0; } } - + if (ImGui.MenuItem("Set UiModule to invalid ptr")) { unsafe @@ -813,7 +813,7 @@ private void DrawDevMenu() framework->UIModule = (UIModule*)0x12345678; } } - + if (ImGui.MenuItem("Deref nullptr in Hook")) { unsafe @@ -834,7 +834,7 @@ private void DrawDevMenu() ImGui.PopStyleVar(); ImGui.PopStyleVar(); } - + ImGui.EndMenu(); } @@ -850,7 +850,7 @@ private void DrawDevMenu() { this.OpenBranchSwitcher(); } - + ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetScmVersion()} CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.ThisAssembly.Git.Commits}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); @@ -867,10 +867,16 @@ private void DrawDevMenu() ImGui.Separator(); - var val = this.interfaceManager.ShowAsserts; - if (ImGui.MenuItem("Enable Asserts", string.Empty, ref val)) + var showAsserts = this.interfaceManager.ShowAsserts; + if (ImGui.MenuItem("Enable assert popups", string.Empty, ref showAsserts)) { - this.interfaceManager.ShowAsserts = val; + this.interfaceManager.ShowAsserts = showAsserts; + } + + var enableVerboseAsserts = this.interfaceManager.EnableVerboseAssertLogging; + if (ImGui.MenuItem("Enable verbose assert logging", string.Empty, ref enableVerboseAsserts)) + { + this.interfaceManager.EnableVerboseAssertLogging = enableVerboseAsserts; } var assertsEnabled = this.configuration.ImGuiAssertsEnabledAtStartup ?? false; @@ -880,6 +886,8 @@ private void DrawDevMenu() this.configuration.QueueSave(); } + ImGui.Separator(); + if (ImGui.MenuItem("Clear focus")) { ImGui.SetWindowFocus(null); @@ -927,7 +935,7 @@ private void DrawDevMenu() { this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo; } - + ImGui.Separator(); if (ImGui.MenuItem("Show loading window")) diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 65f2da705..2908be34a 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -73,7 +73,7 @@ internal partial class InterfaceManager : IInternalDisposableService public const float DefaultFontSizePx = (DefaultFontSizePt * 4.0f) / 3.0f; private static readonly ModuleLog Log = new("INTERFACE"); - + private readonly ConcurrentBag deferredDisposeTextures = new(); private readonly ConcurrentBag deferredDisposeDisposables = new(); @@ -93,7 +93,7 @@ internal partial class InterfaceManager : IInternalDisposableService private readonly ConcurrentQueue runBeforeImGuiRender = new(); private readonly ConcurrentQueue runAfterImGuiRender = new(); - + private readonly AssertHandler assertHandler = new(); private RawDX11Scene? scene; @@ -276,6 +276,13 @@ public bool ShowAsserts set => this.assertHandler.ShowAsserts = value; } + /// + public bool EnableVerboseAssertLogging + { + get => this.assertHandler.EnableVerboseLogging; + set => this.assertHandler.EnableVerboseLogging = value; + } + /// /// Dispose of managed and unmanaged resources. /// @@ -809,14 +816,14 @@ private unsafe void ContinueConstruction( }); }; } - + // This will wait for scene on its own. We just wait for this.dalamudAtlas.BuildTask in this.InitScene. _ = this.dalamudAtlas.BuildFontsAsync(); SwapChainHelper.BusyWaitForGameDeviceSwapChain(); var swapChainDesc = default(DXGI_SWAP_CHAIN_DESC); if (SwapChainHelper.GameDeviceSwapChain->GetDesc(&swapChainDesc).SUCCEEDED) - this.gameWindowHandle = swapChainDesc.OutputWindow; + this.gameWindowHandle = swapChainDesc.OutputWindow; try { @@ -959,7 +966,7 @@ private unsafe void ContinueConstruction( switch (this.dalamudConfiguration.SwapChainHookMode) { - case SwapChainHelper.HookMode.ByteCode: + case SwapChainHelper.HookMode.ByteCode: default: { Log.Information("Hooking using bytecode..."); @@ -1149,7 +1156,7 @@ private void Display() catch (Exception ex) { Log.Error(ex, "Error when invoking global Draw"); - + // We should always handle this in the callbacks. Util.Fatal("An internal error occurred while drawing the Dalamud UI and the game must close.\nPlease report this error.", "Dalamud"); }