Skip to content

Commit 18d9401

Browse files
committed
Update architecture
1 parent 3cef785 commit 18d9401

File tree

3 files changed

+52
-101
lines changed

3 files changed

+52
-101
lines changed

docs/develop/architecture.md

Lines changed: 49 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -45,18 +45,16 @@ sequenceDiagram
4545
participant EffectTask
4646
participant DriverTask
4747
participant LEDs
48-
participant FileSystem
4948
5049
Note over EffectTask,DriverTask: Both tasks synchronized via mutex
5150
5251
User->>WebUI: Adjust effect parameter
5352
WebUI->>SvelteKit: WebSocket message
5453
SvelteKit->>SvelteKit: Update in-memory state
55-
SvelteKit->>SvelteKit: Queue deferred write
5654
5755
Note over EffectTask: Core 0 (PRO_CPU)
5856
EffectTask->>EffectTask: Take mutex (10µs)
59-
EffectTask->>EffectTask: memcpy front→back buffer
57+
EffectTask->>EffectTask: memcpy channelsD → channelsE
6058
EffectTask->>EffectTask: Release mutex
6159
EffectTask->>EffectTask: Compute effects (5-15ms)
6260
EffectTask->>EffectTask: Take mutex (10µs)
@@ -69,11 +67,6 @@ sequenceDiagram
6967
DriverTask->>DriverTask: Release mutex
7068
DriverTask->>DriverTask: Send via DMA (1-5ms)
7169
DriverTask->>LEDs: Pixel data
72-
73-
User->>WebUI: Click "Save Config"
74-
WebUI->>SvelteKit: POST /rest/saveConfig
75-
SvelteKit->>FileSystem: Execute all deferred writes
76-
FileSystem-->>SvelteKit: Write complete (10-50ms)
7770
```
7871

7972
## Core Assignments
@@ -139,19 +132,19 @@ Buffer Architecture (PSRAM Only)
139132
```mermaid
140133
graph LR
141134
subgraph MemoryBuffers["Memory Buffers"]
142-
Front[Front Buffer<br/>channels*]
143-
Back[Back Buffer<br/>channelsBack*]
135+
Effects[Effects Buffer<br/>channelsE*]
136+
Drivers[Drivers Buffer<br/>channelsD*]
144137
end
145138
146-
EffectTask[Effect Task<br/>Core 0] -.->|1. memcpy| Back
147-
EffectTask -.->|2. Compute effects| Back
148-
EffectTask -.->|3. Swap pointers<br/>MUTEX 10µs| Front
139+
EffectTask[Effect Task<br/>Core 0] -.->|1. memcpy| Drivers
140+
EffectTask -.->|2. Compute effects| Drivers
141+
EffectTask -.->|3. Swap pointers<br/>MUTEX 10µs| Effects
149142
150-
DriverTask[Driver Task<br/>Core 1] -->|4. Read pixels| Front
143+
DriverTask[Driver Task<br/>Core 1] -->|4. Read pixels| Effects
151144
DriverTask -->|5. Send via DMA| LEDs[LEDs]
152145
153-
style Front fill:#898f89
154-
style Back fill:#898c8f
146+
style Effects fill:#898f89
147+
style Drivers fill:#898c8f
155148
```
156149

157150
Synchronization Flow
@@ -161,14 +154,50 @@ Synchronization Flow
161154

162155
void effectTask(void* param) {
163156
while (true) {
164-
// tbd ...
165-
vTaskDelay(1);
157+
uint8_t isPositions = layerP.lights.header.isPositions;
158+
bool canProduce = !newFrameReady;
159+
160+
if (isPositions == 0) { // driver task can change this
161+
if (layerP.lights.useDoubleBuffer) {
162+
163+
if (canProduce) {
164+
// Copy previous frame (channelsD) to working buffer (channelsE)
165+
memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels);
166+
167+
layerP.loop();
168+
169+
// Atomic swap channels
170+
uint8_t* temp = layerP.lights.channelsD;
171+
layerP.lights.channelsD = layerP.lights.channelsE;
172+
layerP.lights.channelsE = temp;
173+
newFrameReady = true;
174+
}
175+
176+
} else {
177+
// Single buffer mode
178+
layerP.loop();
179+
}
180+
vTaskDelay(1);
166181
}
167182
}
168183

169184
void driverTask(void* param) {
170185
while (true) {
171-
// tbd ...
186+
if (isPositions == 0) {
187+
if (layerP.lights.useDoubleBuffer) {
188+
if (newFrameReady) {
189+
newFrameReady = false;
190+
// Double buffer: release lock, then send
191+
192+
esp32sveltekit.lps++;
193+
layerP.loopDrivers(); // ✅ No lock needed
194+
}
195+
} else {
196+
// Single buffer: keep lock while sending
197+
esp32sveltekit.lps++;
198+
layerP.loopDrivers(); // ✅ Protected by lock
199+
}
200+
}
172201
vTaskDelay(1);
173202
}
174203
}
@@ -187,79 +216,6 @@ Performance Impact
187216
188217
**Conclusion**: Double buffering overhead is negligible (<1% for typical setups).
189218
190-
## State Persistence & Deferred Writes
191-
192-
Why Deferred Writes?
193-
194-
Flash write operations (LittleFS) **block all CPU cores** for 10-50ms, causing:
195-
196-
- ❌ Dropped frames (2-6 frames at 60fps)
197-
- ❌ Visible LED stutter
198-
- ❌ Poor user experience during settings changes
199-
200-
Solution: Deferred Write Queue
201-
202-
```mermaid
203-
sequenceDiagram
204-
participant User
205-
participant UI
206-
participant Module
207-
participant WriteQueue
208-
participant FileSystem
209-
210-
User->>UI: Move slider
211-
UI->>Module: Update state (in-memory)
212-
Module->>WriteQueue: Queue write operation
213-
Note over WriteQueue: Changes accumulate<br/>in memory
214-
215-
User->>UI: Move slider again
216-
UI->>Module: Update state (in-memory)
217-
Note over WriteQueue: Previous write replaced<br/>No flash access yet
218-
219-
User->>UI: Click "Save Config"
220-
UI->>WriteQueue: Execute all queued writes
221-
WriteQueue->>FileSystem: Write all changes (10-50ms)
222-
Note over FileSystem: Single flash write<br/>for all changes
223-
FileSystem-->>UI: Complete
224-
```
225-
226-
Implementation
227-
228-
**When UI updates state:**
229-
```cpp
230-
// File: SharedFSPersistence.h
231-
void writeToFS(const String& moduleName) {
232-
if (delayedWriting) {
233-
// Add to global queue (no flash write yet)
234-
sharedDelayedWrites.push_back([this, module](char writeOrCancel) {
235-
if (writeOrCancel == 'W') {
236-
this->writeToFSNow(moduleName); // Actual flash write
237-
}
238-
});
239-
}
240-
}
241-
```
242-
243-
**When user clicks "Save Config":**
244-
```cpp
245-
// File: FileManager.cpp
246-
_server->on("/rest/saveConfig", HTTP_POST, [](PsychicRequest* request) {
247-
// Execute all queued writes in a single batch
248-
FSPersistence<int>::writeToFSDelayed('W');
249-
return ESP_OK;
250-
});
251-
```
252-
253-
Benefits
254-
255-
| Aspect | Without Deferred Writes | With Deferred Writes |
256-
|--------|-------------------------|----------------------|
257-
| **Flash writes per slider move** | 1 (10-50ms) | 0 |
258-
| **LED stutter during UI use** | Constant | None |
259-
| **Flash writes per session** | 100+ | 1 |
260-
| **User experience** | Laggy, stuttering | Smooth |
261-
| **Flash wear** | High | Minimal |
262-
263219
## Performance Budget at 60fps
264220
265221
Per-Frame Time Budget (16.66ms)
@@ -315,15 +271,13 @@ Overhead Analysis
315271
| SvelteKit | 0.5-2ms (on Core 1) | 2-3ms (on Core 1) | 5ms |
316272
| Double buffer memcpy | 0.1ms (0.6%) | 0.1ms (0.6%) | 0.1ms |
317273
| Mutex locks | 0.02ms (0.1%) | 0.02ms (0.1%) | 0.02ms |
318-
| Flash writes | **0ms** (deferred) | **0ms** (deferred) | 10-50ms (on save) |
319274
| **Total** | **1-3ms (6-18%)** | **4-8ms (24-48%)** | **Flash: user-triggered** |
320275

321276
**Result**:
322277

323278
- ✅ 60fps sustained during normal operation
324279
- ✅ 52-60fps during heavy WiFi/UI activity
325-
- ✅ No stutter during UI interaction (deferred writes)
326-
- ✅ Only brief stutter when user explicitly saves config (acceptable)
280+
- ✅ No stutter during UI interaction
327281

328282
## Configuration
329283

@@ -389,6 +343,5 @@ This architecture achieves optimal performance through:
389343
2. **Priority Hierarchy**: Driver > SvelteKit ensures LED timing is never compromised
390344
3. **Minimal Locking**: 10µs mutex locks enable 99% parallel execution
391345
4. **Double Buffering**: Eliminates tearing with <1% overhead
392-
5. **Deferred Writes**: Eliminates UI stutter by batching flash operations
393346

394347
**Result**: Smooth 60fps LED effects with responsive UI and stable networking. 🚀

src/MoonLight/Layers/PhysicalLayer.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#include "MoonBase/Utilities.h"
2020
#include "VirtualLayer.h"
2121

22+
extern SemaphoreHandle_t swapMutex;
23+
2224
PhysicalLayer layerP; // global declaration of the physical layer
2325

2426
PhysicalLayer::PhysicalLayer() {
@@ -131,7 +133,6 @@ void PhysicalLayer::onLayoutPre() {
131133
if (pass == 1) {
132134
lights.header.nrOfLights = 0; // for pass1 and pass2 as in pass2 virtual layer needs it
133135
lights.header.size = {0, 0, 0};
134-
extern SemaphoreHandle_t swapMutex;
135136
xSemaphoreTake(swapMutex, portMAX_DELAY);
136137
EXT_LOGD(ML_TAG, "positions in progress (%d -> 1)", lights.header.isPositions);
137138
lights.header.isPositions = 1; // in progress...
@@ -211,7 +212,6 @@ void PhysicalLayer::onLayoutPost() {
211212
lights.header.nrOfChannels = lights.header.nrOfLights * lights.header.channelsPerLight * ((lights.header.lightPreset == lightPreset_RGB2040) ? 2 : 1); // RGB2040 has empty channels
212213
EXT_LOGD(ML_TAG, "pass %d mp:%d #:%d / %d s:%d,%d,%d", pass, monitorPass, lights.header.nrOfLights, lights.header.nrOfChannels, lights.header.size.x, lights.header.size.y, lights.header.size.z);
213214
// send the positions to the UI _socket_emit
214-
extern SemaphoreHandle_t swapMutex;
215215
xSemaphoreTake(swapMutex, portMAX_DELAY);
216216
EXT_LOGD(ML_TAG, "positions stored (%d -> %d)", lights.header.isPositions, lights.header.nrOfLights ? 2 : 3);
217217
lights.header.isPositions = lights.header.nrOfLights ? 2 : 3; // filled with positions, set back to 3 in ModuleEffects, or direct to 3 if no lights (effects will move it to 0)

src/main.cpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,11 @@ void effectTask(void* pvParameters) {
131131
// Check state under lock
132132
xSemaphoreTake(swapMutex, portMAX_DELAY);
133133
uint8_t isPositions = layerP.lights.header.isPositions;
134+
bool canProduce = !newFrameReady;
134135
xSemaphoreGive(swapMutex);
135136

136137
if (isPositions == 0) { // driver task can change this
137138
if (layerP.lights.useDoubleBuffer) {
138-
xSemaphoreTake(swapMutex, portMAX_DELAY);
139-
bool canProduce = !newFrameReady;
140-
xSemaphoreGive(swapMutex);
141139

142140
if (canProduce) {
143141
// Copy previous frame (channelsD) to working buffer (channelsE)

0 commit comments

Comments
 (0)