# MacOS SDL3 Fix to es40-emu (https://github.com/ES40-emu) - Code Changes Summary This document summarizes the actual code changes made to fix SDL3 issues on macOS. As reported here https://github.com/ES40-Emu/es40/issues/97 ## Problem On macOS, SDL3 requires video operations (SDL_Init, SDL_CreateWindow, SDL_PollEvent) to run on the main thread. The ES40 emulator runs the system in a background thread, causing crashes and a black window. ## Solution Overview - Initialize SDL on main thread before spawning worker threads - Run system in background thread while keeping main thread for SDL event loop - Use shared framebuffer for thread-safe rendering (S3 thread writes, main thread renders) - Mirror OCP console output to VGA for visibility (SRM uses OCP on macOS instead of VGA) --- ## Core Changes ### 1. **New Files Created** #### `src/gui/sdl_main_thread.cpp` - Implements main thread SDL event loop and rendering - Functions: - `sdl_init_on_main_thread_if_needed()` - Initialize SDL on main thread - `sdl_create_window_on_main_thread_if_needed()` - Create window on main thread - `sdl_aware_run()` - Run system in background thread, keep main thread for SDL #### `src/gui/sdl_main_thread.h` - Header declarations for main thread functions ### 2. **Modified Files** #### `CMakeLists.txt` Added new source file: ```cmake list(APPEND ES40_SOURCES src/gui/sdl.cpp + src/gui/sdl_main_thread.cpp # NEW src/gui/gui.cpp ) ``` #### `src/AlphaSim.cpp` Initialize SDL on main thread before spawning workers: ```cpp #if defined(HAVE_SDL) // On macOS, SDL_Init and SDL_CreateWindow must be on main thread sdl_init_on_main_thread_if_needed(c); sdl_create_window_on_main_thread_if_needed(); #endif // ... later ... #if defined(HAVE_SDL) sdl_aware_run(theSystem); // Runs system in background, SDL on main #else theSystem->Run(); #endif ``` #### `src/gui/sdl.cpp` Major changes for thread-safe rendering: 1. **Global SDL variables** (for cross-file access): ```cpp -static SDL_Window* sdl_window = NULL; -static SDL_Renderer* sdl_renderer = NULL; -static SDL_Texture* sdl_texture = NULL; +SDL_Window* sdl_window = NULL; // Not static +SDL_Renderer* sdl_renderer = NULL; +SDL_Texture* sdl_texture = NULL; ``` 2. **Shared framebuffer** (macOS only): ```cpp #ifdef __APPLE__ u32* shared_framebuffer = NULL; unsigned shared_fb_width = 0; unsigned shared_fb_height = 0; std::atomic shared_fb_dirty{false}; #endif ``` 3. **specific_init()** - Check if SDL already initialized: ```cpp if (!SDL_WasInit(SDL_INIT_VIDEO)) { if (!SDL_Init(SDL_INIT_VIDEO)) { FAILURE_1(SDL, "Unable to initialize SDL3 video subsystem: %s", SDL_GetError()); } } ``` 4. **text_update()** - Implement VGA text mode rendering: ```cpp void bx_sdl_gui_c::text_update(u8* old_text, u8* new_text, ...) { // Render 80x25 text mode using 8x16 font // Each character: 2 bytes (character + attribute) // Render to 640x400 framebuffer // Call graphics_frame_update() for final rendering } ``` 5. **graphics_frame_update()** - Use shared buffer on macOS: ```cpp #ifdef __APPLE__ // S3 thread: Copy to shared buffer memcpy(shared_framebuffer, pixels, width * height * sizeof(u32)); shared_fb_dirty.store(true, std::memory_order_release); #else // Other platforms: render directly SDL_UpdateTexture(sdl_texture, NULL, pixels, width * sizeof(u32)); SDL_RenderPresent(sdl_renderer); #endif ``` 6. **handle_events()** - No-op on macOS (main thread handles events): ```cpp #ifdef __APPLE__ // Events handled by main thread, not S3 thread return; #else // Original event handling code #endif ``` 7. **dimension_update()** - Defer texture operations on macOS: ```cpp #ifdef __APPLE__ // Just update dimensions, main thread will recreate texture res_x = x; res_y = y; return; #endif ``` #### `src/S3Trio64.cpp` 1. **Video mode tracking** (debug logging): ```cpp static uint8_t last_reported_mode = 0xFF; if (cur_mode != last_reported_mode) { printf("%%S3-I-MODE: Video mode changed to %s\n", mode_name); } ``` 2. **Exception handling** in S3 thread: ```cpp catch (std::exception& e) { printf("Standard exception in S3 thread: %s.\n", e.what()); } catch (...) { printf("Unknown exception in S3 thread (possibly signal/crash).\n"); } ``` 3. **VGA initialization** - Initialize text mode early: ```cpp // Enable VGA subsystem m_vga_subsys_enable = true; // Initialize CRTC for 80x25 text mode vga.crtc.horz_disp_end = 79; vga.crtc.vert_disp_end = 399; vga.crtc.offset = 40; // **CRITICAL** 40 words = 80 bytes per row // Load VGA fonts into VRAM manually for (int ch = 0; ch < 256; ch++) { // Copy font from ROM to VRAM plane 2 at 0x20000 } // Initialize VGA palette for (int i = 0; i < 16; i++) { vga.dac.color[i*3+0] = vga_palette[i][0] >> 2; // ... set RGB values } ``` 4. **Force text mode rendering** when text data detected: ```cpp // Check for text data in VRAM int non_zero_count = 0; for (int i = 0; i < 160; i++) { if (vga.memory[i] != 0) non_zero_count++; } if (non_zero_count > 10) { force_text_mode = true; } ``` #### `src/VGA.cpp` 1. **Text mode row offset fix**: ```cpp // Fix for macOS OCP mirroring: if offset is 0, use default int row_offset = offset() >> 1; if (row_offset == 0) { row_offset = 80; // Standard text mode } ``` 2. **Force text mode for SCREEN_OFF with data**: ```cpp if (cur_mode == SCREEN_OFF) { // Check if text buffer has data if (has_text_data) { vga_vh_text(bitmap, cliprect); // Force text rendering return 0; } } ``` 3. **Helper methods for OCP->VGA mirroring**: ```cpp void CVGA::write_text_char(unsigned row, unsigned col, char ch, uint8_t attr); void CVGA::init_default_font(); void CVGA::init_default_palette(); void CVGA::scroll_text_up(); ``` 4. **Global helper functions**: ```cpp void vga_start_new_message(); // Advance to new line void vga_write_console_char(char ch); // Write char to VGA buffer ``` #### `src/VGA.h` Added public method declarations: ```cpp void write_text_char(unsigned row, unsigned col, char ch, uint8_t attr); void init_default_font(); void init_default_palette(); void scroll_text_up(); ``` #### `src/DPR.cpp` OCP-to-VGA mirroring for macOS console visibility: ```cpp #ifdef __APPLE__ extern void vga_write_console_char(char ch); extern void vga_start_new_message(); // Signal start of new OCP message vga_start_new_message(); // Write each character from OCP buffer to VGA for (int i = 0; i < 16 && buf[i] != 0; i++) { if (buf[i] != '[' && buf[i] != ']') { vga_write_console_char(buf[i]); } } #endif ``` #### `src/Serial.cpp` Global serial port array for keyboard input forwarding: ```cpp CSerial* srl[2] = {nullptr, nullptr}; CSerial::CSerial(...) { // Register in global array if (number < 2) { srl[number] = this; } } ``` #### `src/es40.cfg` Updated VGA BIOS ROM path: ``` pci0.2 = s3 { - rom = "rom\VGABIOSFILE.bin"; + rom = "rom/86c764x1.bin"; } ``` --- ## Key Technical Details ### Thread Architecture - **Main thread**: SDL initialization, window creation, event handling, rendering - **S3 worker thread**: VGA emulation, pixel generation, writes to shared framebuffer - **Synchronization**: Atomic flag (`shared_fb_dirty`) signals when rendering needed ### Rendering Pipeline (macOS) 1. S3 thread generates pixels -> writes to `shared_framebuffer` 2. Sets `shared_fb_dirty = true` 3. Main thread detects dirty flag -> renders from `shared_framebuffer` 4. Main thread calls `SDL_UpdateTexture()` + `SDL_RenderPresent()` ### VGA Text Mode - 80 columns x 25 rows - 2 bytes per character (character byte + attribute byte) - Rendered using 8x16 font to 640x400 framebuffer - Standard 16-color VGA palette ### OCP-to-VGA Mirroring (macOS only) - SRM firmware uses OCP console on macOS (not VGA) - DPR.cpp intercepts OCP writes - Mirrors characters to VGA text buffer - Makes console visible in SDL window --- ## Build Changes Added `src/gui/sdl_main_thread.cpp` to `CMakeLists.txt` ## Platform-Specific Behavior - **macOS (`#ifdef __APPLE__`)**: Shared framebuffer, main thread rendering, OCP mirroring - **Other platforms**: Direct rendering from S3 thread (original behavior) --- ## Testing - Boot SRM firmware - Console output should be visible in SDL window - Text mode should render correctly (80x25) - No crashes from SDL thread violations