Includes making the display driver use a worker task for flushes, so that our double buffering actually does something useful /facepalmcustom
parent
b320a6a863
commit
5ac4d3949c
@ -1,91 +0,0 @@ |
|||||||
#include "db_task.hpp" |
|
||||||
|
|
||||||
#include <functional> |
|
||||||
|
|
||||||
#include "esp_heap_caps.h" |
|
||||||
#include "freertos/FreeRTOS.h" |
|
||||||
#include "freertos/portmacro.h" |
|
||||||
#include "freertos/projdefs.h" |
|
||||||
#include "freertos/queue.h" |
|
||||||
#include "freertos/task.h" |
|
||||||
|
|
||||||
namespace database { |
|
||||||
|
|
||||||
static const std::size_t kDbStackSize = 256 * 1024; |
|
||||||
static StaticTask_t sDbStaticTask; |
|
||||||
static StackType_t* sDbStack = nullptr; |
|
||||||
|
|
||||||
static std::atomic<bool> sTaskRunning(false); |
|
||||||
static QueueHandle_t sWorkQueue; |
|
||||||
|
|
||||||
struct WorkItem { |
|
||||||
std::function<void(void)>* fn; |
|
||||||
bool quit; |
|
||||||
}; |
|
||||||
|
|
||||||
auto SendToDbTask(std::function<void(void)> fn) -> void { |
|
||||||
WorkItem item{ |
|
||||||
.fn = new std::function<void(void)>(fn), |
|
||||||
.quit = false, |
|
||||||
}; |
|
||||||
xQueueSend(sWorkQueue, &item, portMAX_DELAY); |
|
||||||
} |
|
||||||
|
|
||||||
template <> |
|
||||||
auto RunOnDbTask(std::function<void(void)> fn) -> std::future<void> { |
|
||||||
std::shared_ptr<std::promise<void>> promise = |
|
||||||
std::make_shared<std::promise<void>>(); |
|
||||||
SendToDbTask([=]() { |
|
||||||
std::invoke(fn); |
|
||||||
promise->set_value(); |
|
||||||
}); |
|
||||||
return promise->get_future(); |
|
||||||
} |
|
||||||
|
|
||||||
void DatabaseTaskMain(void* args) { |
|
||||||
while (true) { |
|
||||||
WorkItem item; |
|
||||||
if (xQueueReceive(sWorkQueue, &item, portMAX_DELAY)) { |
|
||||||
if (item.fn != nullptr) { |
|
||||||
std::invoke(*item.fn); |
|
||||||
delete item.fn; |
|
||||||
} |
|
||||||
if (item.quit) { |
|
||||||
break; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
vQueueDelete(sWorkQueue); |
|
||||||
sTaskRunning.store(false); |
|
||||||
vTaskDelete(NULL); |
|
||||||
} |
|
||||||
|
|
||||||
auto StartDbTask() -> bool { |
|
||||||
if (sTaskRunning.exchange(true)) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
if (sDbStack == nullptr) { |
|
||||||
sDbStack = reinterpret_cast<StackType_t*>( |
|
||||||
heap_caps_malloc(kDbStackSize, MALLOC_CAP_SPIRAM)); |
|
||||||
} |
|
||||||
sWorkQueue = xQueueCreate(8, sizeof(WorkItem)); |
|
||||||
xTaskCreateStatic(&DatabaseTaskMain, "DB", kDbStackSize, NULL, 1, sDbStack, |
|
||||||
&sDbStaticTask); |
|
||||||
return true; |
|
||||||
} |
|
||||||
|
|
||||||
auto QuitDbTask() -> void { |
|
||||||
if (!sTaskRunning.load()) { |
|
||||||
return; |
|
||||||
} |
|
||||||
WorkItem item{ |
|
||||||
.fn = nullptr, |
|
||||||
.quit = true, |
|
||||||
}; |
|
||||||
xQueueSend(sWorkQueue, &item, portMAX_DELAY); |
|
||||||
while (sTaskRunning.load()) { |
|
||||||
vTaskDelay(pdMS_TO_TICKS(1)); |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} // namespace database
|
|
@ -1,25 +0,0 @@ |
|||||||
#pragma once |
|
||||||
|
|
||||||
#include <functional> |
|
||||||
#include <future> |
|
||||||
#include <memory> |
|
||||||
|
|
||||||
namespace database { |
|
||||||
|
|
||||||
auto StartDbTask() -> bool; |
|
||||||
auto QuitDbTask() -> void; |
|
||||||
|
|
||||||
auto SendToDbTask(std::function<void(void)> fn) -> void; |
|
||||||
|
|
||||||
template <typename T> |
|
||||||
auto RunOnDbTask(std::function<T(void)> fn) -> std::future<T> { |
|
||||||
std::shared_ptr<std::promise<T>> promise = |
|
||||||
std::make_shared<std::promise<T>>(); |
|
||||||
SendToDbTask([=]() { promise->set_value(std::invoke(fn)); }); |
|
||||||
return promise->get_future(); |
|
||||||
} |
|
||||||
|
|
||||||
template <> |
|
||||||
auto RunOnDbTask(std::function<void(void)> fn) -> std::future<void>; |
|
||||||
|
|
||||||
} // namespace database
|
|
@ -1,2 +1,2 @@ |
|||||||
idf_component_register(SRCS "tasks.cpp" INCLUDE_DIRS ".") |
idf_component_register(SRCS "tasks.cpp" INCLUDE_DIRS "." REQUIRES "span") |
||||||
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) |
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) |
||||||
|
@ -1,5 +1,204 @@ |
|||||||
#include "tasks.hpp" |
#include "tasks.hpp" |
||||||
|
#include <functional> |
||||||
|
#include "esp_heap_caps.h" |
||||||
|
#include "freertos/FreeRTOS.h" |
||||||
|
#include "freertos/portmacro.h" |
||||||
|
|
||||||
const UBaseType_t kTaskPriorityLvgl = 4; |
namespace tasks { |
||||||
const UBaseType_t kTaskPriorityAudioPipeline = 5; |
|
||||||
const UBaseType_t kTaskPriorityAudioDrain = 6; |
template <Type t> |
||||||
|
auto Name() -> std::string; |
||||||
|
|
||||||
|
template <> |
||||||
|
auto Name<Type::kUi>() -> std::string { |
||||||
|
return "LVGL"; |
||||||
|
} |
||||||
|
template <> |
||||||
|
auto Name<Type::kUiFlush>() -> std::string { |
||||||
|
return "DISPLAY"; |
||||||
|
} |
||||||
|
template <> |
||||||
|
auto Name<Type::kAudio>() -> std::string { |
||||||
|
return "AUDIO"; |
||||||
|
} |
||||||
|
template <> |
||||||
|
auto Name<Type::kAudioDrain>() -> std::string { |
||||||
|
return "DRAIN"; |
||||||
|
} |
||||||
|
template <> |
||||||
|
auto Name<Type::kDatabase>() -> std::string { |
||||||
|
return "DB"; |
||||||
|
} |
||||||
|
|
||||||
|
template <Type t> |
||||||
|
auto AllocateStack() -> cpp::span<StackType_t>; |
||||||
|
|
||||||
|
// Decoders run on the audio task, and these sometimes require a fairly large
|
||||||
|
// amount of stack space.
|
||||||
|
template <> |
||||||
|
auto AllocateStack<Type::kAudio>() -> cpp::span<StackType_t> { |
||||||
|
std::size_t size = 32 * 1024; |
||||||
|
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)), |
||||||
|
size}; |
||||||
|
} |
||||||
|
template <> |
||||||
|
auto AllocateStack<Type::kAudioDrain>() -> cpp::span<StackType_t> { |
||||||
|
std::size_t size = 1024; |
||||||
|
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)), |
||||||
|
size}; |
||||||
|
} |
||||||
|
// LVGL requires only a relatively small stack. However, it can be allocated in
|
||||||
|
// PSRAM so we give it a bit of headroom for safety.
|
||||||
|
template <> |
||||||
|
auto AllocateStack<Type::kUi>() -> cpp::span<StackType_t> { |
||||||
|
std::size_t size = 16 * 1024; |
||||||
|
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)), |
||||||
|
size}; |
||||||
|
} |
||||||
|
// UI flushes *must* be done from internal RAM. Thankfully, there is very little
|
||||||
|
// stack required to perform them, and the amount of stack needed is fixed.
|
||||||
|
template <> |
||||||
|
auto AllocateStack<Type::kUiFlush>() -> cpp::span<StackType_t> { |
||||||
|
std::size_t size = 1024; |
||||||
|
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)), |
||||||
|
size}; |
||||||
|
} |
||||||
|
// Leveldb is designed for non-embedded use cases, where stack space isn't so
|
||||||
|
// much of a concern. It therefore uses an eye-wateringly large amount of stack.
|
||||||
|
template <> |
||||||
|
auto AllocateStack<Type::kDatabase>() -> cpp::span<StackType_t> { |
||||||
|
std::size_t size = 256 * 1024; |
||||||
|
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)), |
||||||
|
size}; |
||||||
|
} |
||||||
|
|
||||||
|
// 2048 bytes in internal ram
|
||||||
|
// 302 KiB in external ram.
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Please keep the priorities below in descending order for better readability. |
||||||
|
*/ |
||||||
|
|
||||||
|
template <Type t> |
||||||
|
auto Priority() -> UBaseType_t; |
||||||
|
|
||||||
|
// Realtime audio is the entire point of this device, so give this task the
|
||||||
|
// highest priority.
|
||||||
|
template <> |
||||||
|
auto Priority<Type::kAudio>() -> UBaseType_t { |
||||||
|
return 10; |
||||||
|
} |
||||||
|
template <> |
||||||
|
auto Priority<Type::kAudioDrain>() -> UBaseType_t { |
||||||
|
return 10; |
||||||
|
} |
||||||
|
// After audio issues, UI jank is the most noticeable kind of scheduling-induced
|
||||||
|
// slowness that the user is likely to notice or care about. Therefore we place
|
||||||
|
// this task directly below audio in terms of priority.
|
||||||
|
template <> |
||||||
|
auto Priority<Type::kUi>() -> UBaseType_t { |
||||||
|
return 9; |
||||||
|
} |
||||||
|
// UI flushing should use the same priority as the UI task, so as to maximise
|
||||||
|
// the chance of the happy case: one of our cores is writing to the screen,
|
||||||
|
// whilst the other is simultaneously preparing the next buffer to be flushed.
|
||||||
|
template <> |
||||||
|
auto Priority<Type::kUiFlush>() -> UBaseType_t { |
||||||
|
return 9; |
||||||
|
} |
||||||
|
// Database interactions are all inherently async already, due to their
|
||||||
|
// potential for disk access. The user likely won't notice or care about a
|
||||||
|
// couple of ms extra delay due to scheduling, so give this task the lowest
|
||||||
|
// priority.
|
||||||
|
template <> |
||||||
|
auto Priority<Type::kDatabase>() -> UBaseType_t { |
||||||
|
return 8; |
||||||
|
} |
||||||
|
|
||||||
|
template <Type t> |
||||||
|
auto WorkerQueueSize() -> std::size_t; |
||||||
|
|
||||||
|
template <> |
||||||
|
auto WorkerQueueSize<Type::kDatabase>() -> std::size_t { |
||||||
|
return 8; |
||||||
|
} |
||||||
|
|
||||||
|
template <> |
||||||
|
auto WorkerQueueSize<Type::kUiFlush>() -> std::size_t { |
||||||
|
return 2; |
||||||
|
} |
||||||
|
|
||||||
|
auto PersistentMain(void* fn) -> void { |
||||||
|
auto* function = reinterpret_cast<std::function<void(void)>*>(fn); |
||||||
|
std::invoke(*function); |
||||||
|
assert("persistent task quit!" == 0); |
||||||
|
vTaskDelete(NULL); |
||||||
|
} |
||||||
|
|
||||||
|
auto Worker::Main(void* instance) { |
||||||
|
Worker* i = reinterpret_cast<Worker*>(instance); |
||||||
|
while (1) { |
||||||
|
WorkItem item; |
||||||
|
if (xQueueReceive(i->queue_, &item, portMAX_DELAY)) { |
||||||
|
if (item.quit) { |
||||||
|
break; |
||||||
|
} else if (item.fn != nullptr) { |
||||||
|
std::invoke(*item.fn); |
||||||
|
delete item.fn; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
i->is_task_running_.store(false); |
||||||
|
i->is_task_running_.notify_all(); |
||||||
|
// Wait for the instance's destructor to delete this task. We do this instead
|
||||||
|
// of just deleting ourselves so that it's 100% certain that it's safe to
|
||||||
|
// delete or reuse this task's stack.
|
||||||
|
while (1) { |
||||||
|
vTaskDelay(portMAX_DELAY); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
Worker::Worker(const std::string& name, |
||||||
|
cpp::span<StackType_t> stack, |
||||||
|
std::size_t queue_size, |
||||||
|
UBaseType_t priority) |
||||||
|
: stack_(stack.data()), |
||||||
|
queue_(xQueueCreate(queue_size, sizeof(WorkItem))), |
||||||
|
is_task_running_(true), |
||||||
|
task_buffer_(), |
||||||
|
task_(xTaskCreateStatic(&Main, |
||||||
|
name.c_str(), |
||||||
|
stack.size(), |
||||||
|
this, |
||||||
|
priority, |
||||||
|
stack_, |
||||||
|
&task_buffer_)) {} |
||||||
|
|
||||||
|
Worker::~Worker() { |
||||||
|
WorkItem item{ |
||||||
|
.fn = nullptr, |
||||||
|
.quit = true, |
||||||
|
}; |
||||||
|
xQueueSend(queue_, &item, portMAX_DELAY); |
||||||
|
is_task_running_.wait(true); |
||||||
|
vTaskDelete(task_); |
||||||
|
free(stack_); |
||||||
|
} |
||||||
|
|
||||||
|
template <> |
||||||
|
auto Worker::Dispatch(const std::function<void(void)>& fn) |
||||||
|
-> std::future<void> { |
||||||
|
std::shared_ptr<std::promise<void>> promise = |
||||||
|
std::make_shared<std::promise<void>>(); |
||||||
|
WorkItem item{ |
||||||
|
.fn = new std::function<void(void)>([=]() { |
||||||
|
std::invoke(fn); |
||||||
|
promise->set_value(); |
||||||
|
}), |
||||||
|
.quit = false, |
||||||
|
}; |
||||||
|
xQueueSend(queue_, &item, portMAX_DELAY); |
||||||
|
return promise->get_future(); |
||||||
|
} |
||||||
|
|
||||||
|
} // namespace tasks
|
||||||
|
@ -1,7 +1,107 @@ |
|||||||
#pragma once |
#pragma once |
||||||
|
|
||||||
|
#include <atomic> |
||||||
|
#include <functional> |
||||||
|
#include <future> |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
|
||||||
|
#include "freertos/FreeRTOS.h" |
||||||
#include "freertos/portmacro.h" |
#include "freertos/portmacro.h" |
||||||
|
#include "freertos/projdefs.h" |
||||||
|
#include "freertos/queue.h" |
||||||
|
#include "freertos/task.h" |
||||||
|
#include "span.hpp" |
||||||
|
|
||||||
|
namespace tasks { |
||||||
|
|
||||||
|
/*
|
||||||
|
* Enumeration of every task (basically a thread) started within the firmware. |
||||||
|
* These are centralised so that it is easier to reason about the relative |
||||||
|
* priorities of tasks, as well as the amount and location of memory allocated |
||||||
|
* to each one. |
||||||
|
*/ |
||||||
|
enum class Type { |
||||||
|
// The main UI task. This runs the LVGL main loop.
|
||||||
|
kUi, |
||||||
|
// Task for flushing graphics buffers to the display.
|
||||||
|
kUiFlush, |
||||||
|
// The main audio pipeline task.
|
||||||
|
kAudio, |
||||||
|
// Task for flushing PCM samples to the current output.
|
||||||
|
kAudioDrain, |
||||||
|
// Task for running database queries.
|
||||||
|
kDatabase, |
||||||
|
}; |
||||||
|
|
||||||
|
template <Type t> |
||||||
|
auto Name() -> std::string; |
||||||
|
template <Type t> |
||||||
|
auto AllocateStack() -> cpp::span<StackType_t>; |
||||||
|
template <Type t> |
||||||
|
auto Priority() -> UBaseType_t; |
||||||
|
template <Type t> |
||||||
|
auto WorkerQueueSize() -> std::size_t; |
||||||
|
|
||||||
|
auto PersistentMain(void* fn) -> void; |
||||||
|
|
||||||
|
template <Type t> |
||||||
|
auto StartPersistent(const std::function<void(void)>& fn) -> void { |
||||||
|
StaticTask_t* task_buffer = new StaticTask_t; |
||||||
|
cpp::span<StackType_t> stack = AllocateStack<t>(); |
||||||
|
xTaskCreateStatic(&PersistentMain, Name<t>().c_str(), stack.size(), |
||||||
|
new std::function<void(void)>(fn), Priority<t>(), |
||||||
|
stack.data(), task_buffer); |
||||||
|
} |
||||||
|
|
||||||
|
class Worker { |
||||||
|
private: |
||||||
|
Worker(const std::string& name, |
||||||
|
cpp::span<StackType_t> stack, |
||||||
|
std::size_t queue_size, |
||||||
|
UBaseType_t priority); |
||||||
|
|
||||||
|
StackType_t* stack_; |
||||||
|
QueueHandle_t queue_; |
||||||
|
std::atomic<bool> is_task_running_; |
||||||
|
StaticTask_t task_buffer_; |
||||||
|
TaskHandle_t task_; |
||||||
|
|
||||||
|
struct WorkItem { |
||||||
|
std::function<void(void)>* fn; |
||||||
|
bool quit; |
||||||
|
}; |
||||||
|
|
||||||
|
public: |
||||||
|
template <Type t> |
||||||
|
static auto Start() -> Worker* { |
||||||
|
return new Worker(Name<t>(), AllocateStack<t>(), WorkerQueueSize<t>(), |
||||||
|
Priority<t>()); |
||||||
|
} |
||||||
|
|
||||||
|
static auto Main(void* instance); |
||||||
|
|
||||||
|
/*
|
||||||
|
* Schedules the given function to be executed on the worker task, and |
||||||
|
* asynchronously returns the result as a future. |
||||||
|
*/ |
||||||
|
template <typename T> |
||||||
|
auto Dispatch(const std::function<T(void)>& fn) -> std::future<T> { |
||||||
|
std::shared_ptr<std::promise<T>> promise = |
||||||
|
std::make_shared<std::promise<T>>(); |
||||||
|
WorkItem item{ |
||||||
|
.fn = new std::function([=]() { promise->set_value(std::invoke(fn)); }), |
||||||
|
.quit = false, |
||||||
|
}; |
||||||
|
xQueueSend(queue_, &item, portMAX_DELAY); |
||||||
|
return promise->get_future(); |
||||||
|
} |
||||||
|
|
||||||
|
~Worker(); |
||||||
|
}; |
||||||
|
|
||||||
|
/* Specialisation of Evaluate for functions that return nothing. */ |
||||||
|
template <> |
||||||
|
auto Worker::Dispatch(const std::function<void(void)>& fn) -> std::future<void>; |
||||||
|
|
||||||
extern const UBaseType_t kTaskPriorityLvgl; |
} // namespace tasks
|
||||||
extern const UBaseType_t kTaskPriorityAudioPipeline; |
|
||||||
extern const UBaseType_t kTaskPriorityAudioDrain; |
|
||||||
|
Loading…
Reference in new issue