diff --git a/docs/develop/architecture.md b/docs/develop/architecture.md
index b1b388e5..7b67b234 100644
--- a/docs/develop/architecture.md
+++ b/docs/develop/architecture.md
@@ -10,11 +10,11 @@ MoonLight uses a multi-core, multi-task architecture on ESP32 to achieve smooth
|------|------|----------|------------|-----------|---------|
| **WiFi/BT** | 0 (PRO_CPU) | 23 | System | Event-driven | System networking stack |
| **lwIP TCP/IP** | 0 (PRO_CPU) | 18 | System | Event-driven | TCP/IP protocol processing |
-| **Effect Task** | 0 (PRO_CPU) | 3 | 3-4KB | ~60 fps | Calculate LED colors and effects |
+| **Effect Task** | 0 (PRO_CPU) | 10 | 3-4KB | ~60 fps | Calculate LED colors and effects |
| **ESP32SvelteKit** | 1 (APP_CPU) | 2 | System | 10ms | HTTP/WebSocket UI framework |
| **Driver Task** | 1 (APP_CPU) | 3 | 3-4KB | ~60 fps | Output data to LEDs via DMA/I2S/LCD/PARLIO |
-Effect Task (Core 0, Priority 3)
+Effect Task (Core 0, Priority 10)
- **Function**: Pure computation - calculates pixel colors based on effect algorithms
- **Operations**: Reads/writes to `channels` array, performs mathematical calculations
@@ -45,18 +45,16 @@ sequenceDiagram
participant EffectTask
participant DriverTask
participant LEDs
- participant FileSystem
Note over EffectTask,DriverTask: Both tasks synchronized via mutex
User->>WebUI: Adjust effect parameter
WebUI->>SvelteKit: WebSocket message
SvelteKit->>SvelteKit: Update in-memory state
- SvelteKit->>SvelteKit: Queue deferred write
Note over EffectTask: Core 0 (PRO_CPU)
EffectTask->>EffectTask: Take mutex (10µs)
- EffectTask->>EffectTask: memcpy front→back buffer
+ EffectTask->>EffectTask: memcpy channelsD → channelsE
EffectTask->>EffectTask: Release mutex
EffectTask->>EffectTask: Compute effects (5-15ms)
EffectTask->>EffectTask: Take mutex (10µs)
@@ -69,11 +67,6 @@ sequenceDiagram
DriverTask->>DriverTask: Release mutex
DriverTask->>DriverTask: Send via DMA (1-5ms)
DriverTask->>LEDs: Pixel data
-
- User->>WebUI: Click "Save Config"
- WebUI->>SvelteKit: POST /rest/saveConfig
- SvelteKit->>FileSystem: Execute all deferred writes
- FileSystem-->>SvelteKit: Write complete (10-50ms)
```
## Core Assignments
@@ -85,7 +78,7 @@ graph TB
subgraph Core0["Core 0 (PRO_CPU)"]
WiFi[WiFi/BT
Priority 23]
lwIP[lwIP TCP/IP
Priority 18]
- Effect[Effect Task
Priority 3
Computation Only]
+ Effect[Effect Task
Priority 10
Computation Only]
end
subgraph Core1["Core 1 (APP_CPU)"]
@@ -139,19 +132,19 @@ Buffer Architecture (PSRAM Only)
```mermaid
graph LR
subgraph MemoryBuffers["Memory Buffers"]
- Front[Front Buffer
channels*]
- Back[Back Buffer
channelsBack*]
+ Effects[Effects Buffer
channelsE*]
+ Drivers[Drivers Buffer
channelsD*]
end
- EffectTask[Effect Task
Core 0] -.->|1. memcpy| Back
- EffectTask -.->|2. Compute effects| Back
- EffectTask -.->|3. Swap pointers
MUTEX 10µs| Front
+ EffectTask[Effect Task
Core 0] -.->|1. memcpy| Effects
+ EffectTask -.->|2. Compute effects| Effects
+ EffectTask -.->|3. Swap pointers
MUTEX 10µs| Drivers
- DriverTask[Driver Task
Core 1] -->|4. Read pixels| Front
+ DriverTask[Driver Task
Core 1] -->|4. Read pixels| Drivers
DriverTask -->|5. Send via DMA| LEDs[LEDs]
- style Front fill:#898f89
- style Back fill:#898c8f
+ style Effects fill:#898f89
+ style Drivers fill:#898c8f
```
Synchronization Flow
@@ -161,36 +154,49 @@ Synchronization Flow
void effectTask(void* param) {
while (true) {
- if (useDoubleBuffer) {
- // Step 1: Copy front → back (NO LOCK)
- memcpy(channelsBack, channels, nrOfChannels);
-
- // Step 2: Compute effects on back buffer (NO LOCK, 5-15ms)
- uint8_t* temp = channels;
- channels = channelsBack;
- computeEffects(); // Reads and writes channelsBack
-
- // Step 3: BRIEF LOCK - Swap pointers (10µs)
- xSemaphoreTake(swapMutex, portMAX_DELAY);
- channelsBack = channels;
- channels = temp;
- xSemaphoreGive(swapMutex);
+ xSemaphoreTake(swapMutex, portMAX_DELAY);
+
+ if (layerP.lights.header.isPositions == 0 && !newFrameReady) { // within mutex as driver task can change this
+ if (layerP.lights.useDoubleBuffer) {
+ xSemaphoreGive(swapMutex);
+ memcpy(layerP.lights.channelsE, layerP.lights.channelsD, layerP.lights.header.nrOfChannels); // Copy previous frame (channelsD) to working buffer (channelsE)
+ }
+
+ layerP.loop();
+
+ if (layerP.lights.useDoubleBuffer) { // Atomic swap channels
+ xSemaphoreTake(swapMutex, portMAX_DELAY);
+ uint8_t* temp = layerP.lights.channelsD;
+ layerP.lights.channelsD = layerP.lights.channelsE;
+ layerP.lights.channelsE = temp;
+ }
+ newFrameReady = true;
}
+
+ xSemaphoreGive(swapMutex);
vTaskDelay(1);
}
}
void driverTask(void* param) {
while (true) {
- if (useDoubleBuffer) {
- // Step 4: BRIEF LOCK - Capture pointer (10µs)
- xSemaphoreTake(swapMutex, portMAX_DELAY);
- uint8_t* currentFrame = channels;
- xSemaphoreGive(swapMutex);
-
- // Step 5: Send to LEDs (NO LOCK, 1-5ms)
- sendViaDMA(currentFrame);
+ bool mutexGiven = false;
+ xSemaphoreTake(swapMutex, portMAX_DELAY);
+
+ if (layerP.lights.header.isPositions == 0) {
+ if (newFrameReady) {
+ newFrameReady = false;
+ if (layerP.lights.useDoubleBuffer) {
+ xSemaphoreGive(swapMutex); // Double buffer: release lock, then send
+ mutexGiven = true;
+ }
+
+ esp32sveltekit.lps++;
+ layerP.loopDrivers();
+ }
}
+
+ if (!mutexGiven) xSemaphoreGive(swapMutex); // not double buffer or if conditions not met
vTaskDelay(1);
}
}
@@ -209,79 +215,6 @@ Performance Impact
**Conclusion**: Double buffering overhead is negligible (<1% for typical setups).
-## State Persistence & Deferred Writes
-
-Why Deferred Writes?
-
-Flash write operations (LittleFS) **block all CPU cores** for 10-50ms, causing:
-
-- ❌ Dropped frames (2-6 frames at 60fps)
-- ❌ Visible LED stutter
-- ❌ Poor user experience during settings changes
-
-Solution: Deferred Write Queue
-
-```mermaid
-sequenceDiagram
- participant User
- participant UI
- participant Module
- participant WriteQueue
- participant FileSystem
-
- User->>UI: Move slider
- UI->>Module: Update state (in-memory)
- Module->>WriteQueue: Queue write operation
- Note over WriteQueue: Changes accumulate
in memory
-
- User->>UI: Move slider again
- UI->>Module: Update state (in-memory)
- Note over WriteQueue: Previous write replaced
No flash access yet
-
- User->>UI: Click "Save Config"
- UI->>WriteQueue: Execute all queued writes
- WriteQueue->>FileSystem: Write all changes (10-50ms)
- Note over FileSystem: Single flash write
for all changes
- FileSystem-->>UI: Complete
-```
-
-Implementation
-
-**When UI updates state:**
-```cpp
-// File: SharedFSPersistence.h
-void writeToFS(const String& moduleName) {
- if (delayedWriting) {
- // Add to global queue (no flash write yet)
- sharedDelayedWrites.push_back([this, module](char writeOrCancel) {
- if (writeOrCancel == 'W') {
- this->writeToFSNow(moduleName); // Actual flash write
- }
- });
- }
-}
-```
-
-**When user clicks "Save Config":**
-```cpp
-// File: FileManager.cpp
-_server->on("/rest/saveConfig", HTTP_POST, [](PsychicRequest* request) {
- // Execute all queued writes in a single batch
- FSPersistence::writeToFSDelayed('W');
- return ESP_OK;
-});
-```
-
-Benefits
-
-| Aspect | Without Deferred Writes | With Deferred Writes |
-|--------|-------------------------|----------------------|
-| **Flash writes per slider move** | 1 (10-50ms) | 0 |
-| **LED stutter during UI use** | Constant | None |
-| **Flash writes per session** | 100+ | 1 |
-| **User experience** | Laggy, stuttering | Smooth |
-| **Flash wear** | High | Minimal |
-
## Performance Budget at 60fps
Per-Frame Time Budget (16.66ms)
@@ -337,15 +270,13 @@ Overhead Analysis
| SvelteKit | 0.5-2ms (on Core 1) | 2-3ms (on Core 1) | 5ms |
| Double buffer memcpy | 0.1ms (0.6%) | 0.1ms (0.6%) | 0.1ms |
| Mutex locks | 0.02ms (0.1%) | 0.02ms (0.1%) | 0.02ms |
-| Flash writes | **0ms** (deferred) | **0ms** (deferred) | 10-50ms (on save) |
| **Total** | **1-3ms (6-18%)** | **4-8ms (24-48%)** | **Flash: user-triggered** |
**Result**:
- ✅ 60fps sustained during normal operation
- ✅ 52-60fps during heavy WiFi/UI activity
-- ✅ No stutter during UI interaction (deferred writes)
-- ✅ Only brief stutter when user explicitly saves config (acceptable)
+- ✅ No stutter during UI interaction
## Configuration
@@ -357,11 +288,11 @@ Double buffering is **automatically enabled** when PSRAM is detected:
// In PhysicalLayer::setup()
if (psramFound()) {
lights.useDoubleBuffer = true;
- lights.channels = allocMB(maxChannels);
- lights.channelsBack = allocMB(maxChannels);
+ lights.channelsE = allocMB(maxChannels);
+ lights.channelsD = allocMB(maxChannels);
} else {
lights.useDoubleBuffer = false;
- lights.channels = allocMB(maxChannels);
+ lights.channelsE = allocMB(maxChannels);
}
```
@@ -387,7 +318,7 @@ xTaskCreateUniversal(effectTask,
"AppEffectTask",
psramFound() ? 4 * 1024 : 3 * 1024,
NULL,
- 3, // Priority
+ 10, // Priority
&effectTaskHandle,
0 // Core 0 (PRO_CPU)
);
@@ -411,6 +342,5 @@ This architecture achieves optimal performance through:
2. **Priority Hierarchy**: Driver > SvelteKit ensures LED timing is never compromised
3. **Minimal Locking**: 10µs mutex locks enable 99% parallel execution
4. **Double Buffering**: Eliminates tearing with <1% overhead
-5. **Deferred Writes**: Eliminates UI stutter by batching flash operations
**Result**: Smooth 60fps LED effects with responsive UI and stable networking. 🚀
\ No newline at end of file
diff --git a/docs/develop/layers.md b/docs/develop/layers.md
index 89e3d6c5..39d2be22 100644
--- a/docs/develop/layers.md
+++ b/docs/develop/layers.md
@@ -35,7 +35,7 @@
* A Virtual Layer mapping gets updated if a layout, mapping or dimensions change 🚧
* An effect uses a virtual layer. One Virtual layer can have multiple effects. ✅
* Physical layer
- * Lights.header and Lights.channels. CRGB leds[] is using lights.channels (acting like leds[] in FASTLED) ✅
+ * Lights.header and lights.channelsE/D. CRGB leds[] is using lights.channelsE/D (acting like leds[] in FASTLED) ✅
* A Physical layer has one or more virtual layers and a virtual layer has one or more effects using it. ✅
* Presets/playlist: change (part of) the nodes model
diff --git a/interface/src/lib/components/moonbase/RowRenderer.svelte b/interface/src/lib/components/moonbase/RowRenderer.svelte
index 0e00e912..b51a40bc 100644
--- a/interface/src/lib/components/moonbase/RowRenderer.svelte
+++ b/interface/src/lib/components/moonbase/RowRenderer.svelte
@@ -18,7 +18,7 @@
import { page } from '$app/state';
import { slide } from 'svelte/transition';
import { cubicOut } from 'svelte/easing';
- import SearchIcon from '~icons/tabler/search';
+ import SettingsIcon from '~icons/tabler/settings';
import Delete from '~icons/tabler/trash';
import { initCap } from '$lib/stores/moonbase_utilities';
@@ -246,7 +246,7 @@
handleEdit(property.name, itemWrapper.item);
}}
>
-
{#if findItemInDefinition?.crud == null || findItemInDefinition?.crud?.includes('d')}