Refactor SDL/FreeRTOS implementation to support macOS simulator properly (#653)
- Run SDL from main() and place FreeRTOS in separate thread. This fixes macOS support. - Updated GitHub Actions to publish macOS simulator build for testing, updated amd64 to x86_64 for consistent naming.
This commit is contained in:
committed by
Adolfo Reyna
parent
89e8baf517
commit
72089f74f3
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "sdl_bridge.h"
|
||||
#include "sdl_input.h"
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
|
||||
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<std::mutex> 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<std::mutex> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#pragma once
|
||||
|
||||
#include <tactility/error.h>
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
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
|
||||
@@ -1,5 +1,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
#include "sdl_display.h"
|
||||
#include "sdl_bridge.h"
|
||||
|
||||
#include <tactility/device.h>
|
||||
#include <tactility/driver.h>
|
||||
@@ -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<SdlDisplayInternal*>(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<SdlDisplayInternal*>(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<SdlDisplayInternal*>(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;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,16 @@
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <mutex>
|
||||
|
||||
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 };
|
||||
|
||||
} // namespace
|
||||
@@ -26,6 +31,7 @@ uint32_t touch_override_until_tick = 0;
|
||||
#define SIM_TOUCH_HOLD_MS 1500
|
||||
|
||||
extern "C" void sdl_input_set_touch_override(int32_t x, int32_t y, bool pressed) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
touch_override.x = x;
|
||||
touch_override.y = y;
|
||||
touch_override.pressed = pressed;
|
||||
@@ -36,6 +42,7 @@ extern "C" void sdl_input_set_touch_override(int32_t x, int32_t y, bool pressed)
|
||||
}
|
||||
|
||||
extern "C" void sdl_input_clear_touch_override(void) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
touch_override_active = false;
|
||||
touch_override.pressed = false;
|
||||
}
|
||||
@@ -101,58 +108,73 @@ uint32_t keycode_to_key(SDL_Keycode sdl_key, bool shift) {
|
||||
} // namespace
|
||||
|
||||
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;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> 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<uint8_t>(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<uint8_t>(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;
|
||||
}
|
||||
if (quit_requested) {
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
void sdl_input_get_pointer_state(SdlPointerState* out_state) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (touch_override_active) {
|
||||
// Auto-release: viewer sends press only; LVGL needs press then release
|
||||
// to register a click. Hold long enough for several indev polls.
|
||||
@@ -168,6 +190,7 @@ void sdl_input_get_pointer_state(SdlPointerState* out_state) {
|
||||
}
|
||||
|
||||
bool sdl_input_pop_key(uint32_t* out_key) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
if (key_queue_count == 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -178,5 +201,6 @@ bool sdl_input_pop_key(uint32_t* out_key) {
|
||||
}
|
||||
|
||||
bool sdl_input_has_queued_key() {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
return key_queue_count > 0;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user