Nabla icon indicating copy to clipboard operation
Nabla copied to clipboard

[Proposal] NBL ASSERT Macros

Open Erfan-Ahmadi opened this issue 2 years ago • 4 comments

Description

Currently in implementation of nbl functions we use variety of ways to validate inputs and calculations using asserts, ifs and debug breaks.

Problems with the current state of things invlove:

  • Most of the time we debug in RelWithDebug and we might not get important asserts and delay fixing an important bug
  • The code gets longer, dirtier and a less unreadable with a lot of assert(false)s
  • There is no concept of a "AssertLevel" based on build configuration since we use the assert from c
  • Sometimes we want to get the ASSERT but be able to continue and not terminate the application (Based on the configuration or build settings)
  • If someone is not in VisualStudio and not debugging they won't be able to see where the application failed or hit an assert

Of course we can do all above using DEBUG_BREAK, ASSERT, ILogger. but it would require longer and redundant code

Here is some patterns from around the engine:

if (!condition)
{
logger.log("some error happened");
assert(false);
}
if (!condition)
{
assert(false);
return false;
}

My solution described below fixes the mentioned problems. and in addition brings some beauty to how we get the errors and asserts.

Solution proposal

We define and NBL_ASSERT_LEVEL which can take values 0, 1, 2 NBL_ASSERT_LEVEL == 2 -> Strictest -> is for DEBUG NBL_ASSERT_LEVEL == 1 -> somewhat strict -> is for not DEBUG NBL_ASSERT_LEVEL == 0 -> Doesn't do anything on Asserts -> can be overriden by cmake (by defining it first)

Here we define 3 new macros:

NBL_ASSERT 
NBL_ASSERT_STRONG
NBL_ASSERT_REPAIR

** I'll define what NblAssert() function is below

NBL_ASSERT: does nothing in level 0 and 1, but does NblAssert() for level 2 NBL_ASSERT_STRONG: does nothing in level 0, but does NblAssert() for level 1 and 2 NBL_ASSERT_REPAIR: returns (condition) at level 0 and 1, but does NblAssert() for 2 this is a special macro for us to be able to use an assert and check for an if at the same time.

if (NBL_ASSERT_REPAIR(VK_RET==VK_SUCCESS))
 return CVulkanSomething;

Assertion Process

NblAssert() is our internal static function used for assertions and in the macros above

bool NBL_API
NblAssert (
    bool cond, char const * cond_str,
    char const * func, char const * file, int line,
    char const * msg_fmt, ...)

This function constructs a beutifull message from the input strings and cond_str provided with some macro fu**ery in the case that condition fails.

We now have the option to:

  • log to stderr stream using fprintf (seperate from our logging system because it's not global)
  • use MessageBoxA to show a nice message box in windows and ask the user if they want to terminate or continue

If the condition fails we definitely do these:

  • __debugbreak()
  • we use std::terminate(); unless the message box returned ID_CONTINUE(?)

What it would look like:

NBL_ASSERT(inputOffset != InvalidOffset);
NBL_ASSERT(some_value == 0, "some_value should be 0 but It's %d", some_value);
NBL_ASSERT_STRONG(inputOfAHighlyImportantUtility.valid(), "Offff someone messed up");
if(NBL_ASSERT_REPAIR(ret_value == SUCCESS))
{
   // do some more logic
}

Open for discussion

  • How we're going to log, maybe take Logger as an input instead of stderr but we probably need a default or else code gets dirtier (I have some Logger ideas I'll write later in the future that might have an effect here)

Erfan-Ahmadi avatar May 18 '22 09:05 Erfan-Ahmadi

Assert is meant to be used in "absolutely should not happen" scenarios

Sometimes we want to get the ASSERT but be able to continue and not terminate the application (Based on the configuration or build settings)

This is when we'd want to use NBL_DEBUG_BREAK_IF, assert is not meant to be used to handle Vulkan API returned VK_SUCCESS.

Assert is for when the application MUST close, because:

  • the situation makes no sense at all (usually when we use assert(False) without an "error message" string or a comment)
  • there's no fallback or error handling code implemented

Insted of Levels, lets have build configs

For example NBL_ASSERT_CONFIGS=Release;RelWithDebInfo;Debug then we can toggle if assert code actually asserts depending on the builds.

@AnastaZIuk could add NBL_ASSERTS_ENABLED conditionally when the build config matches NBL_ASSERT_CONFIGS, to the generated BuildConfig.h containing all the defines.

We can do likewise for NBL_DEBUG_BREAK_IF and NBL_DEBUG_BREAK_IF_NOT

Suggested macro signatures

#define NBL_ASSERT_CUSTOM_LOGGER(COND,LOGGER,FMT_STRING,...) nbl::core::_assert(COND,LOGGER,FMT_STRING,__FUNCTION__,_FILE_,_LINE_,__VA_ARGS__)

#define NBL_DEBUG_BREAK_IF(COND,LOGGER,FMT_STRING,...) nbl::core::debug_break_if(COND,LOGGER,FMT_STRING,__FUNCTION__,_FILE_,_LINE_,__VA_ARGS__)

#define NBL_DEBUG_BREAK_IF_NOT(COND,LOGGER,FMT_STRING,...) nbl::core::debug_break_if_not(COND,LOGGER,FMT_STRING,__FUNCTION__,_FILE_,_LINE_,__VA_ARGS__)

#defne NBL_DEFAULT_DEBUG_LOGGER nbl::core::IDebugLogger::getDefault()

Sadly macros dont support overloading.

Suggested function signatures

namespace nbl::core
{

class IDebugLogger
{
   public:
      virtual void operator()(const char* fmtString, ...) = 0;
      
      // the only singleton I will EVER allow in the engine
      // implementation of the default operator() MUST be THREAD SAFE
      NBL_API static IDebugLogger* getDefault();
};

void _assert(const bool cond, IDebugLogger* logger, const char* fmtString, const char* funcName, const char* fileName, const uint32_t line, ...);
// default logger overload
inline void _assert(const bool cond, const char* fmtString, const char* funcName, const char* fileName, const uint32_t line, ...)
{
    _assert(cond, fmtString, funcName, fileName, line, va_list);
}

// returns `cond`
bool debug_break_if(const bool cond, IDebugLogger* logger, const char* fmtString, const char* funcName, const char* fileName, const uint32_t line, ...);
// default logger overload
bool debug_break_if(const bool cond, const char* fmtString, const char* funcName, const char* fileName, const uint32_t line, ...)
{
    return debug_break_if(cond,fmtString,funcName,fileName,line,va_list);
}

// also returns `cond`
bool debug_break_if_not(const bol cond, IDebugLogger* logger, const char* fmtString, const char* funcName, const char* fileName, const uint32_t line, ...);
// default logger overload
bool debug_break_if_not(const bool cond, const char* fmtString, const char* funcName, const char* fileName, const uint32_t line, ...)
{
    return debug_break_if_not(cond,fmtString,funcName,fileName,line,va_list);
}

}

The Debug-Break-If should somehow detect if a debugger is attached

Throwing exceptions or trigger breakpoints crashes an application which is without a debugger.

When not in a debugger, we should still log stuff, but without tripping breakpoints.

WE could alternatively move ILogger from system to core and define everything in terms of that.

Ok, lets raise the bar

!!! GLSL ASSERTS !!!

#if _NBL_GLSL_ASSERT_ENABLE_


#extension GL_EXT_debug_printf : enable


#define NBL_GLSL_LOG_ERROR_IF(COND) if (cond) \
{ \
    debugPrintfEXT("Nabla ERROR in File:%s Line:%d",__FILE__,__LINE__); \
    discard; \
}
#define NBL_GLSL_LOG_ERROR_IF_NOT(COND) NBL_GLSL_LOG_ERROR_IF(!COND)

// could decide discard vs return depending on stage type
#define NBL_GLSL_ASSERT(COND) if (!cond) \
{ \
    debugPrintfEXT("Nabla ASSERT in File:%s Line:%d",__FILE__,__LINE__); \
    discard; \
}

#endif

We could also steal some ideas from: https://www.boost.org/doc/libs/master/libs/assert/doc/html/assert.html