diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index faebd73f6c..95748f544a 100644
--- a/Core/GameEngine/CMakeLists.txt
+++ b/Core/GameEngine/CMakeLists.txt
@@ -72,6 +72,7 @@ set(GAMEENGINE_SRC
# Include/Common/MapObject.h
# Include/Common/MapReaderWriterInfo.h
# Include/Common/MessageStream.h
+ Include/Common/MiniDumper.h
# Include/Common/MiniLog.h
Include/Common/MiscAudio.h
# Include/Common/MissionStats.h
@@ -660,6 +661,7 @@ set(GAMEENGINE_SRC
# Source/Common/System/List.cpp
Source/Common/System/LocalFile.cpp
Source/Common/System/LocalFileSystem.cpp
+ Source/Common/System/MiniDumper.cpp
Source/Common/System/ObjectStatusTypes.cpp
# Source/Common/System/QuotedPrintable.cpp
Source/Common/System/Radar.cpp
diff --git a/Core/GameEngine/Include/Common/GameMemory.h b/Core/GameEngine/Include/Common/GameMemory.h
index 8d173afec4..9836b6c946 100644
--- a/Core/GameEngine/Include/Common/GameMemory.h
+++ b/Core/GameEngine/Include/Common/GameMemory.h
@@ -223,6 +223,9 @@ class MemoryPool;
class MemoryPoolFactory;
class DynamicMemoryAllocator;
class BlockCheckpointInfo;
+#ifdef RTS_ENABLE_CRASHDUMP
+class AllocationRangeIterator;
+#endif
// TYPE DEFINES ///////////////////////////////////////////////////////////////
@@ -279,6 +282,14 @@ class Checkpointable
};
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+struct MemoryPoolAllocatedRange
+{
+ const char* allocationAddr;
+ size_t allocationSize;
+};
+#endif
+
// ----------------------------------------------------------------------------
/**
A MemoryPool provides a way to efficiently allocate objects of the same (or similar)
@@ -384,6 +395,9 @@ class MemoryPool
/// return true iff this block was allocated by this pool.
Bool debugIsBlockInPool(void *pBlock);
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+ friend class AllocationRangeIterator;
+#endif
};
// ----------------------------------------------------------------------------
@@ -474,6 +488,11 @@ class DynamicMemoryAllocator
Bool debugIsPoolInDma(MemoryPool *pool);
#endif // MEMORYPOOL_DEBUG
+#ifdef RTS_ENABLE_CRASHDUMP
+ MemoryPoolSingleBlock* getFirstRawBlock() const;
+ MemoryPoolSingleBlock* getNextRawBlock(const MemoryPoolSingleBlock* block) const;
+ void fillAllocationRangeForRawBlock(const MemoryPoolSingleBlock*, MemoryPoolAllocatedRange& allocationRange) const;
+#endif
};
// ----------------------------------------------------------------------------
@@ -481,6 +500,40 @@ class DynamicMemoryAllocator
enum { MAX_SPECIAL_USED = 256 };
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+class AllocationRangeIterator
+{
+ typedef const MemoryPoolAllocatedRange value_type;
+ typedef const MemoryPoolAllocatedRange* pointer;
+ typedef const MemoryPoolAllocatedRange& reference;
+
+public:
+
+ AllocationRangeIterator();
+ AllocationRangeIterator(const MemoryPoolFactory* factory);
+ AllocationRangeIterator(MemoryPool& pool, MemoryPoolBlob& blob);
+ AllocationRangeIterator(MemoryPool* pool, MemoryPoolBlob* blob);
+
+ reference operator*() { return m_range; }
+ pointer operator->() { return &m_range; }
+
+ AllocationRangeIterator& operator++();
+ AllocationRangeIterator operator++(int);
+
+ friend const bool operator== (const AllocationRangeIterator& a, const AllocationRangeIterator& b);
+ friend const bool operator!= (const AllocationRangeIterator& a, const AllocationRangeIterator& b);
+
+private:
+
+ void updateRange();
+ void moveToNextBlob();
+ const MemoryPoolFactory* m_factory;
+ MemoryPool* m_currentPool;
+ MemoryPoolBlob* m_currentBlobInPool;
+ MemoryPoolAllocatedRange m_range;
+};
+#endif
+
// ----------------------------------------------------------------------------
/**
The class that manages all the MemoryPools and DynamicMemoryAllocators.
@@ -573,6 +626,20 @@ class MemoryPoolFactory
void debugResetCheckpoints();
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+ AllocationRangeIterator cbegin() const
+ {
+ return AllocationRangeIterator(this);
+ }
+
+ AllocationRangeIterator cend() const
+ {
+ return AllocationRangeIterator(NULL, NULL);
+ }
+
+ MemoryPool* getFirstMemoryPool() const;
+ friend class AllocationRangeIterator;
+#endif
};
// how many bytes are we allowed to 'waste' per pool allocation before the debug code starts yelling at us...
diff --git a/Core/GameEngine/Include/Common/MiniDumper.h b/Core/GameEngine/Include/Common/MiniDumper.h
new file mode 100644
index 0000000000..057a7031c2
--- /dev/null
+++ b/Core/GameEngine/Include/Common/MiniDumper.h
@@ -0,0 +1,128 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#pragma once
+
+#ifdef RTS_ENABLE_CRASHDUMP
+#include "DbgHelpLoader.h"
+
+enum DumpType CPP_11(: Char)
+{
+ // Smallest dump type with call stacks and some supporting variables
+ DumpType_Minimal = 'M',
+ // Large dump including all memory regions allocated by the GameMemory implementation
+ DumpType_Gamememory = 'X',
+ // Largest dump size including complete memory contents of the process
+ DumpType_Full = 'F',
+};
+
+class MiniDumper
+{
+ enum MiniDumperExitCode CPP_11(: Int)
+ {
+ MiniDumperExitCode_Success = 0x0,
+ MiniDumperExitCode_FailureWait = 0x37DA1040,
+ MiniDumperExitCode_FailureParam = 0x4EA527BB,
+ MiniDumperExitCode_ForcedTerminate = 0x158B1154,
+ };
+
+ enum DumpObjectsState CPP_11(: Int)
+ {
+ DumpObjectsState_Begin,
+ DumpObjectsState_MemoryPools,
+ DumpObjectsState_MemoryPoolAllocations,
+ DumpObjectsState_DMAAllocations,
+ DumpObjectsState_Completed
+ };
+
+public:
+ MiniDumper();
+ Bool IsInitialized() const;
+ void TriggerMiniDump(DumpType dumpType);
+ void TriggerMiniDumpForException(_EXCEPTION_POINTERS* e_info, DumpType dumpType);
+ static void initMiniDumper(const AsciiString& userDirPath);
+ static void shutdownMiniDumper();
+ static LONG WINAPI DumpingExceptionFilter(_EXCEPTION_POINTERS* e_info);
+
+private:
+ void Initialize(const AsciiString& userDirPath);
+ void ShutDown();
+ void CreateMiniDump(DumpType dumpType);
+ void CleanupResources();
+ Bool IsDumpThreadStillRunning() const;
+ void ShutdownDumpThread();
+ Bool ShouldWriteDataSegsForModule(const PWCHAR module) const;
+#ifndef DISABLE_GAMEMEMORY
+ void MoveToNextAllocatorWithRawBlocks();
+ void MoveToNextSingleBlock();
+ void DumpMemoryObjects(ULONG64& memoryBase, ULONG& memorySize);
+#endif
+
+ // Callbacks from dbghelp
+ static BOOL CALLBACK MiniDumpCallback(PVOID CallbackParam, PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput);
+ BOOL CallbackInternal(const MINIDUMP_CALLBACK_INPUT& input, MINIDUMP_CALLBACK_OUTPUT& output);
+
+ // Thread procs
+ static DWORD WINAPI MiniDumpThreadProc(LPVOID lpParam);
+ DWORD ThreadProcInternal();
+
+ // Dump file directory bookkeeping
+ Bool InitializeDumpDirectory(const AsciiString& userDirPath);
+ static void KeepNewestFiles(const std::string& directory, const DumpType dumpType, const Int keepCount);
+
+ // Struct to hold file information
+ struct FileInfo
+ {
+ std::string name;
+ FILETIME lastWriteTime;
+ };
+
+ static bool CompareByLastWriteTime(const FileInfo& a, const FileInfo& b);
+
+private:
+ Bool m_miniDumpInitialized;
+ Bool m_loadedDbgHelp;
+ DumpType m_requestedDumpType;
+
+ // Path buffers
+ Char m_dumpDir[MAX_PATH];
+ Char m_dumpFile[MAX_PATH];
+ WideChar m_executablePath[MAX_PATH];
+
+ // Event handles
+ HANDLE m_dumpRequested;
+ HANDLE m_dumpComplete;
+ HANDLE m_quitting;
+
+ // Thread handles
+ HANDLE m_dumpThread;
+ DWORD m_dumpThreadId;
+
+#ifndef DISABLE_GAMEMEMORY
+ // Internal memory dumping progress state
+ DumpObjectsState m_dumpObjectsState;
+ DynamicMemoryAllocator* m_currentAllocator;
+ MemoryPool* m_currentPool;
+ MemoryPoolSingleBlock* m_currentSingleBlock;
+
+ AllocationRangeIterator m_rangeIter;
+#endif
+};
+
+extern MiniDumper* TheMiniDumper;
+#endif
diff --git a/Core/GameEngine/Source/Common/System/Debug.cpp b/Core/GameEngine/Source/Common/System/Debug.cpp
index 385dd85616..fe20ccffe2 100644
--- a/Core/GameEngine/Source/Common/System/Debug.cpp
+++ b/Core/GameEngine/Source/Common/System/Debug.cpp
@@ -70,6 +70,9 @@
#if defined(DEBUG_STACKTRACE) || defined(IG_DEBUG_STACKTRACE)
#include "Common/StackDump.h"
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+#include "Common/MiniDumper.h"
+#endif
// Horrible reference, but we really, really need to know if we are windowed.
extern bool DX8Wrapper_IsWindowed;
@@ -727,6 +730,22 @@ double SimpleProfiler::getAverageTime()
}
}
+
+static void TriggerMiniDump()
+{
+#ifdef RTS_ENABLE_CRASHDUMP
+ if (TheMiniDumper && TheMiniDumper->IsInitialized())
+ {
+ // Do dumps both with and without extended info
+ TheMiniDumper->TriggerMiniDump(DumpType_Minimal);
+ TheMiniDumper->TriggerMiniDump(DumpType_Gamememory);
+ }
+
+ MiniDumper::shutdownMiniDumper();
+#endif
+}
+
+
void ReleaseCrash(const char *reason)
{
/// do additional reporting on the crash, if possible
@@ -737,6 +756,8 @@ void ReleaseCrash(const char *reason)
}
}
+ TriggerMiniDump();
+
char prevbuf[ _MAX_PATH ];
char curbuf[ _MAX_PATH ];
@@ -813,6 +834,8 @@ void ReleaseCrashLocalized(const AsciiString& p, const AsciiString& m)
return;
}
+ TriggerMiniDump();
+
UnicodeString prompt = TheGameText->fetch(p);
UnicodeString mesg = TheGameText->fetch(m);
diff --git a/Core/GameEngine/Source/Common/System/GameMemory.cpp b/Core/GameEngine/Source/Common/System/GameMemory.cpp
index 78a855c00d..cb062ceacf 100644
--- a/Core/GameEngine/Source/Common/System/GameMemory.cpp
+++ b/Core/GameEngine/Source/Common/System/GameMemory.cpp
@@ -408,9 +408,11 @@ class MemoryPoolSingleBlock
#ifdef MEMORYPOOL_BOUNDINGWALL
Int m_wallPattern; ///< unique seed value for the bounding-walls for this block
#endif
+#if defined(MEMORYPOOL_DEBUG) || defined(RTS_ENABLE_CRASHDUMP)
+ Int m_logicalSize; ///< logical size of block (not including overhead, walls, etc.)
+#endif
#ifdef MEMORYPOOL_DEBUG
const char *m_debugLiteralTagString; ///< ptr to the tagstring for this block.
- Int m_logicalSize; ///< logical size of block (not including overhead, walls, etc.)
Int m_wastedSize; ///< if allocated via DMA, the "wasted" bytes
Short m_magicCookie; ///< magic value used to verify that the block is one of ours (as opposed to random pointer)
Short m_debugFlags; ///< misc flags
@@ -439,13 +441,15 @@ class MemoryPoolSingleBlock
MemoryPoolSingleBlock *getNextFreeBlock();
void setNextFreeBlock(MemoryPoolSingleBlock *b);
- MemoryPoolSingleBlock *getNextRawBlock();
+ MemoryPoolSingleBlock *getNextRawBlock() const;
void setNextRawBlock(MemoryPoolSingleBlock *b);
+#if defined(MEMORYPOOL_DEBUG) || defined(RTS_ENABLE_CRASHDUMP)
+ Int debugGetLogicalSize() const;
+#endif
#ifdef MEMORYPOOL_DEBUG
void debugIgnoreLeaksForThisBlock();
const char *debugGetLiteralTagString();
- Int debugGetLogicalSize();
Int debugGetWastedSize();
void debugSetWastedSize(Int waste);
void debugVerifyBlock();
@@ -501,6 +505,9 @@ class MemoryPoolBlob
#ifdef MEMORYPOOL_CHECKPOINTING
void debugResetCheckpoints();
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+ void fillAllocatedRange(MemoryPoolAllocatedRange& range);
+#endif
};
@@ -604,7 +611,7 @@ inline void MemoryPoolSingleBlock::setNextFreeBlock(MemoryPoolSingleBlock *b)
return the next raw block in this dma. this call assumes that the block
in question does NOT belong to a blob, and will assert if not.
*/
-inline MemoryPoolSingleBlock *MemoryPoolSingleBlock::getNextRawBlock()
+inline MemoryPoolSingleBlock *MemoryPoolSingleBlock::getNextRawBlock() const
{
DEBUG_ASSERTCRASH(m_owningBlob == NULL, ("must be called on raw block"));
return m_nextBlock;
@@ -640,11 +647,11 @@ inline const char *MemoryPoolSingleBlock::debugGetLiteralTagString()
}
#endif
-#ifdef MEMORYPOOL_DEBUG
+#if defined(MEMORYPOOL_DEBUG) || defined(RTS_ENABLE_CRASHDUMP)
/**
accessor
*/
-inline Int MemoryPoolSingleBlock::debugGetLogicalSize()
+inline Int MemoryPoolSingleBlock::debugGetLogicalSize() const
{
//USE_PERF_TIMER(MemoryPoolDebugging) not worth it
return m_logicalSize;
@@ -875,6 +882,8 @@ void MemoryPoolSingleBlock::initBlock(Int logicalSize, MemoryPoolBlob *owningBlo
}
#endif
}
+#elif defined(RTS_ENABLE_CRASHDUMP)
+ m_logicalSize = logicalSize;
#endif // MEMORYPOOL_DEBUG
#ifdef MEMORYPOOL_CHECKPOINTING
@@ -1382,6 +1391,14 @@ void MemoryPoolBlob::debugResetCheckpoints()
}
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+void MemoryPoolBlob::fillAllocatedRange(MemoryPoolAllocatedRange& range)
+{
+ range.allocationAddr = m_blockData;
+ range.allocationSize = m_totalBlocksInBlob * MemoryPoolSingleBlock::calcRawBlockSize(m_owningPool->getAllocationSize());
+}
+#endif
+
//-----------------------------------------------------------------------------
// METHODS for Checkpointable
//-----------------------------------------------------------------------------
@@ -2573,6 +2590,25 @@ void DynamicMemoryAllocator::debugDmaInfoReport( FILE *fp )
}
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+MemoryPoolSingleBlock* DynamicMemoryAllocator::getFirstRawBlock() const
+{
+ return m_rawBlocks;
+}
+
+MemoryPoolSingleBlock* DynamicMemoryAllocator::getNextRawBlock(const MemoryPoolSingleBlock* block) const
+{
+ return block->getNextRawBlock();
+}
+
+void DynamicMemoryAllocator::fillAllocationRangeForRawBlock(const MemoryPoolSingleBlock* block, MemoryPoolAllocatedRange& allocationRange) const
+{
+ allocationRange.allocationAddr = reinterpret_cast(block);
+ allocationRange.allocationSize = block->calcRawBlockSize(block->debugGetLogicalSize());
+}
+
+#endif
+
//-----------------------------------------------------------------------------
// METHODS for MemoryPoolFactory
//-----------------------------------------------------------------------------
@@ -3232,6 +3268,122 @@ void MemoryPoolFactory::debugMemoryReport(Int flags, Int startCheckpoint, Int en
}
#endif
+#ifdef RTS_ENABLE_CRASHDUMP
+MemoryPool* MemoryPoolFactory::getFirstMemoryPool() const
+{
+ return m_firstPoolInFactory;
+}
+
+//-----------------------------------------------------------------------------
+// METHODS for AllocationRangeIterator
+//-----------------------------------------------------------------------------
+
+AllocationRangeIterator::AllocationRangeIterator()
+{
+ m_currentPool = NULL;
+ m_currentBlobInPool = NULL;
+ m_factory = NULL;
+ m_range = MemoryPoolAllocatedRange();
+}
+
+AllocationRangeIterator::AllocationRangeIterator(const MemoryPoolFactory* factory)
+{
+ m_factory = factory;
+ if (factory)
+ {
+ m_currentPool = factory->m_firstPoolInFactory;
+ m_currentBlobInPool = m_currentPool ? m_currentPool->m_firstBlob : NULL;
+ }
+ else
+ {
+ m_currentPool = NULL;
+ m_currentBlobInPool = NULL;
+ }
+
+ m_range = MemoryPoolAllocatedRange();
+}
+
+AllocationRangeIterator::AllocationRangeIterator(MemoryPool& pool, MemoryPoolBlob& blob)
+{
+ m_currentPool = &pool;
+ m_currentBlobInPool = &blob;
+ m_factory = NULL;
+ m_range = MemoryPoolAllocatedRange();
+}
+
+AllocationRangeIterator::AllocationRangeIterator(MemoryPool* pool, MemoryPoolBlob* blob)
+{
+ m_currentPool = pool;
+ m_currentBlobInPool = blob;
+ m_factory = NULL;
+ m_range = MemoryPoolAllocatedRange();
+}
+
+void AllocationRangeIterator::updateRange()
+{
+ if (m_currentBlobInPool == NULL)
+ {
+ m_range.allocationAddr = nullptr;
+ m_range.allocationSize = 0;
+ return;
+ }
+
+ m_currentBlobInPool->fillAllocatedRange(m_range);
+}
+
+void AllocationRangeIterator::moveToNextBlob()
+{
+ if (m_currentBlobInPool == NULL)
+ {
+ return;
+ }
+
+ // Advances to the next blob, advancing to the next MemoryPool if needed.
+ m_currentBlobInPool = m_currentBlobInPool->getNextInList();
+ if (m_currentBlobInPool != NULL)
+ {
+ return;
+ }
+ do
+ {
+ m_currentPool = m_currentPool->getNextPoolInList();
+ } while (m_currentPool != NULL && m_currentPool->m_firstBlob == NULL);
+
+ if (m_currentPool != NULL)
+ {
+ m_currentBlobInPool = m_currentPool->m_firstBlob;
+ }
+ else
+ {
+ m_currentBlobInPool = NULL;
+ }
+}
+
+AllocationRangeIterator& AllocationRangeIterator::operator++()
+{
+ moveToNextBlob();
+ updateRange();
+ return *this;
+}
+
+AllocationRangeIterator AllocationRangeIterator::operator++(int)
+{
+ AllocationRangeIterator tmp = *this;
+ ++(*this);
+ return tmp;
+}
+
+const bool operator== (const AllocationRangeIterator& a, const AllocationRangeIterator& b)
+{
+ return a.m_currentBlobInPool == b.m_currentBlobInPool;
+}
+
+const bool operator!= (const AllocationRangeIterator& a, const AllocationRangeIterator& b)
+{
+ return a.m_currentBlobInPool != b.m_currentBlobInPool;
+}
+#endif
+
//-----------------------------------------------------------------------------
// GLOBAL FUNCTIONS
//-----------------------------------------------------------------------------
diff --git a/Core/GameEngine/Source/Common/System/MiniDumper.cpp b/Core/GameEngine/Source/Common/System/MiniDumper.cpp
new file mode 100644
index 0000000000..22baf488df
--- /dev/null
+++ b/Core/GameEngine/Source/Common/System/MiniDumper.cpp
@@ -0,0 +1,682 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 TheSuperHackers
+**
+** This program is free software: you can redistribute it and/or modify
+** it under the terms of the GNU General Public License as published by
+** the Free Software Foundation, either version 3 of the License, or
+** (at your option) any later version.
+**
+** This program is distributed in the hope that it will be useful,
+** but WITHOUT ANY WARRANTY; without even the implied warranty of
+** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+** GNU General Public License for more details.
+**
+** You should have received a copy of the GNU General Public License
+** along with this program. If not, see .
+*/
+
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#ifdef RTS_ENABLE_CRASHDUMP
+#include "Common/MiniDumper.h"
+#include
+#include "Common/GameMemory.h"
+#include "gitinfo.h"
+
+// Globals for storing the pointer to the exception
+_EXCEPTION_POINTERS* g_dumpException = NULL;
+DWORD g_dumpExceptionThreadId = 0;
+
+MiniDumper* TheMiniDumper = NULL;
+
+// Globals containing state about the current exception that's used for context in the mini dump.
+// These are populated by MiniDumper::DumpingExceptionFilter to store a copy of the exception in case it goes out of scope
+_EXCEPTION_POINTERS g_exceptionPointers = { 0 };
+EXCEPTION_RECORD g_exceptionRecord = { 0 };
+CONTEXT g_exceptionContext = { 0 };
+
+constexpr const char* DumpFileNamePrefix = "Crash";
+
+void MiniDumper::initMiniDumper(const AsciiString& userDirPath)
+{
+ DEBUG_ASSERTCRASH(TheMiniDumper == NULL, ("MiniDumper::initMiniDumper called on already created instance"));
+
+ // Use placement new on the process heap so TheMiniDumper is placed outside the MemoryPoolFactory managed area.
+ // If the crash is due to corrupted MemoryPoolFactory structures, try to mitigate the chances of MiniDumper memory also being corrupted
+ TheMiniDumper = new (::HeapAlloc(::GetProcessHeap(), HEAP_GENERATE_EXCEPTIONS, sizeof(MiniDumper))) MiniDumper;
+ TheMiniDumper->Initialize(userDirPath);
+}
+
+void MiniDumper::shutdownMiniDumper()
+{
+ if (TheMiniDumper)
+ {
+ TheMiniDumper->ShutDown();
+ TheMiniDumper->~MiniDumper();
+ ::HeapFree(::GetProcessHeap(), NULL, TheMiniDumper);
+ TheMiniDumper = NULL;
+ }
+}
+
+MiniDumper::MiniDumper()
+{
+ m_miniDumpInitialized = false;
+ m_loadedDbgHelp = false;
+ m_requestedDumpType = DumpType_Minimal;
+ m_dumpRequested = NULL;
+ m_dumpComplete = NULL;
+ m_quitting = NULL;
+ m_dumpThread = NULL;
+ m_dumpThreadId = 0;
+#ifndef DISABLE_GAMEMEMORY
+ m_dumpObjectsState = DumpObjectsState_Begin;
+ m_currentAllocator = NULL;
+ m_currentSingleBlock = NULL;
+ m_currentPool = NULL;
+#endif
+ m_dumpDir[0] = 0;
+ m_dumpFile[0] = 0;
+ m_executablePath[0] = 0;
+};
+
+LONG WINAPI MiniDumper::DumpingExceptionFilter(_EXCEPTION_POINTERS* e_info)
+{
+ // Store the exception info in the global variables for later use by the dumping thread
+ g_exceptionRecord = *(e_info->ExceptionRecord);
+ g_exceptionContext = *(e_info->ContextRecord);
+ g_exceptionPointers.ContextRecord = &g_exceptionContext;
+ g_exceptionPointers.ExceptionRecord = &g_exceptionRecord;
+ g_dumpException = &g_exceptionPointers;
+
+ return EXCEPTION_EXECUTE_HANDLER;
+}
+
+void MiniDumper::TriggerMiniDump(DumpType dumpType)
+{
+ if (!m_miniDumpInitialized)
+ {
+ DEBUG_LOG(("MiniDumper::TriggerMiniDump: Attempted to use an uninitialized instance."));
+ return;
+ }
+
+ __try
+ {
+ // Use DebugBreak to raise an exception that can be caught in the __except block
+ ::DebugBreak();
+ }
+ __except (DumpingExceptionFilter(GetExceptionInformation()))
+ {
+ TriggerMiniDumpForException(g_dumpException, dumpType);
+ }
+}
+
+void MiniDumper::TriggerMiniDumpForException(_EXCEPTION_POINTERS* e_info, DumpType dumpType)
+{
+ if (!m_miniDumpInitialized)
+ {
+ DEBUG_LOG(("MiniDumper::TriggerMiniDumpForException: Attempted to use an uninitialized instance."));
+ return;
+ }
+
+ g_dumpException = e_info;
+ g_dumpExceptionThreadId = ::GetCurrentThreadId();
+ m_requestedDumpType = dumpType;
+#ifdef DISABLE_GAMEMEMORY
+ if (m_requestedDumpType == DUMP_TYPE_GAMEMEMORY)
+ {
+ // Dump the whole process if the game memory implementation is turned off
+ m_requestedDumpType = DUMP_TYPE_FULL;
+ }
+#endif
+
+ DEBUG_ASSERTCRASH(IsDumpThreadStillRunning(), ("MiniDumper::TriggerMiniDumpForException: Dumping thread has exited."));
+ ::SetEvent(m_dumpRequested);
+ DWORD wait = ::WaitForSingleObject(m_dumpComplete, INFINITE);
+ if (wait != WAIT_OBJECT_0)
+ {
+ if (wait == WAIT_FAILED)
+ {
+ DEBUG_LOG(("MiniDumper::TriggerMiniDumpForException: Waiting for minidump triggering failed: status=%u, error=%u", wait, ::GetLastError()));
+ }
+ else
+ {
+ DEBUG_LOG(("MiniDumper::TriggerMiniDumpForException: Waiting for minidump triggering failed: status=%u", wait));
+ }
+ }
+
+ ::ResetEvent(m_dumpComplete);
+}
+
+void MiniDumper::Initialize(const AsciiString& userDirPath)
+{
+ m_loadedDbgHelp = DbgHelpLoader::load();
+
+ // We want to only use the dbghelp.dll from the OS installation, as the one bundled with the game does not support MiniDump functionality
+ if (!(m_loadedDbgHelp && DbgHelpLoader::isLoadedFromSystem()))
+ {
+ DEBUG_LOG(("MiniDumper::Initialize: Unable to load system-provided dbghelp.dll, minidump functionality disabled."));
+ return;
+ }
+
+ DWORD executableSize = ::GetModuleFileNameW(NULL, m_executablePath, ARRAY_SIZE(m_executablePath));
+ if (executableSize == 0 || executableSize == ARRAY_SIZE(m_executablePath))
+ {
+ DEBUG_LOG(("MiniDumper::Initialize: Could not get executable file name. Returned value=%u", executableSize));
+ return;
+ }
+
+ // Create & store dump folder
+ if (!InitializeDumpDirectory(userDirPath))
+ {
+ return;
+ }
+
+ m_dumpRequested = CreateEvent(NULL, TRUE, FALSE, NULL);
+ m_dumpComplete = CreateEvent(NULL, TRUE, FALSE, NULL);
+ m_quitting = CreateEvent(NULL, TRUE, FALSE, NULL);
+
+ if (m_dumpRequested == NULL || m_dumpComplete == NULL || m_quitting == NULL)
+ {
+ // Something went wrong with the creation of the events..
+ DEBUG_LOG(("MiniDumper::Initialize: Unable to create events: error=%u", ::GetLastError()));
+ return;
+ }
+
+ m_dumpThread = ::CreateThread(NULL, 0, MiniDumpThreadProc, this, CREATE_SUSPENDED, &m_dumpThreadId);
+ if (!m_dumpThread)
+ {
+ DEBUG_LOG(("MiniDumper::Initialize: Unable to create thread: error=%u", ::GetLastError()));
+ return;
+ }
+
+ if (::ResumeThread(m_dumpThread) != 1)
+ {
+ DEBUG_LOG(("MiniDumper::Initialize: Unable to resume thread: error=%u", ::GetLastError()));
+ return;
+ }
+
+ DEBUG_LOG(("MiniDumper::Initialize: Configured to store crash dumps in '%s'", m_dumpDir));
+ m_miniDumpInitialized = true;
+}
+
+Bool MiniDumper::IsInitialized() const
+{
+ return m_miniDumpInitialized;
+}
+
+Bool MiniDumper::IsDumpThreadStillRunning() const
+{
+ DWORD exitCode;
+ if (m_dumpThread != NULL && ::GetExitCodeThread(m_dumpThread, &exitCode) && exitCode == STILL_ACTIVE)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+Bool MiniDumper::InitializeDumpDirectory(const AsciiString& userDirPath)
+{
+ constexpr const Int MaxExtendedFileCount = 2;
+ constexpr const Int MaxFullFileCount = 2;
+ constexpr const Int MaxMiniFileCount = 10;
+
+ strlcpy(m_dumpDir, userDirPath.str(), ARRAY_SIZE(m_dumpDir));
+ strlcat(m_dumpDir, "CrashDumps\\", ARRAY_SIZE(m_dumpDir));
+ if (::_access(m_dumpDir, 0) != 0)
+ {
+ if (!::CreateDirectory(m_dumpDir, NULL))
+ {
+ DEBUG_LOG(("MiniDumper::Initialize: Unable to create path for crash dumps at '%s': error=%u", m_dumpDir, ::GetLastError()));
+ return false;
+ }
+ }
+
+ // Clean up old files (we keep a maximum of 10 small, 2 extended and 2 full)
+ KeepNewestFiles(m_dumpDir, DumpType_Gamememory, MaxExtendedFileCount);
+ KeepNewestFiles(m_dumpDir, DumpType_Full, MaxFullFileCount);
+ KeepNewestFiles(m_dumpDir, DumpType_Minimal, MaxMiniFileCount);
+
+ return true;
+}
+
+void MiniDumper::ShutdownDumpThread()
+{
+ if (IsDumpThreadStillRunning())
+ {
+ DEBUG_ASSERTCRASH(m_quitting != NULL, ("MiniDumper::ShutdownDumpThread: Dump thread still running despite m_quitting being NULL"));
+ ::SetEvent(m_quitting);
+
+ DWORD waitRet = ::WaitForSingleObject(m_dumpThread, 3000);
+ switch (waitRet)
+ {
+ case WAIT_OBJECT_0:
+ // Wait for thread exit was successful
+ break;
+ case WAIT_TIMEOUT:
+ DEBUG_LOG(("MiniDumper::ShutdownDumpThread: Waiting for dumping thread to exit timed out, killing thread", waitRet));
+ ::TerminateThread(m_dumpThread, MiniDumperExitCode_ForcedTerminate);
+ break;
+ case WAIT_FAILED:
+ DEBUG_LOG(("MiniDumper::ShutdownDumpThread: Waiting for minidump triggering failed: status=%u, error=%u", waitRet, ::GetLastError()));
+ break;
+ default:
+ DEBUG_LOG(("MiniDumper::ShutdownDumpThread: Waiting for minidump triggering failed: status=%u", waitRet));
+ break;
+ }
+ }
+}
+
+void MiniDumper::ShutDown()
+{
+ ShutdownDumpThread();
+
+ if (m_dumpThread != NULL)
+ {
+ DEBUG_ASSERTCRASH(!IsDumpThreadStillRunning(), ("MiniDumper::ShutDown: ShutdownDumpThread() was unable to stop Dump thread"));
+ ::CloseHandle(m_dumpThread);
+ m_dumpThread = NULL;
+ }
+
+ if (m_quitting != NULL)
+ {
+ ::CloseHandle(m_quitting);
+ m_quitting = NULL;
+ }
+
+ if (m_dumpComplete != NULL)
+ {
+ ::CloseHandle(m_dumpComplete);
+ m_dumpComplete = NULL;
+ }
+
+ if (m_dumpRequested != NULL)
+ {
+ ::CloseHandle(m_dumpRequested);
+ m_dumpRequested = NULL;
+ }
+
+ if (m_loadedDbgHelp)
+ {
+ DbgHelpLoader::unload();
+ m_loadedDbgHelp = false;
+ }
+
+ m_miniDumpInitialized = false;
+}
+
+DWORD MiniDumper::ThreadProcInternal()
+{
+ while (true)
+ {
+ HANDLE waitEvents[2] = { m_dumpRequested, m_quitting };
+ DWORD event = ::WaitForMultipleObjects(ARRAY_SIZE(waitEvents), waitEvents, FALSE, INFINITE);
+ switch (event)
+ {
+ case WAIT_OBJECT_0 + 0:
+ // A dump is requested (m_dumpRequested)
+ ::ResetEvent(m_dumpComplete);
+ CreateMiniDump(m_requestedDumpType);
+ ::ResetEvent(m_dumpRequested);
+ ::SetEvent(m_dumpComplete);
+ break;
+ case WAIT_OBJECT_0 + 1:
+ // Quit (m_quitting)
+ return MiniDumperExitCode_Success;
+ case WAIT_FAILED:
+ DEBUG_LOG(("MiniDumper::ThreadProcInternal: Waiting for events failed: status=%u, error=%u", event, ::GetLastError()));
+ return MiniDumperExitCode_FailureWait;
+ default:
+ DEBUG_LOG(("MiniDumper::ThreadProcInternal: Waiting for events failed: status=%u", event));
+ return MiniDumperExitCode_FailureWait;
+ }
+ }
+}
+
+DWORD WINAPI MiniDumper::MiniDumpThreadProc(LPVOID lpParam)
+{
+ if (lpParam == NULL)
+ {
+ DEBUG_LOG(("MiniDumper::MiniDumpThreadProc: The provided parameter was NULL, exiting thread."));
+ return MiniDumperExitCode_FailureParam;
+ }
+
+ MiniDumper* dumper = static_cast(lpParam);
+ return dumper->ThreadProcInternal();
+}
+
+
+void MiniDumper::CreateMiniDump(DumpType dumpType)
+{
+ // Create a unique dump file name, using the path from m_dumpDir, in m_dumpFile
+ SYSTEMTIME sysTime;
+ ::GetLocalTime(&sysTime);
+#if RTS_GENERALS
+ const Char product = 'G';
+#elif RTS_ZEROHOUR
+ const Char product = 'Z';
+#endif
+ Char dumpTypeSpecifier = static_cast(dumpType);
+ DWORD currentProcessId = ::GetCurrentProcessId();
+
+ // m_dumpDir is stored with trailing backslash in Initialize
+ snprintf(m_dumpFile, ARRAY_SIZE(m_dumpFile), "%s%s%c%c-%04d%02d%02d-%02d%02d%02d-%s-pid%ld.dmp",
+ m_dumpDir, DumpFileNamePrefix, dumpTypeSpecifier, product, sysTime.wYear, sysTime.wMonth,
+ sysTime.wDay, sysTime.wHour, sysTime.wMinute, sysTime.wSecond,
+ GitShortSHA1, currentProcessId);
+
+ HANDLE dumpFile = ::CreateFile(m_dumpFile, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
+ if (dumpFile == NULL || dumpFile == INVALID_HANDLE_VALUE)
+ {
+ DEBUG_LOG(("MiniDumper::CreateMiniDump: Unable to create dump file '%s': error=%u", m_dumpFile, ::GetLastError()));
+ return;
+ }
+
+ m_dumpObjectsState = DumpObjectsState_Begin;
+
+ PMINIDUMP_EXCEPTION_INFORMATION exceptionInfoPtr = NULL;
+ MINIDUMP_EXCEPTION_INFORMATION exceptionInfo = { 0 };
+ if (g_dumpException != NULL)
+ {
+ exceptionInfo.ExceptionPointers = g_dumpException;
+ exceptionInfo.ThreadId = g_dumpExceptionThreadId;
+ exceptionInfo.ClientPointers = FALSE;
+ exceptionInfoPtr = &exceptionInfo;
+ }
+
+ PMINIDUMP_CALLBACK_INFORMATION callbackInfoPtr = NULL;
+ MINIDUMP_CALLBACK_INFORMATION callBackInfo = { 0 };
+ if (dumpType == DumpType_Gamememory)
+ {
+ callBackInfo.CallbackRoutine = MiniDumpCallback;
+ callBackInfo.CallbackParam = this;
+ callbackInfoPtr = &callBackInfo;
+ }
+
+ int dumpTypeFlags = MiniDumpNormal;
+ switch (dumpType)
+ {
+ case DumpType_Full:
+ dumpTypeFlags |= MiniDumpWithFullMemory;
+ FALLTHROUGH;
+ case DumpType_Gamememory:
+ dumpTypeFlags |= MiniDumpWithDataSegs | MiniDumpWithHandleData | MiniDumpWithThreadInfo | MiniDumpWithFullMemoryInfo | MiniDumpWithPrivateReadWriteMemory;
+ FALLTHROUGH;
+ case DumpType_Minimal:
+ dumpTypeFlags |= MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory;
+ break;
+ }
+
+ MINIDUMP_TYPE miniDumpType = static_cast(dumpTypeFlags);
+ BOOL success = DbgHelpLoader::miniDumpWriteDump(
+ ::GetCurrentProcess(),
+ currentProcessId,
+ dumpFile,
+ miniDumpType,
+ exceptionInfoPtr,
+ NULL,
+ callbackInfoPtr);
+
+ if (!success)
+ {
+ DEBUG_LOG(("MiniDumper::CreateMiniDump: Unable to write minidump file '%s': error=%u", m_dumpFile, ::GetLastError()));
+ }
+ else
+ {
+ DEBUG_LOG(("MiniDumper::CreateMiniDump: Successfully wrote minidump file to '%s'", m_dumpFile));
+ }
+
+ ::CloseHandle(dumpFile);
+}
+
+BOOL CALLBACK MiniDumper::MiniDumpCallback(PVOID CallbackParam, PMINIDUMP_CALLBACK_INPUT CallbackInput, PMINIDUMP_CALLBACK_OUTPUT CallbackOutput)
+{
+ if (CallbackParam == NULL || CallbackInput == NULL || CallbackOutput == NULL)
+ {
+ DEBUG_LOG(("MiniDumper::MiniDumpCallback: Required parameters were null; CallbackParam=%p, CallbackInput=%p, CallbackOutput=%p.",
+ CallbackParam, CallbackInput, CallbackOutput));
+ return false;
+ }
+
+ MiniDumper* dumper = static_cast(CallbackParam);
+ return dumper->CallbackInternal(*CallbackInput, *CallbackOutput);
+}
+
+Bool MiniDumper::ShouldWriteDataSegsForModule(const PWCHAR module) const
+{
+ // Only include data segments for the game, ntdll and kernel32 modules to keep dump size low
+ static constexpr const WideChar* wanted_modules[] = { L"ntdll.dll", L"kernel32.dll"};
+ if (endsWithNoCase(module, m_executablePath))
+ {
+ return true;
+ }
+
+ for (size_t i = 0; i < ARRAY_SIZE(wanted_modules); ++i)
+ {
+ if (endsWithNoCase(module, wanted_modules[i]))
+ {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// This is where the memory regions and things are being filtered
+BOOL MiniDumper::CallbackInternal(const MINIDUMP_CALLBACK_INPUT& input, MINIDUMP_CALLBACK_OUTPUT& output)
+{
+ BOOL success = TRUE;
+ switch (input.CallbackType)
+ {
+ case IncludeModuleCallback:
+ case ThreadCallback:
+ case ThreadExCallback:
+ break;
+ case ModuleCallback:
+ if (output.ModuleWriteFlags & ModuleWriteDataSeg)
+ {
+ if (!ShouldWriteDataSegsForModule(input.Module.FullPath))
+ {
+ // Exclude data segments for the module
+ output.ModuleWriteFlags &= (~ModuleWriteDataSeg);
+ }
+ }
+ break;
+ case IncludeThreadCallback:
+ // We want all threads except the dumping thread
+ if (input.IncludeThread.ThreadId == m_dumpThreadId)
+ {
+ output.ThreadWriteFlags &= (~ThreadWriteThread);
+ }
+ break;
+ case MemoryCallback:
+#ifndef DISABLE_GAMEMEMORY
+ // DumpMemoryObjects will set output.MemorySize to 0 once it's completed,
+ // signalling to end the memory callbacks.
+ DumpMemoryObjects(output.MemoryBase, output.MemorySize);
+#else
+ output.MemoryBase = 0;
+ output.MemorySize = 0;
+#endif
+ break;
+ case ReadMemoryFailureCallback:
+ DEBUG_LOG(("MiniDumper::CallbackInternal: ReadMemoryFailure with MemoryBase=%llu, MemorySize=%lu: error=%u",
+ input.ReadMemoryFailure.Offset, input.ReadMemoryFailure.Bytes, input.ReadMemoryFailure.FailureStatus));
+ break;
+ case CancelCallback:
+ // The cancel callback checks if the current minidump should be cancelled.
+ // Set output to never wish to cancel, nor to receive cancel callbacks in the future.
+ output.Cancel = FALSE;
+ output.CheckCancel = FALSE;
+ break;
+ }
+
+ return success;
+}
+
+#ifndef DISABLE_GAMEMEMORY
+void MiniDumper::MoveToNextAllocatorWithRawBlocks()
+{
+ // Skip over DMAs that have no raw blocks
+ while (m_currentAllocator && !m_currentAllocator->getFirstRawBlock())
+ {
+ m_currentAllocator = m_currentAllocator->getNextDmaInList();
+ }
+
+ if (m_currentAllocator)
+ {
+ m_currentSingleBlock = m_currentAllocator->getFirstRawBlock();
+ }
+}
+
+void MiniDumper::MoveToNextSingleBlock()
+{
+ DEBUG_ASSERTCRASH(m_currentAllocator != NULL, ("MiniDumper::MoveToNextSingleBlock: m_currentAllocator is NULL"));
+ m_currentSingleBlock = m_currentAllocator->getNextRawBlock(m_currentSingleBlock);
+
+ if (m_currentSingleBlock == NULL)
+ {
+ m_currentAllocator = m_currentAllocator->getNextDmaInList();
+ MoveToNextAllocatorWithRawBlocks();
+ }
+}
+
+void MiniDumper::DumpMemoryObjects(ULONG64& memoryBase, ULONG& memorySize)
+{
+ memorySize = 0;
+ do
+ {
+ // m_dumpObjectsState is used to keep track of the current "phase" of the memory dumping process
+ switch (m_dumpObjectsState)
+ {
+ case DumpObjectsState_Begin:
+ m_dumpObjectsState = DumpObjectsState_MemoryPools;
+ if (TheMemoryPoolFactory)
+ {
+ m_currentPool = TheMemoryPoolFactory->getFirstMemoryPool();
+ m_rangeIter = TheMemoryPoolFactory->cbegin();
+ m_currentAllocator = TheDynamicMemoryAllocator;
+ MoveToNextAllocatorWithRawBlocks();
+ }
+ break;
+ case DumpObjectsState_MemoryPools:
+ {
+ // Dump all the MemoryPool instances in TheMemoryPoolFactory
+ // This only dumps the metadata, not the actual MemoryPool contents (done in the next phase).
+ if (m_currentPool == NULL)
+ {
+ m_dumpObjectsState = DumpObjectsState_MemoryPoolAllocations;
+ break;
+ }
+
+ memoryBase = reinterpret_cast(m_currentPool);
+ memorySize = sizeof(MemoryPool);
+ m_currentPool = m_currentPool->getNextPoolInList();
+ break;
+ }
+ case DumpObjectsState_MemoryPoolAllocations:
+ {
+ // Iterate through all the allocations of memory pools and containing blobs that has been done via the memory pool factory
+ // and include all of the storage space allocated for objects
+ if (!TheMemoryPoolFactory || m_rangeIter == TheMemoryPoolFactory->cend())
+ {
+ m_dumpObjectsState = DumpObjectsState_DMAAllocations;
+ break;
+ }
+
+ memoryBase = reinterpret_cast(m_rangeIter->allocationAddr);
+ memorySize = m_rangeIter->allocationSize;
+ ++m_rangeIter;
+ break;
+ }
+ case DumpObjectsState_DMAAllocations:
+ {
+ // Iterate through all the direct allocations ("raw blocks") done by DMAs, as these are done outside of the
+ // memory pool factory allocations dumped in the previous phase.
+ if (m_currentAllocator == NULL)
+ {
+ m_dumpObjectsState = DumpObjectsState_Completed;
+ break;
+ }
+
+ MemoryPoolAllocatedRange rawBlockRange;
+ m_currentAllocator->fillAllocationRangeForRawBlock(m_currentSingleBlock, rawBlockRange);
+ memoryBase = reinterpret_cast(rawBlockRange.allocationAddr);
+ memorySize = rawBlockRange.allocationSize;
+ MoveToNextSingleBlock();
+ break;
+ }
+ case DumpObjectsState_Completed:
+ // Done, set "no more regions to dump" values
+ memoryBase = 0;
+ memorySize = 0;
+ return;
+ default:
+ DEBUG_CRASH(("Invalid object state"));
+ break;
+ }
+ // If memorySize is 0 we transitioned from one phase to the next.
+ // Go again so the memory block info gets populated in the new phase.
+ } while (memorySize == 0);
+}
+#endif
+
+// Comparator for sorting files by last modified time (newest first)
+bool MiniDumper::CompareByLastWriteTime(const FileInfo& a, const FileInfo& b)
+{
+ return ::CompareFileTime(&a.lastWriteTime, &b.lastWriteTime) > 0;
+}
+
+void MiniDumper::KeepNewestFiles(const std::string& directory, const DumpType dumpType, const Int keepCount)
+{
+ // directory already contains trailing backslash
+ std::string searchPath = directory + DumpFileNamePrefix + static_cast(dumpType) + "*";
+ WIN32_FIND_DATA findData;
+ HANDLE hFind = ::FindFirstFile(searchPath.c_str(), &findData);
+
+ if (hFind == INVALID_HANDLE_VALUE)
+ {
+ if (::GetLastError() != ERROR_FILE_NOT_FOUND)
+ {
+ DEBUG_LOG(("MiniDumper::KeepNewestFiles: Unable to find files in directory '%s': error=%u", searchPath.c_str(), ::GetLastError()));
+ }
+
+ return;
+ }
+
+ std::vector files;
+ do
+ {
+ if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
+ {
+ continue;
+ }
+
+ // Store file info
+ FileInfo fileInfo;
+ fileInfo.name = directory + findData.cFileName;
+ fileInfo.lastWriteTime = findData.ftLastWriteTime;
+ files.push_back(fileInfo);
+
+ } while (::FindNextFile(hFind, &findData));
+
+ ::FindClose(hFind);
+
+ // Sort files by last modified time in descending order
+ std::sort(files.begin(), files.end(), CompareByLastWriteTime);
+
+ // Delete files beyond the newest keepCount
+ for (size_t i = keepCount; i < files.size(); ++i)
+ {
+ if (::DeleteFile(files[i].name.c_str()))
+ {
+ DEBUG_LOG(("MiniDumper::KeepNewestFiles: Deleted old dump file '%s'.", files[i].name.c_str()));
+ }
+ else
+ {
+ DEBUG_LOG(("MiniDumper::KeepNewestFiles: Failed to delete file '%s': error=%u", files[i].name.c_str(), ::GetLastError()));
+ }
+ }
+}
+#endif
diff --git a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
index 3eec2f42f8..997a6c0242 100644
--- a/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
+++ b/Core/Libraries/Source/WWVegas/WWLib/CMakeLists.txt
@@ -36,6 +36,7 @@ set(WWLIB_SRC
DbgHelpGuard.h
DbgHelpLoader.cpp
DbgHelpLoader.h
+ DbgHelpLoader_minidump.h
Except.cpp
Except.h
FastAllocator.cpp
diff --git a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp
index 96898d5097..368bcd03ca 100644
--- a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp
+++ b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.cpp
@@ -33,6 +33,9 @@ DbgHelpLoader::DbgHelpLoader()
, m_symSetOptions(NULL)
, m_symFunctionTableAccess(NULL)
, m_stackWalk(NULL)
+#ifdef RTS_ENABLE_CRASHDUMP
+ , m_miniDumpWriteDump(NULL)
+#endif
, m_dllModule(HMODULE(0))
, m_referenceCount(0)
, m_failed(false)
@@ -118,6 +121,9 @@ bool DbgHelpLoader::load()
Inst->m_symSetOptions = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "SymSetOptions"));
Inst->m_symFunctionTableAccess = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "SymFunctionTableAccess"));
Inst->m_stackWalk = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "StackWalk"));
+#ifdef RTS_ENABLE_CRASHDUMP
+ Inst->m_miniDumpWriteDump = reinterpret_cast(::GetProcAddress(Inst->m_dllModule, "MiniDumpWriteDump"));
+#endif
if (Inst->m_symInitialize == NULL || Inst->m_symCleanup == NULL)
{
@@ -171,6 +177,9 @@ void DbgHelpLoader::freeResources()
Inst->m_symSetOptions = NULL;
Inst->m_symFunctionTableAccess = NULL;
Inst->m_stackWalk = NULL;
+#ifdef RTS_ENABLE_CRASHDUMP
+ Inst->m_miniDumpWriteDump = NULL;
+#endif
Inst->m_loadedFromSystem = false;
}
@@ -332,3 +341,22 @@ BOOL DbgHelpLoader::stackWalk(
return FALSE;
}
+
+#ifdef RTS_ENABLE_CRASHDUMP
+BOOL DbgHelpLoader::miniDumpWriteDump(
+ HANDLE hProcess,
+ DWORD ProcessId,
+ HANDLE hFile,
+ MINIDUMP_TYPE DumpType,
+ PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
+ PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
+ PMINIDUMP_CALLBACK_INFORMATION CallbackParam)
+{
+ CriticalSectionClass::LockClass lock(CriticalSection);
+
+ if (Inst != NULL && Inst->m_miniDumpWriteDump)
+ return Inst->m_miniDumpWriteDump(hProcess, ProcessId, hFile, DumpType, ExceptionParam, UserStreamParam, CallbackParam);
+
+ return FALSE;
+}
+#endif
diff --git a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h
index c2167db075..3dda58f206 100644
--- a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h
+++ b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader.h
@@ -23,6 +23,9 @@
#include
#include // Must be included after Windows.h
#include
+#ifdef RTS_ENABLE_CRASHDUMP
+#include
+#endif
#include "mutex.h"
#include "SystemAllocator.h"
@@ -109,6 +112,17 @@ class DbgHelpLoader
PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine,
PTRANSLATE_ADDRESS_ROUTINE TranslateAddress);
+#ifdef RTS_ENABLE_CRASHDUMP
+ static BOOL WINAPI miniDumpWriteDump(
+ HANDLE hProcess,
+ DWORD ProcessId,
+ HANDLE hFile,
+ MINIDUMP_TYPE DumpType,
+ PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
+ PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
+ PMINIDUMP_CALLBACK_INFORMATION CallbackParam);
+#endif
+
private:
static void freeResources();
@@ -167,6 +181,17 @@ class DbgHelpLoader
PGET_MODULE_BASE_ROUTINE GetModuleBaseRoutine,
PTRANSLATE_ADDRESS_ROUTINE TranslateAddress);
+#ifdef RTS_ENABLE_CRASHDUMP
+ typedef BOOL(WINAPI* MiniDumpWriteDump_t)(
+ HANDLE hProcess,
+ DWORD ProcessId,
+ HANDLE hFile,
+ MINIDUMP_TYPE DumpType,
+ PMINIDUMP_EXCEPTION_INFORMATION ExceptionParam,
+ PMINIDUMP_USER_STREAM_INFORMATION UserStreamParam,
+ PMINIDUMP_CALLBACK_INFORMATION CallbackParam);
+#endif
+
SymInitialize_t m_symInitialize;
SymCleanup_t m_symCleanup;
SymLoadModule_t m_symLoadModule;
@@ -177,13 +202,15 @@ class DbgHelpLoader
SymSetOptions_t m_symSetOptions;
SymFunctionTableAccess_t m_symFunctionTableAccess;
StackWalk_t m_stackWalk;
+#ifdef RTS_ENABLE_CRASHDUMP
+ MiniDumpWriteDump_t m_miniDumpWriteDump;
+#endif
typedef std::set, stl::system_allocator > Processes;
Processes m_initializedProcesses;
HMODULE m_dllModule;
int m_referenceCount;
- CriticalSectionClass m_criticalSection;
bool m_failed;
bool m_loadedFromSystem;
};
diff --git a/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader_minidump.h b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader_minidump.h
new file mode 100644
index 0000000000..272fa832f5
--- /dev/null
+++ b/Core/Libraries/Source/WWVegas/WWLib/DbgHelpLoader_minidump.h
@@ -0,0 +1,265 @@
+#pragma once
+
+#ifdef RTS_ENABLE_CRASHDUMP
+
+// Backported defines from minidumpapiset.h for VC6.
+// minidumpapiset.h is Copyright (C) Microsoft Corporation. All rights reserved.
+
+#if defined(_MSC_VER) && _MSC_VER < 1300
+#include
+
+typedef enum _MINIDUMP_CALLBACK_TYPE {
+ ModuleCallback,
+ ThreadCallback,
+ ThreadExCallback,
+ IncludeThreadCallback,
+ IncludeModuleCallback,
+ MemoryCallback,
+ CancelCallback,
+ WriteKernelMinidumpCallback,
+ KernelMinidumpStatusCallback,
+ RemoveMemoryCallback,
+ IncludeVmRegionCallback,
+ IoStartCallback,
+ IoWriteAllCallback,
+ IoFinishCallback,
+ ReadMemoryFailureCallback,
+ SecondaryFlagsCallback,
+ IsProcessSnapshotCallback,
+ VmStartCallback,
+ VmQueryCallback,
+ VmPreReadCallback,
+ VmPostReadCallback
+} MINIDUMP_CALLBACK_TYPE;
+
+typedef struct _MINIDUMP_THREAD_CALLBACK {
+ ULONG ThreadId;
+ HANDLE ThreadHandle;
+#if defined(_ARM64_)
+ ULONG Pad;
+#endif
+ CONTEXT Context;
+ ULONG SizeOfContext;
+ ULONG64 StackBase;
+ ULONG64 StackEnd;
+} MINIDUMP_THREAD_CALLBACK, * PMINIDUMP_THREAD_CALLBACK;
+
+typedef struct _MINIDUMP_THREAD_EX_CALLBACK {
+ ULONG ThreadId;
+ HANDLE ThreadHandle;
+#if defined(_ARM64_)
+ ULONG Pad;
+#endif
+ CONTEXT Context;
+ ULONG SizeOfContext;
+ ULONG64 StackBase;
+ ULONG64 StackEnd;
+ ULONG64 BackingStoreBase;
+ ULONG64 BackingStoreEnd;
+} MINIDUMP_THREAD_EX_CALLBACK, * PMINIDUMP_THREAD_EX_CALLBACK;
+
+typedef struct _MINIDUMP_MODULE_CALLBACK {
+ PWCHAR FullPath;
+ ULONG64 BaseOfImage;
+ ULONG SizeOfImage;
+ ULONG CheckSum;
+ ULONG TimeDateStamp;
+ VS_FIXEDFILEINFO VersionInfo;
+ PVOID CvRecord;
+ ULONG SizeOfCvRecord;
+ PVOID MiscRecord;
+ ULONG SizeOfMiscRecord;
+} MINIDUMP_MODULE_CALLBACK, * PMINIDUMP_MODULE_CALLBACK;
+
+typedef struct _MINIDUMP_INCLUDE_THREAD_CALLBACK {
+ ULONG ThreadId;
+} MINIDUMP_INCLUDE_THREAD_CALLBACK, * PMINIDUMP_INCLUDE_THREAD_CALLBACK;
+
+typedef enum _THREAD_WRITE_FLAGS {
+ ThreadWriteThread = 0x0001,
+ ThreadWriteStack = 0x0002,
+ ThreadWriteContext = 0x0004,
+ ThreadWriteBackingStore = 0x0008,
+ ThreadWriteInstructionWindow = 0x0010,
+ ThreadWriteThreadData = 0x0020,
+ ThreadWriteThreadInfo = 0x0040,
+} THREAD_WRITE_FLAGS;
+
+typedef struct _MINIDUMP_INCLUDE_MODULE_CALLBACK {
+ ULONG64 BaseOfImage;
+} MINIDUMP_INCLUDE_MODULE_CALLBACK, * PMINIDUMP_INCLUDE_MODULE_CALLBACK;
+
+typedef struct _MINIDUMP_IO_CALLBACK {
+ HANDLE Handle;
+ ULONG64 Offset;
+ PVOID Buffer;
+ ULONG BufferBytes;
+} MINIDUMP_IO_CALLBACK, * PMINIDUMP_IO_CALLBACK;
+
+typedef struct _MINIDUMP_READ_MEMORY_FAILURE_CALLBACK
+{
+ ULONG64 Offset;
+ ULONG Bytes;
+ HRESULT FailureStatus;
+} MINIDUMP_READ_MEMORY_FAILURE_CALLBACK,
+* PMINIDUMP_READ_MEMORY_FAILURE_CALLBACK;
+
+typedef struct _MINIDUMP_VM_QUERY_CALLBACK
+{
+ ULONG64 Offset;
+} MINIDUMP_VM_QUERY_CALLBACK, * PMINIDUMP_VM_QUERY_CALLBACK;
+
+typedef struct _MINIDUMP_VM_PRE_READ_CALLBACK
+{
+ ULONG64 Offset;
+ PVOID Buffer;
+ ULONG Size;
+} MINIDUMP_VM_PRE_READ_CALLBACK, * PMINIDUMP_VM_PRE_READ_CALLBACK;
+
+typedef struct _MINIDUMP_VM_POST_READ_CALLBACK
+{
+ ULONG64 Offset;
+ PVOID Buffer;
+ ULONG Size;
+ ULONG Completed;
+ HRESULT Status;
+} MINIDUMP_VM_POST_READ_CALLBACK, * PMINIDUMP_VM_POST_READ_CALLBACK;
+
+typedef struct _MINIDUMP_MEMORY_INFO {
+ ULONG64 BaseAddress;
+ ULONG64 AllocationBase;
+ ULONG32 AllocationProtect;
+ ULONG32 __alignment1;
+ ULONG64 RegionSize;
+ ULONG32 State;
+ ULONG32 Protect;
+ ULONG32 Type;
+ ULONG32 __alignment2;
+} MINIDUMP_MEMORY_INFO, * PMINIDUMP_MEMORY_INFO;
+
+typedef struct _MINIDUMP_CALLBACK_INPUT {
+ ULONG ProcessId;
+ HANDLE ProcessHandle;
+ ULONG CallbackType;
+ union {
+ HRESULT Status;
+ MINIDUMP_THREAD_CALLBACK Thread;
+ MINIDUMP_THREAD_EX_CALLBACK ThreadEx;
+ MINIDUMP_MODULE_CALLBACK Module;
+ MINIDUMP_INCLUDE_THREAD_CALLBACK IncludeThread;
+ MINIDUMP_INCLUDE_MODULE_CALLBACK IncludeModule;
+ MINIDUMP_IO_CALLBACK Io;
+ MINIDUMP_READ_MEMORY_FAILURE_CALLBACK ReadMemoryFailure;
+ ULONG SecondaryFlags;
+ MINIDUMP_VM_QUERY_CALLBACK VmQuery;
+ MINIDUMP_VM_PRE_READ_CALLBACK VmPreRead;
+ MINIDUMP_VM_POST_READ_CALLBACK VmPostRead;
+ };
+} MINIDUMP_CALLBACK_INPUT, * PMINIDUMP_CALLBACK_INPUT;
+
+typedef struct _MINIDUMP_CALLBACK_OUTPUT {
+ union {
+ ULONG ModuleWriteFlags;
+ ULONG ThreadWriteFlags;
+ ULONG SecondaryFlags;
+ struct {
+ ULONG64 MemoryBase;
+ ULONG MemorySize;
+ };
+ struct {
+ BOOL CheckCancel;
+ BOOL Cancel;
+ };
+ HANDLE Handle;
+ struct {
+ MINIDUMP_MEMORY_INFO VmRegion;
+ BOOL Continue;
+ };
+ struct {
+ HRESULT VmQueryStatus;
+ MINIDUMP_MEMORY_INFO VmQueryResult;
+ };
+ struct {
+ HRESULT VmReadStatus;
+ ULONG VmReadBytesCompleted;
+ };
+ HRESULT Status;
+ };
+} MINIDUMP_CALLBACK_OUTPUT, * PMINIDUMP_CALLBACK_OUTPUT;
+
+typedef struct _MINIDUMP_EXCEPTION_INFORMATION {
+ DWORD ThreadId;
+ PEXCEPTION_POINTERS ExceptionPointers;
+ BOOL ClientPointers;
+} MINIDUMP_EXCEPTION_INFORMATION, * PMINIDUMP_EXCEPTION_INFORMATION;
+
+typedef struct _MINIDUMP_USER_STREAM {
+ ULONG32 Type;
+ ULONG BufferSize;
+ PVOID Buffer;
+
+} MINIDUMP_USER_STREAM, * PMINIDUMP_USER_STREAM;
+
+typedef struct _MINIDUMP_USER_STREAM_INFORMATION {
+ ULONG UserStreamCount;
+ PMINIDUMP_USER_STREAM UserStreamArray;
+} MINIDUMP_USER_STREAM_INFORMATION, * PMINIDUMP_USER_STREAM_INFORMATION;
+
+typedef
+BOOL
+(WINAPI* MINIDUMP_CALLBACK_ROUTINE) (
+ PVOID CallbackParam,
+ PMINIDUMP_CALLBACK_INPUT CallbackInput,
+ PMINIDUMP_CALLBACK_OUTPUT CallbackOutput
+ );
+
+typedef struct _MINIDUMP_CALLBACK_INFORMATION {
+ MINIDUMP_CALLBACK_ROUTINE CallbackRoutine;
+ PVOID CallbackParam;
+} MINIDUMP_CALLBACK_INFORMATION, * PMINIDUMP_CALLBACK_INFORMATION;
+
+typedef enum _MINIDUMP_TYPE {
+ MiniDumpNormal = 0x00000000,
+ MiniDumpWithDataSegs = 0x00000001,
+ MiniDumpWithFullMemory = 0x00000002,
+ MiniDumpWithHandleData = 0x00000004,
+ MiniDumpFilterMemory = 0x00000008,
+ MiniDumpScanMemory = 0x00000010,
+ MiniDumpWithUnloadedModules = 0x00000020,
+ MiniDumpWithIndirectlyReferencedMemory = 0x00000040,
+ MiniDumpFilterModulePaths = 0x00000080,
+ MiniDumpWithProcessThreadData = 0x00000100,
+ MiniDumpWithPrivateReadWriteMemory = 0x00000200,
+ MiniDumpWithoutOptionalData = 0x00000400,
+ MiniDumpWithFullMemoryInfo = 0x00000800,
+ MiniDumpWithThreadInfo = 0x00001000,
+ MiniDumpWithCodeSegs = 0x00002000,
+ MiniDumpWithoutAuxiliaryState = 0x00004000,
+ MiniDumpWithFullAuxiliaryState = 0x00008000,
+ MiniDumpWithPrivateWriteCopyMemory = 0x00010000,
+ MiniDumpIgnoreInaccessibleMemory = 0x00020000,
+ MiniDumpWithTokenInformation = 0x00040000,
+ MiniDumpWithModuleHeaders = 0x00080000,
+ MiniDumpFilterTriage = 0x00100000,
+ MiniDumpWithAvxXStateContext = 0x00200000,
+ MiniDumpWithIptTrace = 0x00400000,
+ MiniDumpScanInaccessiblePartialPages = 0x00800000,
+ MiniDumpFilterWriteCombinedMemory = 0x01000000,
+ MiniDumpValidTypeFlags = 0x01ffffff,
+ MiniDumpNoIgnoreInaccessibleMemory = 0x02000000,
+ MiniDumpValidTypeFlagsEx = 0x03ffffff,
+} MINIDUMP_TYPE;
+
+typedef enum _MODULE_WRITE_FLAGS {
+ ModuleWriteModule = 0x0001,
+ ModuleWriteDataSeg = 0x0002,
+ ModuleWriteMiscRecord = 0x0004,
+ ModuleWriteCvRecord = 0x0008,
+ ModuleReferencedByMemory = 0x0010,
+ ModuleWriteTlsData = 0x0020,
+ ModuleWriteCodeSegs = 0x0040,
+} MODULE_WRITE_FLAGS;
+
+#include
+#endif
+#endif
diff --git a/Generals/Code/Main/WinMain.cpp b/Generals/Code/Main/WinMain.cpp
index 303d6da1ce..12b0af5df8 100644
--- a/Generals/Code/Main/WinMain.cpp
+++ b/Generals/Code/Main/WinMain.cpp
@@ -64,6 +64,9 @@
#include "BuildVersion.h"
#include "GeneratedVersion.h"
#include "resource.h"
+#ifdef RTS_ENABLE_CRASHDUMP
+#include "Common/MiniDumper.h"
+#endif
// GLOBALS ////////////////////////////////////////////////////////////////////
@@ -741,6 +744,16 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5;
static LONG WINAPI UnHandledExceptionFilter( struct _EXCEPTION_POINTERS* e_info )
{
DumpExceptionInfo( e_info->ExceptionRecord->ExceptionCode, e_info );
+#ifdef RTS_ENABLE_CRASHDUMP
+ if (TheMiniDumper && TheMiniDumper->IsInitialized())
+ {
+ // Do dumps both with and without extended info
+ TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Minimal);
+ TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Gamememory);
+ }
+
+ MiniDumper::shutdownMiniDumper();
+#endif
return EXCEPTION_EXECUTE_HANDLER;
}
@@ -801,6 +814,10 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
CommandLine::parseCommandLineForStartup();
+#ifdef RTS_ENABLE_CRASHDUMP
+ // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup
+ MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData());
+#endif
// register windows class and create application window
if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false)
return exitcode;
@@ -868,6 +885,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
}
+#ifdef RTS_ENABLE_CRASHDUMP
+ MiniDumper::shutdownMiniDumper();
+#endif
TheAsciiStringCriticalSection = NULL;
TheUnicodeStringCriticalSection = NULL;
TheDmaCriticalSection = NULL;
diff --git a/GeneralsMD/Code/Main/WinMain.cpp b/GeneralsMD/Code/Main/WinMain.cpp
index 6017276941..fb494dc438 100644
--- a/GeneralsMD/Code/Main/WinMain.cpp
+++ b/GeneralsMD/Code/Main/WinMain.cpp
@@ -67,6 +67,9 @@
#include "resource.h"
#include
+#ifdef RTS_ENABLE_CRASHDUMP
+#include "Common/MiniDumper.h"
+#endif
// GLOBALS ////////////////////////////////////////////////////////////////////
@@ -763,6 +766,16 @@ static CriticalSection critSec1, critSec2, critSec3, critSec4, critSec5;
static LONG WINAPI UnHandledExceptionFilter( struct _EXCEPTION_POINTERS* e_info )
{
DumpExceptionInfo( e_info->ExceptionRecord->ExceptionCode, e_info );
+#ifdef RTS_ENABLE_CRASHDUMP
+ if (TheMiniDumper && TheMiniDumper->IsInitialized())
+ {
+ // Do dumps both with and without extended info
+ TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Minimal);
+ TheMiniDumper->TriggerMiniDumpForException(e_info, DumpType_Gamememory);
+ }
+
+ MiniDumper::shutdownMiniDumper();
+#endif
return EXCEPTION_EXECUTE_HANDLER;
}
@@ -847,6 +860,10 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
#endif
CommandLine::parseCommandLineForStartup();
+#ifdef RTS_ENABLE_CRASHDUMP
+ // Initialize minidump facilities - requires TheGlobalData so performed after parseCommandLineForStartup
+ MiniDumper::initMiniDumper(TheGlobalData->getPath_UserData());
+#endif
// register windows class and create application window
if(!TheGlobalData->m_headless && initializeAppWindows(hInstance, nCmdShow, TheGlobalData->m_windowed) == false)
@@ -916,6 +933,9 @@ Int APIENTRY WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,
}
+#ifdef RTS_ENABLE_CRASHDUMP
+ MiniDumper::shutdownMiniDumper();
+#endif
TheUnicodeStringCriticalSection = NULL;
TheDmaCriticalSection = NULL;
TheMemoryPoolCriticalSection = NULL;
diff --git a/cmake/config-memory.cmake b/cmake/config-memory.cmake
index 72a28d1c94..5daa6f110a 100644
--- a/cmake/config-memory.cmake
+++ b/cmake/config-memory.cmake
@@ -20,6 +20,9 @@ option(RTS_MEMORYPOOL_DEBUG_INTENSE_VERIFY "Enables intensive verifications afte
option(RTS_MEMORYPOOL_DEBUG_CHECK_BLOCK_OWNERSHIP "Enables debug to verify that a block actually belongs to the pool it is called with. This is great for debugging, but can be realllly slow, so is OFF by default." OFF)
option(RTS_MEMORYPOOL_DEBUG_INTENSE_DMA_BOOKKEEPING "Prints statistics for memory usage of Memory Pools." OFF)
+# Memory dump options
+option(RTS_CRASHDUMP_ENABLE "Enables writing crash dumps on unhandled exceptions or release crash failures." ON)
+
# Game Memory features
add_feature_info(GameMemoryEnable RTS_GAMEMEMORY_ENABLE "Build with the original game memory implementation")
@@ -37,6 +40,8 @@ add_feature_info(MemoryPoolDebugIntenseVerify RTS_MEMORYPOOL_DEBUG_INTENSE_VERIF
add_feature_info(MemoryPoolDebugCheckBlockOwnership RTS_MEMORYPOOL_DEBUG_CHECK_BLOCK_OWNERSHIP "Build with Memory Pool block ownership checks")
add_feature_info(MemoryPoolDebugIntenseDmaBookkeeping RTS_MEMORYPOOL_DEBUG_INTENSE_DMA_BOOKKEEPING "Build with Memory Pool intense DMA bookkeeping")
+# Memory dump features
+add_feature_info(CrashDumpEnable RTS_CRASHDUMP_ENABLE "Build with Crash Dumps")
# Game Memory features
if(NOT RTS_GAMEMEMORY_ENABLE)
@@ -87,3 +92,10 @@ else()
target_compile_definitions(core_config INTERFACE INTENSE_DMA_BOOKKEEPING=1)
endif()
endif()
+
+if(RTS_CRASHDUMP_ENABLE)
+ target_compile_definitions(core_config INTERFACE RTS_ENABLE_CRASHDUMP=1)
+ if (IS_VS6_BUILD AND NOT RTS_BUILD_OPTION_VC6_FULL_DEBUG)
+ message(STATUS "Crash Dumps will be significantly less useful in VC6 builds without full debug info enabled")
+ endif()
+endif()