Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 52 additions & 122 deletions docs/develop/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -85,7 +78,7 @@ graph TB
subgraph Core0["Core 0 (PRO_CPU)"]
WiFi[WiFi/BT<br/>Priority 23]
lwIP[lwIP TCP/IP<br/>Priority 18]
Effect[Effect Task<br/>Priority 3<br/>Computation Only]
Effect[Effect Task<br/>Priority 10<br/>Computation Only]
end

subgraph Core1["Core 1 (APP_CPU)"]
Expand Down Expand Up @@ -139,19 +132,19 @@ Buffer Architecture (PSRAM Only)
```mermaid
graph LR
subgraph MemoryBuffers["Memory Buffers"]
Front[Front Buffer<br/>channels*]
Back[Back Buffer<br/>channelsBack*]
Effects[Effects Buffer<br/>channelsE*]
Drivers[Drivers Buffer<br/>channelsD*]
end

EffectTask[Effect Task<br/>Core 0] -.->|1. memcpy| Back
EffectTask -.->|2. Compute effects| Back
EffectTask -.->|3. Swap pointers<br/>MUTEX 10µs| Front
EffectTask[Effect Task<br/>Core 0] -.->|1. memcpy| Effects
EffectTask -.->|2. Compute effects| Effects
EffectTask -.->|3. Swap pointers<br/>MUTEX 10µs| Drivers

DriverTask[Driver Task<br/>Core 1] -->|4. Read pixels| Front
DriverTask[Driver Task<br/>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
Expand All @@ -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);
}
}
Expand All @@ -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<br/>in memory

User->>UI: Move slider again
UI->>Module: Update state (in-memory)
Note over WriteQueue: Previous write replaced<br/>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<br/>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<int>::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)
Expand Down Expand Up @@ -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

Expand All @@ -357,11 +288,11 @@ Double buffering is **automatically enabled** when PSRAM is detected:
// In PhysicalLayer::setup()
if (psramFound()) {
lights.useDoubleBuffer = true;
lights.channels = allocMB<uint8_t>(maxChannels);
lights.channelsBack = allocMB<uint8_t>(maxChannels);
lights.channelsE = allocMB<uint8_t>(maxChannels);
lights.channelsD = allocMB<uint8_t>(maxChannels);
} else {
lights.useDoubleBuffer = false;
lights.channels = allocMB<uint8_t>(maxChannels);
lights.channelsE = allocMB<uint8_t>(maxChannels);
}
```

Expand All @@ -387,7 +318,7 @@ xTaskCreateUniversal(effectTask,
"AppEffectTask",
psramFound() ? 4 * 1024 : 3 * 1024,
NULL,
3, // Priority
10, // Priority
&effectTaskHandle,
0 // Core 0 (PRO_CPU)
);
Expand All @@ -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. 🚀
2 changes: 1 addition & 1 deletion docs/develop/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions interface/src/lib/components/moonbase/RowRenderer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -246,7 +246,7 @@
handleEdit(property.name, itemWrapper.item);
}}
>
<SearchIcon class="h-6 w-6" /></button
<SettingsIcon class="h-6 w-6" /></button
>
{#if findItemInDefinition?.crud == null || findItemInDefinition?.crud?.includes('d')}
<button
Expand Down
Loading