Skip to content

Conversation

@sayhiben
Copy link

@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

Summary by CodeRabbit

  • New Features

    • Per-segment masking to control which LEDs each segment updates.
    • Mask selection and inversion controls added to segment UI; masks are enumerated and can be chosen per segment.
    • Masks persist in saved configurations and are loaded on startup.
    • Support for multiple segment masks (configurable limits; typical defaults: 16, 10 on ESP8266).
  • Bug Fixes / Reliability

    • Mask size/geometry validated to prevent mismatches and ensure rendering respects masks.

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

@coderabbitai
Copy link

coderabbitai bot commented Jan 21, 2026

📝 Walkthrough

Walkthrough

Adds WLEDMM per-segment masking: new segment mask state and API, segmask JSON loading/enumeration, UI controls, JSON (de)serialization, and masking checks integrated into 1D/2D rendering and initialization.

Changes

Cohort / File(s) Summary
Header declarations
wled00/FX.h, wled00/wled.h
Added Segment mask fields (maskId, maskInvert, _mask, _maskW, _maskH, _maskLen, _maskValid); new methods setMask(), clearMask(), hasMask(), maskAllows(), maskAllowsXY(); WS2812FX::enumerateSegmasks(); segMasks type conditional on WLED_MAX_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 Segment::setMask() / clearMask() including segmask JSON loading, parsing, validation, memory management, synchronization; integrated mask state into lifecycle and rendering gating; added WS2812FX::enumerateSegmasks() and segMasks tracking.
2D pixel rendering
wled00/FX_2Dfcn.cpp
Validate _maskValid in startFrame(); added maskAllowsXY() guards in both fast and slow setPixelColorXY() paths to skip masked pixels.
JSON serialization
wled00/json.cpp
deserializeSegment() reads "mask" and "minv" (applies/clamps/clears mask); serializeSegment() writes "mask" and "minv"; serializeInfo() adds "masks" array.
UI / Frontend
wled00/data/index.js
Added per-segment mask UI controls (mask selector, invert toggle, info); new setMask() and setMaskInv() functions; mask state integrated into segment rendering and FX mapping UI logic.
Initialization integration
wled00/set.cpp, wled00/wled.cpp
Call strip.enumerateSegmasks() added alongside LED map enumeration during settings init and post-ledmap-load.

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: setMask(seg, id)
    App->>App: requestJson({seg:{id,mask}})
    App->>Seg: deserializeSegment() with "mask"
    Seg->>Seg: setMask(maskId)
    Seg->>FS: open segmask{maskId}.json
    FS-->>Seg: return JSON (w,h,inv,mask bits)
    Seg->>Seg: parse, validate, allocate _mask, set _maskValid
    Seg-->>App: mask applied (state updated)
    App->>UI: update UI state

    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

  • willmmiles
  • softhack007

Poem

🐇 I found a mask beneath a file,

Bits and JSON made me smile,
I hop, I parse, I softly hum,
Pixels bloom where masks say "come",
Hoppy lights — a masked delight ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.91% 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 corresponds to the main feature added: per-segment clipping masks that allow overlapping segments to render distinct effects while preserving segment geometry.

✏️ 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: 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;
     }

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.

1 participant