-
Notifications
You must be signed in to change notification settings - Fork 21
macOS port: Fix SDL initialization, race conditions, and shutdown deadlock #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
- Enable compile_commands.json generation in CMakeLists.txt - Add CMAKE_EXPORT_COMPILE_COMMANDS to CMakePresets.json base preset - This enables C++ LSP (clangd) to work properly in IDEs
- Add InitSDLOnMainThread() method to initialize SDL on main thread - Call InitSDLOnMainThread() from main() before starting worker threads - Modify OpenGL::Start() to verify SDL is initialized on macOS - Fixes NSInternalInconsistencyException crash on macOS On macOS, SDL initialization (SDL_VideoInit and SDL_CreateWindow) must happen on the main thread due to AppKit requirements. Previously, these calls were happening in OpenGL::Start() which runs on a worker thread, causing a crash.
…thread On macOS, all SDL subsystem initialization must happen on the main thread due to AppKit requirements. Previously, SDL_Init(SDL_INIT_AUDIO) and SDL_Init(SDL_INIT_EVENTS) were being called from worker threads, causing trace traps. - Add InitSDLOnMainThread() methods to audio::sdl2::SDL2 and input::sdl2::SDL2 - Call these methods from main() before starting worker threads (similar to graphics) - Modify Start() methods to assert SDL is already initialized on macOS - Add debug instrumentation to track thread IDs and SDL initialization state
Remove #ifdef __APPLE__ guards from the dispatch queue system to make it available on all platforms. This provides consistent SDL operation routing through the main thread across Linux, Windows, and macOS. - Remove guards from MainThreadDispatch.h and .cpp - Always include MainThreadDispatch.cpp in CMakeLists.txt - Remove guards from Engine.cpp ProcessQueue() call - Remove guards from SDL2 event polling and clipboard operations - Remove guards from OpenGL window operations (SetFullscreen/SetWindowed) - Keep platform-specific pthread_main_np() checks conditional (macOS-only) The dispatch queue now routes all SDL operations through the main thread on all platforms, providing consistent behavior and easier maintenance.
- Request OpenGL 3.3 Core Profile context for GLSL 3.30 support on macOS - Update shaders to use texture() instead of deprecated texture2D() - Add VAO binding for Core Profile requirements (shader validation, texture ops) - Fix GL thread tracking: update MemoryWatcher GL thread ID when context switches - Add stack trace printing on crashes (backtrace) for better diagnostics - Ensure OpenGL context is current on worker threads before GL calls - Add GL_RGBA internal format support to MemoryWatcher - Improve shader compilation/linking error messages
- Add fallback path resolution in GSE::RunScript() to handle paths starting with ../ - When a path starts with ../, also try the same path without the ../ prefix - This allows both --datapath ../GLSMAC_data and --datapath GLSMAC_data to work - Use std::filesystem::weakly_canonical() to properly resolve .. components
- Remove the fallback code that tried paths without ../ prefix - Keep proper path normalization using std::filesystem::weakly_canonical() - Users should pass the correct datapath argument relative to current working directory
- Fix data race in common::Class::Log() by adding mutex to protect last_time global variable accessed by multiple threads - Fix OpenGL context race condition on macOS by releasing context from worker thread before dispatching swap to main thread, and using synchronous dispatch to ensure swap completes before continuing - Remove all debug logging code added during debugging session - Clean up unnecessary includes (<fstream>, <chrono>) added for debugging
- Add mutex to protect m_unit_updates unordered_map from concurrent access - QueueUnitUpdate() and PushUpdates() are called from different threads - Protect all access to m_unit_updates (find, insert, erase, empty, iteration, clear)
…ng for threads - MAIN thread waits for SDL_GL_SwapWindow() dispatched to main thread - Main thread was waiting for MAIN thread to stop without processing dispatch queue - Continue calling ProcessQueue() while waiting for threads to stop - This allows pending swap operations to complete and unblock MAIN thread
- Revert m_accumulations and m_accumulated_objects back to unordered_set - The mutex fixes added elsewhere likely resolved the underlying race conditions - Tree-based containers were a workaround; hash-based containers should work with proper synchronization
afwbkbc
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To sum up:
- do not fix what is not broken (even if AI thinks otherwise)
- keep only changes related to Apple (MainThreadDispatch can stay but I would make it a module and include in
t_maininEngine.cpp, instead of completely independent piece of code) - OpenGL is broken on Linux. When I removed those 3 lines, it works, however, moving map causes terrible framerates ( see example https://www.youtube.com/watch?v=-NLW8bq8-kg ), this was caused by changes in this MR. Zooming in/out also got small but noticeable (maybe 100ms) response lag, was near-instant before this MR.
Thanks.
| #define STRLEN_LITERAL(s) (sizeof(s) - 1) | ||
|
|
||
| // Alternate signal stack for crash handler (needed for SA_ONSTACK) | ||
| static char g_sigstack[SIGSTKSZ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
error: size of array 'g_sigstack' is not an integral constant-expression
20 | static char g_sigstack[SIGSTKSZ];
this is strange because SIGSTKSZ is a define with value 8192, but if I replace SIGSTKSZ with 8192 then it compiles. Using hardcoded 8192 is not right solution tho.
| #ifdef __APPLE__ | ||
| void SDL2::InitSDLOnMainThread() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason to NOT use initialization in main thread for all platforms?
I don't see any.
Maybe rename such methods to just Init() and remove #ifdef APPLE
| #ifdef __APPLE__ | ||
| // On macOS, SDL initialization must happen on the main thread due to AppKit requirements | ||
| // SDL should already be initialized via InitSDLOnMainThread() called from main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same - I'd make such behavior default on all platforms
|
|
||
| #ifdef DEBUG | ||
| static uint64_t last_time = 0; | ||
| static std::mutex last_time_mutex; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see a disadvantage of lowering log throughput. In case of heavy logging it make slow things down.
I myself never encountered any issue with logs without that mutex.
We can keep it for now but if logs get too slow I'll remove it.
| @@ -0,0 +1,54 @@ | |||
| #include "MainThreadDispatch.h" | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See t_main in Engine.cpp.
Also, this class belongs in util::, not common::
| // Normalize path to resolve .. components | ||
| std::string normalized_path = path; | ||
| if ( !util::FS::IsAbsolutePath( path, PATH_SEPARATOR ) ) { | ||
| // Convert to absolute path first, then normalize | ||
| normalized_path = util::FS::GetAbsolutePath( path, PATH_SEPARATOR ); | ||
| } | ||
| // Use std::filesystem to resolve .. components (weakly_canonical doesn't require path to exist) | ||
| try { | ||
| std::filesystem::path fs_path( normalized_path ); | ||
| normalized_path = std::filesystem::weakly_canonical( fs_path ).string(); | ||
| } | ||
| catch ( const std::exception& ) { | ||
| // If canonicalization fails, try lexically_normal as fallback | ||
| try { | ||
| std::filesystem::path fs_path( normalized_path ); | ||
| normalized_path = fs_path.lexically_normal().string(); | ||
| } | ||
| catch ( const std::exception& ) { | ||
| // If normalization fails, use original path | ||
| normalized_path = path; | ||
| } | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This definetely does not belong here.
There is already util::FS::NormalizePath(), why not use it? Update with apple-specific logic if needed.
| } | ||
| } | ||
|
|
||
| gse::Value* const Interpreter::EvaluateExpression( context::Context* ctx, ExecutionPointer& ep, const Expression* expression, bool* returnflag ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there actual problem that is being fixed here? Or it's just AI thinks 'there might be a bug'.
Scripting engine is thoroughly and carefully tested and any change must have a very good reason, especially if it involves memory operations.
And how is it even related to Apple?
| } | ||
| return result; | ||
| // SDL_GetClipboardText() dispatched to main thread for consistency | ||
| auto future = common::MainThreadDispatch::GetInstance()->Dispatch<std::string>([]() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If it's used often I'd make a macro like MAIN_THREAD_CALL( ... )
| graphics::opengl::OpenGL graphics( title, window_size.x, window_size.y, vsync, start_fullscreen ); | ||
| audio::sdl2::SDL2 audio; | ||
| input::sdl2::SDL2 input; | ||
| #ifdef __APPLE__ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe would make sense to use this for all platforms
| Log( "Starting task [" + ( *it )->GetName() + "]" ); | ||
| ( *it )->Start(); | ||
| try { | ||
| Log( "Starting task [" + ( *it )->GetName() + "]" ); | ||
| ( *it )->Start(); | ||
| } | ||
| catch ( const std::exception& e ) { | ||
| throw; | ||
| } | ||
| catch ( ... ) { | ||
| throw; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is this?
Summary
This PR ports GLSMAC to macOS, fixing threading issues, race conditions, and shutdown problems discovered during the port.
Key Changes
macOS-Specific Fixes
SDL Initialization on Main Thread (macOS requirement)
InitSDLOnMainThread()methods to graphics, audio, and input modulesMainThreadDispatchsystem to queue operations for the main threadOpenGL 3.3 Core Profile Support
texture2D()withtexture()GL_RGBAtoGL_RGBA8for Core Profile compatibilityOpenGL Context Thread Safety
SDL_GL_MakeCurrent()calls before OpenGL operationsRace Condition Fixes (Detected by ThreadSanitizer)
Logging Race Condition
common::Class::Log()wherelast_timeglobal variable was accessed without synchronizationlast_timeaccessUnitManager Race Condition
UnitManager::m_unit_updatesunordered_mapQueueUnitUpdate()andPushUpdates()called from different threadsm_unit_updatesOther Improvements
Improved Crash Diagnostics
Path Resolution
Build System
CMAKE_EXPORT_COMPILE_COMMANDSfor LSP supportossp-uuidlibrary configurationTesting