diff --git a/wled00/FX.h b/wled00/FX.h index ddfa3c8777..c2fadc403a 100644 --- a/wled00/FX.h +++ b/wled00/FX.h @@ -454,6 +454,8 @@ typedef struct Segment { uint16_t startY; // start Y coodrinate 2D (top); there should be no more than 255 rows, but we cannot be sure. uint16_t stopY; // stop Y coordinate 2D (bottom); there should be no more than 255 rows, but we cannot be sure. char *name = nullptr; // WLEDMM initialize to nullptr + uint8_t maskId = 0; // WLEDMM segment mask ID (0 = none) + bool maskInvert = false; // WLEDMM invert mask bits (useful for "outside" masks) // runtime data unsigned long next_time; // millis() of next update @@ -469,6 +471,12 @@ typedef struct Segment { void *jMap = nullptr; //WLEDMM jMap private: + uint8_t *_mask = nullptr; // WLEDMM bit-packed mask, 1 bit per virtual pixel + uint16_t _maskW = 0; // WLEDMM mask width in virtual pixels + uint16_t _maskH = 0; // WLEDMM mask height in virtual pixels + size_t _maskLen = 0; // WLEDMM total bits (w*h), not bytes + bool _maskValid = false; // WLEDMM cached dimension check vs virtual size + union { uint8_t _capabilities; struct { @@ -563,6 +571,8 @@ typedef struct Segment { startY(0), stopY(1), name(nullptr), + maskId(0), // WLEDMM + maskInvert(false), // WLEDMM next_time(0), step(0), call(0), @@ -571,6 +581,11 @@ typedef struct Segment { data(nullptr), ledsrgb(nullptr), ledsrgbSize(0), //WLEDMM + _mask(nullptr), // WLEDMM + _maskW(0), // WLEDMM + _maskH(0), // WLEDMM + _maskLen(0), // WLEDMM + _maskValid(false), // WLEDMM _capabilities(0), _dataLen(0), _t(nullptr) @@ -612,6 +627,7 @@ typedef struct Segment { if ((Segment::_globalLeds == nullptr) && !strip_uses_global_leds() && (ledsrgb != nullptr)) {free(ledsrgb); ledsrgb = nullptr;} // WLEDMM we need "!strip_uses_global_leds()" to avoid crashes (#104) if (name) { delete[] name; name = nullptr; } if (_t) { transitional = false; delete _t; _t = nullptr; } + clearMask(); // WLEDMM deallocateData(); } @@ -666,6 +682,8 @@ typedef struct Segment { inline void markForReset(void) { reset = true; } // setOption(SEG_OPTION_RESET, true) inline void markForBlank(void) { needsBlank = true; } // WLEDMM serialize "blank" requests, avoid parallel drawing from different task void setUpLeds(void); // set up leds[] array for loseless getPixelColor() + bool setMask(uint8_t id); // WLEDMM + void clearMask(); // WLEDMM // transition functions void startTransition(uint16_t dur); // transition has to start before actual segment values change @@ -704,6 +722,23 @@ typedef struct Segment { #else inline uint16_t virtualLength(void) const {return _virtuallength;} #endif + inline bool hasMask(void) const { return _mask != nullptr; } // WLEDMM + 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 + 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; + } void setPixelColor(int n, uint32_t c); // set relative pixel within segment with color inline void setPixelColor(int n, byte r, byte g, byte b, byte w = 0) { setPixelColor(n, RGBW32(r,g,b,w)); } // automatically inline inline void setPixelColor(int n, CRGB c) { setPixelColor(n, uint32_t(c) & 0x00FFFFFF); } // automatically inline @@ -1044,7 +1079,8 @@ class WS2812FX { // 96 bytes fixInvalidSegments(), show(void), setTargetFps(uint8_t fps), - enumerateLedmaps(); //WLEDMM (from fcn_declare) + enumerateLedmaps(), //WLEDMM (from fcn_declare) + enumerateSegmasks(); // WLEDMM void setColor(uint8_t slot, uint8_t r, uint8_t g, uint8_t b, uint8_t w = 0) { setColor(slot, RGBW32(r,g,b,w)); } void fill(uint32_t c) { for (int i = 0; i < getLengthTotal(); i++) setPixelColor(i, c); } // fill whole strip with color (inline) diff --git a/wled00/FX_2Dfcn.cpp b/wled00/FX_2Dfcn.cpp index d12153d5ff..ec67a5ced7 100644 --- a/wled00/FX_2Dfcn.cpp +++ b/wled00/FX_2Dfcn.cpp @@ -234,6 +234,15 @@ void Segment::startFrame(void) { _2dWidth = _isValid2D ? calc_virtualWidth() : calc_virtualLength(); _virtuallength = calc_virtualLength(); #endif + + // WLEDMM validate mask dimensions against current virtual size + if (_mask) { + uint16_t vW = calc_virtualWidth(); + uint16_t vH = calc_virtualHeight(); + _maskValid = (_maskW == vW && _maskH == vH); + } else { + _maskValid = false; + } } // WLEDMM end @@ -245,6 +254,7 @@ void Segment::startFrame(void) { // * expects scaled color (final brightness) as additional input parameter, plus segment virtualWidth() and virtualHeight() void IRAM_ATTR __attribute__((hot)) Segment::setPixelColorXY_fast(int x, int y, uint32_t col, uint32_t scaled_col, int cols, int rows) const //WLEDMM { + if (_maskValid && !maskAllowsXY(x, y)) return; // WLEDMM mask gate for 2D pixels unsigned i = UINT_MAX; bool sameColor = false; if (ledsrgb) { // WLEDMM small optimization @@ -301,6 +311,7 @@ void IRAM_ATTR_YN Segment::setPixelColorXY(int x, int y, uint32_t col) //WLEDMM: const int_fast16_t rows = virtualHeight(); if (x<0 || y<0 || x >= cols || y >= rows) return; // if pixel would fall out of virtual segment just exit + if (_maskValid && !maskAllowsXY(x, y)) return; // WLEDMM mask gate for 2D pixels unsigned i = UINT_MAX; bool sameColor = false; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 996bb57b60..fbdab3c6f0 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -95,6 +95,13 @@ Segment::Segment(const Segment &orig) { data = nullptr; _dataLen = 0; _t = nullptr; + _mask = nullptr; // WLEDMM + _maskLen = 0; // WLEDMM + _maskW = 0; // WLEDMM + _maskH = 0; // WLEDMM + _maskValid = false; // WLEDMM + maskId = 0; // WLEDMM keep id in sync with buffer + maskInvert = false; // WLEDMM keep invert in sync with buffer if (ledsrgb && !Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;} // WLEDMM if (orig.name) { name = new(std::nothrow) char[strlen(orig.name)+1]; if (name) strcpy(name, orig.name); } if (orig.data) { if (allocateData(orig._dataLen, true)) memcpy(data, orig.data, orig._dataLen); } @@ -142,6 +149,148 @@ void Segment::allocLeds() { } } +void Segment::clearMask() { // WLEDMM + uint8_t* oldMask = nullptr; + strip_wait_until_idle("Segment::clearMask"); // WLEDMM avoid swapping while renderer is active + if (esp32SemTake(segmentMux, 2100) == pdTRUE) { // WLEDMM serialize mask pointer changes with renderer + oldMask = _mask; + _mask = nullptr; + _maskLen = 0; + _maskW = 0; + _maskH = 0; + _maskValid = false; + maskId = 0; // WLEDMM keep id in sync with buffer + esp32SemGive(segmentMux); + } else { + DEBUG_PRINTLN(F("Segment::clearMask: Failed to acquire segmentMux, skipping clear.")); + return; + } + if (oldMask) free(oldMask); +} + +bool Segment::setMask(uint8_t id) { // WLEDMM + clearMask(); + if (id >= WLED_MAX_SEGMASKS) { + return false; + } + if (id == 0) return true; + + char fileName[24] = {'\0'}; + snprintf_P(fileName, sizeof(fileName), PSTR("/segmask%d.json"), id); + if (!WLED_FS.exists(fileName)) { + DEBUG_PRINTF("Segment mask missing: %s\n", fileName); + maskId = 0; // WLEDMM avoid repeated reload attempts + return false; + } + + File f = WLED_FS.open(fileName, "r"); + if (!f) { + maskId = 0; // WLEDMM avoid repeated reload attempts + return false; + } + + uint16_t w = 0; + uint16_t h = 0; + size_t bitLen = 0; // WLEDMM total mask pixels (w*h) + uint8_t* bits = nullptr; // WLEDMM bit-packed mask, 1 bit per pixel + bool inv = maskInvert; // WLEDMM default to current invert setting + auto fail = [&]() -> bool { + if (bits) free(bits); + maskId = 0; // WLEDMM avoid repeated reload attempts + f.close(); + return false; + }; + + char buf[32] = { '\0' }; + if (!f.find("\"w\":")) return fail(); + f.readBytesUntil('\n', buf, sizeof(buf)-1); + w = atoi(cleanUpName(buf)); + + f.seek(0); + if (!f.find("\"h\":")) return fail(); + memset(buf, 0, sizeof(buf)); + f.readBytesUntil('\n', buf, sizeof(buf)-1); + h = atoi(cleanUpName(buf)); + + if (w == 0 || h == 0) return fail(); + bitLen = size_t(w) * size_t(h); + if (bitLen == 0 || bitLen > (size_t(Segment::maxWidth) * size_t(Segment::maxHeight))) return fail(); + + // WLEDMM pack 8 pixels per byte, LSB-first (bit 0 = pixel 0) + size_t byteLen = (bitLen + 7) / 8; + bits = (uint8_t*)calloc(byteLen, 1); + if (!bits) { + errorFlag = ERR_LOW_MEM; // WLEDMM raise errorflag + return fail(); + } + + f.seek(0); + // WLEDMM strict "inv" boolean parsing (true/false only) + if (f.find("\"inv\":")) { + String entry = f.readStringUntil(','); + int end = entry.indexOf('}'); + if (end >= 0) entry.remove(end); + entry.trim(); + if (entry == "true") inv = true; + else if (entry == "false") inv = false; + else return fail(); + } + + f.seek(0); + // WLEDMM strict 0/1 mask array, use streaming parse (ledmap-style) + if (!f.find("\"mask\":")) return fail(); + f.readBytesUntil('[', buf, sizeof(buf)-1); + + size_t i = 0; + bool endOfArray = false; + while (f.available() && !endOfArray) { + String entry = f.readStringUntil(','); + int bracket = entry.indexOf(']'); + if (bracket >= 0) { + entry.remove(bracket); + endOfArray = true; + } + entry.trim(); + if (entry.length() == 0) return fail(); + if (i >= bitLen) return fail(); // WLEDMM guard against overflow + if (entry == "1") bits[i >> 3] |= (0x01U << (i & 7)); // WLEDMM set bit (pixel i) in packed mask + else if (entry != "0") return fail(); + i++; + if (i > bitLen) return fail(); + } + + if (!endOfArray || i != bitLen) return fail(); + + bool ok = false; + strip_wait_until_idle("Segment::setMask"); // WLEDMM avoid swapping while renderer is active + // WLEDMM clear segment before enabling mask to avoid stale pixels outside the mask + if (esp32SemTake(busDrawMux, 250) == pdTRUE) { + fill(BLACK); + esp32SemGive(busDrawMux); + } else { + DEBUG_PRINTLN(F("Segment::setMask: Failed to acquire busDrawMux, skipping pre-mask clear.")); + } + if (esp32SemTake(segmentMux, 2100) == pdTRUE) { // WLEDMM serialize mask pointer changes with renderer + _mask = bits; + bits = nullptr; + _maskW = w; + _maskH = h; + _maskLen = bitLen; + maskInvert = inv; + _maskValid = (_maskW == calc_virtualWidth() && _maskH == calc_virtualHeight()); + maskId = id; // WLEDMM commit mask id only on success + esp32SemGive(segmentMux); + ok = true; + } else { + DEBUG_PRINTLN(F("Segment::setMask: Failed to acquire segmentMux, skipping mask update.")); + } + + if (!ok && bits) free(bits); + if (!ok) maskId = 0; // WLEDMM avoid repeated reload attempts + f.close(); + return ok; +} + // move constructor --> moves everything (including buffer) from orig to this Segment::Segment(Segment &&orig) noexcept { DEBUG_PRINTLN(F("-- Move segment constructor --")); @@ -163,6 +312,11 @@ Segment::Segment(Segment &&orig) noexcept { orig.ledsrgb = nullptr; //WLEDMM orig.ledsrgbSize = 0; // WLEDMM orig.jMap = nullptr; //WLEDMM jMap + orig._mask = nullptr; // WLEDMM + orig._maskLen = 0; // WLEDMM + orig._maskW = 0; // WLEDMM + orig._maskH = 0; // WLEDMM + orig._maskValid = false; // WLEDMM } // copy assignment --> overwrite segment with orig - deletes old buffers in "this", but does not change orig! @@ -173,6 +327,11 @@ Segment& Segment::operator= (const Segment &orig) { transitional = false; // copied segment cannot be in transition if (name) delete[] name; if (_t) delete _t; + 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; + } CRGB* oldLeds = ledsrgb; size_t oldLedsSize = ledsrgbSize; if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); @@ -191,6 +350,13 @@ Segment& Segment::operator= (const Segment &orig) { data = nullptr; _dataLen = 0; _t = nullptr; + _mask = nullptr; // WLEDMM + _maskLen = 0; // WLEDMM + _maskW = 0; // WLEDMM + _maskH = 0; // WLEDMM + _maskValid = false; // WLEDMM + maskId = 0; // WLEDMM keep id in sync with buffer + maskInvert = false; // WLEDMM keep invert in sync with buffer //if (!Segment::_globalLeds) {ledsrgb = oldLeds; ledsrgbSize = oldLedsSize;}; // WLEDMM reuse leds instead of ledsrgb = nullptr; if (!Segment::_globalLeds) {ledsrgb = nullptr; ledsrgbSize = 0;}; // WLEDMM copy has no buffers (yet) // copy source data @@ -211,6 +377,11 @@ Segment& Segment::operator= (Segment &&orig) noexcept { transitional = false; // just temporary if (name) { delete[] name; name = nullptr; } // free old name deallocateData(); // free old runtime data + if (_mask) { // WLEDMM free mask buffer directly to avoid deadlocks + strip_wait_until_idle("Segment::operator= move mask cleanup"); // WLEDMM avoid freeing while renderer is active + free(_mask); + _mask = nullptr; + } if (_t) { delete _t; _t = nullptr; } if (ledsrgb && !Segment::_globalLeds) free(ledsrgb); //WLEDMM: not needed anymore as we will use leds from copy. no need to nullify ledsrgb as it gets new value in memcpy @@ -230,6 +401,11 @@ Segment& Segment::operator= (Segment &&orig) noexcept { orig.ledsrgb = nullptr; //WLEDMM: do not free as moved to here orig.ledsrgbSize = 0; //WLEDMM orig.jMap = nullptr; //WLEDMM jMap + orig._mask = nullptr; // WLEDMM + orig._maskLen = 0; // WLEDMM + orig._maskW = 0; // WLEDMM + orig._maskH = 0; // WLEDMM + orig._maskValid = false; // WLEDMM } return *this; } @@ -587,6 +763,14 @@ void Segment::setUp(uint16_t i1, uint16_t i2, uint8_t grp, uint8_t spc, uint16_t spacing = spc; } if (ofs < UINT16_MAX) offset = ofs; + // WLEDMM keep mask validity aligned with current virtual geometry + if (_mask) { + uint16_t vW = calc_virtualWidth(); + uint16_t vH = calc_virtualHeight(); + _maskValid = (_maskW == vW && _maskH == vH); + } else { + _maskValid = false; + } esp32SemGive(segmentMux); } else { DEBUG_PRINTLN(F("Segment::setUp: Failed to acquire segmentMux, skipping bounds update.")); @@ -1001,6 +1185,8 @@ void IRAM_ATTR_YN WLED_O2_ATTR __attribute__((hot)) Segment::setPixelColor(int i i &= 0xFFFF; if (unsigned(i) >= virtualLength()) return; // if pixel would fall out of segment just exit //WLEDMM unsigned(i)>SEGLEN also catches "i<0" + if (_maskValid && !maskAllows(i)) return; // WLEDMM mask gate for 1D segments + #ifndef WLED_DISABLE_2D if (is2D()) { uint16_t vH = virtualHeight(); // segment height in logical pixels @@ -1397,6 +1583,8 @@ uint8_t Segment::differs(Segment& b) const { if (check3 != b.check3) d |= SEG_DIFFERS_FX; if (startY != b.startY) d |= SEG_DIFFERS_BOUNDS; if (stopY != b.stopY) d |= SEG_DIFFERS_BOUNDS; + if (maskId != b.maskId) d |= SEG_DIFFERS_OPT; // WLEDMM + if (maskInvert != b.maskInvert) d |= SEG_DIFFERS_OPT; // WLEDMM //bit pattern: (msb first) set:2, sound:1, mapping:3, transposed, mirrorY, reverseY, [transitional, reset,] paused, mirrored, on, reverse, [selected] if ((options & 0b1111111110011110U) != (b.options & 0b1111111110011110U)) d |= SEG_DIFFERS_OPT; @@ -1791,6 +1979,16 @@ void WS2812FX::enumerateLedmaps() { } } +// WLEDMM enumerate all segmaskX.json files on FS +void WS2812FX::enumerateSegmasks() { + segMasks = 0; + for (int i = 1; i < WLED_MAX_SEGMASKS; i++) { + char fileName[24] = {'\0'}; + snprintf_P(fileName, sizeof(fileName), PSTR("/segmask%d.json"), i); + if (WLED_FS.exists(fileName)) segMasks |= 1 << i; + } +} + //do not call this method from system context (network callback) void WS2812FX::finalizeInit(void) @@ -1806,6 +2004,7 @@ void WS2812FX::finalizeInit(void) // if we do it in json.cpp (serializeInfo()) we are getting flashes on LEDs // unfortunately this means we do not get updates after uploads enumerateLedmaps(); + enumerateSegmasks(); // WLEDMM _hasWhiteChannel = _isOffRefreshRequired = false; diff --git a/wled00/const.h b/wled00/const.h index c81854dad0..7b04172292 100644 --- a/wled00/const.h +++ b/wled00/const.h @@ -91,6 +91,18 @@ #endif #endif +// WLEDMM segment mask slots (filesystem segmaskX.json) +#if defined(WLED_MAX_SEGMASKS) && (WLED_MAX_SEGMASKS > 32 || WLED_MAX_SEGMASKS < 4) + #undef WLED_MAX_SEGMASKS +#endif +#ifndef WLED_MAX_SEGMASKS + #ifdef ESP8266 + #define WLED_MAX_SEGMASKS 10 + #else + #define WLED_MAX_SEGMASKS 16 + #endif +#endif + #ifndef WLED_MAX_SEGNAME_LEN #ifdef ESP8266 #define WLED_MAX_SEGNAME_LEN 32 diff --git a/wled00/data/index.js b/wled00/data/index.js index d1e367dd55..b365afe8ff 100644 --- a/wled00/data/index.js +++ b/wled00/data/index.js @@ -791,6 +791,10 @@ function populateSegments(s) let staY = inst.startY; let stoY = inst.stopY; let isMSeg = isM && staX 0); // WLEDMM let rvXck = ``; let miXck = ``; let rvYck = "", miYck =""; @@ -817,6 +821,34 @@ function populateSegments(s) ``+ ``+ ``; + // WLEDMM begin: segment mask UI + let maskSel = ""; + let maskInvSel = ""; + let maskInfo = ""; + let maskList = Array.isArray(li.masks) ? li.masks : []; + if (maskList.length > 0 || maskId > 0) { + let opts = ``; + let hasMaskId = false; + for (const m of maskList) { + if (m.id == maskId) hasMaskId = true; + opts += ``; + } + if (maskId > 0 && !hasMaskId) { + opts += ``; // WLEDMM keep unknown mask visible + } + maskSel = `
Mask ☾
`+ + `
`+ + `
`; + if (maskActive) { + maskInvSel = ``; + } + } + // WLEDMM end: segment mask UI //WLEDMM ARTIFX let fxName = eJson.find((o)=>{return o.id==selectedFx}).name; let cusEff = `
`; @@ -870,6 +902,7 @@ function populateSegments(s) `
`+ (!isMSeg ? rvXck : '') + (isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') + + maskSel + maskInvSel + // WLEDMM (s.AudioReactive && s.AudioReactive.on ? "" : sndSim) + (s.ARTIFX && s.ARTIFX.on && fxName.includes("ARTI-FX") ? cusEff : "") + // `