Implement posix app loading (#643)

- Added POSIX desktop support for SDK builds, application packaging, and integration testing.
- Added POSIX filesystem partitions and improved application path handling.
- Added simulator support for loading and running applications dynamically.
- Improved simulator task stack handling and display startup reliability.
- Improved simulator display scaling, resizing, high-DPI support, and pointer accuracy.
- Fixed LVGL timers and input polling on POSIX. (fixes simulator with Linux on some Intel graphics platforms)
- Standardized data paths across platforms.
- Logging now works via separate task: this allows apps to write to log without it affecting their stdout (for apps that output text as relevant date for other apps, like the File Selection app)
- Fixes for app stdio
This commit is contained in:
Ken Van Hoeylandt
2026-08-31 22:09:04 +02:00
committed by GitHub
parent d3656bcd3d
commit 643cbc3806
66 changed files with 2139 additions and 336 deletions
+184
View File
@@ -0,0 +1,184 @@
// SPDX-License-Identifier: Apache-2.0
#include <tactility/log_queue.h>
#include <tactility/freertos/queue.h>
#include <tactility/freertos/task.h>
#include <tactility/memory.h>
#include <atomic>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#if defined(ESP_PLATFORM)
#include <esp_log.h>
#endif
namespace {
constexpr size_t LOG_QUEUE_DEPTH = 8;
constexpr size_t DRAIN_TASK_STACK_DEPTH = 4096 / sizeof(StackType_t);
struct LogQueueMessage {
uint16_t length;
char text[LOG_QUEUE_MESSAGE_MAX_LENGTH];
};
std::atomic<QueueHandle_t> g_queue { nullptr };
// The one and only place a real console write happens for log output - deliberately never
// through app_io_write()'s fd-table redirection (Modules/app-module/source/io.cpp). That
// redirection only ever triggers for a task app_task_main() (Modules/app-module/source/
// scheduler.cpp) set up as a running app instance; this drain task is never that, so
// app_scheduler_current_app_id() always reads 0 on it and app_io_write()'s lookup is skipped
// unconditionally - a plain write() already reaches the real syscall, on every platform, with no
// wrap-bypassing needed here.
void write_real(const char* data, size_t length) {
if (length == 0) {
return;
}
#if defined(ESP_PLATFORM)
constexpr int fd = 1; // ESP-IDF's default console fd
#else
constexpr int fd = 2; // matches log_generic()'s stderr choice
#endif
::write(fd, data, length);
}
void drain_task_main(void* context) {
// The queue handle is already valid by the time this task starts (it's created before the
// task is), but reading it back from g_queue here would race: g_queue is only published
// (log_queue_init()'s g_queue.store()) AFTER task creation returns, so this task can start
// running before that store happens. Take it directly as the task's own argument instead.
auto queue = static_cast<QueueHandle_t>(context);
LogQueueMessage message;
while (true) {
if (xQueueReceive(queue, &message, portMAX_DELAY) == pdTRUE) {
write_real(message.text, message.length);
}
}
}
#if defined(ESP_PLATFORM)
// ESP-IDF's own log macros pre-format the entire line (color, level letter, timestamp, tag,
// message, color reset, newline - see esp_log_write()/LOG_FORMAT() in esp_log.h) before handing
// it to the installed vprintf_like_t as fmt+args, so this hook only needs one vsnprintf - no
// prefix building of its own (unlike log_generic() in log.cpp, which builds its own prefix).
// Replaces (does not chain through) the previously-installed vprintf: the whole point is
// removing the direct-to-stdout path, not adding a second consumer of it.
int log_queue_vprintf_hook(const char* format, va_list args) {
char buffer[LOG_QUEUE_MESSAGE_MAX_LENGTH];
int written = vsnprintf(buffer, sizeof(buffer), format, args);
if (written > 0) {
size_t length = static_cast<size_t>(written) < sizeof(buffer) ? static_cast<size_t>(written) : sizeof(buffer) - 1;
log_queue_write(buffer, length);
}
return written;
}
#endif
} // namespace
extern "C" {
void log_queue_init(void) {
if (g_queue.load(std::memory_order_relaxed) != nullptr) {
return; // already initialized
}
#if defined(ESP_PLATFORM)
// Message storage: prefer PSRAM/external memory, fall back to internal automatically if
// unavailable (MemoryPolicy's documented semantics) - this is the bulk of the queue's memory
// (24 * 256 bytes), so it's the part worth offloading to PSRAM when there is any.
MemoryPolicy storage_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
auto* queue_storage = static_cast<uint8_t*>(memory_alloc_with_policy(LOG_QUEUE_DEPTH * sizeof(LogQueueMessage), &storage_policy));
if (queue_storage == nullptr) {
return;
}
// The queue's own control block and the drain task's TCB are both small, fixed-size kernel
// objects (not the bulk data) - keep them in internal RAM like scheduler.cpp's
// APP_TASK_TCB_POLICY already does for app instance tasks.
MemoryPolicy internal_policy = { .required = MEMORY_CAPABILITY_INTERNAL, .desired = 0, .alignment = 0 };
auto* queue_struct = static_cast<StaticQueue_t*>(memory_alloc_with_policy(sizeof(StaticQueue_t), &internal_policy));
if (queue_struct == nullptr) {
memory_free(queue_storage);
return;
}
QueueHandle_t queue = xQueueCreateStatic(LOG_QUEUE_DEPTH, sizeof(LogQueueMessage), queue_storage, queue_struct);
if (queue == nullptr) {
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
// Stack: same PSRAM-preferred/internal-fallback policy as the message storage above.
MemoryPolicy stack_policy = { .required = 0, .desired = MEMORY_CAPABILITY_EXTERNAL, .alignment = 0 };
auto* stack_buffer = static_cast<StackType_t*>(memory_alloc_with_policy(DRAIN_TASK_STACK_DEPTH * sizeof(StackType_t), &stack_policy));
if (stack_buffer == nullptr) {
vQueueDelete(queue);
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
auto* task_tcb = static_cast<StaticTask_t*>(memory_alloc_with_policy(sizeof(StaticTask_t), &internal_policy));
if (task_tcb == nullptr) {
memory_free(stack_buffer);
vQueueDelete(queue);
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
TaskHandle_t task_handle = xTaskCreateStatic(drain_task_main, "log_drain", DRAIN_TASK_STACK_DEPTH, queue, tskIDLE_PRIORITY + 1, stack_buffer, task_tcb);
if (task_handle == nullptr) {
memory_free(task_tcb);
memory_free(stack_buffer);
vQueueDelete(queue);
memory_free(queue_struct);
memory_free(queue_storage);
return;
}
#else
QueueHandle_t queue = xQueueCreate(LOG_QUEUE_DEPTH, sizeof(LogQueueMessage));
if (queue == nullptr) {
return;
}
TaskHandle_t task_handle = nullptr;
if (xTaskCreate(drain_task_main, "log_drain", DRAIN_TASK_STACK_DEPTH, queue, tskIDLE_PRIORITY + 1, &task_handle) != pdPASS) {
vQueueDelete(queue);
return;
}
#endif
g_queue.store(queue, std::memory_order_release); // published last
#if defined(ESP_PLATFORM)
esp_log_set_vprintf(log_queue_vprintf_hook);
#endif
}
void log_queue_write(const char* data, size_t length) {
if (data == nullptr || length == 0) {
return;
}
QueueHandle_t queue = g_queue.load(std::memory_order_acquire);
if (queue == nullptr) {
write_real(data, length); // pre-init fallback
return;
}
LogQueueMessage message;
size_t copy_length = length < sizeof(message.text) ? length : sizeof(message.text) - 1;
memcpy(message.text, data, copy_length);
message.length = static_cast<uint16_t>(copy_length);
xQueueSend(queue, &message, portMAX_DELAY);
}
} // extern "C"