Skip to content

Feat: Segment masks#324

Open
sayhiben wants to merge 16 commits intoMoonModules:mdevfrom
sayhiben:segment-mask
Open

Feat: Segment masks#324
sayhiben wants to merge 16 commits intoMoonModules:mdevfrom
sayhiben:segment-mask

Conversation

@sayhiben
Copy link
Copy Markdown

@sayhiben sayhiben commented Jan 21, 2026

Overview

image image image

I'm designing a fancy sign for a craft fair booth with an engraved sheet of acrylic in front. In order to properly light it, I would like to define segments for various shapes in the engraved sheet such that I can apply different effects to each area. Currently, support for this is limited to ledmaps (only one across all segments), jMaps (1D -> 2D translation only), and grouping (only works for specific designs)

This PR adds support for clipping masks per segment, defined in virtual LED space. This enables overlapping segments to render distinct effects/palettes while preserving normal segment geometry and mappings.

Summary of changes

  • Add per‑segment mask clipping via segmaskX.json files (1D + 2D).
  • Expose mask selection + invert in the UI (MM‑specific items marked with 🌜).
  • Stream‑parse mask files (ledmap‑style) and enumerate available masks.
  • Make 1D mask gating work on matrix installs and add a safety reset on failed mask loads.

How it works

  • Mask storage: bit‑packed 0/1 mask loaded from /segmaskX.json (stream parser like ledmap).
  • Mask checks:
    • 2D: mask checked in setPixelColorXY_*.
    • 1D: mask checked in setPixelColor with !is2D() so 1D segments still mask on matrix installs.
  • JSON API: segment state uses mask + minv; /json/info exposes available mask IDs.
  • UI: adds “Mask 🌜” dropdown and “Invert mask 🌜”

Segmask.json

Masks are defined by 1/0 values in a list similar to ledmapN.json files:

{"w":16,"h":32,"mask":[0,0,0,1,1,1,0,0,0,0,...,0,1,1],"inv":false}
  • w: Mask width
  • h: Mask height
  • mask: JSON array of bits
  • inv: Invert mask by default (can be changed at runtime in the UI)

Other Notes

  • I'd appreciate confirmation that this works on something other than the ESP32-S3 boards I have on hand
  • Performance seems unchanged on three segments with masks over 512 LEDs despite checking each virtual LED on each segment against its segment mask. I'm using bitwise ops here and storing the masks as u_int8_t byte arrays, so lookups and memory footprint remains efficient. Still, I expected a small hit to perf, and I'm seeing a perf gain when enabling segment masks on my device. I'd love someone to double check on a larger installation
  • Documentation PR: Feat: Segment Masks WLED-Docs#13

Summary by CodeRabbit

  • New Features

    • Per-segment masks: discover, select and invert masks from the UI; masks persisted in configs and reported in device info.
    • Device-aware default limit for mask slots (different default for ESP8266 and other platforms).
    • UI controls to set/clear masks and invert mask bits per segment.
  • Bug Fixes / Reliability

    • Mask geometry validated at render time; rendering respects masks across 1D/2D update paths.
    • Mask state initialized, cleaned up and synchronized safely during segment lifecycle.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds per-segment masks: mask state and lifecycle in Segment, filesystem loading/parsing of segmaskN.json, UI controls and JSON (de)serialization, render-time gating to skip masked pixels, and enumeration/init hooks to register available masks.

Changes

Cohort / File(s) Summary
Header declarations
wled00/FX.h, wled00/wled.h
Added maskId, maskInvert (public) and _mask, _maskW, _maskH, _maskLen, _maskValid (private) to Segment; added setMask(), clearMask(), accessors hasMask(), maskAllows(), maskAllowsXY(); added WS2812FX::enumerateSegmasks() and global segMasks.
Configuration constants
wled00/const.h
Introduced WLED_MAX_SEGMASKS with range guard (4–32) and platform defaults (ESP8266:10, others:16).
Core masking implementation
wled00/FX_fcn.cpp
Implemented setMask() to load/parse segmaskN.json (dimensions, invert, 1-bit mask), clearMask() to free state, mask state synchronization (segmentMux / busDrawMux), lifecycle propagation (copy/move/assign). Added segMasks enumeration and finalize init hook.
2D pixel rendering
wled00/FX_2Dfcn.cpp
Validate _maskValid in startFrame(); added maskAllowsXY() gating in fast and slow setPixelColorXY() paths to skip masked pixels.
1D rendering gating & render-time checks
wled00/FX_fcn.cpp
Apply maskAllows(i) gating for 1D pixel writes and respect mask validity during rendering.
JSON (de)serialization
wled00/json.cpp
deserializeSegment() reads "mask" (clamped, apply/clear) and "minv"; serializeSegment() writes "mask" and "minv"; serializeInfo() adds "masks" array of active masks.
UI / Frontend
wled00/data/index.js
Added per-segment mask UI controls (selector, invert toggle, info), integrated mask state into segment UI/mapping logic, and added setMask() / setMaskInv() client functions.
Initialization / Settings integration
wled00/set.cpp, wled00/wled.cpp
Call strip.enumerateSegmasks() alongside enumerateLedmaps() during settings init and after LED-map load; finalizeInit() calls enumeration.

Sequence Diagram(s)

sequenceDiagram
    participant UI as Frontend UI
    participant App as WLED App
    participant Seg as Segment
    participant FS as Filesystem
    participant Render as Renderer

    UI->>App: requestJson setMask(seg, id)
    App->>Seg: deserializeSegment() with "mask"
    Seg->>Seg: setMask(maskId)
    Seg->>FS: open segmask{maskId}.json
    FS-->>Seg: return JSON (w,h,inv,bitarray)
    Seg->>Seg: parse, alloc _mask, set _maskValid
    Seg-->>App: updated segment state
    App-->>UI: update UI

    Note over Render,Seg: During render loop
    Render->>Seg: setPixelColorXY(x,y,color)
    Seg->>Seg: maskAllowsXY(x,y)?
    alt allowed
        Seg->>Seg: apply color
    else masked
        Seg-->>Seg: skip pixel
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

enhancement

Suggested reviewers

  • softhack007

Poem

🐇 I found a mask inside a file,

Bits and JSON made me smile,
I hop through pixels, skip and play,
Masked lights blink night and day,
Hoppy code and glowing style ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 52.94% 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 'Feat: Segment masks' directly and clearly describes the main feature addition—per-segment clipping masks for LED rendering.

✏️ 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
Copy Markdown

@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)
wled00/FX_fcn.cpp (1)

94-103: Mask reload path missing after copy/assignment.

Copy ctor/assignment null out _mask (and _maskValid) but preserve maskId/maskInvert via memcpy. When rendering, maskAllows() checks if (!_mask || !_maskValid) return true;, so copied segments silently skip mask application even though maskId remains set. There is no automatic reload path—the effect must manually call setMask(maskId) after copying, or maskId should be cleared.

🤖 Fix all issues with AI agents
In `@wled00/FX_fcn.cpp`:
- Around line 253-260: _maskValid is only set once when loading the mask and can
become stale if virtual geometry changes; update the validity check by
recomputing it whenever geometry may change (for example at start of setUp() and
startFrame() or immediately before any mask application routine). Locate where
_maskValid is used (e.g., in FX_fcn::_maskValid checks and any applyMask/paint
routines) and replace the single-time assignment with a runtime check that
compares _maskW/_maskH to calc_virtualWidth()/calc_virtualHeight() (or reassign
_maskValid at the start of setUp()/startFrame()) so the mask validity reflects
current virtual geometry. Ensure no behavioral change besides recomputing
validity.
- Around line 150-176: The clearMask()/setMask() flow can free or replace _mask
while maskAllows() may be reading it on the other core; to fix, serialize the
pointer swap/free with the renderer by acquiring the same mutex used by the
render loop (e.g., segmentMux) around the critical section in Segment::clearMask
and Segment::setMask (before freeing or assigning _mask and updating
_maskLen/_maskW/_maskH/_maskValid/maskId) or alternatively wait for the strip to
be idle using the existing idle/wait API before changing the buffer; locate and
protect the critical sections in Segment::clearMask, Segment::setMask and
anywhere maskAllows reads _mask to prevent use-after-free.
🧹 Nitpick comments (2)
wled00/json.cpp (1)

364-372: Prefer an explicit clear path when mask is 0.
This makes intent obvious and avoids any unintended file load if setMask(0) isn’t a no-op.

♻️ Proposed tweak
 if (elem.containsKey("mask")) { // WLEDMM segment mask id
   int maskVal = elem["mask"] | 0;
   if (maskVal < 0) maskVal = 0;
   uint8_t maskId = constrain(maskVal, 0, WLED_MAX_SEGMASKS-1);
-  if (maskId != seg.maskId || (maskId != 0 && !seg.hasMask())) seg.setMask(maskId);
+  if (maskId == 0) {
+    if (seg.hasMask()) seg.clearMask();
+  } else if (maskId != seg.maskId || !seg.hasMask()) {
+    seg.setMask(maskId);
+  }
 }
 if (elem.containsKey("minv")) { // WLEDMM segment mask invert
   seg.maskInvert = elem["minv"] | seg.maskInvert;
 }
wled00/FX.h (1)

725-741: Consider adding [[gnu::hot]] attribute for hot-path optimization.

The mask accessor logic is correct:

  • Fail-open design (returns true when mask is absent/invalid) prevents black pixels on error
  • Bounds checking prevents buffer overread
  • Bit-packing/unpacking is efficient and correct (LSB-first)

Since maskAllows() and maskAllowsXY() are called for every pixel during rendering, consider adding the [[gnu::hot]] attribute (like progress() at line 692 and currentBri() at line 700) to hint the compiler for optimization.

♻️ Suggested optimization
-    inline bool hasMask(void) const { return _mask != nullptr; } // WLEDMM
-    inline bool maskAllows(uint16_t i) const { // WLEDMM
+    [[gnu::hot]] inline bool hasMask(void) const { return _mask != nullptr; } // WLEDMM
+    [[gnu::hot]] inline bool maskAllows(uint16_t i) const { // WLEDMM
       if (!_mask || !_maskValid) return true;
       if (size_t(i) >= _maskLen) return false;
       // WLEDMM: bit-packed mask (LSB-first): byte = i>>3, bit = i&7
       bool bit = (_mask[i >> 3] >> (i & 7)) & 0x01;
       return maskInvert ? !bit : bit;
     }
-    inline bool maskAllowsXY(int x, int y) const { // WLEDMM
+    [[gnu::hot]] inline bool maskAllowsXY(int x, int y) const { // WLEDMM
       if (!_mask || !_maskValid) return true;
       if (x < 0 || y < 0) return false;
       size_t idx = size_t(x) + (size_t(y) * _maskW);
       if (idx >= _maskLen) return false;
       // WLEDMM: row-major (x + y*w), bit-packed mask (LSB-first in each byte)
       bool bit = (_mask[idx >> 3] >> (idx & 7)) & 0x01;
       return maskInvert ? !bit : bit;
     }

Copy link
Copy Markdown

@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)
wled00/FX_fcn.cpp (1)

94-103: Reset mask ID and invert flag when clearing mask buffer on segment copy.

After memcpy, both the copy constructor and copy assignment operator null _mask and reset mask buffer state, but leave maskId and maskInvert from the source. This causes the copied segment to report a mask ID that has no actual buffer applied. Reset these to match clearMask() behavior:

Fix for both locations
   _maskW = 0; // WLEDMM
   _maskH = 0; // WLEDMM
   _maskValid = false; // WLEDMM
+  maskId = 0; // WLEDMM
+  maskInvert = false; // WLEDMM

Copy link
Copy Markdown

@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 `@wled00/FX_fcn.cpp`:
- Around line 171-176: Segment::setMask currently calls clearMask() but proceeds
even if clearMask failed to release the existing mask, which can leak the old
buffer; after calling clearMask() in Segment::setMask, check the _mask member
and abort (return false) if it is still non-null to avoid overwriting/leaking
the previous buffer—use the existing Segment::clearMask() call, then immediately
guard with a check on _mask before continuing (also preserve the existing id==0
behavior).

Comment on lines +171 to +176
bool Segment::setMask(uint8_t id) { // WLEDMM
clearMask();
if (id >= WLED_MAX_SEGMASKS) {
return false;
}
if (id == 0) return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Abort mask load if clearMask() couldn't clear.

If clearMask() fails to acquire segmentMux, _mask can remain non-null; setMask() can then overwrite it, leaking the old buffer. Add a guard to bail out when _mask is still set.

🛠️ Suggested guard
 bool Segment::setMask(uint8_t id) { // WLEDMM
   clearMask();
+  if (_mask) {
+    DEBUG_PRINTLN(F("Segment::setMask: clearMask failed, aborting."));
+    return false;
+  }
   if (id >= WLED_MAX_SEGMASKS) {
     return false;
   }
🤖 Prompt for AI Agents
In `@wled00/FX_fcn.cpp` around lines 171 - 176, Segment::setMask currently calls
clearMask() but proceeds even if clearMask failed to release the existing mask,
which can leak the old buffer; after calling clearMask() in Segment::setMask,
check the _mask member and abort (return false) if it is still non-null to avoid
overwriting/leaking the previous buffer—use the existing Segment::clearMask()
call, then immediately guard with a check on _mask before continuing (also
preserve the existing id==0 behavior).

@softhack007
Copy link
Copy Markdown
Collaborator

@sayhiben I really like the overall idea 😃 its a nice way to handle overlaying properly, and it optimizes pixel drawing by skipping masked out pixels at the same time 👍

@sayhiben
Copy link
Copy Markdown
Author

@softhack007 Thanks for the thorough review! I'll work through comments today

Copy link
Copy Markdown

@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 `@wled00/FX_fcn.cpp`:
- Around line 330-334: Direct free(_mask) is unsafe because maskAllows() reads
_mask without mutex protection; replace this with a protected pointer swap (or
call clearMask()) so readers never see a freed pointer. Either call clearMask()
from here, or replicate its sequence: esp32SemTake(&segmentMux) to lock, oldMask
= _mask; _mask = nullptr; esp32SemGive(&segmentMux) to unlock, then call
strip_wait_until_idle("Segment::operator= mask cleanup") if still desired and
free(oldMask). Also remove or update the misleading comment about deadlocks and
avoid freeing while still holding the mutex.
♻️ Duplicate comments (1)
wled00/FX_fcn.cpp (1)

171-176: Add guard to prevent memory leak if clearMask() fails.

If clearMask() fails to acquire segmentMux, _mask remains non-null. Proceeding with the load would overwrite the pointer and leak the old buffer. This was flagged in a previous review but doesn't appear to be addressed.

🛠️ Suggested fix
 bool Segment::setMask(uint8_t id) { // WLEDMM
   clearMask();
+  if (_mask) {
+    DEBUG_PRINTLN(F("Segment::setMask: clearMask failed, aborting."));
+    return false;
+  }
   if (id >= WLED_MAX_SEGMASKS) {
     return false;
   }
   if (id == 0) return true;
🧹 Nitpick comments (1)
wled00/FX_fcn.cpp (1)

1188-1189: Consider moving mask gate to the 1D-only path for efficiency.

The mask check at line 1188 runs for all segments including 2D, but 2D segments already have mask gating in setPixelColorXY_* functions. While the check is cheap (bit lookup with early _maskValid check), placing it in the 1D-only else branch (around line 1361+) would avoid redundant checks for 2D paths.

A previous reviewer suggested: "Instead of doing if !is2D() && !maskAllows(..) here, it's better to put it into the else branch for line 1005."

♻️ Alternative placement

Move the mask gate to the 1D path only (after line 1371):

   }
 `#endif`
+
+  if (_maskValid && !maskAllows(i)) return; // WLEDMM mask gate for 1D segments

   if (ledsrgb) ledsrgb[i] = col;

And remove from line 1188-1189.

Comment on lines +330 to +334
if (_mask) { // WLEDMM free mask buffer directly to avoid deadlocks
strip_wait_until_idle("Segment::operator= mask cleanup"); // WLEDMM avoid freeing while renderer is active
free(_mask);
_mask = nullptr;
}
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Jan 24, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Examine clearMask, maskAllows, and segmentMux usage
rg -n "clearMask\(\)|maskAllows|segmentMux" wled00/FX_fcn.cpp wled00/FX.h --context=3 | head -100

Repository: MoonModules/WLED-MM

Length of output: 5748


🏁 Script executed:

#!/bin/bash
# Find the clearMask function definition
rg -n "void clearMask|void.*clearMask" wled00/FX_fcn.cpp wled00/FX.h -A 10

Repository: MoonModules/WLED-MM

Length of output: 1448


🏁 Script executed:

#!/bin/bash
# Find maskAllows function definition
rg -n "maskAllows" wled00/FX_fcn.cpp wled00/FX.h -B 2 -A 8

Repository: MoonModules/WLED-MM

Length of output: 1891


Use clearMask() or acquire segmentMux to protect pointer swap before freeing.

The direct free(_mask) is unsafe because maskAllows() (called from rendering tasks on other cores) reads _mask without mutex protection. While strip_wait_until_idle() ensures the renderer is idle now, it doesn't prevent the next render cycle from starting immediately after.

Compare with clearMask() (lines 152–169), which correctly:

  1. Acquires segmentMux with esp32SemTake()
  2. Swaps the pointer atomically (oldMask = _mask; _mask = nullptr)
  3. Releases the mutex
  4. Frees the old buffer after releasing the lock

Either call clearMask() directly or follow this same pattern to ensure the pointer swap is serialized with reader tasks. The comment about deadlocks is misleading—heap operations are thread-safe; the issue is protecting the pointer change itself.

🤖 Prompt for AI Agents
In `@wled00/FX_fcn.cpp` around lines 330 - 334, Direct free(_mask) is unsafe
because maskAllows() reads _mask without mutex protection; replace this with a
protected pointer swap (or call clearMask()) so readers never see a freed
pointer. Either call clearMask() from here, or replicate its sequence:
esp32SemTake(&segmentMux) to lock, oldMask = _mask; _mask = nullptr;
esp32SemGive(&segmentMux) to unlock, then call
strip_wait_until_idle("Segment::operator= mask cleanup") if still desired and
free(oldMask). Also remove or update the misleading comment about deadlocks and
avoid freeing while still holding the mutex.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@softhack007 I believe I've hit all of your other suggestions at this point, but this quirk is tripping me up a little. Do you have any recommendations?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

@softhack007 softhack007 added this to the 14.7.3 milestone Jan 25, 2026
@softhack007
Copy link
Copy Markdown
Collaborator

@sayhiben i'll use your PR as a testcase for our new AI review rules ;-) it also means your PR is still on my list 😊

@softhack007
Copy link
Copy Markdown
Collaborator

@coderabbitai please re-review this PR. Use the new review instructions and coding guidelines (.coderabbit.yaml) that were just added in the main mdev branch.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

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

🧩 Analysis chain

The PR description images show:

  1. An LED matrix display showing pixel art (a character/figure) — demonstrating a real-world use case.
  2. The Segments UI panel displaying a "Mask 🌜" dropdown (currently showing "segmask1.j…") and an "Invert mask 🌜" toggle — the new MM-specific UI elements.
  3. An "Effect mode" display on an LED matrix showing the masking in action — one segment showing an effect confined to a masked region.

Let me fetch the current CodeRabbit configuration and the PR code changes to perform a thorough re-review.


🏁 Script executed:

#!/bin/bash
# Fetch .coderabbit.yaml from mdev branch
gh api repos/MoonModules/WLED-MM/contents/.coderabbit.yaml?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "Not found at root"

# Also check if it's in a subdirectory
gh api "repos/MoonModules/WLED-MM/git/trees/mdev?recursive=1" --jq '.tree[] | select(.path | contains("coderabbit"))' 2>/dev/null

Length of output: 3804


🛠 Debug Information Gathered


Good, the .coderabbit.yaml is active and valid. Now let me fetch the coding guideline files and the PR diff.


🏁 Script executed:

#!/bin/bash
# Fetch coding guideline files from mdev branch
echo "=== .github/copilot-instructions.md ==="
gh api repos/MoonModules/WLED-MM/contents/.github/copilot-instructions.md?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "Not found"

echo ""
echo "=== .github/cpp.instructions.md ==="
gh api repos/MoonModules/WLED-MM/contents/.github/cpp.instructions.md?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "Not found"

echo ""
echo "=== .github/web.instructions.md ==="
gh api repos/MoonModules/WLED-MM/contents/.github/web.instructions.md?ref=mdev --jq '.content' | base64 -d 2>/dev/null || echo "Not found"

Length of output: 30727


Good. Now let me fetch the actual PR diff and key changed files.


🏁 Script executed:

#!/bin/bash
# Fetch the PR diff
gh pr diff 324 --repo MoonModules/WLED-MM 2>/dev/null | head -3000

Length of output: 25336


@softhack007 — here is a full re-review of PR #324 applying the new .coderabbit.yaml guidelines (.github/cpp.instructions.md, .github/copilot-instructions.md, .github/web.instructions.md).


Overall Assessment

The approach is solid — bit-packed masks, ledmap-style streaming parse, _maskValid cache for dimension consistency, and proper mutex-protected pointer swaps are all good patterns aligned with the codebase. The following issues range from a real undefined-behaviour bug down to style nits.


🔴 High — Undefined Behaviour in enumerateSegmasks()

wled00/FX_fcn.cpp

if (WLED_FS.exists(fileName)) segMasks |= 1 << i;
//                                        ^^^^^^^

1 is a signed int literal. When i reaches 31, 1 << 31 shifts into the sign bit — that is UB under C++11/14/17 on all platforms (Xtensa and RISC-V). The existing ledMaps counterpart in the same file has the same form, but this new code should not repeat the pattern.

// Fix:
if (WLED_FS.exists(fileName)) segMasks |= (1U << i);

WLED_MAX_SEGMASKS is capped at 32 by the const.h guard, so 1U << 31 is the maximum shift; that is well-defined for uint32_t.


🟠 Medium — calloc in setMask() Is Not PSRAM-Aware

wled00/FX_fcn.cpp

bits = (uint8_t*)calloc(byteLen, 1);

Per cpp.instructions.md §Memory: use d_malloc() (prefer DRAM) or p_malloc() (prefer PSRAM) from util.h rather than bare calloc. The matching free is free() (which works for both), so only the allocation side needs changing:

bits = (uint8_t*)d_malloc(byteLen);   // prefer DRAM; falls back to PSRAM
if (bits) memset(bits, 0, byteLen);   // calloc zero-fill equivalent

🟠 Medium — 1D Mask Check Applied to 2D Segments Before Routing

wled00/FX_fcn.cpp

if (_maskValid && !maskAllows(i)) return; // WLEDMM mask gate for 1D segments
`#ifndef` WLED_DISABLE_2D
if (is2D()) { ... }   // routes to setPixelColorXY → maskAllowsXY()

The comment says "1D segments" but there is no !is2D() guard. For a 2D segment, both the 1D maskAllows(i) and the 2D maskAllowsXY(x,y) checks run. They map to the same bit only if the virtual 1D layout is strict row-major (x + y*w). If the two indices ever diverge (e.g. transposed segments), the 1D check becomes incorrect and silent.

Options:

  • Add a guard: if (_maskValid && !is2D() && !maskAllows(i)) return;
  • Or document explicitly why the two checks are always equivalent for 2D segments.

🟠 Medium — maskAllows / maskAllowsXY Missing Hot-Path Attributes

wled00/FX.h

Both inline accessors are called directly from IRAM_ATTR __attribute__((hot)) functions. Per cpp.instructions.md §Hot-Path:

Header-defined accessors should use [[gnu::hot]] inline

// Suggested:
[[gnu::hot]] inline bool maskAllows(uint16_t i) const { ... }
[[gnu::hot]] inline bool maskAllowsXY(int x, int y) const { ... }

🟠 Medium — mask / minv Deserialization Ordering

wled00/json.cpp

// 1) mask key is processed first:
seg.setMask(maskId);          // copies current maskInvert → inv

// 2) minv key processed after:
seg.maskInvert = elem["minv"] | seg.maskInvert;

Inside setMask(), inv = maskInvert is captured before the file is read. If a single JSON update sends both "mask" and "minv" together (e.g. restoring a preset), the mask is loaded with the old invert value; the new minv is written to maskInvert afterwards but was not used during setMask(). The display corrects itself on the next render frame, but this means the first rendered frame after a preset restore may invert incorrectly.

Fix: process minv before mask, or pass the JSON-supplied inv value into setMask().


🟠 Medium — clearMask() Timeout Leaves maskId Stale

wled00/FX_fcn.cpp

} else {
  DEBUG_PRINTLN(F("Segment::clearMask: Failed to acquire segmentMux, skipping clear."));
  return;   // ← returns without resetting maskId
}

On timeout the function returns with _mask == (original value) (not yet cleared by the failed mutex path) AND maskId unchanged. The caller is now in an inconsistent state — maskId > 0 but the mask pointer was never freed. Adding maskId = 0; before the return (outside the mutex, just to stop retry loops) would reduce the damage:

} else {
  maskId = 0; // WLEDMM avoid re-entry; mask buffer intentionally leaked on deadlock
  DEBUG_PRINTLN(F("Segment::clearMask: Failed to acquire segmentMux – mask buffer leaked."));
  return;
}

🟡 Low — String Objects in the Mask Parse Loop

wled00/FX_fcn.cpp

while (f.available() && !endOfArray) {
  String entry = f.readStringUntil(',');   // heap alloc per pixel

cpp.instructions.md says to avoid String in performance-sensitive paths. setMask() is not the rendering hot path, but for a 512-pixel mask this is 512 heap alloc/free cycles on load. A small char buf[8] with f.readBytesUntil(',', buf, sizeof(buf)-1) (already used earlier in the same function for w/h) would be more consistent and lighter.


🟡 Low — Redundant Overflow Guard in Parse Loop

wled00/FX_fcn.cpp

if (i >= bitLen) return fail(); // guard against overflow
// ... set bit ...
i++;
if (i > bitLen) return fail();  // ← always false; preceding check catches it

After the first check passes (i < bitLen), the maximum value after i++ is bitLen — which fails > bitLen. The second guard is unreachable; removing it avoids confusion.


🟡 Low — Unused Variable maskInfo in JS

wled00/data/index.js

let maskInfo = "";   // declared but never appended to any HTML

Dead variable. Either remove it or use it (e.g., to show mask dimensions).


🟡 Low — serializeSegment Always Emits mask/minv

wled00/json.cpp

root["mask"] = seg.maskId;     // emitted even when 0
root["minv"] = seg.maskInvert; // emitted even when false

Every /json/state response for every segment now carries these two extra fields. Consider omitting defaults:

if (seg.maskId > 0) {
  root["mask"] = seg.maskId;
  root["minv"] = seg.maskInvert;
}

🟡 Low — Missing Function-Level Comments

Per cpp.instructions.md §Comments, every non-trivial function should have a brief /* ***** */ block:

  • Segment::setMask(uint8_t id) — large function, no doc block
  • Segment::clearMask() — no doc block
  • WS2812FX::enumerateSegmasks() — no doc block
  • maskAllows() / maskAllowsXY() — parameter meanings not self-evident (_maskLen, bit-packing order)

🟡 Low — const.h Guard Silently Drops Out-of-Range Value

`#if` defined(WLED_MAX_SEGMASKS) && (WLED_MAX_SEGMASKS > 32 || WLED_MAX_SEGMASKS < 4)
  `#undef` WLED_MAX_SEGMASKS
`#endif`

cpp.instructions.md recommends static_assert over silent #undef / #error. The existing WLED_MAX_LEDMAPS uses the same pattern so consistency matters, but it is worth at least adding a USER_PRINTF or compile-time note so developers know their override was ignored.


✅ Things Done Well

  • Bit-packed storage — 1 bit per pixel is the right choice; at most 64 bytes for a 512-pixel mask.
  • _maskValid dimension cache — re-validated in both startFrame() and setUp(), so geometry changes don't silently misapply the mask.
  • Ledmap-style streaming parse — avoids loading the whole file into a JSON buffer.
  • Move constructor properly transfers mask ownership via memcpy + null-on-orig.
  • differs() extendedmaskId/maskInvert changes correctly trigger re-render via SEG_DIFFERS_OPT.
  • WLEDMM comment discipline — all new code is properly annotated.
  • fill(BLACK) before mask enable — prevents stale pixels outside the mask on first frame.
  • enumerateSegmasks() called in finalizeInit(), handleSettingsSet(), and WLED::loop() — mirrors the ledmap pattern correctly.

[full_review]


🧠 Learnings used
Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: wled00/fcn_declare.h:403-406
Timestamp: 2025-10-22T21:59:24.331Z
Learning: In MoonModules/WLED-MM PR `#270`, the out-of-bounds null-terminator write in wled00/util.cpp (extractModeSlider) is deferred and tracked in Issue `#272`; do not address it within PR `#270`.

Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h:155-157
Timestamp: 2025-10-22T21:50:25.839Z
Learning: In WLED-MM PR `#270`, only Effect (Mode) IDs are migrated to 16-bit; Palette IDs and counts remain 8-bit. Usermod code should widen mode-related indices/loops to uint16_t while keeping palette-related indices/loops as uint8_t.

@softhack007
Copy link
Copy Markdown
Collaborator

softhack007 commented Apr 4, 2026

@sayhiben wow that review result is better than I thought 😀 .
You don't need to add Function-Level comments atm, especially not for previously existing functions.
Please go through the [list above] and pick what makes sense for you. I'll come back to your questions after the easter weekend.

BTW, you can always ask @coderabbitai for help or explanations on its comments, you can even discuss. The AI we use has become quite "communicative"👍

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

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

The PR description images show:

  1. Image 1: A wood-framed LED matrix panel displaying vibrant pixel art of a character (in purple, blue, red, and green tones) on a dark background — a real-world demonstration of the feature.
  2. Image 2: The WLED Segments UI showing "Segment 0" (16×32, 512 LEDs) with the new MM-specific "Mask 🌜" dropdown (currently segmask1.j…) and "Invert mask 🌜" toggle below standard segment controls.
  3. Image 3: An LED matrix showing "Effect mode" — a penguin-like silhouette where a segment effect is rendered only within the masked region, with the surrounding area dark, clearly demonstrating the masking in action.

@softhack007 @sayhiben — glad the review was useful! 😊

To summarize the priority order for @sayhiben when working through the list:

Priority Item Quick hint
🔴 High 1 << i → UB at bit 31 in enumerateSegmasks() Change to 1U << i
🟠 Medium calloc → use d_malloc + memset Aligns with PSRAM-aware policy
🟠 Medium 1D mask gate applies to 2D segments too Add !is2D() guard or add a comment
🟠 Medium mask/minv JSON ordering (wrong invert on first frame) Process minv before mask
🟠 Medium clearMask() mutex timeout leaves maskId stale Set maskId = 0 before returning
🟡 Low String in parse loop → char buf[] Reduces heap churn on load
🟡 Low Redundant overflow guard after i >= bitLen check Remove the unreachable second guard
🟡 Low Unused maskInfo JS variable Remove or wire up
🟡 Low Always emitting mask/minv in serializeSegment Skip when maskId == 0

Feel free to ask me to clarify any of the above, show a more complete diff for a fix, or explain the reasoning — happy to dig into any of them! 🙂


🧠 Learnings used
Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: wled00/fcn_declare.h:403-406
Timestamp: 2025-10-22T21:59:24.331Z
Learning: In MoonModules/WLED-MM PR `#270`, the out-of-bounds null-terminator write in wled00/util.cpp (extractModeSlider) is deferred and tracked in Issue `#272`; do not address it within PR `#270`.

Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h:155-157
Timestamp: 2025-10-22T21:50:25.839Z
Learning: In WLED-MM PR `#270`, only Effect (Mode) IDs are migrated to 16-bit; Palette IDs and counts remain 8-bit. Usermod code should widen mode-related indices/loops to uint16_t while keeping palette-related indices/loops as uint8_t.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Tip

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

The PR description images show: (1) a pixel-art LED matrix mounted in a laser-cut wooden frame displaying a colourful character; (2) the WLED Segments UI with the new "Mask 🌜" dropdown (set to segmask1.j…) and "Invert mask 🌜" toggle; (3) an LED matrix labelled "Effect mode" showing a masked region with distinct effects inside vs. outside the mask shape.


@softhack007 — noted, and thank you for the kind words! The low-priority "Missing Function-Level Comments" item has been dropped from my checklist for this PR. I'll focus future reviews on substantive issues.

@sayhiben — happy to help clarify any of the flagged items above. The highest-priority ones to look at first would be:

  1. UB in enumerateSegmasks() — the 1U << i fix is a one-character change.
  2. callocd_malloc() — keeps memory allocation consistent with the WLED-MM conventions.
  3. 1D gate in setPixelColor applied before is2D() check — small guard addition.

Feel free to ask me to explain any of them in more detail!


✏️ Learnings added
Learnt from: softhack007
URL: https://github.com/MoonModules/WLED-MM/pull/324

Timestamp: 2026-04-04T00:29:49.113Z
Learning: In MoonModules/WLED-MM, function-level doc comment blocks (e.g. /* ***** */ style headers) are NOT required during code reviews, especially not for previously existing functions. Do not flag missing function-level comments as review issues in this repository.

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 used
Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: wled00/fcn_declare.h:403-406
Timestamp: 2025-10-22T21:59:24.331Z
Learning: In MoonModules/WLED-MM PR `#270`, the out-of-bounds null-terminator write in wled00/util.cpp (extractModeSlider) is deferred and tracked in Issue `#272`; do not address it within PR `#270`.

Learnt from: softhack007
Repo: MoonModules/WLED-MM PR: 270
File: usermods/usermod_v2_rotary_encoder_ui_ALT/usermod_v2_rotary_encoder_ui_ALT.h:155-157
Timestamp: 2025-10-22T21:50:25.839Z
Learning: In WLED-MM PR `#270`, only Effect (Mode) IDs are migrated to 16-bit; Palette IDs and counts remain 8-bit. Usermod code should widen mode-related indices/loops to uint16_t while keeping palette-related indices/loops as uint8_t.

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