Skip to content

Conversation

@ewowi
Copy link
Collaborator

@ewowi ewowi commented Jan 19, 2026

Summary by CodeRabbit

  • New Features

    • Four new visual effects (ColorTwinkle, Plasma, Julia, Police)
    • Audio-driven dynamic palettes and live palette previews in the UI
  • Improvements

    • Expanded palette library (60+ gradients) with selectable palettes
    • Simplified device links using mDNS names and device IPs
    • Refined animation timing and virtual layer index mapping
  • Bug Fixes

    • Safer update handling and reduced mutex contention to avoid stalled updates
  • Documentation

    • Clarified device access guidance (mDNS names and device IPs)

✏️ Tip: You can customize this high-level summary in your review settings.

frontend
=======
- Field renderer: add genPalPrev, add type palette (palette preview)

backend
=======
- Effects: add ColorTwinkle, Plasma, Julia, Police - WIP
- Lights control: add WLED palettes and show previews
@coderabbitai
Copy link

coderabbitai bot commented Jan 19, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'review', 'context'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
📝 Walkthrough

Walkthrough

Adds 60+ gradient palettes and runtime palette APIs, surfaces palettes in the lights UI with previews, introduces four new WLED-style effects, updates audio/infrared drivers to use dynamic palettes, centralizes a VirtualLayer preset index correction, and changes Module update/mutex behavior.

Changes

Cohort / File(s) Summary
Palette data
src/MoonLight/Modules/palettes.h
New file: 60+ gradient palettes as byte arrays, gGradientPalettes, palette_names, plus getGradientPalette(uint8_t) and getPaletteHexString(uint8_t) runtime helpers. Large static data blob to review.
Lights control / UI defs
src/MoonLight/Modules/ModuleLightsControl.h
Switches hard-coded palettes to dynamic palette control; populates options from palette_names, exposes per-palette hex via getPaletteHexString, and loads palettes at runtime.
Frontend palette preview & link rendering
interface/src/lib/components/moonbase/FieldRenderer.svelte
Adds genPalPrev() for CSS linear-gradient preview, introduces palette control UI with preview, simplifies IP/mDNS anchor rendering, and removes a select slot.
Drivers: audio & infrared
src/MoonLight/Nodes/Drivers/D_AudioSync.h, src/MoonLight/Nodes/Drivers/D_Infrared.h
AudioSync: adds getCRGBForBand and getAudioPalette to generate FFT-driven palettes and load them onto layers. Infrared: increases max palette index bounds to cover larger palette set.
Effects: registration & new effects
src/MoonLight/Modules/ModuleEffects.h, src/MoonLight/Nodes/Effects/E_WLED.h
Reorders StarSkyEffect registration/allocation; adds ColorTwinkleEffect, PlasmaEffect, JuliaEffect, PoliceEffect classes. Note: duplicate class declarations appear in E_WLED.h and need deduplication.
Layer mapping correction & blur refactor
src/MoonLight/Layers/VirtualLayer.h, src/MoonLight/Layers/VirtualLayer.cpp
Adds VirtualLayer::presetCorrection(nrOfLights_t&) used for RGB2040 index mapping; refactors blur pipeline (blurRows/blurColumns) to be dimension-agnostic and updates calls.
Module concurrency & updates
src/MoonBase/Module.h, src/MoonBase/Module.cpp
ModuleState::update early-returns on empty data; postUpdate/getUpdate switch to timed mutex attempts and a retry loop to avoid indefinite blocking and reduce critical-section work.
Docs
docs/moonbase/devices.md
Minor wording edits: removed “(controls module)” and updated phrasing to access device “via its IP address”.
Config
platformio.ini
Bump of APP_DATE build flag from 2026011820260122.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as FieldRenderer.svelte
    participant Control as ModuleLightsControl
    participant Pal as palettes.h
    participant Driver as AudioSyncDriver
    participant Effect as WLED_Effect

    User->>UI: select palette (name/index)
    UI->>UI: genPalPrev(hex) -> render preview
    UI->>Control: send palette selection
    Control->>Pal: request palette bytes by index
    Pal-->>Control: return gradient bytes or sentinel
    Control->>Control: getGradientPalette() / getPaletteHexString()
    Control->>Driver: load dynamic palette onto layer
    Driver->>Effect: apply palette to layer
    Effect->>Effect: render loop uses new palette
    Effect-->>User: LEDs updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • prep71 #106 — Similar changes to ModuleState update/mutex handling (postUpdate/getUpdate paths).
  • V070 preps #79 — Overlapping frontend edits to FieldRenderer.svelte (link and palette rendering changes).
  • Board preset add presets #70 — Related VirtualLayer mapping/blur API and palette handling changes.

Suggested labels

enhancement

Poem

🐰
I nibble gradients, byte by byte,
Sixty hues that glow at night,
New twinkles, plasma, Julia spins,
I hop and stash the color bins,
LEDs wake — a rabbit's delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add palettes and effects' directly and clearly summarizes the main changes: introducing new color palettes (palettes.h with 60+ gradient definitions) and new effects (ColorTwinkleEffect, PlasmaEffect, JuliaEffect, PoliceEffect).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@docs/moonbase/devices.md`:
- Around line 9-10: The possessive "it's" is incorrect in the device
instructions; update the text "Click on IP to go to the device via it's IP
address" to use the possessive form "its" instead. Locate the sentence in
docs/moonbase/devices.md (the line containing "Click on IP to go to the device
via it's IP address") and replace "it's" with "its" so it reads "Click on IP to
go to the device via its IP address".

In `@src/MoonLight/Modules/ModuleLightsControl.h`:
- Around line 281-355: The loop that builds hexString for gGradientPalettes uses
a hard cap while (j < 100) which can truncate palettes—replace this fixed limit
with a safe bounds check tied to the actual palette byte length (or a defined
MAX_PALETTE_BYTES) and iterate in 4-byte steps until you encounter an index==255
termination; specifically update the loop that reads from gGradientPalettes[i]
(the variables palette and j) so it ensures (j + 4) <= palette_length (or <=
MAX_PALETTE_BYTES) before reading the next 4 bytes, append the 4 bytes per
entry, and break when palette[j] (the entry index) == 255 to avoid truncation.
- Around line 425-427: The fixed-size memcpy truncates palettes and drops the
0xFF terminator; change to copying from PROGMEM byte-by-byte using
pgm_read_byte() from gGradientPalettes[index] (index computed as
updatedItem.value.as<uint8_t>() - 11) into tcp until you encounter 0xFF or reach
the tcp buffer limit, then ensure tcp contains a valid 0xFF terminator before
calling layerP.palette.loadDynamicGradientPalette(tcp); also ensure tcp’s size
is sufficient for the largest expected palette and set the final byte to 0xFF if
the loop exits due to the size limit.

In `@src/MoonLight/Modules/palettes.h`:
- Around line 28-31: The header uses the Arduino type `byte` for `ib_jul01_gp`
but doesn't include Arduino.h, making compilation order-dependent; update the
header to be self-contained by adding a guarded include or fallback typedef:
check for a prior definition of `byte` (or for ARDUINO macro) and if missing
either include <Arduino.h> or typedef unsigned char byte so `const byte
ib_jul01_gp[]` has a defined type within this header.

In `@src/MoonLight/Nodes/Drivers/D_Infrared.h`:
- Around line 367-369: The clamp for palette increase in the combined_code ==
codePaletteInc branch currently uses MIN(state.data["palette"].as<uint8_t>() +
1, 8 + 3 + 61) which allows an out‑of‑range index (72); change the ceiling to
the last valid index by subtracting 1 (i.e. use (8 + 3 + 61) - 1 or, better, a
shared MAX_PALETTE_INDEX constant) so newState["palette"] is clamped to
0..(count-1); update the same logic location (the combined_code ==
codePaletteInc branch that sets newState["palette"]) and ensure any other uses
of the literal 8+3+61 are replaced with the shared constant or consistently
subtract 1.

In `@src/MoonLight/Nodes/Effects/E_WLED.h`:
- Around line 2143-2254: The Julia and Police effect loop() implementations are
empty causing no output; fix by implementing a minimal placeholder render in
each effect's loop (e.g., in the loop() method in E_WLED.h for Julia and
similarly for Police around the region noted at 2304-2308): call
SEGMENT.setUpLeds(), clear the frame with SEGMENT.fill(BLACK) (or BLACK with
desired brightness), then draw a simple deterministic indicator (for example a
single moving pixel or a low-cost color stripe using SEGMENT.setPixelColorXY or
SEGMENT.setPixelColor) based on strip.now or SEGMENT.custom1 so the effect
visibly updates, and return; alternatively, if you prefer not to ship a
placeholder, unregister/remove these effects from the registry until full ports
are implemented. Ensure you reference and modify the loop() methods for the
Julia and Police effect classes and use SEGMENT APIs (setUpLeds, fill,
setPixelColorXY) to avoid leaving previous frames visible.
- Around line 2010-2063: The loop() for this effect (the overridden loop() in
E_WLED.h / ColorTwinkle) is currently empty and leaves stale LEDs; add a minimal
placeholder: on first call (check SEGENV.call == 0) call SEGMENT.setUpLeds()
then clear the strip by setting every pixel to BLACK (use SEGLEN and
SEGMENT.setPixelColor), or alternatively call SEGMENT.fill(BLACK) to ensure no
stale output, and ensure any required SEGENV.allocateData(...) is invoked if the
effect expects per-pixel state (use SEGENV.allocateData with dataSize computed
from SEGLEN) so the effect doesn’t crash; keep the implementation tiny and
deterministic until the full twinkle logic is ported.
- Around line 2068-2109: PlasmaEffect currently advertises 3D via dim() but only
writes a single column in loop() (layer->setRGB(Coord3D(0, i), ...)), leaving
other LEDs untouched; either change dim() to return _1D to match current
rendering, or modify loop() to iterate X and Z (and optionally clear the layer
first) and call layer->setRGB(Coord3D(x, i, z), ColorFromPalette(...)) for each
coordinate so the effect covers the full 2D/3D plane; update the dim()
implementation or the loop() iteration accordingly in the PlasmaEffect class.
🧹 Nitpick comments (2)
src/MoonLight/Modules/ModuleEffects.h (1)

138-141: Keep WLED effects list alphabetical.

The section is labeled “alphabetically” but the new effects are out of order. Reordering keeps UI and allocation lists consistent.

♻️ Suggested reorder
-    addControlValue(control, getNameAndTags<ColorTwinkleEffect>());
-    addControlValue(control, getNameAndTags<PlasmaEffect>());
-    addControlValue(control, getNameAndTags<JuliaEffect>());
-    addControlValue(control, getNameAndTags<PoliceEffect>());
+    addControlValue(control, getNameAndTags<ColorTwinkleEffect>());
+    addControlValue(control, getNameAndTags<JuliaEffect>());
+    addControlValue(control, getNameAndTags<PlasmaEffect>());
+    addControlValue(control, getNameAndTags<PoliceEffect>());
-    if (!node) node = checkAndAlloc<ColorTwinkleEffect>(name);
-    if (!node) node = checkAndAlloc<PlasmaEffect>(name);
-    if (!node) node = checkAndAlloc<JuliaEffect>(name);
-    if (!node) node = checkAndAlloc<PoliceEffect>(name);
+    if (!node) node = checkAndAlloc<ColorTwinkleEffect>(name);
+    if (!node) node = checkAndAlloc<JuliaEffect>(name);
+    if (!node) node = checkAndAlloc<PlasmaEffect>(name);
+    if (!node) node = checkAndAlloc<PoliceEffect>(name);

Also applies to: 235-238

interface/src/lib/components/moonbase/FieldRenderer.svelte (1)

189-207: Move the inline <style> block to component scope.

Embedding <style> inside the template inserts a <style> element per row and bypasses Svelte’s scoped styling. Prefer a top-level <style> section.

♻️ Suggested change (remove inline style)
-		<style>
-			.palette-preview {
-				width: 250px;
-				height: 40px;
-				border: 1px solid `#ccc`;
-				border-radius: 3px;
-			}
-		</style>

Add this at the bottom of the component instead:

<style>
	.palette-preview {
		width: 250px;
		height: 40px;
		border: 1px solid `#ccc`;
		border-radius: 3px;
	}
</style>

Comment on lines 425 to 427
byte tcp[76] = {255}; // WLEDMM: prevent out-of-range access in loadDynamicGradientPalette()
memcpy(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[updatedItem.value.as<uint8_t>() - 11])), 72);
layerP.palette.loadDynamicGradientPalette(tcp);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Find files related to palette handling
fd -t f "\.h$|\.cpp$" | xargs rg -l "loadDynamicGradientPalette|gGradientPalettes" | head -20

Repository: MoonModules/MoonLight

Length of output: 141


🏁 Script executed:

# Search for loadDynamicGradientPalette implementation details
rg -A 10 "loadDynamicGradientPalette" --type cpp --type h

Repository: MoonModules/MoonLight

Length of output: 1361


🏁 Script executed:

# Check gradient palette definitions and sizes
rg -B 2 -A 5 "gGradientPalettes" --type cpp --type h | head -50

Repository: MoonModules/MoonLight

Length of output: 2130


🏁 Script executed:

# Look for FastLED palette structure in the codebase
rg "CRGBPalette" --type cpp --type h | head -20

Repository: MoonModules/MoonLight

Length of output: 546


🏁 Script executed:

# Read the palettes.h file to see palette definitions
cat src/MoonLight/Modules/palettes.h | head -100

Repository: MoonModules/MoonLight

Length of output: 4786


🏁 Script executed:

# Search for the actual gradient palette definitions
rg -A 2 "gp =" src/MoonLight/Modules/palettes.h | head -40

Repository: MoonModules/MoonLight

Length of output: 47


🏁 Script executed:

# Check what size gradients typically are by looking at their PROGMEM definitions
rg "PROGMEM|gp\[" src/MoonLight/Modules/palettes.h | head -20

Repository: MoonModules/MoonLight

Length of output: 2895


🏁 Script executed:

# Count the actual byte size of all palette definitions
python3 << 'EOF'
import re

with open('src/MoonLight/Modules/palettes.h', 'r') as f:
    content = f.read()

# Find all palette definitions
pattern = r'const byte (\w+)\[\] = \{([^}]+)\}'
matches = re.findall(pattern, content)

sizes = []
for name, data in matches:
    # Count commas to get byte count
    byte_count = len(data.split(','))
    sizes.append((name, byte_count))
    
# Sort by size
sizes.sort(key=lambda x: x[1], reverse=True)

print("Top 15 largest palettes:")
for name, size in sizes[:15]:
    print(f"  {name}: {size} bytes")
    
print(f"\nTotal palettes: {len(sizes)}")
print(f"Max size: {max(s[1] for s in sizes)} bytes")
print(f"Min size: {min(s[1] for s in sizes)} bytes")
EOF

Repository: MoonModules/MoonLight

Length of output: 151


🏁 Script executed:

# Check if there are any palettes >= 144 bytes
rg "const byte.*\[\] = \{" src/MoonLight/Modules/palettes.h | wc -l

Repository: MoonModules/MoonLight

Length of output: 67


🏁 Script executed:

# Look for the loadDynamicGradientPalette implementation in FastLED
rg -B 5 -A 15 "loadDynamicGradientPalette" --type h | head -60

Repository: MoonModules/MoonLight

Length of output: 2338


🏁 Script executed:

# Find the largest palette definitions
rg "const byte.*\[\] = \{" src/MoonLight/Modules/palettes.h -A 0 | while read line; do
  name=$(echo "$line" | sed 's/.*const byte \([a-zA-Z_]*\).*/\1/')
  # Extract just the declaration and count braces
  size=$(rg "const byte $name\[\] = \{[^}]+\}" src/MoonLight/Modules/palettes.h | grep -o "{[^}]*}" | head -1 | tr ',' '\n' | wc -l)
  echo "$size $name"
done | sort -rn | head -20

Repository: MoonModules/MoonLight

Length of output: 592


🏁 Script executed:

# Search for documented sizes in the file
rg "Size: [0-9]+ bytes" src/MoonLight/Modules/palettes.h | sed 's/.*Size: \([0-9]*\).*/\1/' | sort -rn | uniq

Repository: MoonModules/MoonLight

Length of output: 106


🏁 Script executed:

# Get specific info about the largest palettes mentioned
rg -B 3 "Size: [0-9]+ bytes" src/MoonLight/Modules/palettes.h | grep -E "(Size:|Gradient palette)" | paste - - | sort -t: -k2 -rn | head -20

Repository: MoonModules/MoonLight

Length of output: 108


🏁 Script executed:

# Find which palette is 144 bytes
rg -B 5 "Size: 144 bytes" src/MoonLight/Modules/palettes.h

Repository: MoonModules/MoonLight

Length of output: 351


🏁 Script executed:

# Get all palettes sorted by size to understand the distribution
rg -B 3 "Size: [0-9]+ bytes" src/MoonLight/Modules/palettes.h | rg "Size: [0-9]+" | sed 's/.*Size: \([0-9]*\).*/\1/' | sort -n | tail -20

Repository: MoonModules/MoonLight

Length of output: 125


🏁 Script executed:

# Search for FastLED palette loading code in the repository
rg -i "loadDynamicGradientPalette|gradient.*palette" --type h --type cpp | grep -i "load\|terminator\|255" | head -20

Repository: MoonModules/MoonLight

Length of output: 310


🏁 Script executed:

# Check the exact temperature_gp definition to understand its structure
rg -A 40 "Gradient palette.*temperature_gp" src/MoonLight/Modules/palettes.h

Repository: MoonModules/MoonLight

Length of output: 1995


Copy gradient palettes completely until terminator, not with fixed byte limit.

The current code copies only 72 bytes, which truncates the temperature_gp palette (144 bytes) and removes its terminator (0xFF entry), causing loadDynamicGradientPalette() to read out-of-bounds. The buffer initialization byte tcp[76] = {255} only protects position 0; the rest is zero-initialized, not a valid terminator. Repack with a copy-until-terminator loop using pgm_read_byte() to safely handle palettes of any size up to the buffer limit.

🤖 Prompt for AI Agents
In `@src/MoonLight/Modules/ModuleLightsControl.h` around lines 425 - 427, The
fixed-size memcpy truncates palettes and drops the 0xFF terminator; change to
copying from PROGMEM byte-by-byte using pgm_read_byte() from
gGradientPalettes[index] (index computed as updatedItem.value.as<uint8_t>() -
11) into tcp until you encounter 0xFF or reach the tcp buffer limit, then ensure
tcp contains a valid 0xFF terminator before calling
layerP.palette.loadDynamicGradientPalette(tcp); also ensure tcp’s size is
sufficient for the largest expected palette and set the final byte to 0xFF if
the loop exits due to the size limit.

Comment on lines +367 to +369
} else if (combined_code == codePaletteInc) { // palette increase
newState["palette"] = MIN(state.data["palette"].as<uint8_t>() + 1, 8 + 3 + 61); // 8 FastLED + 3 custom + 61 WLED palettes. to do: replace nr with max palette count
} else if (combined_code == codePaletteDec) { // palette decrease
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clamp max palette index to the last valid value.

Line 368 clamps to 72 (the palette count), but valid indices are 0–71. This allows palette=72, which will index past gGradientPalettes in ModuleLightsControl. Clamp to count − 1 (or a shared max constant).

🛠️ Proposed fix
-            newState["palette"] = MIN(state.data["palette"].as<uint8_t>() + 1, 8 + 3 + 61);  // 8 FastLED + 3 custom + 61 WLED palettes. to do: replace nr with max palette count
+            newState["palette"] = MIN(state.data["palette"].as<uint8_t>() + 1, (uint8_t)(8 + 3 + 61 - 1));  // max index = count-1
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} else if (combined_code == codePaletteInc) { // palette increase
newState["palette"] = MIN(state.data["palette"].as<uint8_t>() + 1, 8 + 3 + 61); // 8 FastLED + 3 custom + 61 WLED palettes. to do: replace nr with max palette count
} else if (combined_code == codePaletteDec) { // palette decrease
} else if (combined_code == codePaletteInc) { // palette increase
newState["palette"] = MIN(state.data["palette"].as<uint8_t>() + 1, (uint8_t)(8 + 3 + 61 - 1)); // max index = count-1
} else if (combined_code == codePaletteDec) { // palette decrease
🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Drivers/D_Infrared.h` around lines 367 - 369, The clamp
for palette increase in the combined_code == codePaletteInc branch currently
uses MIN(state.data["palette"].as<uint8_t>() + 1, 8 + 3 + 61) which allows an
out‑of‑range index (72); change the ceiling to the last valid index by
subtracting 1 (i.e. use (8 + 3 + 61) - 1 or, better, a shared MAX_PALETTE_INDEX
constant) so newState["palette"] is clamped to 0..(count-1); update the same
logic location (the combined_code == codePaletteInc branch that sets
newState["palette"]) and ensure any other uses of the literal 8+3+61 are
replaced with the shared constant or consistently subtract 1.

backend
=======
- virtual layer: add presetCorrection hook in forEachLightIndex (for lightPreset_RGB2040)
- AudioSync: implement audio reactive palettes
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/MoonLight/Layers/VirtualLayer.h (1)

134-138: Apply presetCorrection in the unmapped path for RGB2040 support.

The presetCorrection applies an index adjustment specific to RGB2040 preset (indexP += (indexP / 20) * 20), but this is only called in the m_oneLight and m_moreLights cases. When oneToOneMapping is true (direct virtual-to-physical index mapping), the unmapped path is taken and calls callback(indexV) directly, bypassing the correction.

Since RGB2040 preset is orthogonal to mapping type, it can be used with regular layouts that maintain 1:1 mapping. In that scenario, the preset correction would not be applied, resulting in incorrect index handling for RGB2040 LEDs. Add presetCorrection(indexV) before the callback in the unmapped path.

🧹 Nitpick comments (2)
src/MoonLight/Nodes/Drivers/D_AudioSync.h (1)

69-91: Consider initializing value or adding a default case for defensive coding.

If pal is outside the 0-2 range, value relies on CRGB's default constructor (which initializes to black). While currently safe since callers only pass 0-2, explicit initialization would make the intent clearer and guard against future changes.

♻️ Suggested improvement
   CRGB getCRGBForBand(int x, uint8_t* fftResult, int pal) {
-    CRGB value;
+    CRGB value = CRGB::Black;
     CHSV hsv;
     if (pal == 0) {  // bit hacky to use palette id here, but don't want to litter the code with lots of different methods. TODO: add enum for palette creation type
src/MoonLight/Layers/VirtualLayer.cpp (1)

84-87: Consider adding a brief comment explaining the RGB2040 physical layout.

The formula indexP += (indexP / 20) * 20 creates a sparse physical mapping where lights 0-19 stay at 0-19, lights 20-39 map to 40-59, etc. A short comment explaining why this layout is needed for RGB2040 would aid maintainability.

📝 Suggested documentation
 void VirtualLayer::presetCorrection(nrOfLights_t& indexP) const {
+  // RGB2040 has physical layout with alternating 20-light segments:
+  // virtual [0..19] -> physical [0..19], virtual [20..39] -> physical [40..59], etc.
   if (layerP->lights.header.lightPreset == lightPreset_RGB2040) indexP += (indexP / 20) * 20;
 }

back end
========
- Module: fix bug in post/getUpdate ! Avoid missing updates
- Effects: Implement ColorTwinkle, Plasma, Julia, Police
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/MoonLight/Nodes/Effects/E_WLED.h`:
- Around line 2002-2049: The loop() method uses the data pointer (accessing
data[index] and bitRead/bitWrite) without checking for null; add a guard at the
start of E_WLED::loop() that returns immediately if data == nullptr or layer ==
nullptr or layer->nrOfLights == 0 to match other effects (e.g.,
BouncingBallsEffect/TetrixEffect); this prevents dereferencing data before it is
allocated in onSizeChanged() — locate the data accesses in loop(), add the
null-check and early return, and keep existing behavior otherwise.
🧹 Nitpick comments (3)
src/MoonBase/Module.cpp (2)

289-293: Consider removing commented-out debug code.

This commented-out logging block adds noise without providing value. If debugging is needed in the future, it can be easily re-added or enabled via a debug flag.

🧹 Suggested removal
       bool changed = state.checkReOrderSwap("", state.data, newData, updatedItem);

-      // if (originId != "devicesserver" && originId != "tasksserver") {
-      //   String ss;
-      //   serializeJson(newData, ss);
-      //   EXT_LOGD(ML_TAG, "newData %s from %s", ss.c_str(), originId.c_str());
-      // }
-
       // EXT_LOGD(ML_TAG, "update isNew %d changed %d", isNew, changed);

279-310: Early guard for empty data is a good defensive addition.

The logic correctly short-circuits when newData is empty. One minor suggestion: inverting the condition would reduce nesting depth and improve readability.

♻️ Optional: flatten structure with early return
 StateUpdateResult ModuleState::update(JsonObject& newData, ModuleState& state, const String& originId) {
   updateOriginId = originId;

-  if (newData.size() != 0) {  // in case of empty file
-
-    // check which controls have updated
-    if (newData != state.data) {
-      UpdatedItem updatedItem;
-
-      bool changed = state.checkReOrderSwap("", state.data, newData, updatedItem);
-
-      if (state.compareRecursive("", state.data, newData, updatedItem)) {
-        if (changed) EXT_LOGW(ML_TAG, "checkReOrderSwap changed, compareRecursive also changed? %s", originId.c_str());
-        changed = true;
-      }
-
-      return (changed) ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED;
-    } else
-      return StateUpdateResult::UNCHANGED;
-  } else {
+  if (newData.size() == 0) {
     EXT_LOGW(MB_TAG, "empty newData %s", originId.c_str());
     return StateUpdateResult::UNCHANGED;
   }
+
+  // check which controls have updated
+  if (newData == state.data) {
+    return StateUpdateResult::UNCHANGED;
+  }
+
+  UpdatedItem updatedItem;
+  bool changed = state.checkReOrderSwap("", state.data, newData, updatedItem);
+
+  if (state.compareRecursive("", state.data, newData, updatedItem)) {
+    if (changed) EXT_LOGW(ML_TAG, "checkReOrderSwap changed, compareRecursive also changed? %s", originId.c_str());
+    changed = true;
+  }
+
+  return changed ? StateUpdateResult::CHANGED : StateUpdateResult::UNCHANGED;
 }
src/MoonLight/Nodes/Effects/E_WLED.h (1)

2237-2253: Variable shadowing: local width shadows member width.

Line 2242 declares a local uint16_t width that shadows the member variable uint8_t width. While the code works (the member is used on the right side before shadowing takes effect), this is confusing and error-prone for future maintenance.

♻️ Rename local variable for clarity
   void police_base(uint32_t color1, uint32_t color2) {
     uint16_t delay = 1 + (1000 / 40 << 3) / layer->size.y;  // longer segments should change faster
     uint32_t it = pal::millis() / map(speed, 0, 255, delay << 4, delay);
     uint16_t offset = it % layer->size.y;

-    uint16_t width = ((layer->size.y * (width + 1)) >> 9);  // max width is half the strip
-    if (!width) width = 1;
-    for (int i = 0; i < width; i++) {
+    uint16_t pixelWidth = ((layer->size.y * (width + 1)) >> 9);  // max width is half the strip
+    if (!pixelWidth) pixelWidth = 1;
+    for (int i = 0; i < pixelWidth; i++) {
       uint16_t indexR = (offset + i) % layer->size.y;
       uint16_t indexB = (offset + i + (layer->size.y >> 1)) % layer->size.y;

Comment on lines 2002 to 2049
void loop() override {
CRGB fastled_col, prev;
uint8_t brightness = 128; // strip.getBrightness()
fract8 fadeUpAmount = brightness > 28 ? 8 + (fadeSpeed >> 2) : 68 - brightness;
fract8 fadeDownAmount = brightness > 28 ? 8 + (fadeSpeed >> 3) : 68 - brightness;
for (uint16_t i = 0; i < layer->nrOfLights; i++) {
fastled_col = layer->getRGB(i);
prev = fastled_col;
uint16_t index = i >> 3;
uint8_t bitNum = i & 0x07;
bool fadeUp = bitRead(data[index], bitNum);

if (fadeUp) {
CRGB incrementalColor = fastled_col;
incrementalColor.nscale8_video(fadeUpAmount);
fastled_col += incrementalColor;

if (fastled_col.red == 255 || fastled_col.green == 255 || fastled_col.blue == 255) {
bitWrite(data[index], bitNum, false);
}
layer->setRGB(i, fastled_col);

if (layer->getRGB(i) == prev) { // fix "stuck" pixels
fastled_col += fastled_col;
layer->setRGB(i, fastled_col);
}
} else {
fastled_col.nscale8(255 - fadeDownAmount);
layer->setRGB(i, fastled_col);
}
}

for (uint16_t j = 0; j <= layer->nrOfLights / 50; j++) {
if (random8() <= spawnSpeed) {
for (uint8_t times = 0; times < 5; times++) { // attempt to spawn a new pixel 5 times
int i = random16(layer->nrOfLights);
if (layer->getRGB(i) == 0) {
fastled_col = ColorFromPalette(layerP.palette, random8(), 64, NOBLEND);
uint16_t index = i >> 3;
uint8_t bitNum = i & 0x07;
bitWrite(data[index], bitNum, true);
layer->setRGB(i, fastled_col);
break; // only spawn 1 new pixel per frame per 50 LEDs
}
}
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing null check for data pointer before use.

The loop() method accesses data[index] without first checking if data is nullptr. If allocation fails in onSizeChanged() or if loop() is called before onSizeChanged(), this will cause a crash. Other effects in this file guard against this (e.g., BouncingBallsEffect at line 38, TetrixEffect at line 991).

🐛 Proposed fix
   void loop() override {
+    if (!data) return;
+
     CRGB fastled_col, prev;
     uint8_t brightness = 128;  // strip.getBrightness()
🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Effects/E_WLED.h` around lines 2002 - 2049, The loop()
method uses the data pointer (accessing data[index] and bitRead/bitWrite)
without checking for null; add a guard at the start of E_WLED::loop() that
returns immediately if data == nullptr or layer == nullptr or layer->nrOfLights
== 0 to match other effects (e.g., BouncingBallsEffect/TetrixEffect); this
prevents dereferencing data before it is allocated in onSizeChanged() — locate
the data accesses in loop(), add the null-check and early return, and keep
existing behavior otherwise.

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 21, 2026

@coderabbitai, can you review the update on postUpdate / getUpdate in latest commit, can you confirm it was faulty before and okay now?

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the diff for Module.h to see the actual changes
git diff main...HEAD -- src/MoonBase/Module.h

Length of output: 33


🏁 Script executed:

#!/bin/bash
# Get recent commits for Module.h
git log --oneline -5 -- src/MoonBase/Module.h

echo "=== Latest commit diff for Module.h ==="
# Get the diff from the latest commit that touched this file
git log -1 --pretty=format:"%H" -- src/MoonBase/Module.h | xargs -I {} git show {}:src/MoonBase/Module.h > /dev/null 2>&1

# Show the latest commit's changes to Module.h
git log -p -1 -- src/MoonBase/Module.h

Length of output: 239


🏁 Script executed:

#!/bin/bash
# Show the diff for Module.h from the latest commit
echo "=== Changes in commit 0312be5 ==="
git show 0312be5 -- src/MoonBase/Module.h

echo ""
echo "=== Current Module.h content (relevant sections) ==="
# Show the postUpdate and getUpdate methods
sed -n '/postUpdate/,/^[[:space:]]*}/p' src/MoonBase/Module.h | head -50
echo ""
sed -n '/getUpdate/,/^[[:space:]]*}/p' src/MoonBase/Module.h | head -50

Length of output: 3256


@ewowi, yes, I can confirm the previous implementation had a critical bug and the new code fixes it correctly.

The Problem in the Old Code

The old postUpdate had a race condition that could cause lost updates:

// OLD CODE - BUGGY
if (xSemaphoreTake(updateMutex, portMAX_DELAY) == pdTRUE) {
    this->updatedItem = updatedItem;  // ⚠️ Overwrites without checking updatePending!
    updatePending = true;
    xSemaphoreGive(updateMutex);
}

The bug: If a producer called postUpdate() while updatePending was still true (the consumer hadn't processed the previous update yet), the old code would immediately overwrite updatedItem with the new data. The previous update would be silently dropped.

How the New Code Fixes It

The new implementation properly serializes updates:

// NEW CODE - CORRECT
while (true) {
    if (xSemaphoreTake(updateMutex, pdMS_TO_TICKS(10)) == pdTRUE) {
        if (!updatePending) {  // ✓ Only write when previous update consumed
            this->updatedItem = updatedItem;
            updatePending = true;
            xSemaphoreGive(updateMutex);
            break;
        }
        xSemaphoreGive(updateMutex);
    }
    vTaskDelay(pdMS_TO_TICKS(1));  // Wait and retry
}

Key improvements:

  1. Prevents data loss: Waits in a loop until !updatePending before writing
  2. Safer timeout: Uses 10ms timeout instead of portMAX_DELAY (prevents indefinite blocking)
  3. Proper synchronization: Ensures only one update is queued at a time

The fix is correct and addresses a real concurrency issue. Well done on catching and fixing this! 👍

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/MoonLight/Nodes/Effects/E_WLED.h`:
- Around line 1999-2001: Add a destructor to class E_WLED that frees the heap
buffer allocated in onSizeChanged: if the member uint8_t* data is non-null, call
delete[] data, set data to nullptr and dataSize to 0; also consider marking copy
constructor/assignment as deleted or implement proper copy/move semantics to
avoid double-free/ownership bugs since data is a raw pointer.
♻️ Duplicate comments (1)
src/MoonLight/Modules/palettes.h (1)

25-31: Make byte definition self-contained.

This header uses byte (an Arduino-specific type alias for uint8_t) without including Arduino.h. This causes compilation failures in non-Arduino build environments, as confirmed by static analysis.

🛠️ Proposed fix
 `#ifndef` Palettes_h
 `#define` Palettes_h

-// `#include` <Arduino.h>
+#ifndef byte
+  `#include` <cstdint>
+  using byte = uint8_t;
+#endif

 const byte ib_jul01_gp[] = {0, 194, 1, 1, 94, 1, 29, 18, 132, 57, 131, 28, 255, 113, 1, 1};

Comment on lines +1999 to +2001
uint8_t* data = nullptr;
size_t dataSize = 0;

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing destructor causes memory leak.

The data pointer allocated in onSizeChanged is never freed. Add a destructor to prevent memory leaks when the effect is destroyed.

🐛 Proposed fix
   uint8_t* data = nullptr;
   size_t dataSize = 0;

+  ~ColorTwinkleEffect() override {
+    if (data) freeMB(data, "data");
+  }
+
   void onSizeChanged(const Coord3D& prevSize) override { reallocMB2<uint8_t>(data, dataSize, (layer->nrOfLights + 7) >> 3, "data"); }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
uint8_t* data = nullptr;
size_t dataSize = 0;
uint8_t* data = nullptr;
size_t dataSize = 0;
~ColorTwinkleEffect() override {
if (data) freeMB(data, "data");
}
void onSizeChanged(const Coord3D& prevSize) override { reallocMB2<uint8_t>(data, dataSize, (layer->nrOfLights + 7) >> 3, "data"); }
🤖 Prompt for AI Agents
In `@src/MoonLight/Nodes/Effects/E_WLED.h` around lines 1999 - 2001, Add a
destructor to class E_WLED that frees the heap buffer allocated in
onSizeChanged: if the member uint8_t* data is non-null, call delete[] data, set
data to nullptr and dataSize to 0; also consider marking copy
constructor/assignment as deleted or implement proper copy/move semantics to
avoid double-free/ownership bugs since data is a raw pointer.

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 21, 2026

@coderabbitai, can you review the blur functions in virtual layer.h as they do not work as intended, eg blackhole calls blur2d but is not showing blur

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read VirtualLayer.h to examine blur function implementations
echo "=== VirtualLayer.h blur functions ==="
sed -n '/blur/,/^[[:space:]]*}$/p' src/MoonLight/Layers/VirtualLayer.h | head -200

echo ""
echo "=== Blackhole effect usage of blur ==="
# Find Blackhole class and its blur usage
sed -n '/class.*Blackhole/,/^};$/p' src/MoonLight/Nodes/Effects/E_WLED.h | grep -A 10 -B 10 'blur'

Length of output: 3731


🏁 Script executed:

#!/bin/bash
# Get more context on the blur function definitions
echo "=== Full blur2d function ==="
ast-grep --pattern $'void blur2d($$$) {
  $$$
}'

echo ""
echo "=== Search for all blur-related methods in VirtualLayer ==="
rg -n 'blur' src/MoonLight/Layers/VirtualLayer.h -A 5 -B 2

Length of output: 2258


🏁 Script executed:

#!/bin/bash
# Check how Blackhole uses blur2d - get the full loop method
echo "=== Blackhole loop method ==="
sed -n '/class Blackhole/,/^class [A-Z]/p' src/MoonLight/Nodes/Effects/E_WLED.h | sed -n '/void loop()/,/^[[:space:]]*}$/p' | head -100

Length of output: 667


🏁 Script executed:

#!/bin/bash
# Check getRGB and setRGB implementations to see which buffer they operate on
echo "=== getRGB implementation in VirtualLayer ==="
rg -n 'CRGB getRGB' src/MoonLight/Layers/VirtualLayer.h -A 10 -B 2

echo ""
echo "=== setRGB implementation in VirtualLayer ==="
rg -n 'void setRGB' src/MoonLight/Layers/VirtualLayer.h -A 10 -B 2

echo ""
echo "=== addRGB implementation in VirtualLayer ==="
rg -n 'void addRGB' src/MoonLight/Layers/VirtualLayer.h -A 10 -B 2

Length of output: 7224


🏁 Script executed:

#!/bin/bash
# Check if there's any channel buffer management in VirtualLayer
echo "=== Channel buffer references in VirtualLayer ==="
rg -n 'channels[DE]|buffer' src/MoonLight/Layers/VirtualLayer.h -B 2 -A 2 | head -50

echo ""
echo "=== VirtualLayer class definition and member variables ==="
sed -n '/^class VirtualLayer/,/^private:/p' src/MoonLight/Layers/VirtualLayer.h | head -80

Length of output: 6475


🏁 Script executed:

#!/bin/bash
# Look at the base Layer class to understand buffer architecture
echo "=== Layer.h class definition ==="
rg -n 'class Layer' src/MoonLight/Layers/Layer.h -A 30 | head -100

echo ""
echo "=== channelsE usage in Layer ==="
rg -n 'channelsE' src/MoonLight/Layers/ -A 2 -B 2 | head -50

Length of output: 4957


🏁 Script executed:

#!/bin/bash
# Check fadeToBlackBy implementation to understand what it does
echo "=== fadeToBlackBy implementation ==="
rg -n 'void fadeToBlackBy' src/MoonLight/Layers/ -A 15 -B 2

echo ""
echo "=== Check if there's a fadeToBlackMin implementation ==="
rg -n 'void fadeToBlackMin' src/MoonLight/Layers/ -A 15 -B 2

Length of output: 2985


🏁 Script executed:

#!/bin/bash
# Look for PhysicalLayer definition to understand the lights struct
echo "=== Search for PhysicalLayer struct/class ==="
rg -n 'struct PhysicalLayer|class PhysicalLayer' src/MoonLight/ -A 20 | head -100

echo ""
echo "=== Look for lights.channelsE and channelsD definitions ==="
rg -n 'channelsE|channelsD' src/MoonLight/Layers/VirtualLayer.cpp -B 2 -A 2 | head -60

Length of output: 3328


🏁 Script executed:

#!/bin/bash
# Check the complete blurRows and blurColumns to see if there's a missing piece
echo "=== Complete blurRows function ==="
sed -n '/void blurRows/,/^[[:space:]]*}$/p' src/MoonLight/Layers/VirtualLayer.h

echo ""
echo "=== Complete blurColumns function ==="
sed -n '/void blurColumns/,/^[[:space:]]*}$/p' src/MoonLight/Layers/VirtualLayer.h

Length of output: 882


🏁 Script executed:

#!/bin/bash
# Get complete blurRows and blurColumns functions with more context
echo "=== Complete blurRows function (with more lines) ==="
sed -n '/void blurRows/,/^  }/p' src/MoonLight/Layers/VirtualLayer.h | head -50

echo ""
echo "=== Line numbers around blur functions ==="
rg -n 'void blur(Rows|Columns|2d)' src/MoonLight/Layers/VirtualLayer.h

Length of output: 1076


🏁 Script executed:

#!/bin/bash
# Extract the entire blur section from the file
echo "=== All blur-related code in VirtualLayer.h ==="
awk '/void blur1d/,/^  void [a-z]/ {print NR": "$0}' src/MoonLight/Layers/VirtualLayer.h | head -100

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Check lines 320-365 to see the complete blur functions
echo "=== Lines 315-365 of VirtualLayer.h (blur functions region) ==="
sed -n '315,365p' src/MoonLight/Layers/VirtualLayer.h

Length of output: 1762


@ewowi, I found the bug! 🎯 Both blurRows and blurColumns are missing the final carryover write at the end of their loops.

The Problem

Looking at lines 320-361 in VirtualLayer.h, both blur functions follow this pattern:

for (uint16_t i = 0; i < width; i++) {  // (or height for columns)
    CRGB cur = getRGB(...);
    CRGB part = cur;
    part.nscale8(seep);
    cur.nscale8(keep);
    cur += carryover;
    if (i) addRGB(..., part);  // add to PREVIOUS pixel
    setRGB(..., cur);
    carryover = part;  // save for next iteration
}
// ⚠️ Loop ends here - carryover is lost!

After the loop completes, carryover contains the seep portion from the last pixel in each row/column, but it's never written anywhere. This means:

  • Each row loses the blur contribution from its rightmost pixel
  • Each column loses the blur contribution from its bottom pixel
  • The blur effect is significantly weakened (losing ~3-4% of blur data per pass)

The Fix

Add the final carryover write after each inner loop:

For blurRows (after line 341, before the closing }):

for (uint16_t row = 0; row < height; row++) {
    CRGB carryover = CRGB::Black;
    for (uint16_t i = 0; i < width; i++) {
        // ... existing code ...
        carryover = part;
    }
    // Add this line:
    addRGB(Coord3D(width - 1, row), carryover);
}

For blurColumns (after line 360, before the closing }):

for (uint16_t col = 0; col < width; ++col) {
    CRGB carryover = CRGB::Black;
    for (uint16_t i = 0; i < height; ++i) {
        // ... existing code ...
        carryover = part;
    }
    // Add this line:
    addRGB(Coord3D(col, height - 1), carryover);
}

This ensures the blur properly distributes across the entire matrix and should make the blur visible in Blackhole and other effects.

Would you like me to create a code change plan to fix both functions?


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 21, 2026

@coderabbitai, that is better indeed , is it also possible to keep the original pixels as bright as they were and also the blur
Pixels more bright? It now dims the effects too much

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

Tip

For best results, initiate chat on the files or code changes.

@ewowi, yes! The current blur is energy-redistributing (like FastLED's design)—it dims pixels because each pixel keeps only ~33% of its brightness and gives the rest to neighbors. FastLED's blur intentionally fades to black over repeated calls.

For additive blur (bright originals + blur on top), we need to change the algorithm:

Current Algorithm (Energy-Redistributing)

keep = 255 - blur_amount;  // 83 (~33%)
seep = blur_amount >> 1;    // 86 (~34%)
cur.nscale8(keep);  // ⚠️ Dims to 33%
cur += carryover;

New Algorithm (Additive Blur)

Remove the cur.nscale8(keep) line so pixels keep 100% brightness:

blurRows (around line 327-341):

void blurRows(uint8_t blur_amount) {
    uint8_t seep = blur_amount >> 1;  // Neighbor contribution
    // NO keep scaling - pixels stay 100% bright
    
    for (uint16_t row = 0; row < height; row++) {
        CRGB carryover = CRGB::Black;
        for (uint16_t i = 0; i < width; i++) {
            CRGB cur = getRGB(Coord3D(i, row));
            CRGB part = cur;
            part.nscale8(seep);
            // DON'T scale cur - keep original brightness
            cur += carryover;
            if (i) addRGB(Coord3D(i - 1, row), part);
            setRGB(Coord3D(i, row), cur);
            carryover = part;
        }
        addRGB(Coord3D(width - 1, row), carryover);  // Final carryover
    }
}

blurColumns (similar change around line 346-360):

void blurColumns(uint8_t blur_amount) {
    uint8_t seep = blur_amount >> 1;
    
    for (uint16_t col = 0; col < width; ++col) {
        CRGB carryover = CRGB::Black;
        for (uint16_t i = 0; i < height; ++i) {
            CRGB cur = getRGB(Coord3D(col, i));
            CRGB part = cur;
            part.nscale8(seep);
            // DON'T scale cur
            cur += carryover;
            if (i) addRGB(Coord3D(col, i - 1), part);
            setRGB(Coord3D(col, i), cur);
            carryover = part;
        }
        addRGB(Coord3D(col, height - 1), carryover);  // Final carryover
    }
}

This way:

  • ✓ Original pixels stay at 100% brightness
  • ✓ Blur adds light from neighbors (seep ~33%)
  • ✓ Effects stay bright while getting soft glow

The trade-off: brightness increases, so you may want to reduce blur_amount or the effect's master brightness to prevent saturation. Would you like me to generate a code change plan?


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:06.484Z
Learning: In MoonLight: VirtualLayer.size is ONLY synchronized with PhysicalLayer.lights.header.size during pass 2 (VirtualLayer::onLayoutPre at line 349 sets size = layerP->lights.header.size). If pass 2 (requestMapVirtual) doesn't run, VirtualLayer.size remains at its default {16, 16, 1}, causing effects to allocate buffers and iterate over the wrong dimensions. Pass 2 must always run after pass 1 to ensure VirtualLayer gets the correct size from PhysicalLayer.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 109
File: src/MoonLight/Layers/VirtualLayer.h:160-167
Timestamp: 2026-01-19T11:04:19.864Z
Learning: In MoonLight channel buffer architecture: channelsE and channelsD always store RGBW in canonical fixed order at offsetRGBW+0 (R), offsetRGBW+1 (G), offsetRGBW+2 (B), offsetRGBW+3 (W). The offsetRed/Green/Blue/White fields are NOT used for channelsE/D layout—they are used ONLY by drivers (rgbwBufferMapping, parlio, etc.) to reorder data when writing to hardware output buffers. This keeps effects cache-friendly (contiguous RGB writes) while allowing drivers to handle hardware-specific color orders like GRB, BGR, WRGB, etc.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).

💡 Use @coderabbitai plan to create a plan for code changes

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 21, 2026

@coderabbitai, if we don't scale cur , the whole screen fills up with solid color,if we scale cur, to much blur means nothing is shown. Is there a better algorithm which keeps the brightness not not fill up the whole screen ?

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

Tip

For best results, initiate chat on the files or code changes.

@ewowi, you're right! The issue is that additive blur accumulates energy (fills the screen), while scaled blur loses energy (dims too much). We need a normalized box blur that maintains total brightness while redistributing it smoothly.

The Solution: Weighted Average Box Blur

Instead of scaling then adding, compute a weighted average of each pixel with its neighbors:

void blurRows(uint8_t blur_amount) {
    if (blur_amount == 0) return;
    
    // Convert blur_amount to a fraction (0-255 → 0.0-1.0)
    uint8_t weight = blur_amount;
    
    for (uint16_t row = 0; row < height; row++) {
        CRGB prev = CRGB::Black;
        CRGB cur = getRGB(Coord3D(0, row));
        
        for (uint16_t i = 0; i < width; i++) {
            CRGB next = (i < width - 1) ? getRGB(Coord3D(i + 1, row)) : CRGB::Black;
            
            // Weighted average: center pixel + neighbors
            CRGB result = cur;  // Start with 100% current pixel
            
            // Add weighted contribution from neighbors
            CRGB neighborSum = prev;
            neighborSum += next;
            neighborSum.nscale8_video(weight);  // Scale by blur_amount
            
            // Blend: keep most of original, add scaled neighbors
            result.nscale8_video(255 - weight);
            result += neighborSum.nscale8_video(weight >> 1);  // Divide by 2 (two neighbors)
            
            setRGB(Coord3D(i, row), result);
            
            prev = cur;
            cur = next;
        }
    }
}

Wait, that's still complex. Let me give you a simpler, energy-conserving approach that FastLED uses:

Simpler Solution: Three-Tap Box Blur

void blurRows(uint8_t blur_amount) {
    if (blur_amount == 0) return;
    
    uint8_t keep = 255 - blur_amount;
    uint8_t seep = blur_amount >> 1;  // Divide by 2 for two neighbors
    
    for (uint16_t row = 0; row < height; row++) {
        CRGB carryover = CRGB::Black;
        
        for (uint16_t i = 0; i < width; i++) {
            CRGB cur = getRGB(Coord3D(i, row));
            
            // Scale down current pixel
            CRGB part = cur;
            part.nscale8(seep);
            cur.nscale8(keep);
            
            // Add carryover from previous pixel
            cur += carryover;
            
            // Write result
            setRGB(Coord3D(i, row), cur);
            
            // Save for next iteration
            carryover = part;
        }
        
        // Wrap final carryover to first pixel to conserve energy
        if (width > 1) {
            addRGB(Coord3D(0, row), carryover);
        }
    }
}

Key changes:

  1. ✅ Scales cur to prevent accumulation
  2. ✅ Wraps the final carryover back to the first pixel (not last) to conserve total energy
  3. ✅ Total brightness stays constant (energy in = energy out)

Apply the same pattern to blurColumns (wrap to row 0 instead of row height-1).

This is energy-conserving: no brightness is lost or gained, it's just redistributed smoothly. The wrapping prevents the "disappearing" effect while the scaling prevents the "filling" effect.

Would you like me to create a code change plan for both blurRows and blurColumns with this approach?


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:06.484Z
Learning: In MoonLight: VirtualLayer.size is ONLY synchronized with PhysicalLayer.lights.header.size during pass 2 (VirtualLayer::onLayoutPre at line 349 sets size = layerP->lights.header.size). If pass 2 (requestMapVirtual) doesn't run, VirtualLayer.size remains at its default {16, 16, 1}, causing effects to allocate buffers and iterate over the wrong dimensions. Pass 2 must always run after pass 1 to ensure VirtualLayer gets the correct size from PhysicalLayer.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-12T17:15:32.074Z
Learning: In MoonLight's Monitor.ts (interface/src/routes/moonbase/monitor/monitor.ts), the camera distance calculation uses a padding multiplier in the formula: `cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * padding`. A padding value of 5 (not 1.5) is needed because the effectiveSize calculation underestimates the actual visible model extent after scaling by [normalizedWidth, normalizedHeight, 1]. Larger padding values move the camera farther away, making the object smaller with more margin. Using 5 is safe and prevents clipping for all LED matrix aspect ratios.

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 22, 2026

@coderabbitai, for some reason class OctopusEffect is showing random pixels while it worked fine earlier, can you see what might have changed which caused this?

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find OctopusEffect class definition
echo "=== Finding OctopusEffect class ==="
rg -n "class.*OctopusEffect" src/ -A 5 -B 2

echo ""
echo "=== Check git diff for OctopusEffect in this PR ==="
git diff main...HEAD -- "**/*Octopus*" "**/*octopus*"

echo ""
echo "=== Find all files containing Octopus ==="
rg -l "Octopus" src/

Length of output: 1032


🏁 Script executed:

#!/bin/bash
# Get the full OctopusEffect implementation
echo "=== Full OctopusEffect class implementation ==="
sed -n '/class OctopusEffect/,/^class [A-Z]/p' src/MoonLight/Nodes/Effects/E_WLED.h | head -150

echo ""
echo "=== Check if there are changes to E_WLED.h in recent commits ==="
git log --oneline -5 -- src/MoonLight/Nodes/Effects/E_WLED.h

echo ""
echo "=== Show changes to E_WLED.h in the latest commit ==="
git show HEAD:src/MoonLight/Nodes/Effects/E_WLED.h > /tmp/current_E_WLED.h 2>/dev/null
git show HEAD~1:src/MoonLight/Nodes/Effects/E_WLED.h > /tmp/previous_E_WLED.h 2>/dev/null
echo "Checking Octopus section differences..."
diff -u <(sed -n '/class OctopusEffect/,/^class [A-Z]/p' /tmp/previous_E_WLED.h 2>/dev/null || echo "Previous version not found") \
        <(sed -n '/class OctopusEffect/,/^class [A-Z]/p' /tmp/current_E_WLED.h 2>/dev/null || echo "Current version not found") || true

Length of output: 3938


🏁 Script executed:

#!/bin/bash
# Check what changed in the latest commit for effects
echo "=== Show full diff of latest commit ==="
git show 6436c8e --stat

echo ""
echo "=== Check if Octopus was modified in recent commits ==="
git log -p -3 --all -- src/MoonLight/Nodes/Effects/E_WLED.h | grep -A 50 -B 5 "Octopus" | head -100

echo ""
echo "=== Check if there are issues with layer initialization or buffer clearing ==="
rg -n "class.*Effect.*:.*public Node" src/MoonLight/Nodes/Effects/E_WLED.h | head -20

echo ""
echo "=== Check if OctopusEffect has a proper setup that clears the buffer ==="
sed -n '1284,1380p' src/MoonLight/Nodes/Effects/E_WLED.h

Length of output: 5135


@ewowi, found it! 🎯 OctopusEffect has an initialization bugrMap is allocated but never initialized on startup.

The Problem

Looking at the code flow:

  1. onSizeChanged (line 1363) allocates rMap memory:

    reallocMB2<Map_t>(rMap, rMapSize, layer->size.x * layer->size.y, "rMap");

    ⚠️ This allocates memory but doesn't initialize it (contains garbage values).

  2. setRMap() (lines 1333-1348) initializes the angle/radius values:

    rMap[indexV].angle = 40.7436f * atan2f(...);
    rMap[indexV].radius = hypotf(...) * mapp;

    But setRMap() is only called in onUpdate() when the offset changes (line 1331). It's never called on initial startup!

  3. loop() (lines 1365-1388) immediately reads from uninitialized rMap:

    byte angle = rMap[indexV].angle;   // 🐛 Reading garbage!
    byte radius = rMap[indexV].radius; // 🐛 Reading garbage!

Result: Random pixel colors from uninitialized memory.

The Fix

Call setRMap() after allocation in onSizeChanged:

void onSizeChanged(const Coord3D& prevSize) override { 
    reallocMB2<Map_t>(rMap, rMapSize, layer->size.x * layer->size.y, "rMap");
    if (rMap) setRMap();  // Initialize after allocation
}

This ensures rMap is always properly initialized before loop() tries to read from it. The bug probably appeared when you recently changed something that affected initialization order or memory allocation behavior.

Would you like me to create a code change plan to fix this?


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-06T13:45:03.067Z
Learning: For MoonLight on ESP32 at extreme loads (e.g., 49K channels @ 100 fps): Task watchdog can fire when AppDrivers/AppEffects process many nodes without yielding. Even with vTaskDelay(1) at loop end, the node iteration for-loops (PhysicalLayer::loopDrivers line 108-111, PhysicalLayer::loop line 73-75) can take >5 seconds without yielding, starving IDLE task (priority 0). Solution: add taskYIELD() or vTaskDelay(1) inside the node/layer iteration loops to give IDLE task chances to run and reset watchdog. Lowering task priority won't help since IDLE is already at 0 (lowest).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:06.484Z
Learning: In MoonLight: VirtualLayer.size is ONLY synchronized with PhysicalLayer.lights.header.size during pass 2 (VirtualLayer::onLayoutPre at line 349 sets size = layerP->lights.header.size). If pass 2 (requestMapVirtual) doesn't run, VirtualLayer.size remains at its default {16, 16, 1}, causing effects to allocate buffers and iterate over the wrong dimensions. Pass 2 must always run after pass 1 to ensure VirtualLayer gets the correct size from PhysicalLayer.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-12T17:15:32.074Z
Learning: In MoonLight's Monitor.ts (interface/src/routes/moonbase/monitor/monitor.ts), the camera distance calculation uses a padding multiplier in the formula: `cameraDistance = effectiveSize / (2 * Math.tan(fov / 2)) * padding`. A padding value of 5 (not 1.5) is needed because the effectiveSize calculation underestimates the actual visible model extent after scaling by [normalizedWidth, normalizedHeight, 1]. Larger padding values move the camera farther away, making the object smaller with more margin. Using 5 is safe and prevents clipping for all LED matrix aspect ratios.

backend
=======
- Virtual layer: blur tweaks
- Lights control: use palette functions
- palettes: byte to uint8_t, refactor combining all types and alphabetically
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/MoonLight/Layers/VirtualLayer.h (1)

320-358: Guard blurRows/blurColumns against zero dimensions.

If size.x or size.y is 0, the loops skip but addRGB(size.x - 1, …) / addRGB(…, size.y - 1) underflows and writes out of bounds.

🛠️ Suggested fix
 void blurRows(fract8 blur_amount) {
+  if (size.x == 0 || size.y == 0) return;
   // blur rows same as columns, for irregular matrix
   uint8_t keep = 255 - blur_amount;
   uint8_t seep = blur_amount >> 1;
@@
-      addRGB(Coord3D(size.x - 1, row), carryover);
+      addRGB(Coord3D(size.x - 1, row), carryover);
     }
   }

 // blurColumns: perform a blur1d on each column of a rectangular matrix
 void blurColumns(fract8 blur_amount) {
+  if (size.x == 0 || size.y == 0) return;
   // blur columns
   uint8_t keep = 255 - blur_amount;
   uint8_t seep = blur_amount >> 1;
@@
-       addRGB(Coord3D(col, size.y - 1), carryover);
+      addRGB(Coord3D(col, size.y - 1), carryover);
     }
   }
🤖 Fix all issues with AI agents
In `@src/MoonLight/Modules/ModuleLightsControl.h`:
- Around line 331-334: The palette assignment trusts external input and can
index gGradientPalettes out of bounds via getGradientPalette(updatedItem.value);
validate or clamp updatedItem.value before calling getGradientPalette (or update
getGradientPalette to handle bounds) so you never read past gGradientPalettes;
on invalid index, choose a safe default palette (e.g., first palette) and then
assign to layerP.palette to avoid OOB reads when handling "palette" in the
updatedItem branch.

In `@src/MoonLight/Modules/palettes.h`:
- Around line 652-663: The code in the gpArray[0] == 255 branch uses a too-small
stack buffer (char buf[3]) for sprintf("%02x%02x%02x%02x"), causing stack
corruption; update the buffer to a safe size (at least 9 bytes for 8 hex chars +
NUL) and replace sprintf calls with snprintf(buf, sizeof(buf), ...) to avoid
overflow, or use a std::string/ostringstream to format into hexString directly;
adjust the formatting calls around buf, palette[], hexString and the final entry
to use the new safe approach.
♻️ Duplicate comments (4)
src/MoonLight/Modules/palettes.h (3)

24-28: Make palettes.h self‑contained for uint8_t/byte.

This header relies on uint8_t and byte without including <cstdint> or <Arduino.h>, which breaks compilation when include order changes. Add a guarded include/typedef.

🛠️ Suggested fix
 `#ifndef` Palettes_h
 `#define` Palettes_h
+
+#if __has_include(<Arduino.h>)
+#include <Arduino.h>
+#else
+#include <cstdint>
+using byte = uint8_t;
+#endif

638-641: Avoid truncating gradient palettes in getGradientPalette().

The fixed 72‑byte copy can truncate larger gradients (e.g., 144‑byte palettes), dropping the 0xFF terminator and causing loadDynamicGradientPalette() to read out‑of‑bounds.

🛠️ Suggested fix
-  } else {                 // gradient array palettes
-    byte tcp[76] = {255};  // WLEDMM: prevent out-of-range access in loadDynamicGradientPalette()
-    memcpy(tcp, (byte*)pgm_read_dword(&(gGradientPalettes[index])), 72);
-    palette.loadDynamicGradientPalette(tcp);
-  }
+  } else {  // gradient array palettes
+    constexpr size_t kMaxPaletteBytes = 256;  // adjust if you expect larger
+    byte tcp[kMaxPaletteBytes];
+    size_t j = 0;
+    while (j + 3 < kMaxPaletteBytes) {
+      byte b = pgm_read_byte(((const byte*)pgm_read_dword(&(gGradientPalettes[index]))) + j);
+      tcp[j++] = b;
+      // copy 4‑byte entries
+      for (int k = 1; k < 4; k++) {
+        tcp[j] = pgm_read_byte(((const byte*)pgm_read_dword(&(gGradientPalettes[index]))) + j);
+        j++;
+      }
+      if (tcp[j - 4] == 255) break;
+    }
+    if (tcp[j - 4] != 255 && j + 4 <= kMaxPaletteBytes) tcp[j++] = 255;  // ensure terminator
+    palette.loadDynamicGradientPalette(tcp);
+  }

667-675: Avoid the hard 100‑byte cap when building palette previews.

Some gradients exceed 100 bytes; the fixed limit truncates them and yields incomplete previews. Use a safe upper bound and stop only at the 0xFF terminator.

🛠️ Suggested fix
-    while (j < 100) {  // Safety limit
+    const size_t kMaxPaletteBytes = 256;
+    while (j + 3 < kMaxPaletteBytes) {
@@
-      if (gpArray[j - 4] == 255) break;
+      if (gpArray[j - 4] == 255) break;
     }
src/MoonLight/Nodes/Effects/E_WLED.h (1)

2060-2063: Add debug label to freeMB call for consistency with other destructors.

The freeMB signature accepts an optional label parameter (const char* name = nullptr). All other effect destructors in this file provide a label for memory tracking. Align ColorTwinkleEffect with this pattern:

~ColorTwinkleEffect() { freeMB(data, "data"); }
🧹 Nitpick comments (1)
src/MoonLight/Nodes/Effects/E_WLED.h (1)

2067-2093: Align PlasmaEffect::dim() with 2D/3D rendering.

The loop writes across x and z for every y, so _1D under‑represents the effect and can affect filtering/selection. Consider _2D (or _3D if you want it available on volumetric layouts), or keep _1D and only render a single line.

🛠️ Possible change
-  static uint8_t dim() { return _1D; }
+  static uint8_t dim() { return _2D; }

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 22, 2026

@coderabbitai, is the blur1d() function redundant and can we use blurColumns() for it?

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find blur1d implementation in VirtualLayer.h
echo "=== blur1d() implementation ==="
rg -n "void blur1d" src/MoonLight/Layers/VirtualLayer.h -A 20 -B 2

echo ""
echo "=== blurColumns() implementation ==="
rg -n "void blurColumns" src/MoonLight/Layers/VirtualLayer.h -A 25 -B 2

echo ""
echo "=== Search for blur1d() usages in codebase ==="
rg -n "blur1d\(" src/ -C 3

echo ""
echo "=== Check if any 1D effects exist ==="
rg -n "dim\(\).*return 1" src/MoonLight/Nodes/Effects/ -B 5 | head -50

Length of output: 4021


@ewowi, no, blur1d() is NOT redundant — they serve different purposes:

Key Difference

  • blur1d(blur_amount, x) blurs one specific column x (along the Y-axis)
  • blurColumns(blur_amount) blurs all columns (loops through every x from 0 to size.x-1)

Current Usages

Looking at the codebase:

  1. Line 936 (some effect): layer->blur1d(blur >> 1);
    Uses default x=0, blurs only column 0 (might be intentional for a 1D strip effect)

  2. Line 1681 (another effect): layer->blur1d(blurAmount, x);
    Comment says: // only blur x column. todo: check blurColumn ...
    This explicitly needs to blur a single specific column, not all columns

Why Both Are Needed

  • blurColumns() is appropriate for 2D matrix effects that want smooth blur across the entire display
  • blur1d() is needed for effects that process columns independently or want to blur only specific columns (e.g., per-column fire/plasma effects)

If you replaced blur1d(blurAmount, x) with blurColumns(blurAmount), you'd blur all columns when the effect only wants to blur one column, which would be incorrect and much slower.

So keep both functions — they're complementary, not redundant! 👍


🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:06.484Z
Learning: In MoonLight: VirtualLayer.size is ONLY synchronized with PhysicalLayer.lights.header.size during pass 2 (VirtualLayer::onLayoutPre at line 349 sets size = layerP->lights.header.size). If pass 2 (requestMapVirtual) doesn't run, VirtualLayer.size remains at its default {16, 16, 1}, causing effects to allocate buffers and iterate over the wrong dimensions. Pass 2 must always run after pass 1 to ensure VirtualLayer gets the correct size from PhysicalLayer.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/MoonLight/Layers/VirtualLayer.h (1)

298-313: Missing final carryover write causes incomplete blur.

After the loop ends, carryover contains the last pixel's seep contribution but is never written. This loses the bottom pixel's blur contribution, weakening the overall effect.

Proposed fix
       setRGB(Coord3D(x, row), cur);
       carryover = part;
     }
+    if (size.y) addRGB(Coord3D(x, size.y - 1), carryover);
   }
🤖 Fix all issues with AI agents
In `@src/MoonLight/Layers/VirtualLayer.h`:
- Around line 320-338: In blurRows, the conditional that guards adding the
"part" to the previous column uses the wrong variable ("if (row)"), which causes
missed blur contributions and underflow when col == 0; change that check to "if
(col)" in the loop inside blurRows (the loop iterating uint16_t col) so
addRGB(Coord3D(col - 1, row), part) only runs when col > 0; verify the final
addRGB(Coord3D(size.x - 1, row), carryover) remains unchanged.
♻️ Duplicate comments (1)
src/MoonLight/Modules/palettes.h (1)

24-27: Add an explicit stdint/Arduino include so uint8_t is defined.

Clang reports unknown type name uint8_t at Line 27; this header currently depends on include order. Add a standard include (or guarded Arduino.h) to make it self-contained.

🛠️ Proposed fix
 `#ifndef` Palettes_h
 `#define` Palettes_h
+
+#include <stdint.h>
🧹 Nitpick comments (1)
src/MoonLight/Nodes/Effects/E_WLED.h (1)

2186-2219: Consider adding taskYIELD() to prevent task starvation on large matrices.

The nested loops iterate over every pixel (rows × cols), and each pixel runs an inner while loop up to maxIterations times. For a 128×128 matrix with maxIterations = 63, this can exceed 1 million operations per frame without yielding, potentially starving higher-priority tasks.

♻️ Suggested fix
     // Start y
     float y = ymin;
     for (int j = 0; j < rows; j++) {
+      taskYIELD();  // prevent task starvation on large matrices
       // Start x
       float x = xmin;
       for (int i = 0; i < cols; i++) {

Based on learnings, the GameOfLifeEffect had similar issues with nested loops causing the effect to disappear when higher-priority tasks run.

@ewowi
Copy link
Collaborator Author

ewowi commented Jan 22, 2026

@coderabbitai, regarding learning

In ModuleLightsControl::onUpdate (src/MoonLight/Modules/ModuleLightsControl.h), bounds checking for palette index in getGradientPalette(updatedItem.value) is the programmer's responsibility. The code trusts that the palette index is valid and does not perform defensive validation. This is an intentional design decision consistent with the codebase's performance-first philosophy.

Update the learning to be more general, in your own words: checks and protections needs to be added for end user behavior, not for programmer behavior eg out of bounds checks, where out of bounds can be prevented by good programming , the reason is that this system is build for high performance and we don't want extra code for bad programming

@coderabbitai
Copy link

coderabbitai bot commented Jan 22, 2026

Tip

For best results, initiate chat on the files or code changes.

@ewowi, I've updated the learning to reflect the broader design philosophy. Here's the new version:

New general principle: MoonLight adds input validation and bounds checking to protect against end-user behavior (invalid UI inputs, malformed network data, hardware failures), but intentionally omits defensive checks for programmer errors that can be prevented by correct coding (e.g., array out-of-bounds, invalid function arguments, null pointer dereferences). The system is built for high performance and will not add runtime overhead to catch programming mistakes. Programmers are responsible for writing correct code; the architecture trusts that internal function calls and data structures are used properly.

This applies throughout the codebase and explains why you'll see validation at system boundaries (UI, network, hardware) but not in internal APIs and hot paths. 👍


✏️ Learnings added
Learnt from: ewowi
URL: https://github.com/MoonModules/MoonLight/pull/110

Timestamp: 2026-01-22T12:36:47.871Z
Learning: In MoonLight, input validation and bounds checking are added to protect against end-user behavior (invalid UI inputs, malformed network data, hardware failures), but NOT for programmer errors that can be prevented by correct code (e.g., array out-of-bounds, invalid function arguments, null pointer dereferences). The codebase is built for high performance and intentionally omits defensive checks against programming mistakes in internal APIs and hot paths. Programmers are responsible for ensuring their code is correct; the system adds validation only at external boundaries (UI, network, hardware) where untrusted input enters the system.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.


⛔ Learnings removed
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 110
File: src/MoonLight/Modules/ModuleLightsControl.h:331-334
Timestamp: 2026-01-22T12:28:48.997Z
Learning: In ModuleLightsControl::onUpdate (src/MoonLight/Modules/ModuleLightsControl.h), bounds checking for palette index in getGradientPalette(updatedItem.value) is the programmer's responsibility. The code trusts that the palette index is valid and does not perform defensive validation. This is an intentional design decision consistent with the codebase's performance-first philosophy.

🧠 Learnings used
Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T12:49:07.297Z
Learning: In MoonLight channel reordering architecture: Keep rgbwBufferMapping in driver task (not effect task) because: (1) Driver has spare CPU cycles waiting for DMA/I2S/Art-Net timing; effect task is CPU-bound and starvation-prone. (2) Contiguous RGB writes in effects are cache-friendly; scattered writes at final offsets are cache-hostile for non-RGB orders (GRB, BGR, etc.). (3) forEachLightIndex already handles sparse mapping iteration; moving reordering to effects adds complexity without benefit since drivers must send full frames anyway (can't skip iteration). (4) rgbwBufferMapping does brightness/gamma LUT + white extraction + reordering—moving just reordering creates code duplication. (5) Effects should express color intent (CRGB), not hardware details (offsetRed/Green/Blue). Driver-side transformation maintains separation of concerns and task load balance for scaling to 98K+ pixels.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-18T17:07:33.344Z
Learning: FastLED library does not support direct 4-byte-per-pixel (CRGBW) buffers. It always expects CRGB (3-byte) arrays and internally generates the W channel from RGB using .setRgbw() API. Casting a 4-byte RGBW buffer to CRGB* causes misalignment (W becomes next pixel's R). For MoonLight's FastLED driver with channelsPerLight=4: must either (1) repack RGB in-place before FastLED.show() then restore after, (2) use driver-specific channelsPerLight=3 layout, or (3) allocate temporary RGB-only buffer. Option 1 (in-place repacking) is best for ESP32-D0 memory constraints.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:15:57.828Z
Learning: In DripEffect (E_WLED.h), dropsSize must be set to layer->size.y (not layer->size.x) in onSizeChanged(), because the drops array is allocated as [layer->size.y][maxNumDrops]. Using layer->size.x causes out-of-bounds access when iterating over y-coordinates.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 84
File: src/MoonLight/Modules/ModuleLightsControl.h:367-379
Timestamp: 2025-12-28T10:28:24.157Z
Learning: In MoonLight PR `#84`: wrapping effectTask's layerP.loop() with monitorMutex while reading channelsE for monitor emission is thread-safe and prevents partial frames, but introduces serialization between effects rendering and monitor emission. Reading from channelsD (driver/front buffer) instead—with monitorMutex held only during the brief pointer swap—is superior because it maintains full parallelism (effects ║ monitor ║ driver), shows actual LED output, and minimizes mutex hold time.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:06.484Z
Learning: In MoonLight: VirtualLayer.size is ONLY synchronized with PhysicalLayer.lights.header.size during pass 2 (VirtualLayer::onLayoutPre at line 349 sets size = layerP->lights.header.size). If pass 2 (requestMapVirtual) doesn't run, VirtualLayer.size remains at its default {16, 16, 1}, causing effects to allocate buffers and iterate over the wrong dimensions. Pass 2 must always run after pass 1 to ensure VirtualLayer gets the correct size from PhysicalLayer.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T12:38:33.598Z
Learning: In MoonLight GameOfLifeEffect (src/MoonLight/Nodes/Effects/E_MoonModules.h): The effect's nested loops (especially lines 257-323) iterate through all cells (e.g., 128×128 = 16,384) with nested neighbor checks (18 per cell = 294,912 operations) without yielding. This starves effectTask (priority 3) when higher-priority tasks like HTTP server (priority 5) run, causing the effect to disappear. UI actions (save, refresh) temporarily show one frame because HTTP completes and briefly releases CPU back to effectTask. Solution: Add taskYIELD() inside outer X loop to prevent task starvation. This is the same root cause as issue `#102` (Starsky glitches with many driver nodes).

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2026-01-15T15:13:55.631Z
Learning: In MoonLight effectTask (src/main.cpp): The memcpy(channelsE, channelsD) at line 137 happens AFTER releasing swapMutex, allowing driverTask to swap the channelsD/channelsE pointers mid-copy, causing buffer corruption. This specifically breaks effects like GameOfLife that use getRGB() to read previous-frame data for blending decisions. Other effects work because they only read current-frame data they just wrote. Fix: hold swapMutex during the entire memcpy operation to prevent pointer swap during copy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 0
File: :0-0
Timestamp: 2025-12-18T15:59:30.556Z
Learning: In DripEffect (E_WLED.h), the bounce fails because the damped velocity is too weak to keep the drop at pos > 0 for multiple frames. When the drop returns to pos <= 0 while still in the bouncing state (colIndex == bouncing), it immediately resets to init (line 1663-1664) before any visible bounce occurs. The bounce velocity must be strong enough (divide by 1.1 or less, or use minimal damping like *= 0.9) so the drop stays airborne long enough for a visible bounce arc before the eventual second ground hit triggers the reset.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:212-221
Timestamp: 2026-01-06T15:24:29.770Z
Learning: In VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking for unmapped lights (the else branch around lines 212-221) is intentionally disabled for performance when driving 100K+ LEDs. The design places responsibility on effect developers to stay within maxChannels. This is an explicit performance vs safety tradeoff for extreme LED counts.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 110
File: src/MoonLight/Modules/ModuleLightsControl.h:331-334
Timestamp: 2026-01-22T12:28:48.997Z
Learning: In ModuleLightsControl::onUpdate (src/MoonLight/Modules/ModuleLightsControl.h), bounds checking for palette index in getGradientPalette(updatedItem.value) is the programmer's responsibility. The code trusts that the palette index is valid and does not perform defensive validation. This is an intentional design decision consistent with the codebase's performance-first philosophy.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 93
File: src/MoonLight/Layers/VirtualLayer.cpp:164-171
Timestamp: 2026-01-06T15:24:17.118Z
Learning: In VirtualLayer::setLight and VirtualLayer::getLight (src/MoonLight/Layers/VirtualLayer.cpp), bounds checking in the unmapped path (when indexV >= mappingTableSize or oneToOneMapping is true) is intentionally disabled for performance when driving very large LED counts (100K+). Effect developers are responsible for ensuring their code stays within bounds. This is an explicit design decision prioritizing performance over defensive checks in the hot path.

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 104
File: src/MoonLight/Layers/PhysicalLayer.cpp:145-147
Timestamp: 2026-01-13T15:18:48.740Z
Learning: In MoonLight's PhysicalLayer and Node architecture (src/MoonLight/Layers/PhysicalLayer.cpp, src/MoonBase/Nodes.h): effectsMutex, driversMutex, and layerMutex are mandatory for system operation. These mutexes are validated at creation time (constructor checks log errors if xSemaphoreCreateMutex returns nullptr). Runtime null checks on these mutex pointers are intentionally omitted—the design treats successful mutex allocation as an invariant after initialization, placing responsibility on the programmer to ensure proper allocation. This is a performance-oriented "fail-fast at boot" approach rather than defensive runtime checking.
<!-- </add_learning>

Learnt from: ewowi
Repo: MoonModules/MoonLight PR: 104
File: src/MoonLight/Nodes/Drivers/parlio.cpp:409-414
Timestamp: 2026-01-13T08:32:57.101Z
Learning: In parlio.cpp's show_parlio function, the `components` parameter (color channels per pixel: 3 for RGB, 4 for RGBW) is guaranteed by API contract to never be 0. Invalid values are considered caller errors, and defensive checks are intentionally omitted for performance. This matches the design philosophy of placing responsibility on callers in performance-critical paths.

@ewowi ewowi merged commit c79a3c4 into main Jan 22, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants