Rewrite Asynchronous Loader to Prevent Speeder Crash with Smart Pointers and Cancellation Tokens
Objective: Resolve the legacy 0xc0000005 "Speeder Crash" caused by race conditions during rapid asynchronous object loading and deletion.
The SWG client engine (2003) relies on a main thread to manage the game world and an AsynchronousLoader thread to fetch and construct heavy graphical assets (meshes, shaders, textures) in the background.
When a player moves rapidly across the terrain (e.g., riding a speeder), they trigger a massive queue of asynchronous load requests. Simultaneously, the main thread's AlterScheduler culls objects that fall out of range.
The Bug: The engine currently uses raw pointers (void* data) for its loader callbacks and direct memory deallocation (delete object;) in the AlterScheduler. If the AlterScheduler deletes an object while the background thread is still processing its graphical assets, the background thread eventually attempts to write the loaded assets to the now-freed memory address, resulting in a fatal Access Violation (0xc0000005).
- Raw Pointer Callbacks: The
AsynchronousLoader::add(Callback, void *data)method passes raw pointers. The loader has no way of knowing if thedatapointer is still valid when the callback fires. - Aggressive Deallocation:
AlterScheduler::deleteforcefully frees memory immediately. It does not check if the object has pending asynchronous jobs. - Non-Thread-Safe Cancellation: While
AsynchronousLoader::removeexists, it struggles to safely cancel a job that is already actively processing on the background thread.
To solve this, the object lifecycle management and the asynchronous loader pipeline must be modernized to guarantee thread safety. This involves three major phases:
The core of the fix requires abandoning aggressive raw delete calls in favor of a reference-counted lifetime model (similar to std::shared_ptr or the engine's existing intrusive Pointer<> system, if applicable).
- Loader Ownership: When the main thread requests an asset, it passes a strong reference (or smart pointer) to the
AsynchronousLoader. - Deferred Deletion:
AlterScheduler::deletemust be refactored. Instead of callingdelete object;, it will mark the object asisDead = trueand release its main-thread reference. - Safe Memory Free: The memory will only be deallocated once the
AsynchronousLoaderfinishes its job, realizes the object is dead, and releases the final reference.
To prevent the client from wasting CPU and I/O cycles building assets for an object that is already dead, we need a cancellation mechanism.
- Job Tickets:
AsynchronousLoader::addshould return aJobTicketorCancellationToken. - Pre-flight Checks: The background thread must check the
CancellationTokenimmediately before reading the disk, before processing the mesh, and before dispatching the callback. - AlterScheduler Hook: When
AlterSchedulerkills an object, it triggers the cancellation token. The background thread safely aborts the job.
The AsynchronousLoader::processCallbacks() function currently executes the raw void* data callbacks on the main thread once the background thread completes.
- Validation Wrapper: Wrap all callbacks in a functor that validates the smart pointer.
- If the object was marked
isDeadby theAlterSchedulerwhile the job was in flight, the callback immediately discards the loaded asset and gracefully exits instead of attempting to apply a shader to a nullified object.
This rewrite will touch foundational systems of the SWG client.
src/engine/shared/library/sharedFile/src/shared/AsynchronousLoader.h/cpp- Refactoring to support smart pointers/cancellation tokens instead of
void*.
- Refactoring to support smart pointers/cancellation tokens instead of
src/engine/shared/library/sharedObject/src/shared/object/AlterScheduler.cpp- Refactoring lines 1284-1294 to use deferred deletion/ref-counting instead of
delete object;.
- Refactoring lines 1284-1294 to use deferred deletion/ref-counting instead of
src/engine/client/library/clientObject/src/shared/object/ClientObject.cpp- Updating how graphical assets are bound to the object post-load.
- Memory Leaks (Reference Cycles): Implementing reference counting in a complex object hierarchy can lead to circular references (e.g., an object holds a reference to a child component, and the component holds a reference back).
weak_ptrconcepts must be strictly enforced. - Stuttering / CPU Overhead: Introducing atomic reference counting and thread-safe locks on the asynchronous queue may slightly increase CPU overhead, requiring profiling to ensure client frame rates aren't degraded.