diff --git a/.github/actions/build-simulator/action.yml b/.github/actions/build-simulator/action.yml index ac41dbf20..3ed5180b0 100644 --- a/.github/actions/build-simulator/action.yml +++ b/.github/actions/build-simulator/action.yml @@ -4,11 +4,8 @@ inputs: os_name: description: A descriptive name for the operating system (e.g. linux, windows) required: true - platform_name: - description: A descriptive name for the target platform (e.g. amd64, aarch64, etc.) - required: true - publish: - description: A boolean that enables publishing of artifacts + architecture: + description: A descriptive name for the target architecture (e.g. x86_64, aarch64, etc.) required: true runs: @@ -55,11 +52,10 @@ runs: run: cmake --build buildsim --target Tactility - name: 'Release' shell: bash - run: Buildscripts/release-simulator.sh buildsim release/Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }} + run: Buildscripts/release-simulator.sh buildsim release/Simulator-${{ inputs.os_name }}-${{ inputs.architecture }} - name: 'Upload Artifact' uses: actions/upload-artifact@v4 - if: ${{ inputs.publish == 'true' }} with: - name: Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }} - path: release/Simulator-${{ inputs.os_name }}-${{ inputs.platform_name }} + name: Simulator-${{ inputs.os_name }}-${{ inputs.architecture }} + path: release/Simulator-${{ inputs.os_name }}-${{ inputs.architecture }} retention-days: 30 diff --git a/.github/workflows/build-simulator.yml b/.github/workflows/build-simulator.yml index 6be91df20..1f0261abf 100644 --- a/.github/workflows/build-simulator.yml +++ b/.github/workflows/build-simulator.yml @@ -17,8 +17,7 @@ jobs: uses: ./.github/actions/build-simulator with: os_name: linux - platform_name: amd64 - publish: true + architecture: x86_64 macOS: runs-on: macos-latest steps: @@ -29,6 +28,4 @@ jobs: uses: ./.github/actions/build-simulator with: os_name: macos - platform_name: aarch64 - # macOS simulator currently fails due to main thread requirement for rendering - publish: false + architecture: aarch64 diff --git a/Devices/simulator/Source/Main.cpp b/Devices/simulator/Source/Main.cpp index 85e834eab..2b9d5dd16 100644 --- a/Devices/simulator/Source/Main.cpp +++ b/Devices/simulator/Source/Main.cpp @@ -10,13 +10,13 @@ constexpr auto* TAG = "FreeRTOS"; namespace simulator { -MainFunction mainFunction = nullptr; +static MainFunction mainFunction = nullptr; void setMain(MainFunction newMainFunction) { mainFunction = newMainFunction; } -static void freertosMainTask(void* parameter) { +static void freertosMainTask(void*) { LOG_I(TAG, "starting app_main()"); assert(simulator::mainFunction); mainFunction(); diff --git a/Devices/simulator/Source/Simulator.h b/Devices/simulator/Source/Simulator.h index 34da78694..d1e900c5f 100644 --- a/Devices/simulator/Source/Simulator.h +++ b/Devices/simulator/Source/Simulator.h @@ -1,6 +1,11 @@ #pragma once #include "Main.h" +#include "drivers/sdl_bridge.h" + +#include +#include +#include namespace simulator { /** Set the function pointer of the real app_main() */ @@ -14,8 +19,23 @@ void app_main(); // ESP-IDF's main function, implemented in the application } int main() { - // Actual main function that passes on app_main() (to be executed in a FreeRTOS task) and bootstraps FreeRTOS + // The FreeRTOS POSIX port arms a process-wide SIGALRM timer for its tick and expects every one + // of its task pthreads to have all signals but SIGINT blocked. + // (see prvSetupSignalsAndSchedulerPolicy() in FreeRTOS-Kernel's Posix port.c) + // A signal-generated SIGALRM can land on any thread in the process that doesn't block it. + // This thread stays a plain OS thread (running the SDL loop below, never a FreeRTOS task), + // so without this it's eligible to catch a tick SIGALRM and freeze inside the scheduler's handler. + // Block the same set here, before anything else, so it never can. + sigset_t all_signals_except_sigint; + sigfillset(&all_signals_except_sigint); + sigdelset(&all_signals_except_sigint, SIGINT); + pthread_sigmask(SIG_SETMASK, &all_signals_except_sigint, nullptr); + + // FreeRTOS and app_main() run on a separate thread: macOS requires SDL/Cocoa window creation, + // event pumping and rendering to happen on the real OS main thread, which sdl_bridge_run_main_loop() + // below takes over. freertosMain() never returns, so this thread is detached rather than joined. simulator::setMain(app_main); - simulator::freertosMain(); + std::thread(simulator::freertosMain).detach(); + sdl_bridge_run_main_loop(); return 0; } diff --git a/Devices/simulator/Source/drivers/sdl_bridge.cpp b/Devices/simulator/Source/drivers/sdl_bridge.cpp new file mode 100644 index 000000000..ce4a38ddd --- /dev/null +++ b/Devices/simulator/Source/drivers/sdl_bridge.cpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "sdl_bridge.h" +#include "sdl_input.h" + +#include + +#include +#include +#include + +namespace { + +struct PresentJob { + Device* device; + void* internal; + int32_t x_start; + int32_t y_start; + int32_t x_end; + int32_t y_end; + const void* color_data; + error_t result; +}; + +std::mutex job_mutex; +std::condition_variable job_ready_cv; +std::condition_variable job_done_cv; +bool job_pending = false; +bool job_done = false; +PresentJob pending_job; + +} + +error_t sdl_bridge_present(Device* device, void* internal, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + std::unique_lock lock(job_mutex); + + pending_job = { device, internal, x_start, y_start, x_end, y_end, color_data, ERROR_NONE }; + job_pending = true; + job_done = false; + job_ready_cv.notify_one(); + + job_done_cv.wait(lock, [] { return job_done; }); + return pending_job.result; +} + +void sdl_bridge_run_main_loop() { + while (true) { + sdl_input_pump(); + + std::unique_lock lock(job_mutex); + if (job_ready_cv.wait_for(lock, std::chrono::milliseconds(1), [] { return job_pending; })) { + PresentJob job = pending_job; + lock.unlock(); + + job.result = sdl_display_execute_draw_bitmap(job.device, job.internal, job.x_start, job.y_start, job.x_end, job.y_end, job.color_data); + + lock.lock(); + pending_job.result = job.result; + job_pending = false; + job_done = true; + lock.unlock(); + job_done_cv.notify_one(); + } + } +} diff --git a/Devices/simulator/Source/drivers/sdl_bridge.h b/Devices/simulator/Source/drivers/sdl_bridge.h new file mode 100644 index 000000000..14b65d8f8 --- /dev/null +++ b/Devices/simulator/Source/drivers/sdl_bridge.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include + +#include + +struct Device; + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Runs forever, pumping SDL input and executing display present jobs submitted via + * sdl_bridge_present(). Must be called exactly once, from the real OS main thread: macOS requires + * SDL/Cocoa window creation, event pumping and rendering to happen there, but FreeRTOS tasks + * (including the lvgl task that owns display flush and indev polling) run on separate pthreads + * spawned by the FreeRTOS POSIX port, not on that thread. + */ +void sdl_bridge_run_main_loop(void); + +/** + * @brief Hands a display flush off to the main thread and blocks until it has finished copying + * the pixel data out (see sdl_display_execute_draw_bitmap() in sdl_display.cpp). Called from the + * lvgl task. Must block: the caller's pixel buffer is single-buffered and gets reused as soon as + * this returns. + */ +error_t sdl_bridge_present(struct Device* device, void* internal, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data); + +/** + * @brief Implemented in sdl_display.cpp: the actual SDL work behind a display flush (lazy window + * init on first call, SDL_UpdateTexture, present). Only ever called from sdl_bridge_run_main_loop() + * on the main thread. + */ +error_t sdl_display_execute_draw_bitmap(struct Device* device, void* internal, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data); + +#ifdef __cplusplus +} +#endif diff --git a/Devices/simulator/Source/drivers/sdl_display.cpp b/Devices/simulator/Source/drivers/sdl_display.cpp index ebe4c5e17..5d9501ac2 100644 --- a/Devices/simulator/Source/drivers/sdl_display.cpp +++ b/Devices/simulator/Source/drivers/sdl_display.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include "sdl_display.h" +#include "sdl_bridge.h" #include #include @@ -141,8 +142,10 @@ static bool sdl_display_lazy_init(Device* device, SdlDisplayInternal* internal) return true; } -static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { - auto* internal = static_cast(device_get_driver_data(device)); +// Only ever called from sdl_bridge_run_main_loop() on the real main thread - required for +// SDL/Cocoa window creation and rendering on macOS. +error_t sdl_display_execute_draw_bitmap(Device* device, void* internal_ptr, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(internal_ptr); if (internal->init_failed) { return ERROR_RESOURCE; @@ -166,6 +169,16 @@ static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t return ERROR_NONE; } +static error_t sdl_display_draw_bitmap(Device* device, int32_t x_start, int32_t y_start, int32_t x_end, int32_t y_end, const void* color_data) { + auto* internal = static_cast(device_get_driver_data(device)); + + if (internal->init_failed) { + return ERROR_RESOURCE; + } + + return sdl_bridge_present(device, internal, x_start, y_start, x_end, y_end, color_data); +} + static enum DisplayColorFormat sdl_display_get_color_format(Device*) { return DISPLAY_COLOR_FORMAT_RGB565; } diff --git a/Devices/simulator/Source/drivers/sdl_input.cpp b/Devices/simulator/Source/drivers/sdl_input.cpp index 747f55444..0b056d70c 100644 --- a/Devices/simulator/Source/drivers/sdl_input.cpp +++ b/Devices/simulator/Source/drivers/sdl_input.cpp @@ -7,11 +7,16 @@ #include #include +#include namespace { constexpr size_t KEY_QUEUE_CAPACITY = 32; +// Written by sdl_input_pump() on the real main thread, read by sdl_input_get_pointer_state()/ +// sdl_input_pop_key()/sdl_input_has_queued_key() on the lvgl task. +std::mutex state_mutex; + SdlPointerState pointer_state = { 0, 0, false }; uint32_t key_queue[KEY_QUEUE_CAPACITY]; @@ -73,62 +78,78 @@ uint32_t keycode_to_key(SDL_Keycode sdl_key, bool shift) { } void sdl_input_pump() { - if (!text_input_started) { - SDL_StartTextInput(); - text_input_started = true; - } + // exit() must run with state_mutex unlocked: it never returns, so a lock_guard held across it + // would never release the mutex, hanging any other thread that later calls into this file's + // other functions (all of which lock state_mutex) while exit() tears the process down. + bool quit_requested = false; - SDL_Event event; - while (SDL_PollEvent(&event)) { - switch (event.type) { - case SDL_MOUSEMOTION: - set_pointer_position(event.motion.x, event.motion.y); - break; - case SDL_MOUSEBUTTONDOWN: - if (event.button.button == SDL_BUTTON_LEFT) { - // event.button.x/y can be stale immediately after a window resize (an - // SDL/X11 event-queue quirk - confirmed by comparing against a live - // SDL_GetWindowSize() at the same instant). SDL_GetMouseState() queries the - // OS for the current pointer position directly, sidestepping that entirely. - int live_x, live_y; - SDL_GetMouseState(&live_x, &live_y); - set_pointer_position(live_x, live_y); - pointer_state.pressed = true; - } - break; - case SDL_MOUSEBUTTONUP: - if (event.button.button == SDL_BUTTON_LEFT) { - pointer_state.pressed = false; - } - break; - case SDL_KEYDOWN: - push_key(keycode_to_key(event.key.keysym.sym, (event.key.keysym.mod & KMOD_SHIFT) != 0)); - break; - case SDL_TEXTINPUT: - // ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard. - push_key(static_cast(event.text.text[0])); - break; - case SDL_WINDOWEVENT: - // Resizing doesn't change what LVGL last rendered, only how large it should - // appear - re-present the existing frame at the new scale immediately, rather - // than leaving stale-looking content on screen until the next LVGL-driven flush. - if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { - sdl_display_present_now(); - } - break; - case SDL_QUIT: - exit(0); - default: - break; + { + std::lock_guard lock(state_mutex); + + if (!text_input_started) { + SDL_StartTextInput(); + text_input_started = true; } + + SDL_Event event; + while (SDL_PollEvent(&event)) { + switch (event.type) { + case SDL_MOUSEMOTION: + set_pointer_position(event.motion.x, event.motion.y); + break; + case SDL_MOUSEBUTTONDOWN: + if (event.button.button == SDL_BUTTON_LEFT) { + // event.button.x/y can be stale immediately after a window resize (an + // SDL/X11 event-queue quirk - confirmed by comparing against a live + // SDL_GetWindowSize() at the same instant). SDL_GetMouseState() queries the + // OS for the current pointer position directly, sidestepping that entirely. + int live_x, live_y; + SDL_GetMouseState(&live_x, &live_y); + set_pointer_position(live_x, live_y); + pointer_state.pressed = true; + } + break; + case SDL_MOUSEBUTTONUP: + if (event.button.button == SDL_BUTTON_LEFT) { + pointer_state.pressed = false; + } + break; + case SDL_KEYDOWN: + push_key(keycode_to_key(event.key.keysym.sym, (event.key.keysym.mod & KMOD_SHIFT) != 0)); + break; + case SDL_TEXTINPUT: + // ASCII only (first byte of event.text.text) - sufficient for a simulator keyboard. + push_key(static_cast(event.text.text[0])); + break; + case SDL_WINDOWEVENT: + // Resizing doesn't change what LVGL last rendered, only how large it should + // appear - re-present the existing frame at the new scale immediately, rather + // than leaving stale-looking content on screen until the next LVGL-driven flush. + if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + sdl_display_present_now(); + } + break; + case SDL_QUIT: + quit_requested = true; + break; + default: + break; + } + } + } + + if (quit_requested) { + exit(0); } } void sdl_input_get_pointer_state(SdlPointerState* out_state) { + std::lock_guard lock(state_mutex); *out_state = pointer_state; } bool sdl_input_pop_key(uint32_t* out_key) { + std::lock_guard lock(state_mutex); if (key_queue_count == 0) { return false; } @@ -139,5 +160,6 @@ bool sdl_input_pop_key(uint32_t* out_key) { } bool sdl_input_has_queued_key() { + std::lock_guard lock(state_mutex); return key_queue_count > 0; } diff --git a/Devices/simulator/Source/drivers/sdl_input.h b/Devices/simulator/Source/drivers/sdl_input.h index 8e7158efa..c3a1f0d51 100644 --- a/Devices/simulator/Source/drivers/sdl_input.h +++ b/Devices/simulator/Source/drivers/sdl_input.h @@ -19,9 +19,9 @@ struct SdlPointerState { /** * @brief Drains all pending SDL events exactly once, updating the pointer state and key queue - * below. Safe to call from both the sdl-pointer and sdl-keyboard drivers' polling functions: - * SDL_PollEvent() drains a single global queue, so whichever driver is polled first on a given - * LVGL indev tick pumps events for both. + * below. Must be called only from the real OS main thread (sdl_bridge_run_main_loop()): SDL + * requires event pumping to happen there on macOS. The getters below are safe to call from a + * different thread (the lvgl task, via sdl-pointer/sdl-keyboard's polling functions). */ void sdl_input_pump(void); diff --git a/Devices/simulator/Source/drivers/sdl_keyboard.cpp b/Devices/simulator/Source/drivers/sdl_keyboard.cpp index 09be781c0..82e1bbef3 100644 --- a/Devices/simulator/Source/drivers/sdl_keyboard.cpp +++ b/Devices/simulator/Source/drivers/sdl_keyboard.cpp @@ -16,8 +16,6 @@ static error_t stop(Device*) { return ERROR_NONE; } // region KeyboardApi static error_t sdl_keyboard_read_key(Device*, KeyboardKeyData* data) { - sdl_input_pump(); - uint32_t key = 0; if (sdl_input_pop_key(&key)) { data->key = key; diff --git a/Devices/simulator/Source/drivers/sdl_pointer.cpp b/Devices/simulator/Source/drivers/sdl_pointer.cpp index 33b48bc14..0ca628c00 100644 --- a/Devices/simulator/Source/drivers/sdl_pointer.cpp +++ b/Devices/simulator/Source/drivers/sdl_pointer.cpp @@ -16,7 +16,6 @@ static error_t stop(Device*) { return ERROR_NONE; } // region PointerApi static error_t sdl_pointer_read_data(Device*, TickType_t) { - sdl_input_pump(); return ERROR_NONE; }