From b77decc74ed582dc76fee9474157191d418eef38 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Tue, 7 Jul 2026 23:11:28 +0200 Subject: [PATCH 1/9] move gamma correction to bus level - add new global bool to check if gamma needs to be applied - restrict digital LED brightness for better color preservation - move from gamma then brightness to "restricted" brightness then gamma (except for HUB75 where brightness is at driver level - use full resolution gamma on PWM buses - skip black pixels in busDigital setPixelColor - fix brightness calculation when using gammaCorrectBri --- wled00/FX_fcn.cpp | 8 ++---- wled00/bus_manager.cpp | 61 +++++++++++++++++++++++++++--------------- wled00/bus_manager.h | 1 + wled00/cfg.cpp | 1 + wled00/colors.cpp | 4 +-- wled00/colors.h | 5 ++-- wled00/set.cpp | 1 + wled00/udp.cpp | 2 ++ wled00/wled.h | 1 + 9 files changed, 53 insertions(+), 31 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 34fde40058..a43b2ec9c1 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1766,8 +1766,6 @@ void WS2812FX::show() { int oldCCT = Bus::getCCT(); // store original CCT value (since it is global) // when cctFromRgb is true we implicitly calculate WW and CW from RGB values (cct==-1) if (cctFromRgb) BusManager::setSegmentCCT(-1); - // use color gamma correction if enabled, not in realtime mode with gamma disabled or currently overriding RT mode - bool useGammaCorrection = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); for (size_t i = 0; i < totalLen; i++) { // when correctWB is true setSegmentCCT() will convert CCT into K with which we can then @@ -1775,10 +1773,7 @@ void WS2812FX::show() { if (_pixelCCT) { // cctFromRgb already exluded at allocation if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB); } - uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32) - if (c > 0 && useGammaCorrection) - c = gamma32(c); // apply gamma correction if enabled note: applying gamma after brightness has too much color loss BusManager::setPixelColor(getMappedPixelIndex(i), c); } Bus::setCCT(oldCCT); // restore old CCT for ABL adjustments @@ -1854,7 +1849,8 @@ void WS2812FX::setCCT(uint16_t k) { // direct=true either expects the caller to call show() themselves (realtime modes) or be ok waiting for the next frame for the change to apply // direct=false immediately triggers an effect redraw void WS2812FX::setBrightness(uint8_t b, bool direct) { - if (gammaCorrectBri) b = gamma8(b); + if (b > 0 && gammaCorrectBri) + b = gamma8(map(b, 1, 255, gamma8inv(1), 255)); // map from 1 to 255 in gamma space or strip will turn off at low brightness as gamma(b) goes to 0 if (_brightness == b) return; _brightness = b; if (_brightness == 0) { //unfreeze all segments on power off diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 1ba808d8b0..cb5c1e7924 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -266,33 +266,44 @@ void BusDigital::setStatusPixel(uint32_t c) { } } +void BusDigital::setBrightness(uint8_t b) { + _bri = b; + if (_bri < 255 && applyGamma) + _bri = gammaCorrectBri ? map(_bri, 1, 255, gamma8inv(2), 255) : gamma8inv(_bri+1); // limit min brightness so gamma does not dim to black +} + // note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { - if (!_valid) return; - if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + if (_reversed) pix = _len - pix -1; + pix += _skip; uint8_t cctWW = 0, cctCW = 0; uint16_t wwcw = 0; - if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW); - c = color_fade(c, _bri, true); // apply brightness - - if (hasCCT()) { - wwcw = ((cctCW + 1) * _bri) & 0xFF00; // apply brightness to CCT (store CW in upper byte) - wwcw |= ((cctWW + 1) * _bri) >> 8; - if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w - } + // apply brightness, color correction and white calculation, if black, we can skip all of it as they are no-ops + if (c > 0) { + c = color_fade(c, _bri, false); // apply brightness + c = gamma32(c); // apply gamma correction if (currently) used + if (!_valid) return; + if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT + + if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW); + + if (hasCCT()) { + wwcw = (cctCW + 1) & 0xFF00; // apply brightness to CCT (store CW in upper byte) + wwcw |= (cctWW + 1) >> 8; + if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w + } - if (BusManager::_useABL) { - // if using ABL, sum all color channels to estimate current and limit brightness in show() - uint8_t r = R(c), g = G(c), b = B(c); - if (_milliAmpsPerLed < 255) { // normal ABL - _colorSum += r + g + b + W(c); - } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) - _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); + if (BusManager::_useABL) { + // if using ABL, sum all color channels to estimate current and limit brightness in show() + uint8_t r = R(c), g = G(c), b = B(c); + if (_milliAmpsPerLed < 255) { // normal ABL + _colorSum += r + g + b + W(c); + } else { // wacky WS2815 power model, ignore white channel, use max of RGB (issue #549) + _colorSum += ((r > g) ? ((r > b) ? r : b) : ((g > b) ? g : b)); + } } } - if (_reversed) pix = _len - pix -1; - pix += _skip; const uint8_t co = _colorOrderMap.getPixelColorOrder(pix+_start, _colorOrder); if (_type == TYPE_WS2812_1CH_X3) { // map to correct IC, each controls 3 LEDs unsigned pOld = pix; @@ -469,6 +480,7 @@ BusPwm::BusPwm(const BusConfig &bc) void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel + c = gamma32(c); // apply gamma correction if (currently) used if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } @@ -560,8 +572,14 @@ void BusPwm::show() { // for all other cases it will just try to "spread" the load on PSU // Phase shifting requires that LEDC timers are synchronised (see setup()). For PWM CCT (and H-bridge) it is // also mandatory that both channels use the same timer (pinManager takes care of that). + unsigned duty; for (unsigned i = 0; i < numPins; i++) { - unsigned duty = (_data[i] * pwmBri) / 255; + unsigned duty; + if (applyGamma) { + duty = (unsigned)(powf((float)_data[i] / 255.0f, gammaCorrectVal) * pwmBri); // apply full resolution gamma correction: way more accurate than using gamma8(_data[i]) + } else { + duty = (_data[i] * pwmBri) / 255; + } unsigned deadTime = 0; if (_type == TYPE_ANALOG_2CH && Bus::_cctBlend <= 0) { @@ -714,6 +732,7 @@ BusNetwork::BusNetwork(const BusConfig &bc) void BusNetwork::setPixelColor(unsigned pix, uint32_t c) { if (!_valid || pix >= _len) return; uint8_t ww, cw; // dummy, unused + // TODO: should gamma be applied here or better leave it to the receiver? if (_hasWhite) c = autoWhiteCalc(c, ww, cw); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT unsigned offset = pix * _UDPchannels; @@ -1109,8 +1128,8 @@ BusHub75Matrix::BusHub75Matrix(const BusConfig &bc) : Bus(bc.type, bc.start, bc. void IRAM_ATTR BusHub75Matrix::setPixelColor(unsigned pix, uint32_t c) { if (!_valid) return; // note: no need to check pix >= _len as that is checked in containsPixel() + c = gamma32(c); // apply gamma correction if (currently) used // if (_cct >= 1900) c = colorBalanceFromKelvin(_cct, c); //color correction from CCT - if (_ledBuffer) { CRGB fastled_col = CRGB(c); if (_ledBuffer[pix] != fastled_col) { diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index eeb10cff24..fab4f70ba8 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -253,6 +253,7 @@ class BusDigital : public Bus { void show() override; bool canShow() const override; void setStatusPixel(uint32_t c) override; + void setBrightness(uint8_t b) override; [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c) override; void setColorOrder(uint8_t colorOrder) override; [[gnu::hot]] uint32_t getPixelColor(unsigned pix) const override; diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 2e458e7da9..60b19cc0ba 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -530,6 +530,7 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { gammaCorrectBri = false; gammaCorrectCol = false; } + applyGamma = gammaCorrectCol; // apply gamma to colors if used NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables JsonObject light_tr = light["tr"]; diff --git a/wled00/colors.cpp b/wled00/colors.cpp index 6ddc4ec892..5c40c9bde0 100644 --- a/wled00/colors.cpp +++ b/wled00/colors.cpp @@ -663,13 +663,13 @@ void NeoGammaWLEDMethod::calcGammaTable(float gamma) uint8_t NeoGammaWLEDMethod::Correct(uint8_t value) { - if (!gammaCorrectCol) return value; + if (!applyGamma) return value; // gamma is (currently) disabled return gammaT[value]; } uint32_t NeoGammaWLEDMethod::inverseGamma32(uint32_t color) { - if (!gammaCorrectCol) return color; + if (!applyGamma) return color; uint8_t w = W(color); uint8_t r = R(color); uint8_t g = G(color); diff --git a/wled00/colors.h b/wled00/colors.h index 00fe4fb498..a5fadcdcdc 100644 --- a/wled00/colors.h +++ b/wled00/colors.h @@ -29,7 +29,8 @@ struct CRGBW; // forward declations struct CHSV32; -extern bool gammaCorrectCol; +extern bool applyGamma; +extern float gammaCorrectVal; // similar to NeoPixelBus NeoGammaTableMethod but allows dynamic changes (superseded by NPB::NeoGammaDynamicTableMethod) class NeoGammaWLEDMethod { public: @@ -39,7 +40,7 @@ class NeoGammaWLEDMethod { static inline uint8_t rawGamma8(uint8_t val) { return gammaT[val]; } // get value from Gamma table (WLED specific, not used by NPB) static inline uint8_t rawInverseGamma8(uint8_t val) { return gammaT_inv[val]; } // get value from inverse Gamma table (WLED specific, not used by NPB) static inline uint32_t Correct32(uint32_t color) { // apply Gamma to RGBW32 color (WLED specific, not used by NPB) - if (!gammaCorrectCol) return color; // no gamma correction + if (!applyGamma) return color; // gamma is (currently) disabled uint8_t w = byte(color>>24), r = byte(color>>16), g = byte(color>>8), b = byte(color); // extract r, g, b, w channels w = gammaT[w]; r = gammaT[r]; g = gammaT[g]; b = gammaT[b]; return (uint32_t(w) << 24) | (uint32_t(r) << 16) | (uint32_t(g) << 8) | uint32_t(b); diff --git a/wled00/set.cpp b/wled00/set.cpp index fb516ac7d6..74bfe0844c 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -382,6 +382,7 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) gammaCorrectBri = false; gammaCorrectCol = false; } + applyGamma = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); // update gamma use (disable if needed) NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables t = request->arg(F("TD")).toInt(); diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 156a20f990..8c67afea01 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -433,6 +433,7 @@ void realtimeLock(uint32_t timeoutMs, byte md) realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs; } realtimeMode = md; + applyGamma = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); // update gamma use (disable if needed) if (realtimeOverride) return; if (arlsForceMaxBri) strip.setBrightness(255, true); @@ -442,6 +443,7 @@ void realtimeLock(uint32_t timeoutMs, byte md) void exitRealtime() { if (!realtimeMode) return; if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; + applyGamma = gammaCorrectCol; // reenable gamma if used strip.setBrightness(bri, true); realtimeTimeout = 0; // cancel realtime mode immediately realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately diff --git a/wled00/wled.h b/wled00/wled.h index 626f433dc1..103c2cafce 100644 --- a/wled00/wled.h +++ b/wled00/wled.h @@ -410,6 +410,7 @@ WLED_GLOBAL bool cctICused _INIT(true); // CCT IC used (Athom 15W bulb WLED_GLOBAL bool cctICused _INIT(false); // CCT IC used (Athom 15W bulbs) #endif WLED_GLOBAL bool gammaCorrectCol _INIT(true); // use gamma correction on colors +WLED_GLOBAL bool applyGamma _INIT(true); // apply gamma correction to colors if not (temporarily) disabled WLED_GLOBAL bool gammaCorrectBri _INIT(false); // use gamma correction on brightness WLED_GLOBAL float gammaCorrectVal _INIT(2.2f); // gamma correction value From 38772c7051f8e07f4bd6edc1e512c94140b33e6f Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 8 Jul 2026 22:35:29 +0200 Subject: [PATCH 2/9] do not double apply gamma to bri if using gamma for bri --- wled00/bus_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index cb5c1e7924..0d37c529d8 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -269,7 +269,7 @@ void BusDigital::setStatusPixel(uint32_t c) { void BusDigital::setBrightness(uint8_t b) { _bri = b; if (_bri < 255 && applyGamma) - _bri = gammaCorrectBri ? map(_bri, 1, 255, gamma8inv(2), 255) : gamma8inv(_bri+1); // limit min brightness so gamma does not dim to black + _bri = gamma8inv(_bri+1); // limit min brightness so gamma does not dim to black } // note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash From 3839694daa45d80a64ce9a87f67aed537d3e2e9a Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Wed, 8 Jul 2026 23:08:11 +0200 Subject: [PATCH 3/9] fix regressions --- wled00/bus_manager.cpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 0d37c529d8..fd0753188c 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -268,13 +268,14 @@ void BusDigital::setStatusPixel(uint32_t c) { void BusDigital::setBrightness(uint8_t b) { _bri = b; - if (_bri < 255 && applyGamma) - _bri = gamma8inv(_bri+1); // limit min brightness so gamma does not dim to black + if (_bri > 0 && _bri < 255 && applyGamma) + _bri = gamma8inv(_bri + 1); // limit min brightness so gamma does not dim to black } // note: using WLED_O2_ATTR makes this function ~7% faster at the expense of 600 bytes of flash void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { - if (_reversed) pix = _len - pix -1; + if (!_valid) return; + if (_reversed) pix = _len - pix - 1; pix += _skip; uint8_t cctWW = 0, cctCW = 0; uint16_t wwcw = 0; @@ -282,14 +283,13 @@ void IRAM_ATTR BusDigital::setPixelColor(unsigned pix, uint32_t c) { if (c > 0) { c = color_fade(c, _bri, false); // apply brightness c = gamma32(c); // apply gamma correction if (currently) used - if (!_valid) return; if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT if (hasWhite()) c = autoWhiteCalc(c, cctWW, cctCW); if (hasCCT()) { - wwcw = (cctCW + 1) & 0xFF00; // apply brightness to CCT (store CW in upper byte) - wwcw |= (cctWW + 1) >> 8; + wwcw = uint16_t(cctCW) << 8; // store CW in upper byte + wwcw |= cctWW; if (_type == TYPE_WS2812_WWA) c = RGBW32(wwcw, wwcw >> 8, 0, W(c)); // ww,cw, 0, w } @@ -480,7 +480,6 @@ BusPwm::BusPwm(const BusConfig &bc) void BusPwm::setPixelColor(unsigned pix, uint32_t c) { if (pix != 0 || !_valid) return; //only react to first pixel - c = gamma32(c); // apply gamma correction if (currently) used if (Bus::_cct >= 1900 && (_type == TYPE_ANALOG_3CH || _type == TYPE_ANALOG_4CH)) { c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT } From bf75a4cc96a4e2f9cc1d8f48697c536387370e99 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 24 Sep 2026 17:52:33 +0200 Subject: [PATCH 4/9] add helper to update gamma usage (keep the logic in one place), bugfix in restoreColorLossy() --- wled00/FX_fcn.cpp | 1 + wled00/bus_manager.cpp | 7 ++++++- wled00/bus_manager.h | 2 ++ wled00/cfg.cpp | 2 +- wled00/set.cpp | 2 +- wled00/udp.cpp | 5 ++--- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index a43b2ec9c1..be035661d6 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1738,6 +1738,7 @@ void WS2812FX::show() { return; // no pixels allocated, nothing to show } + BusManager::updateGammaUse(); unsigned long showNow = millis(); size_t diff = showNow - _lastShow; diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index fd0753188c..58420282d6 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -267,6 +267,7 @@ void BusDigital::setStatusPixel(uint32_t c) { } void BusDigital::setBrightness(uint8_t b) { + BusManager::updateGammaUse(); _bri = b; if (_bri > 0 && _bri < 255 && applyGamma) _bri = gamma8inv(_bri + 1); // limit min brightness so gamma does not dim to black @@ -731,7 +732,7 @@ BusNetwork::BusNetwork(const BusConfig &bc) void BusNetwork::setPixelColor(unsigned pix, uint32_t c) { if (!_valid || pix >= _len) return; uint8_t ww, cw; // dummy, unused - // TODO: should gamma be applied here or better leave it to the receiver? + // note: gamma correction is done by the receiver if (_hasWhite) c = autoWhiteCalc(c, ww, cw); if (Bus::_cct >= 1900) c = colorBalanceFromKelvin(Bus::_cct, c); //color correction from CCT unsigned offset = pix * _UDPchannels; @@ -1426,6 +1427,10 @@ void BusManager::off() { _gMilliAmpsUsed = 0; // reset, assume no LED idle current if relay is off } +void BusManager::updateGammaUse() { + applyGamma = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); +} + void BusManager::show() { applyABL(); // apply brightness limit, updates _gMilliAmpsUsed for (auto &bus : busses) { diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index fab4f70ba8..c7096cdab6 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -291,6 +291,7 @@ class BusDigital : public Bus { static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { + gamma32inv(c); // note: if ABL is used, this can skew colors (chain is scale->gamma->scaleABL) if (restoreBri < 255) { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { @@ -560,6 +561,7 @@ namespace BusManager { void on(); void off(); + void updateGammaUse(); [[gnu::hot]] void setPixelColor(unsigned pix, uint32_t c); [[gnu::hot]] uint32_t getPixelColor(unsigned pix); void show(); diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 60b19cc0ba..5080a349fe 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -530,8 +530,8 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { gammaCorrectBri = false; gammaCorrectCol = false; } - applyGamma = gammaCorrectCol; // apply gamma to colors if used NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables + BusManager::updateGammaUse(); JsonObject light_tr = light["tr"]; int tdd = light_tr["dur"] | -1; diff --git a/wled00/set.cpp b/wled00/set.cpp index 74bfe0844c..8a66a40d2b 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -382,8 +382,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) gammaCorrectBri = false; gammaCorrectCol = false; } - applyGamma = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); // update gamma use (disable if needed) NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables + BusManager::updateGammaUse(); t = request->arg(F("TD")).toInt(); if (t >= 0) transitionDelayDefault = t; diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 8c67afea01..19a52f8a90 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -433,7 +433,6 @@ void realtimeLock(uint32_t timeoutMs, byte md) realtimeTimeout = (timeoutMs == 255001 || timeoutMs == 65000) ? UINT32_MAX : millis() + timeoutMs; } realtimeMode = md; - applyGamma = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); // update gamma use (disable if needed) if (realtimeOverride) return; if (arlsForceMaxBri) strip.setBrightness(255, true); @@ -443,10 +442,10 @@ void realtimeLock(uint32_t timeoutMs, byte md) void exitRealtime() { if (!realtimeMode) return; if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; - applyGamma = gammaCorrectCol; // reenable gamma if used - strip.setBrightness(bri, true); realtimeTimeout = 0; // cancel realtime mode immediately realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately + BusManager::updateGammaUse(); + strip.setBrightness(bri, true); realtimeIP[0] = 0; if (useMainSegmentOnly) { // unfreeze live segment again strip.getMainSegment().freeze = false; From 1446d3d766dffc0ca4bb9989d3330ee4aa070ee0 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 24 Sep 2026 18:51:39 +0200 Subject: [PATCH 5/9] add missing include, assign value (bugfix) --- wled00/bus_manager.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index c7096cdab6..0a0d3701be 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -14,6 +14,7 @@ */ #include "const.h" +#include "colors.h" #include "pin_manager.h" #include #include @@ -291,7 +292,7 @@ class BusDigital : public Bus { static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { - gamma32inv(c); // note: if ABL is used, this can skew colors (chain is scale->gamma->scaleABL) + c = gamma32inv(c); // note: if ABL is used, this can skew colors (chain is scale->gamma->scaleABL) if (restoreBri < 255) { uint8_t* chan = (uint8_t*) &c; for (uint_fast8_t i=0; i<4; i++) { From b30ba63ff6f3c7764250008e235211af8fc13b66 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 24 Sep 2026 19:15:38 +0200 Subject: [PATCH 6/9] move restoreColorLossy out of h file to fix compile errors --- wled00/bus_manager.cpp | 12 ++++++++++++ wled00/bus_manager.h | 14 +------------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 1d2e0222f8..05328d7691 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -354,6 +354,18 @@ size_t BusDigital::getBusSize() const { return sizeof(BusDigital) + (isOk() ? PolyBus::getDataSize(_busPtr, _iType) : 0); // does not include common I2S DMA buffer } +uint32_t BusDigital::restoreColorLossy(uint32_t c, uint8_t restoreBri) const { + c = gamma32inv(c); // note: if ABL is used, this can skew colors (chain is scale->gamma->scaleABL) + if (restoreBri < 255) { + uint8_t* chan = (uint8_t*) &c; + for (uint_fast8_t i = 0; i < 4; i++) { + uint_fast16_t val = chan[i]; + chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); + } + } + return gamma32inv(c); +} + void BusDigital::setColorOrder(uint8_t colorOrder) { // upper nibble contains W swap information if ((colorOrder & 0x0F) > 5) return; diff --git a/wled00/bus_manager.h b/wled00/bus_manager.h index 0a0d3701be..00c412d64a 100644 --- a/wled00/bus_manager.h +++ b/wled00/bus_manager.h @@ -14,7 +14,6 @@ */ #include "const.h" -#include "colors.h" #include "pin_manager.h" #include #include @@ -273,6 +272,7 @@ class BusDigital : public Bus { bool isI2S(); // true if this bus uses I2S driver void begin() override; void cleanup(); + uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const; static std::vector getLEDTypes(); @@ -290,18 +290,6 @@ class BusDigital : public Bus { void *_busPtr; static uint16_t _milliAmpsTotal; // is overwitten/recalculated on each show() - - inline uint32_t restoreColorLossy(uint32_t c, uint8_t restoreBri) const { - c = gamma32inv(c); // note: if ABL is used, this can skew colors (chain is scale->gamma->scaleABL) - if (restoreBri < 255) { - uint8_t* chan = (uint8_t*) &c; - for (uint_fast8_t i=0; i<4; i++) { - uint_fast16_t val = chan[i]; - chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); //adding _bri slightly improves recovery / stops degradation on re-scale - } - } - return c; - } }; From cfc323744c49cb7a392c2f0abe68122723720a37 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 24 Sep 2026 19:55:54 +0200 Subject: [PATCH 7/9] fix double gamma correction --- wled00/bus_manager.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 05328d7691..6a0d3f5f98 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -363,7 +363,7 @@ uint32_t BusDigital::restoreColorLossy(uint32_t c, uint8_t restoreBri) const { chan[i] = ((val << 8) + restoreBri) / (restoreBri + 1); } } - return gamma32inv(c); + return c; } void BusDigital::setColorOrder(uint8_t colorOrder) { From 79cd85c6cee48f59829c20882883e047e1123d8d Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 9 Oct 2026 18:31:45 +0200 Subject: [PATCH 8/9] remove redundant updateGammaUse() calls, handle it all in show() --- wled00/FX_fcn.cpp | 2 +- wled00/bus_manager.cpp | 2 +- wled00/cfg.cpp | 1 - wled00/set.cpp | 1 - wled00/udp.cpp | 1 - 5 files changed, 2 insertions(+), 5 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3003e2ea19..fffa162987 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -1763,7 +1763,7 @@ void WS2812FX::show() { return; // no pixels allocated, nothing to show } - BusManager::updateGammaUse(); + BusManager::updateGammaUse(); // update gamma use (and bus brightness correction) in case something changed unsigned long showNow = millis(); size_t diff = showNow - _lastShow; diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index 6a0d3f5f98..b0a18cf724 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -267,7 +267,6 @@ void BusDigital::setStatusPixel(uint32_t c) { } void BusDigital::setBrightness(uint8_t b) { - BusManager::updateGammaUse(); _bri = b; if (_bri > 0 && _bri < 255 && applyGamma) _bri = gamma8inv(_bri + 1); // limit min brightness so gamma does not dim to black @@ -1481,6 +1480,7 @@ void BusManager::off() { void BusManager::updateGammaUse() { applyGamma = gammaCorrectCol && !(realtimeMode && arlsDisableGammaCorrection && !realtimeOverride); + BusManager::setBrightness(scaledBri(bri)); // update brightness for all (digital) buses in case gamma usage changed } void BusManager::show() { diff --git a/wled00/cfg.cpp b/wled00/cfg.cpp index 1183c39a7b..e50ac3f90b 100644 --- a/wled00/cfg.cpp +++ b/wled00/cfg.cpp @@ -540,7 +540,6 @@ bool deserializeConfig(JsonObject doc, bool fromFS) { gammaCorrectCol = false; } NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables - BusManager::updateGammaUse(); JsonObject light_tr = light["tr"]; int tdd = light_tr["dur"] | -1; diff --git a/wled00/set.cpp b/wled00/set.cpp index 2ec17b055f..f68520a6d0 100644 --- a/wled00/set.cpp +++ b/wled00/set.cpp @@ -405,7 +405,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) gammaCorrectCol = false; } NeoGammaWLEDMethod::calcGammaTable(gammaCorrectVal); // fill look-up tables - BusManager::updateGammaUse(); t = request->arg(F("TD")).toInt(); if (t >= 0) transitionDelayDefault = t; diff --git a/wled00/udp.cpp b/wled00/udp.cpp index 5d104f6866..1489cb0d19 100644 --- a/wled00/udp.cpp +++ b/wled00/udp.cpp @@ -441,7 +441,6 @@ void exitRealtime() { if (realtimeOverride == REALTIME_OVERRIDE_ONCE) realtimeOverride = REALTIME_OVERRIDE_NONE; realtimeTimeout = 0; // cancel realtime mode immediately realtimeMode = REALTIME_MODE_INACTIVE; // inform UI immediately - BusManager::updateGammaUse(); strip.setBrightness(bri, true); realtimeIP[0] = 0; if (useMainSegmentOnly) { // unfreeze live segment again From cc93c8604cfbfaa3fb67875eb5ee90438c798d94 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 9 Oct 2026 18:41:36 +0200 Subject: [PATCH 9/9] do not gamma correct cct-IC channels, they are linear. --- wled00/bus_manager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/wled00/bus_manager.cpp b/wled00/bus_manager.cpp index b0a18cf724..f115fc4886 100644 --- a/wled00/bus_manager.cpp +++ b/wled00/bus_manager.cpp @@ -591,7 +591,8 @@ void BusPwm::show() { unsigned duty; for (unsigned i = 0; i < numPins; i++) { unsigned duty; - if (applyGamma) { + bool isLinearChannel = cctICused && ((_type == TYPE_ANALOG_2CH && i == 1) || (_type == TYPE_ANALOG_5CH && i == 4)); + if (applyGamma && !isLinearChannel) { duty = (unsigned)(powf((float)_data[i] / 255.0f, gammaCorrectVal) * pwmBri); // apply full resolution gamma correction: way more accurate than using gamma8(_data[i]) } else { duty = (_data[i] * pwmBri) / 255;