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')}