Skip to content

Commit

Permalink
Error Handling: enabled experimental recovery systems. (#1651, #5654)
Browse files Browse the repository at this point in the history
Setup a couple of features to configure them, including ways to display error tooltips instead of assserting.
  • Loading branch information
ocornut committed Sep 27, 2024
1 parent 8776678 commit 30c29d2
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 95 deletions.
18 changes: 16 additions & 2 deletions docs/CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,22 @@ Breaking changes:

Other changes:

- Error Handling: rewired asserts in PopID(), PopFont(), PopItemFlag(), EndDisabled(),
PopTextWrapPos(), PopFocusScope(), PopItemWidth() to use IM_ASSERT_USER_ERROR(). (#1651)
- Error Handling: Enabled/improved error recovery systems. (#1651, #5654)
- Error recovery is not perfect nor guaranteed! It is a feature to ease development.
- Functions that support error recovery are using IM_ASSERT_USER_ERROR() instead of IM_ASSERT().
- You not are not supposed to rely on it in the course of a normal application run.
- Possible usage: facilitate recovery from errors triggered from a scripting language or
after specific exceptions handlers. Surface errors to programmers in less agressive ways.
- Always ensure that on programmers seats you have at minimum Asserts or Tooltips enabled
when making direct imgui API calls! Otherwise it would severely hinder your ability to
catch and correct mistakes!
- Added io.ConfigErrorRecovery to enable error recovery support.
- Added io.ConfigErrorRecoveryEnableAssert to assert on recoverable errors.
- Added io.ConfigErrorRecoveryEnableDebugLog to output to debug log on recoverable errors.
- Added io.ConfigErrorRecoveryEnableTooltip to enable displaying an error tooltip on recoverable errors.
The tooltip include a way to enable asserts if they were disabled.
- All options are enabled by default.
- Read https://github.com/ocornut/imgui/wiki/Error-Handling for a bit more details.
- Windows: BeginChild(): made it possible to call SetNextWindowSize() on a child window
using ImGuiChildFlags_ResizeX,ImGuiChildFlags_ResizeY in order to override its current
size. (#1710, #8020)
Expand Down
194 changes: 120 additions & 74 deletions imgui.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1401,6 +1401,11 @@ ImGuiIO::ImGuiIO()
ConfigDebugBeginReturnValueOnce = false;
ConfigDebugBeginReturnValueLoop = false;

ConfigErrorRecovery = true;
ConfigErrorRecoveryEnableAssert = true;
ConfigErrorRecoveryEnableDebugLog = true;
ConfigErrorRecoveryEnableTooltip = true;

// Inputs Behaviors
MouseDoubleClickTime = 0.30f;
MouseDoubleClickMaxDist = 6.0f;
Expand Down Expand Up @@ -3978,6 +3983,12 @@ ImGuiContext::ImGuiContext(ImFontAtlas* shared_font_atlas)
LogDepthRef = 0;
LogDepthToExpand = LogDepthToExpandDefault = 2;

ErrorCallback = NULL;
ErrorCallbackUserData = NULL;
ErrorFirst = true;
ErrorCountCurrentFrame = 0;
StackSizesInBeginForCurrentWindow = NULL;

DebugDrawIdConflictsCount = 0;
DebugLogFlags = ImGuiDebugLogFlags_EventError | ImGuiDebugLogFlags_OutputToTTY;
DebugLocateId = 0;
Expand Down Expand Up @@ -4211,6 +4222,7 @@ static void SetCurrentWindow(ImGuiWindow* window)
{
ImGuiContext& g = *GImGui;
g.CurrentWindow = window;
g.StackSizesInBeginForCurrentWindow = g.CurrentWindow ? &g.CurrentWindowStack.back().StackSizesInBegin : NULL;
g.CurrentTable = window && window->DC.CurrentTableIdx != -1 ? g.Tables.GetByIndex(window->DC.CurrentTableIdx) : NULL;
g.CurrentDpiScale = 1.0f; // FIXME-DPI: WIP this is modified in docking
if (window)
Expand Down Expand Up @@ -5249,6 +5261,10 @@ void ImGui::NewFrame()
Begin("Debug##Default");
IM_ASSERT(g.CurrentWindow->IsFallbackWindow == true);

// Store stack sizes
g.ErrorCountCurrentFrame = 0;
ErrorRecoveryStoreState(&g.StackSizesInNewFrame);

// [DEBUG] When io.ConfigDebugBeginReturnValue is set, we make Begin()/BeginChild() return false at different level of the window-stack,
// allowing to validate correct Begin/End behavior in user code.
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
Expand Down Expand Up @@ -5467,6 +5483,9 @@ void ImGui::EndFrame()

CallContextHooks(&g, ImGuiContextHookType_EndFramePre);

// [EXPERIMENTAL] Recover from errors
if (g.IO.ConfigErrorRecovery)
ErrorRecoveryTryToRecoverState(&g.StackSizesInNewFrame);
ErrorCheckEndFrameSanityChecks();
ErrorCheckEndFrameFinalizeErrorTooltip();

Expand Down Expand Up @@ -6960,12 +6979,13 @@ bool ImGui::Begin(const char* name, bool* p_open, ImGuiWindowFlags flags)

// Add to stack
g.CurrentWindow = window;
ImGuiWindowStackData window_stack_data;
g.CurrentWindowStack.resize(g.CurrentWindowStack.Size + 1);
ImGuiWindowStackData& window_stack_data = g.CurrentWindowStack.back();
window_stack_data.Window = window;
window_stack_data.ParentLastItemDataBackup = g.LastItemData;
window_stack_data.StackSizesOnBegin.SetToContextState(&g);
window_stack_data.DisabledOverrideReenable = (flags & ImGuiWindowFlags_Tooltip) && (g.CurrentItemFlags & ImGuiItemFlags_Disabled);
g.CurrentWindowStack.push_back(window_stack_data);
ErrorRecoveryStoreState(&window_stack_data.StackSizesInBegin);
g.StackSizesInBeginForCurrentWindow = &window_stack_data.StackSizesInBegin;
if (flags & ImGuiWindowFlags_ChildMenu)
g.BeginMenuDepth++;

Expand Down Expand Up @@ -7693,7 +7713,11 @@ void ImGui::End()
g.BeginMenuDepth--;
if (window->Flags & ImGuiWindowFlags_Popup)
g.BeginPopupStack.pop_back();
window_stack_data.StackSizesOnBegin.CompareWithContextState(&g);

// Error handling, state recovery
if (g.IO.ConfigErrorRecovery)
ErrorRecoveryTryToRecoverWindowState(&window_stack_data.StackSizesInBegin);

g.CurrentWindowStack.pop_back();
SetCurrentWindow(g.CurrentWindowStack.Size == 0 ? NULL : g.CurrentWindowStack.back().Window);
}
Expand Down Expand Up @@ -8454,7 +8478,7 @@ void ImGui::PushFocusScope(ImGuiID id)
void ImGui::PopFocusScope()
{
ImGuiContext& g = *GImGui;
if (g.FocusScopeStack.Size <= 0)
if (g.FocusScopeStack.Size <= g.StackSizesInBeginForCurrentWindow->SizeOfFocusScopeStack)
{
IM_ASSERT_USER_ERROR(0, "Calling PopFocusScope() too many times!");
return;
Expand Down Expand Up @@ -10303,9 +10327,10 @@ bool ImGui::Shortcut(ImGuiKeyChord key_chord, ImGuiInputFlags flags, ImGuiID own
// - ErrorCheckUsingSetCursorPosToExtendParentBoundaries()
// - ErrorCheckNewFrameSanityChecks()
// - ErrorCheckEndFrameSanityChecks()
// - ErrorCheckEndFrameRecover()
// - ErrorCheckEndWindowRecover()
// - ImGuiStackSizes
// - ErrorRecoveryStoreState()
// - ErrorRecoveryTryToRecoverState()
// - ErrorRecoveryTryToRecoverWindowState()
// - ErrorLog()
//-----------------------------------------------------------------------------

// Verify ABI compatibility between caller code and compiled version of Dear ImGui. This helps detects some build issues.
Expand Down Expand Up @@ -10404,6 +10429,10 @@ static void ImGui::ErrorCheckNewFrameSanityChecks()
IM_ASSERT(g.IO.KeyMap[ImGuiKey_Space] != -1 && "ImGuiKey_Space is not mapped, required for keyboard navigation.");
#endif

// Error handling: we do not accept 100% silent recovery! Please contact me if you feel this is getting in your way.
if (g.IO.ConfigErrorRecovery)
IM_ASSERT(g.IO.ConfigErrorRecoveryEnableAssert || g.IO.ConfigErrorRecoveryEnableDebugLog || g.IO.ConfigErrorRecoveryEnableTooltip || g.ErrorCallback != NULL);

// Remap legacy clipboard handlers (OBSOLETED in 1.91.1, August 2024)
#ifndef IMGUI_DISABLE_OBSOLETE_FUNCTIONS
if (g.IO.GetClipboardTextFn != NULL && (g.PlatformIO.Platform_GetClipboardTextFn == NULL || g.PlatformIO.Platform_GetClipboardTextFn == Platform_GetClipboardTextFn_DefaultImpl))
Expand All @@ -10426,43 +10455,35 @@ static void ImGui::ErrorCheckEndFrameSanityChecks()
IM_ASSERT((key_mods == 0 || g.IO.KeyMods == key_mods) && "Mismatching io.KeyCtrl/io.KeyShift/io.KeyAlt/io.KeySuper vs io.KeyMods");
IM_UNUSED(key_mods);

// [EXPERIMENTAL] Recover from errors: You may call this yourself before EndFrame().
//ErrorCheckEndFrameRecover();

// Report when there is a mismatch of Begin/BeginChild vs End/EndChild calls. Important: Remember that the Begin/BeginChild API requires you
// to always call End/EndChild even if Begin/BeginChild returns false! (this is unfortunately inconsistent with most other Begin* API).
if (g.CurrentWindowStack.Size != 1)
{
if (g.CurrentWindowStack.Size > 1)
{
ImGuiWindow* window = g.CurrentWindowStack.back().Window; // <-- This window was not Ended!
IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you forget to call End/EndChild?");
IM_UNUSED(window);
while (g.CurrentWindowStack.Size > 1)
End();
}
else
{
IM_ASSERT_USER_ERROR(g.CurrentWindowStack.Size == 1, "Mismatched Begin/BeginChild vs End/EndChild calls: did you call End/EndChild too much?");
}
}
if (g.CurrentWindowStack.Size >= 1)
IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow);
IM_ASSERT(g.CurrentWindowStack.Size == 1);
IM_ASSERT(g.CurrentWindowStack[0].Window->IsFallbackWindow);
}

IM_ASSERT_USER_ERROR(g.GroupStack.Size == 0, "Missing EndGroup call!");
// Save current stack sizes. Called e.g. by NewFrame() and by Begin() but may be called for manual recovery.
void ImGui::ErrorRecoveryStoreState(ImGuiErrorRecoveryState* state_out)
{
ImGuiContext& g = *GImGui;
state_out->SizeOfWindowStack = (short)g.CurrentWindowStack.Size;
state_out->SizeOfIDStack = (short)g.CurrentWindow->IDStack.Size;
state_out->SizeOfColorStack = (short)g.ColorStack.Size;
state_out->SizeOfStyleVarStack = (short)g.StyleVarStack.Size;
state_out->SizeOfFontStack = (short)g.FontStack.Size;
state_out->SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size;
state_out->SizeOfGroupStack = (short)g.GroupStack.Size;
state_out->SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size;
state_out->SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size;
state_out->SizeOfDisabledStack = (short)g.DisabledStackSize;
}

// Experimental recovery from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls.
// Must be called during or before EndFrame().
// This is generally flawed as we are not necessarily End/Popping things in the right order.
// FIXME: Can't recover from inside BeginTabItem/EndTabItem yet.
void ImGui::ErrorCheckEndFrameRecover()
// Chosen name "Try to recover" over e.g. "Restore" to suggest this is not a 100% guaranteed recovery.
// Called by e.g. EndFrame() but may be called for manual recovery.
// Attempt to recover full window stack.
void ImGui::ErrorRecoveryTryToRecoverState(const ImGuiErrorRecoveryState* state_in)
{
// PVS-Studio V1044 is "Loop break conditions do not depend on the number of iterations"
ImGuiContext& g = *GImGui;
while (g.CurrentWindowStack.Size > 0) //-V1044
while (g.CurrentWindowStack.Size > state_in->SizeOfWindowStack) //-V1044
{
ErrorCheckEndWindowRecover();
// Recap:
// - Begin()/BeginChild() return false to indicate the window is collapsed or fully clipped.
// - Always call a matching End() for each Begin() call, regardless of its return value!
Expand All @@ -10480,21 +10501,24 @@ void ImGui::ErrorCheckEndFrameRecover()
End();
}
}
if (g.CurrentWindowStack.Size == state_in->SizeOfWindowStack)
ErrorRecoveryTryToRecoverWindowState(state_in);
}

// Must be called before End()/EndChild()
void ImGui::ErrorCheckEndWindowRecover()
// Called by e.g. End() but may be called for manual recovery.
// Read '// Error Handling [BETA]' block in imgui_internal.h for details.
// Attempt to recover from incorrect usage of BeginXXX/EndXXX/PushXXX/PopXXX calls.
void ImGui::ErrorRecoveryTryToRecoverWindowState(const ImGuiErrorRecoveryState* state_in)
{
ImGuiContext& g = *GImGui;

while (g.CurrentTable != NULL && g.CurrentTable->InnerWindow == g.CurrentWindow)
{
IM_ASSERT_USER_ERROR(0, "Missing EndTable()");
EndTable();
}

ImGuiWindow* window = g.CurrentWindow;
IM_ASSERT(window != NULL);
ImGuiStackSizes* state_in = &g.CurrentWindowStack.back().StackSizesOnBegin;

// FIXME: Can't recover from inside BeginTabItem/EndTabItem yet.
while (g.CurrentTabBar != NULL && g.CurrentTabBar->Window == window) //-V1044
Expand Down Expand Up @@ -10560,45 +10584,52 @@ void ImGui::ErrorCheckEndWindowRecover()
IM_ASSERT_USER_ERROR(0, "Missing PopFocusScope()");
PopFocusScope();
}
//IM_ASSERT(g.FocusScopeStack.Size == state_in->SizeOfFocusScopeStack);
}

// Save current stack sizes for later compare
void ImGuiStackSizes::SetToContextState(ImGuiContext* ctx)
bool ImGui::ErrorLog(const char* msg)
{
ImGuiContext& g = *ctx;
ImGuiWindow* window = g.CurrentWindow;
SizeOfIDStack = (short)window->IDStack.Size;
SizeOfColorStack = (short)g.ColorStack.Size;
SizeOfStyleVarStack = (short)g.StyleVarStack.Size;
SizeOfFontStack = (short)g.FontStack.Size;
SizeOfFocusScopeStack = (short)g.FocusScopeStack.Size;
SizeOfGroupStack = (short)g.GroupStack.Size;
SizeOfItemFlagsStack = (short)g.ItemFlagsStack.Size;
SizeOfBeginPopupStack = (short)g.BeginPopupStack.Size;
SizeOfDisabledStack = (short)g.DisabledStackSize;
}
ImGuiContext& g = *GImGui;

// Compare to detect usage errors
void ImGuiStackSizes::CompareWithContextState(ImGuiContext* ctx)
{
ImGuiContext& g = *ctx;
// Output to debug log
#ifndef IMGUI_DISABLE_DEBUG_TOOLS
ImGuiWindow* window = g.CurrentWindow;
IM_UNUSED(window);

// Window stacks
// NOT checking: DC.ItemWidth, DC.TextWrapPos (per window) to allow user to conveniently push once and not pop (they are cleared on Begin)
IM_ASSERT(SizeOfIDStack == window->IDStack.Size && "PushID/PopID or TreeNode/TreePop Mismatch!");
if (g.IO.ConfigErrorRecoveryEnableDebugLog)
{
if (g.ErrorFirst)
IMGUI_DEBUG_LOG_ERROR("[imgui-error] (current settings: Assert=%d, Log=%d, Tooltip=%d)\n",
g.IO.ConfigErrorRecoveryEnableAssert, g.IO.ConfigErrorRecoveryEnableDebugLog, g.IO.ConfigErrorRecoveryEnableTooltip);
IMGUI_DEBUG_LOG_ERROR("[imgui-error] In window '%s': %s\n", window ? window->Name : "NULL", msg);
}
g.ErrorFirst = false;

// Output to tooltip
if (g.IO.ConfigErrorRecoveryEnableTooltip)
{
if (BeginErrorTooltip())
{
if (g.ErrorCountCurrentFrame < 20)
{
Text("In window '%s': %s", window ? window->Name : "NULL", msg);
if (window && (!window->IsFallbackWindow || window->WasActive))
GetForegroundDrawList(window)->AddRect(window->Pos, window->Pos + window->Size, IM_COL32(255, 0, 0, 255));
}
if (g.ErrorCountCurrentFrame == 20)
Text("(and more errors)");
// EndFrame() will amend debug buttons to this window, after all errors have been submitted.
EndErrorTooltip();
}
g.ErrorCountCurrentFrame++;
}
#endif

// Output to callback
if (g.ErrorCallback != NULL)
g.ErrorCallback(&g, g.ErrorCallbackUserData, msg);

// Global stacks
// For color, style and font stacks there is an incentive to use Push/Begin/Pop/.../End patterns, so we relax our checks a little to allow them.
IM_ASSERT(SizeOfGroupStack == g.GroupStack.Size && "BeginGroup/EndGroup Mismatch!");
IM_ASSERT(SizeOfBeginPopupStack == g.BeginPopupStack.Size && "BeginPopup/EndPopup or BeginMenu/EndMenu Mismatch!");
IM_ASSERT(SizeOfDisabledStack == g.DisabledStackSize && "BeginDisabled/EndDisabled Mismatch!");
IM_ASSERT(SizeOfItemFlagsStack >= g.ItemFlagsStack.Size && "PushItemFlag/PopItemFlag Mismatch!");
IM_ASSERT(SizeOfColorStack >= g.ColorStack.Size && "PushStyleColor/PopStyleColor Mismatch!");
IM_ASSERT(SizeOfStyleVarStack >= g.StyleVarStack.Size && "PushStyleVar/PopStyleVar Mismatch!");
IM_ASSERT(SizeOfFontStack >= g.FontStack.Size && "PushFont/PopFont Mismatch!");
IM_ASSERT(SizeOfFocusScopeStack == g.FocusScopeStack.Size && "PushFocusScope/PopFocusScope Mismatch!");
// Return whether we should assert
return g.IO.ConfigErrorRecoveryEnableAssert;
}

void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip()
Expand All @@ -10625,6 +10656,21 @@ void ImGui::ErrorCheckEndFrameFinalizeErrorTooltip()
g.PlatformIO.Platform_OpenInShellFn(&g, "https://github.com/ocornut/imgui/blob/master/docs/FAQ.md#qa-usage");
EndErrorTooltip();
}

if (g.ErrorCountCurrentFrame > 0 && BeginErrorTooltip()) // Amend at end of frame
{
Separator();
Text("(Hold CTRL and:");
SameLine();
if (SmallButton("Enable Asserts"))
g.IO.ConfigErrorRecoveryEnableAssert = true;
//SameLine();
//if (SmallButton("Hide Error Tooltips"))
// g.IO.ConfigErrorRecoveryEnableTooltip = false; // Too dangerous
SameLine(0, 0);
Text(")");
EndErrorTooltip();
}
#endif
}

Expand Down
21 changes: 19 additions & 2 deletions imgui.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
// Library Version
// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345')
#define IMGUI_VERSION "1.91.3 WIP"
#define IMGUI_VERSION_NUM 19122
#define IMGUI_VERSION_NUM 19123
#define IMGUI_HAS_TABLE

/*
Expand Down Expand Up @@ -2265,6 +2265,23 @@ struct ImGuiIO
// Debug options
//------------------------------------------------------------------

// Options to configure how we handle recoverable errors [EXPERIMENTAL]
// - Error recovery is not perfect nor guaranteed! It is a feature to ease development.
// - Functions that support error recovery are using IM_ASSERT_USER_ERROR() instead of IM_ASSERT().
// - You not are not supposed to rely on it in the course of a normal application run.
// - Possible usage: facilitate recovery from errors triggered from a scripting language or after specific exceptions handlers.
// - Always ensure that on programmers seat you have at minimum Asserts or Tooltips enabled when making direct imgui API calls!
// Otherwise it would severely hinder your ability to catch and correct mistakes!
// Read https://github.com/ocornut/imgui/wiki/Error-Handling for details about typical usage scenarios:
// - Programmer seats: keep asserts (default), or disable asserts and keep error tooltips (new and nice!)
// - Non-programmer seats: maybe disable asserts, but make sure errors are resurfaced (visible log entries, use callback etc.)
// - Recovery after error from scripting language: record stack sizes before running script, disable assert, trigger breakpoint from ErrorCallback, recover with ErrorRecoveryTryToRecoverState(), restore settings.
// - Recovery after an exception handler: record stack sizes before try {} block, disable assert, set log callback, recover with ErrorRecoveryTryToRecoverState(), restore settings.
bool ConfigErrorRecovery; // = true // Enable error recovery support. Some errors won't be detected and lead to direct crashes if recovery is disabled.
bool ConfigErrorRecoveryEnableAssert; // = true // Enable asserts on recoverable error. By default call IM_ASSERT() when returning from a failing IM_ASSERT_USER_ERROR()
bool ConfigErrorRecoveryEnableDebugLog; // = true // Enable debug log output on recoverable errors.
bool ConfigErrorRecoveryEnableTooltip; // = true // Enable tooltip on recoverable errors. The tooltip include a way to enable asserts if they were disabled.

// Option to enable various debug tools showing buttons that will call the IM_DEBUG_BREAK() macro.
// - The Item Picker tool will be available regardless of this being enabled, in order to maximize its discoverability.
// - Requires a debugger being attached, otherwise IM_DEBUG_BREAK() options will appear to crash your application.
Expand Down Expand Up @@ -2293,7 +2310,7 @@ struct ImGuiIO
bool ConfigDebugIniSettings; // = false // Save .ini data with extra comments (particularly helpful for Docking, but makes saving slower)

//------------------------------------------------------------------
// Platform Functions
// Platform Identifiers
// (the imgui_impl_xxxx backend files are setting those up for you)
//------------------------------------------------------------------

Expand Down
Loading

0 comments on commit 30c29d2

Please sign in to comment.