Skip to content

Conversation

@jhurliman
Copy link

@jhurliman jhurliman commented Dec 28, 2025

Screenshot 2025-12-27 at 6 07 02 PM

Summary

This PR ports GLSMAC to macOS, fixing threading issues, race conditions, and shutdown problems discovered during the port.

Key Changes

macOS-Specific Fixes

  1. SDL Initialization on Main Thread (macOS requirement)

    • SDL video, audio, and event subsystems must be initialized on the main thread due to AppKit requirements
    • Added InitSDLOnMainThread() methods to graphics, audio, and input modules
    • Created cross-platform MainThreadDispatch system to queue operations for the main thread
  2. OpenGL 3.3 Core Profile Support

    • Requested OpenGL 3.3 Core Profile for GLSL 3.30 compatibility
    • Fixed shader code: replaced deprecated texture2D() with texture()
    • Added VAO (Vertex Array Object) binding as required by Core Profile
    • Changed texture format from GL_RGBA to GL_RGBA8 for Core Profile compatibility
  3. OpenGL Context Thread Safety

    • OpenGL contexts are thread-local on macOS
    • Added SDL_GL_MakeCurrent() calls before OpenGL operations
    • Fixed race condition where main thread could make context current while worker thread was using it
    • Release context from worker thread before dispatching swap to main thread

Race Condition Fixes (Detected by ThreadSanitizer)

  1. Logging Race Condition

    • Fixed data race in common::Class::Log() where last_time global variable was accessed without synchronization
    • Added mutex to protect last_time access
  2. UnitManager Race Condition

    • Fixed data race in UnitManager::m_unit_updates unordered_map
    • QueueUnitUpdate() and PushUpdates() called from different threads
    • Added mutex to protect all access to m_unit_updates

Other Improvements

  1. Improved Crash Diagnostics

    • Enhanced crash handler with async-signal-safe stack trace printing
    • Added alternate signal stack for more reliable crash handling
    • Improved error messages and diagnostics
  2. Path Resolution

    • Fixed script path resolution for relative datapath arguments
  3. Build System

    • Added CMAKE_EXPORT_COMPILE_COMMANDS for LSP support
    • Fixed vendored ossp-uuid library configuration

Testing

  • Tested on macOS with ThreadSanitizer enabled
  • All race conditions detected by TSAN in basic startup have been fixed
  • Application runs and a new game can be started
  • Game functionality verified with GoG SMAC installation

- 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
afwbkbc previously approved these changes Dec 30, 2025
@afwbkbc afwbkbc dismissed their stale review December 30, 2025 14:59

misclick

Copy link
Owner

@afwbkbc afwbkbc left a comment

Choose a reason for hiding this comment

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

To sum up:

  1. do not fix what is not broken (even if AI thinks otherwise)
  2. keep only changes related to Apple (MainThreadDispatch can stay but I would make it a module and include in t_main in Engine.cpp, instead of completely independent piece of code)
  3. 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];
Copy link
Owner

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.

Comment on lines +30 to +31
#ifdef __APPLE__
void SDL2::InitSDLOnMainThread() {
Copy link
Owner

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

Comment on lines +52 to +54
#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()
Copy link
Owner

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;
Copy link
Owner

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"
Copy link
Owner

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::

Comment on lines +177 to +199
// 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;
}
}

Copy link
Owner

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 ) {
Copy link
Owner

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>([]() {
Copy link
Owner

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__
Copy link
Owner

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

Comment on lines -22 to +31
Log( "Starting task [" + ( *it )->GetName() + "]" );
( *it )->Start();
try {
Log( "Starting task [" + ( *it )->GetName() + "]" );
( *it )->Start();
}
catch ( const std::exception& e ) {
throw;
}
catch ( ... ) {
throw;
}
Copy link
Owner

Choose a reason for hiding this comment

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

What is this?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants