Skip to content

Conversation

@slurmlord
Copy link

@slurmlord slurmlord commented Sep 18, 2025

This change introduces the option to generate crash dumps, aka. mini dumps, on fatal errors.
The main minidump functionality is done by explicitly loading the dbghelp.dll from the system directory, as the dbghelp.dll that is bundled with the game is an older version that does not include this functionality. There is an option to create small dumps or full memory dumps, currently both are created.

Small dumps

These mostly contain stacks for the process threads and some stack variables, or to create dumps with extended info. The use case for these is to quickly determine where a crash occured, the type of crash, if it was already fixed etc. In addition, if the memory allocation structures are corrupted enough, an extended info dump might not succeed while the small dump should. The size of these dumps are typically on the order of 250kB.

Full memory

These contain all the memory regions in the process, as they specify MiniDumpWithFullMemory flag to MiniDumpWriteDump. They are considerably larger than the small dumps but compress relatively well. As an example, a ~400MB dump file is compressed to ~70MB using the Windows 11 built-in 7Z compression.

Storage Location

Crash dumps are stored in a new folder called 'CrashDumps' under the userDir ("Documents\Command and Conquer Generals Zero Hour Data"), and on startup it will create this directory if it doesn't exist and delete any older dumps so only the 10 newest small and 2 newest full memory dumps are left. This is to preserve disk space, as the full memory files can be several hundred MB.

Integration points

For VS2022 builds, unhandled exceptions end up in the UnhandledExceptionFilter in WinMain, which then get a reference to the actual exception that occurred and includes that in the dump.
For VC6 builds, unhandled exceptions are caught in the catch(...) blocks of GameEngine::execute which then calls RELEASE_CRASH. As there is no exception data available in this case to populate _EXCEPTION_POINTERS from, an intentional exception is triggered to get the trace of the current thread. This makes the stack traces for VC6 a bit more cryptic than VS2022 builds as the C++ exception handling gets included in the trace.

Limitations

In the longer run we'll probably want to replace this code with a more mature solution, like CrashPad, but that currently depends on a newer compiler than VC6.
As the code is intended to be temporary, it's kept behind a new CMake feature so it can be easily removed. There are also some other decisions made with this in mind:

  • Minidump is created in-process. Ideally, the dump should be performed by a process external to the crashing/failing process, but to avoid having to ship an extra binary, in-process was chosen instead. It's being performed in a separate thread to hopefully have a clean stack to work with.
  • Depends on RTS_BUILD_OPTION_VC6_FULL_DEBUG for VC6 builds. The PDBs generated with the default VC6 compile options are lacking in information, making the mini dumps less useful. The option RTS_BUILD_OPTION_VC6_FULL_DEBUG should be enabled for VC6 builds to ensure maximum usability. VS2022 builds produce better PDBs and require no extra options.
  • Directory management code is contained within the MiniDumper class, not re-usable by other components.
  • Many Win32-specific types and functions are used directly without regards for portability.
  • As the MiniDump feature is not available for VC6, a lot of headers have been borrowed from minidumpapiset.h and included in the DbgHelpLoader_minidump.h file.
  • Globals are used for storing the current exception info.
  • Only enabled for the games, tools are currently not included.

Copy link

@OmniBlade OmniBlade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, tested dump generation with a 2022 build.

@xezon xezon added Major Severity: Minor < Major < Critical < Blocker Gen Relates to Generals ZH Relates to Zero Hour Debug Is mostly debug functionality System Is Systems related labels Oct 8, 2025
@slurmlord
Copy link
Author

Another approach I realized after publishing could be to move the GameMemory allocations from using GlobalAlloc to instead create a separate GameMemory heap and use HeapAlloc.
The benefit would be a lot less code required to traverse the allocations for inclusions the dump, as it could be done with HeapWalk instead for only the GameMemory heap.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall. Just a bunch of small comments.

@slurmlord
Copy link
Author

Pushed a commit attempting to address most of the comments in the previous review:

  • Made TheMiniDumper a pointer, declared in MiniDumper.cpp
  • Moved extern TheMiniDumper to MiniDumper.h
  • Moved MiniDumper ctor body to MiniDumper.cpp
  • (Attempted to) find and correct all the stylistically misplaced "{"
  • Added enum for dump type
  • Fall back to DUMP_TYPE_FULL when DUMP_TYPE_GAMEMEMORY is requested and game memory implementation is turned off
  • Removed cmake feature dependency on RTS_GAMEMEMORY_ENABLE
  • Removed thread Id from dump file name
  • Replace hard-coded list of executable names to match with GetModuleFileNameW
  • (Attempted to) clean up log format
  • Added Enum values for dump thread exit codes
  • Removed MiniDumper::m_endRangeIter

In addition:

  • Added new static init/shutdown methods, using placement new on the process heap
  • Prefixed win32 api function calls with global namespace ::
  • Added asserts for dumping thread active state

Remaining:
Merge with #1066 when merged.

@xezon xezon added the Approved Pull Request was approved label Oct 21, 2025
@Skyaero42
Copy link

I think this should be enabled by default for now and included in our weekly pre-releases and legi.cc/patch distributions. The patch still crashes now and then without being able to find a cause (replays are send to us, but there is no crash to be found). For example, Legi (and only Legi) crashed yesterday on stream while being on the patch. No cause was found.

Players on the patch (with crashdump on) can send us the crashdumps and thus help finding where the issues/instabilities are.

@slurmlord
Copy link
Author

I think this should be enabled by default for now and included in our weekly pre-releases and legi.cc/patch distributions. The patch still crashes now and then without being able to find a cause (replays are send to us, but there is no crash to be found). For example, Legi (and only Legi) crashed yesterday on stream while being on the patch. No cause was found.

Players on the patch (with crashdump on) can send us the crashdumps and thus help finding where the issues/instabilities are.

Agreed, that's the main use case I was hoping this could address. Once this is merged, the remaining hurdle would be to get the weekly builds to enable the Vc6FullDebug cmake option.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the cleanup in MiniDumper class needs another look. Make it simple and robust.

xezon
xezon previously approved these changes Nov 19, 2025
Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks good to me. A few minor comments that could be looked into.

MiniDumpFilterWriteCombinedMemory = 0x01000000,
MiniDumpValidTypeFlags = 0x01ffffff,
MiniDumpNoIgnoreInaccessibleMemory = 0x02000000,
MiniDumpValidTypeFlagsEx = 0x03ffffff,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There does seem to be some discrepancy between the doc and the SDK headers. The SDK headers defines it explicitly as 0x03ffffff, you can see this in the original file at C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um\minidumpapiset.h
My money would be on the docs being wrong, and as we don't use this value directly it should be fine for our purpose. Does that seem reasonable?
And just out of curiosity, how did you notice this?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include\10.0.26100.0\

I only have two older versions and MiniDumpValidTypeFlags is the last value for both versions.

Does that seem reasonable?

Yeah, seems reasonable to me.

And just out of curiosity, how did you notice this?

I was wondering why we're defining all these structs / enums, so I happened to check a couple or maybe just this one to see where they were coming from.

@slurmlord
Copy link
Author

Rebased on main.

@slurmlord
Copy link
Author

Refactored a bit, introducing an enum for the memory range phases and modified the output variable of the callback routine so that our callback can always return TRUE.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me it looks like we can get rid of the concept of m_dumpObjectsSubState by caching the actual pointers to memory structures and keep iterating until they reach NULL and then go to the next until all lists reached the end.

Performance wise it is probably not the biggest of deals but it should make the logic cleaner.

@slurmlord
Copy link
Author

Got rid of the m_dumpObjectsSubState and cached the various objects that's being iterated. This also reduced the changes required in GameMemory. Looks better!

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks much cleaner indeed.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bunch more small comments.

m_currentAllocator = TheDynamicMemoryAllocator;
if (m_currentAllocator)
m_currentSingleBlock = m_currentAllocator->getFirstRawBlock();
MoveToNextAllocatorWithRawBlocks();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine, but I was just wondering if the TheDynamicMemoryAllocator can have an iterator like you added for TheMemoryPoolFactory?

Basically have a MemoryPoolIterator and DynamicMemoryIterator and then use them the same way. Or is there a specific reason why Memory Pool needed the iterator while Dynamic Memory did not?

#endif

#ifdef RTS_ENABLE_CRASHDUMP
class AllocationRangeIterator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps also put "MemoryPool" into its name.

Alternatively, you could also make this class a member of "MemoryPool" and then simply name it "Iterator".

It then would become MemoryPool::Iterator

Unless there is a reason for it to be standalone or it is too much work to touch.


AllocationRangeIterator();
AllocationRangeIterator(const MemoryPoolFactory* factory);
AllocationRangeIterator(MemoryPool& pool, MemoryPoolBlob& blob);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This constructor is redundant and unused. Can remove.


void updateRange();
void moveToNextBlob();
const MemoryPoolFactory* m_factory;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This member looks not necessary

m_currentPool = NULL;
m_currentBlobInPool = NULL;
m_factory = NULL;
m_range = MemoryPoolAllocatedRange();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means m_range is still uninitialized, because MemoryPoolAllocatedRange has no initializer.

Are the constructors missing a call to updateRange() down here? It looks like the very first iterator is malformed.

m_currentBlobInPool->fillAllocatedRange(m_range);
}

void AllocationRangeIterator::moveToNextBlob()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function looks over-complicated to the eye. Maybe. I asked Chat to simplify it and it claims that the following is logically identical

void AllocationRangeIterator::moveToNextBlob()
{
    if (m_currentBlobInPool)
        m_currentBlobInPool = m_currentBlobInPool->getNextInList();

    while (!m_currentBlobInPool && m_currentPool)
    {
        m_currentPool = m_currentPool->getNextPoolInList();
        if (m_currentPool)
            m_currentBlobInPool = m_currentPool->m_firstBlob;
    }
}

{
m_range.allocationAddr = nullptr;
m_range.allocationSize = 0;
return;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use else {} instead of return.

}
}

void MiniDumper::MoveToNextSingleBlock()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MoveToNextAllocatorWithRawBlocks and MoveToNextSingleBlock can likely be one function like how you did it in the Memory Pool Iterator.

@slurmlord
Copy link
Author

I'm considering to reduce the scope of this PR a bit to make it easier to review etc.
The proposal is to remove the GameMemory dump mode, and only have Small and Full in this PR. GameMemory can be added in a subsequent PR if we see that the Full dumps are required often and are too large.
The benefits of this reduction in scope are:

  • No changes required in GameMemory
  • Less code in MiniDumper (everything wrapped in #ifndef DISABLE_GAMEMEMORY)
  • Less complexity and chance for things to go wrong
  • Easier to review
  • Gives us insights in how often the crashdumps are actually useful and if it's worth investing in the GameMemory/extended type dumps before going to full CrashPad or similar solutions.

Does this seem like a reasonable path forward?

@xezon
Copy link

xezon commented Dec 6, 2025

You can do that if you like. I think it would be possible to finalize this change as is as well.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good.

void MiniDumper::CreateMiniDump(DumpType dumpType)
{
// Create a unique dump file name, using the path from m_dumpDir, in m_dumpFile
// Create a unique dump file name, using the path from m_dumpDir, in m_dumpFile
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing space :)

@xezon xezon added this to the Important features milestone Dec 7, 2025
@xezon xezon merged commit f4608df into TheSuperHackers:main Dec 7, 2025
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Approved Pull Request was approved Debug Is mostly debug functionality Gen Relates to Generals Major Severity: Minor < Major < Critical < Blocker System Is Systems related ZH Relates to Zero Hour

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants