Skip to content

Commit 7928125

Browse files
committed
Implement double buffering - step 1
Back end ======== - platformIO: sveltekit on core 1, interval 20ms - main: add swapMutex and newFrameReady: effectstask: if dubbelbuffer swap buffers each iteration. Drivertask: if doublebuffer then release lock early - Physical layer: if psramFound create doubleBuffer - Lights control: use swapMutex for monitor emit
1 parent a46ef98 commit 7928125

File tree

5 files changed

+109
-62
lines changed

5 files changed

+109
-62
lines changed

platformio.ini

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,14 +56,15 @@ build_flags =
5656
-D BUILD_TARGET=\"$PIOENV\"
5757
-D APP_NAME=\"MoonLight\" ; 🌙 Must only contain characters from [a-zA-Z0-9-_] as this is converted into a filename
5858
-D APP_VERSION=\"0.7.0\" ; semver compatible version string
59-
-D APP_DATE=\"20251222\" ; 🌙
59+
-D APP_DATE=\"20251226\" ; 🌙
6060

6161
-D PLATFORM_VERSION=\"pioarduino-55.03.35\" ; 🌙 make sure it matches with above plaftform
6262

6363
-D FT_MOONBASE=1
6464

6565
; Move all networking stuff to the protocol core 0 and leave business logic on application core 1
66-
-D ESP32SVELTEKIT_RUNNING_CORE=0
66+
-D ESP32SVELTEKIT_RUNNING_CORE=1
67+
-D ESP32SVELTEKIT_LOOP_INTERVAL=20
6768

6869
; Uncomment EMBED_WWW to embed the WWW data in the firmware binary
6970
-D EMBED_WWW

src/MoonLight/Layers/PhysicalLayer.cpp

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,26 @@ PhysicalLayer::PhysicalLayer() {
3838
void PhysicalLayer::setup() {
3939
// allocate lights.channels
4040

41-
if (psramFound())
42-
lights.maxChannels = MIN(ESP.getPsramSize() / 2, 61440 * 3); // fill halve with channels, max 120 pins * 512 LEDs, still addressable with uint16_t
43-
else
41+
if (psramFound()) {
42+
lights.maxChannels = MIN(ESP.getPsramSize() / 4, 61440 * 3); // fill halve with channels, max 120 pins * 512 LEDs, still addressable with uint16_t
43+
lights.useDoubleBuffer = true; // Enable double buffering
44+
} else {
4445
lights.maxChannels = 4096 * 3; // esp32-d0: max 1024->2048->4096 Leds ATM
46+
lights.useDoubleBuffer = false; // Single buffer mode
47+
}
4548

4649
lights.channels = allocMB<uint8_t>(lights.maxChannels);
4750

4851
if (lights.channels) {
4952
EXT_LOGD(ML_TAG, "allocated %d bytes in %s", lights.maxChannels, isInPSRAM(lights.channels) ? "PSRAM" : "RAM");
53+
// Allocate back buffer only if PSRAM available
54+
if (lights.useDoubleBuffer) {
55+
lights.channelsBack = allocMB<uint8_t>(lights.maxChannels);
56+
if (!lights.channelsBack) {
57+
EXT_LOGW(ML_TAG, "Failed to allocate back buffer, disabling double buffering");
58+
lights.useDoubleBuffer = false;
59+
}
60+
}
5061
} else {
5162
EXT_LOGE(ML_TAG, "failed to allocated %d bytes of RAM or PSRAM", lights.maxChannels);
5263
lights.maxChannels = 0;

src/MoonLight/Layers/PhysicalLayer.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,10 @@ struct LightsHeader {
8888

8989
struct Lights {
9090
LightsHeader header;
91-
uint8_t* channels = nullptr; // //pka leds, created in constructor
91+
uint8_t* channels = nullptr; // pka leds, created in constructor
92+
uint8_t* channelsBack = nullptr; // Back buffer (being written by effects)
9293
size_t maxChannels = 0;
94+
bool useDoubleBuffer = false; // Only when PSRAM available
9395

9496
// std::vector<size_t> universes; //tells at which byte the universe starts
9597
};

src/MoonLight/Modules/ModuleLightsControl.h

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ class ModuleLightsControl : public Module {
326326

327327
if (pinPushButtonLightsOn != UINT8_MAX) {
328328
int state = digitalRead(pinPushButtonLightsOn);
329-
if ((state != lastState) && ((((millis() - lastDebounceTime) > debounceDelay) || (millis() < lastDebounceTime)))) {
329+
if ((state != lastState) && ((((millis() - lastDebounceTime) > debounceDelay) || (millis() < lastDebounceTime)))) {
330330
lastDebounceTime = millis();
331331
// Trigger only on button press (HIGH to LOW transition for INPUT_PULLUP)
332332
if (state == LOW) {
@@ -341,7 +341,7 @@ class ModuleLightsControl : public Module {
341341

342342
if (pinToggleButtonLightsOn != UINT8_MAX) {
343343
int state = digitalRead(pinToggleButtonLightsOn);
344-
if ((state != lastState) && ((((millis() - lastDebounceTime) > debounceDelay) || (millis() < lastDebounceTime)))) {
344+
if ((state != lastState) && ((((millis() - lastDebounceTime) > debounceDelay) || (millis() < lastDebounceTime)))) {
345345
lastDebounceTime = millis();
346346
JsonDocument doc;
347347
JsonObject newState = doc.to<JsonObject>();
@@ -364,9 +364,17 @@ class ModuleLightsControl : public Module {
364364
});
365365
} else if (layerP.lights.header.isPositions == 0 && layerP.lights.header.nrOfLights) { // send to UI
366366
EVERY_N_MILLIS(layerP.lights.header.nrOfLights / 12) {
367+
367368
read([&](ModuleState& _state) {
368-
if (_socket->getConnectedClients() && _state.data["monitorOn"]) _socket->emitEvent("monitor", (char*)layerP.lights.channels, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels));
369+
if (_socket->getConnectedClients() && _state.data["monitorOn"]) {
370+
extern SemaphoreHandle_t swapMutex;
371+
372+
xSemaphoreTake(swapMutex, portMAX_DELAY);
373+
_socket->emitEvent("monitor", (char*)layerP.lights.channels, MIN(layerP.lights.header.nrOfChannels, layerP.lights.maxChannels));
374+
xSemaphoreGive(swapMutex);
375+
}
369376
});
377+
370378
}
371379
}
372380
#endif

src/main.cpp

Lines changed: 78 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ ModuleLiveScripts moduleLiveScripts = ModuleLiveScripts(&server, &esp32sveltekit
114114
ModuleChannels moduleChannels = ModuleChannels(&server, &esp32sveltekit);
115115
ModuleMoonLightInfo moduleMoonLightInfo = ModuleMoonLightInfo(&server, &esp32sveltekit);
116116

117-
volatile xSemaphoreHandle lowLevelSemaphore = xSemaphoreCreateBinary();
117+
SemaphoreHandle_t swapMutex = NULL;
118+
volatile bool newFrameReady = false;
118119

119120
TaskHandle_t effectTaskHandle = NULL;
120121
TaskHandle_t driverTaskHandle = NULL;
@@ -123,24 +124,38 @@ void effectTask(void* pvParameters) {
123124
// 🌙
124125

125126
layerP.setup(); // setup virtual layers (no node setup here as done in addNode)
127+
static unsigned long last20ms = 0;
126128

127129
for (;;) {
128-
esp32sveltekit.lps++; // 🌙 todo: not moonlight specific?
130+
if (layerP.lights.useDoubleBuffer) {
131+
// effectTask always writes to channelsBack, reads previous channelsBack
132+
layerP.lights.channels = layerP.lights.channelsBack;
133+
layerP.loop(); // getRGB and setRGB both use channelsBack
129134

130-
if (xSemaphoreTake(lowLevelSemaphore, pdMS_TO_TICKS(100)) == pdFALSE) {
131-
// EXT_LOGW(ML_TAG, "effectSemaphore wait too long"); //happens if no driver!, but let effects continue (for monitor) at 10 fps
132-
//EXT_LOGW(ML_TAG, "effect: semaphore wait too long");
133-
}
134-
else {
135-
layerP.loop(); // run all the effects of all virtual layers (currently only one layer)
135+
if (millis() - last20ms >= 20) {
136+
last20ms = millis();
137+
layerP.loop20ms();
138+
}
139+
140+
// Atomic swap channels
141+
xSemaphoreTake(swapMutex, portMAX_DELAY);
142+
uint8_t* temp = layerP.lights.channelsBack;
143+
layerP.lights.channelsBack = layerP.lights.channels;
144+
layerP.lights.channels = temp;
145+
newFrameReady = true;
146+
xSemaphoreGive(swapMutex);
147+
148+
} else {
149+
// Single buffer mode
150+
xSemaphoreTake(swapMutex, portMAX_DELAY);
151+
layerP.loop();
136152

137-
static unsigned long last20ms = 0;
138153
if (millis() - last20ms >= 20) {
139154
last20ms = millis();
140155
layerP.loop20ms();
141156
}
142157

143-
xSemaphoreGive(lowLevelSemaphore);
158+
xSemaphoreGive(swapMutex);
144159
}
145160

146161
vTaskDelay(1); // yield to other tasks, 1 tick (~1ms)
@@ -153,15 +168,25 @@ void driverTask(void* pvParameters) {
153168
// layerP.setup() done in effectTask
154169

155170
for (;;) {
156-
if (xSemaphoreTake(lowLevelSemaphore, pdMS_TO_TICKS(100)) == pdFALSE) {
157-
//EXT_LOGW(ML_TAG, "driver: semaphore wait too long");
171+
xSemaphoreTake(swapMutex, portMAX_DELAY);
172+
esp32sveltekit.lps++;
173+
174+
if (layerP.lights.useDoubleBuffer) {
175+
if (newFrameReady) {
176+
newFrameReady = false;
177+
// Double buffer: release lock, then send
178+
xSemaphoreGive(swapMutex);
179+
layerP.loopDrivers(); // ✅ No lock needed
180+
} else {
181+
xSemaphoreGive(swapMutex);
182+
}
183+
} else {
184+
// Single buffer: keep lock while sending
185+
layerP.loopDrivers(); // ✅ Protected by lock
186+
xSemaphoreGive(swapMutex);
158187
}
159-
else {
160-
layerP.loopDrivers();
161188

162-
xSemaphoreGive(lowLevelSemaphore);
163-
}
164-
vTaskDelay(1); // yield to other tasks, 1 tick (~1ms)
189+
vTaskDelay(1);
165190
}
166191
}
167192
#endif
@@ -222,39 +247,39 @@ void setup() {
222247
safeModeMB = true;
223248
}
224249

225-
// // check sizes ...
226-
// sizeof(esp32sveltekit); // 4152
227-
// sizeof(WiFiSettingsService); // 456
228-
// sizeof(SystemStatus); // 16
229-
// sizeof(UploadFirmwareService); // 32
230-
// sizeof(HttpEndpoint<ModuleState>); // 152
231-
// sizeof(EventEndpoint<ModuleState>); // 112
232-
// sizeof(SharedEventEndpoint); // 8
233-
// sizeof(WebSocketServer<ModuleState>); // 488
234-
// sizeof(SharedWebSocketServer); // 352
235-
// sizeof(FSPersistence<ModuleState>); // 128
236-
// sizeof(PsychicHttpServer*); // 8
237-
// sizeof(HttpEndpoint<APSettings>); // 152
238-
// sizeof(SharedHttpEndpoint); // 16
239-
// sizeof(FSPersistence<APSettings>); // 128
240-
// sizeof(APSettingsService); // 600;
241-
// sizeof(PsychicWebSocketHandler); // 336
242-
// sizeof(fileManager); // 864
243-
// sizeof(Module); // 1144 -> 472
244-
// sizeof(moduleDevices); // 1320
245-
// sizeof(moduleIO); // 1144
246-
// #if FT_ENABLED(FT_MOONLIGHT)
247-
// sizeof(moduleEffects); // 1208
248-
// sizeof(moduleDrivers); // 1216
249-
// sizeof(moduleLightsControl); // 1176
250-
// #if FT_ENABLED(FT_LIVESCRIPT)
251-
// sizeof(moduleLiveScripts); // 1176
252-
// #endif
253-
// sizeof(moduleChannels); // 1144
254-
// sizeof(moduleMoonLightInfo); // 1144
255-
// sizeof(layerP.lights); // 56
256-
// sizeof(layerP.lights.header); // 40
257-
// #endif
250+
// // check sizes ...
251+
// sizeof(esp32sveltekit); // 4152
252+
// sizeof(WiFiSettingsService); // 456
253+
// sizeof(SystemStatus); // 16
254+
// sizeof(UploadFirmwareService); // 32
255+
// sizeof(HttpEndpoint<ModuleState>); // 152
256+
// sizeof(EventEndpoint<ModuleState>); // 112
257+
// sizeof(SharedEventEndpoint); // 8
258+
// sizeof(WebSocketServer<ModuleState>); // 488
259+
// sizeof(SharedWebSocketServer); // 352
260+
// sizeof(FSPersistence<ModuleState>); // 128
261+
// sizeof(PsychicHttpServer*); // 8
262+
// sizeof(HttpEndpoint<APSettings>); // 152
263+
// sizeof(SharedHttpEndpoint); // 16
264+
// sizeof(FSPersistence<APSettings>); // 128
265+
// sizeof(APSettingsService); // 600;
266+
// sizeof(PsychicWebSocketHandler); // 336
267+
// sizeof(fileManager); // 864
268+
// sizeof(Module); // 1144 -> 472
269+
// sizeof(moduleDevices); // 1320
270+
// sizeof(moduleIO); // 1144
271+
// #if FT_ENABLED(FT_MOONLIGHT)
272+
// sizeof(moduleEffects); // 1208
273+
// sizeof(moduleDrivers); // 1216
274+
// sizeof(moduleLightsControl); // 1176
275+
// #if FT_ENABLED(FT_LIVESCRIPT)
276+
// sizeof(moduleLiveScripts); // 1176
277+
// #endif
278+
// sizeof(moduleChannels); // 1144
279+
// sizeof(moduleMoonLightInfo); // 1144
280+
// sizeof(layerP.lights); // 56
281+
// sizeof(layerP.lights.header); // 40
282+
// #endif
258283

259284
// start ESP32-SvelteKit
260285
esp32sveltekit.begin();
@@ -328,16 +353,16 @@ void setup() {
328353
false);
329354
#endif
330355

331-
xSemaphoreGive(lowLevelSemaphore);
356+
swapMutex = xSemaphoreCreateMutex();
332357

333358
// 🌙
334359
xTaskCreateUniversal(effectTask, // task function
335360
"AppEffectTask", // name
336361
psramFound() ? 4 * 1024 : 3 * 1024, // d0-tuning... stack size (without livescripts we can do with 12...). updated from 4 to 6 to support preset loop
337362
NULL, // parameter
338-
3, // priority (between 5 and 10: ASYNC_WORKER_TASK_PRIORITY and Restart/Sleep), don't set it higher then 10...
363+
10, // priority (between 5 and 10: ASYNC_WORKER_TASK_PRIORITY and Restart/Sleep), don't set it higher then 10...
339364
&effectTaskHandle, // task handle
340-
1 // core (0 or 1)
365+
0 // core (0 or 1)
341366
);
342367

343368
xTaskCreateUniversal(driverTask, // task function

0 commit comments

Comments
 (0)