use video scaling instead of NPB luminance & new ABL (#4798)

* updated color scaling to preserve hue at low brightness resulting in much better colors
* replace NPBlg with NPB, moved brightness scaling to bus manager
* improved gamma table calculation: fixed mismatch in inverting gamma table calculation: inversion should now be as good as it gets
* code cleanup, fixed gamma being applied in unnecessary places

Improvements to ABL handling:
- removed strip level handling, ist now all done on bus level
- limiter now respects pixel mapping
- proper handling of white channel
- improved current estimation
- current is now always correctly reported to UI
- minimal FPS impact if the ABL is not limiting but slighly higher impact for global ABL limit due to double-scaling

- moved brightness scaling to BusDigital
- created new header file colors.h to be able to access color functions in bus-manager.
- updated colo_fade() with better video scaling to preserve hue's at low brightness
- added IRAM_ATTR to color_fade (negligible speed impact when compared to inline and benefits other functions)
- added IRAM_ATTR to color_blend as it is used a lot throughout the code, did not test speed impact but adding it to color_fade made it almost on-par with an inlined function

Additional changes:
- fixes for properly handling `scaledBri()` (by @blazoncek)
- also use bit-shift instead of division in blending for ESP8266
- improvements for faster "softlight" calculation in blending
- changed some variables to uint8_t to maybe let the compiler optimize better, uint8_t can be faster if read, store and set are all done in uint8_t, which is the case in the ones I changed
- various minor code formatting changes
This commit is contained in:
Damian Schneider
2025-08-29 17:12:10 +02:00
committed by GitHub
parent 8aeb9e1abe
commit d5d7fde30f
15 changed files with 452 additions and 538 deletions

View File

@@ -1194,8 +1194,9 @@ void WS2812FX::finalizeInit() {
if (busEnd > _length) _length = busEnd;
// This must be done after all buses have been created, as some kinds (parallel I2S) interact
bus->begin();
bus->setBrightness(bri);
bus->setBrightness(scaledBri(bri));
}
BusManager::initializeABL(); // init brightness limiter
DEBUG_PRINTF_P(PSTR("Heap after buses: %d\n"), ESP.getFreeHeap());
Segment::maxWidth = _length;
@@ -1297,7 +1298,7 @@ static uint8_t _add (uint8_t a, uint8_t b) { unsigned t = a + b; return t
static uint8_t _subtract (uint8_t a, uint8_t b) { return b > a ? (b - a) : 0; }
static uint8_t _difference(uint8_t a, uint8_t b) { return b > a ? (b - a) : (a - b); }
static uint8_t _average (uint8_t a, uint8_t b) { return (a + b) >> 1; }
#ifdef CONFIG_IDF_TARGET_ESP32C3
#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3)
static uint8_t _multiply (uint8_t a, uint8_t b) { return ((a * b) + 255) >> 8; } // faster than division on C3 but slightly less accurate
#else
static uint8_t _multiply (uint8_t a, uint8_t b) { return (a * b) / 255; } // origianl uses a & b in range [0,1]
@@ -1308,10 +1309,10 @@ static uint8_t _darken (uint8_t a, uint8_t b) { return a < b ? a : b; }
static uint8_t _screen (uint8_t a, uint8_t b) { return 255 - _multiply(~a,~b); } // 255 - (255-a)*(255-b)/255
static uint8_t _overlay (uint8_t a, uint8_t b) { return b < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); }
static uint8_t _hardlight (uint8_t a, uint8_t b) { return a < 128 ? 2 * _multiply(a,b) : (255 - 2 * _multiply(~a,~b)); }
#ifdef CONFIG_IDF_TARGET_ESP32C3
static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a) + 255) >> 8) + 2 * a * b + 255) >> 8; } // Pegtop's formula (1 - 2a)b^2 + 2ab
#if defined(ESP8266) || defined(CONFIG_IDF_TARGET_ESP32C3)
static uint8_t _softlight (uint8_t a, uint8_t b) { return (((b * b * (255 - 2 * a))) + ((2 * a * b + 256) << 8)) >> 16; } // Pegtop's formula (1 - 2a)b^2
#else
static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) / 255 + 2 * a * b) / 255; } // Pegtop's formula (1 - 2a)b^2 + 2ab
static uint8_t _softlight (uint8_t a, uint8_t b) { return (b * b * (255 - 2 * a) + 255 * 2 * a * b) / (255 * 255); } // Pegtop's formula (1 - 2a)b^2 + 2ab
#endif
static uint8_t _dodge (uint8_t a, uint8_t b) { return _divide(~a,b); }
static uint8_t _burn (uint8_t a, uint8_t b) { return ~_divide(a,~b); }
@@ -1550,66 +1551,6 @@ void WS2812FX::blendSegment(const Segment &topSegment) const {
Segment::setClippingRect(0, 0); // disable clipping for overlays
}
// To disable brightness limiter we either set output max current to 0 or single LED current to 0
static uint8_t estimateCurrentAndLimitBri(uint8_t brightness, uint32_t *pixels) {
unsigned milliAmpsMax = BusManager::ablMilliampsMax();
if (milliAmpsMax > 0) {
unsigned milliAmpsTotal = 0;
unsigned avgMilliAmpsPerLED = 0;
unsigned lengthDigital = 0;
bool useWackyWS2815PowerModel = false;
for (size_t i = 0; i < BusManager::getNumBusses(); i++) {
const Bus *bus = BusManager::getBus(i);
if (!(bus && bus->isDigital() && bus->isOk())) continue;
unsigned maPL = bus->getLEDCurrent();
if (maPL == 0 || bus->getMaxCurrent() > 0) continue; // skip buses with 0 mA per LED or max current per bus defined (PP-ABL)
if (maPL == 255) {
useWackyWS2815PowerModel = true;
maPL = 12; // WS2815 uses 12mA per channel
}
avgMilliAmpsPerLED += maPL * bus->getLength();
lengthDigital += bus->getLength();
// sum up the usage of each LED on digital bus
uint32_t busPowerSum = 0;
for (unsigned j = 0; j < bus->getLength(); j++) {
uint32_t c = pixels[j + bus->getStart()];
byte r = R(c), g = G(c), b = B(c), w = W(c);
if (useWackyWS2815PowerModel) { //ignore white component on WS2815 power calculation
busPowerSum += (max(max(r,g),b)) * 3;
} else {
busPowerSum += (r + g + b + w);
}
}
// RGBW led total output with white LEDs enabled is still 50mA, so each channel uses less
if (bus->hasWhite()) {
busPowerSum *= 3;
busPowerSum >>= 2; //same as /= 4
}
// powerSum has all the values of channels summed (max would be getLength()*765 as white is excluded) so convert to milliAmps
milliAmpsTotal += (busPowerSum * maPL * brightness) / (765*255);
}
if (lengthDigital > 0) {
avgMilliAmpsPerLED /= lengthDigital;
if (milliAmpsMax > MA_FOR_ESP && avgMilliAmpsPerLED > 0) { //0 mA per LED and too low numbers turn off calculation
unsigned powerBudget = (milliAmpsMax - MA_FOR_ESP); //80/120mA for ESP power
if (powerBudget > lengthDigital) { //each LED uses about 1mA in standby, exclude that from power budget
powerBudget -= lengthDigital;
} else {
powerBudget = 0;
}
if (milliAmpsTotal > powerBudget) {
//scale brightness down to stay in current limit
unsigned scaleB = powerBudget * 255 / milliAmpsTotal;
brightness = ((brightness * scaleB) >> 8) + 1;
}
}
}
}
return brightness;
}
void WS2812FX::show() {
if (!_pixels) return; // no pixels allocated, nothing to show
@@ -1637,10 +1578,6 @@ void WS2812FX::show() {
show_callback callback = _callback;
if (callback) callback(); // will call setPixelColor or setRealtimePixelColor
// determine ABL brightness
uint8_t newBri = estimateCurrentAndLimitBri(_brightness, _pixels);
if (newBri != _brightness) BusManager::setBrightness(newBri);
// paint actual pixels
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)
@@ -1651,7 +1588,11 @@ void WS2812FX::show() {
if (_pixelCCT) { // cctFromRgb already exluded at allocation
if (i == 0 || _pixelCCT[i-1] != _pixelCCT[i]) BusManager::setSegmentCCT(_pixelCCT[i], correctWB);
}
BusManager::setPixelColor(getMappedPixelIndex(i), realtimeMode && arlsDisableGammaCorrection ? _pixels[i] : gamma32(_pixels[i]));
uint32_t c = _pixels[i]; // need a copy, do not modify _pixels directly (no byte access allowed on ESP32)
if(c > 0 && !(realtimeMode && arlsDisableGammaCorrection))
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
@@ -1663,9 +1604,6 @@ void WS2812FX::show() {
// See https://github.com/Makuna/NeoPixelBus/wiki/ESP32-NeoMethods#neoesp32rmt-methods
BusManager::show();
// restore brightness for next frame
if (newBri != _brightness) BusManager::setBrightness(_brightness);
if (diff > 0) { // skip calculation if no time has passed
size_t fpsCurr = (1000 << FPS_CALC_SHIFT) / diff; // fixed point math
_cumulativeFps = (FPS_CALC_AVG * _cumulativeFps + fpsCurr + FPS_CALC_AVG / 2) / (FPS_CALC_AVG + 1); // "+FPS_CALC_AVG/2" for proper rounding
@@ -1730,7 +1668,7 @@ void WS2812FX::setBrightness(uint8_t b, bool direct) {
if (_brightness == 0) { //unfreeze all segments on power off
for (const Segment &seg : _segments) seg.freeze = false; // freeze is mutable
}
BusManager::setBrightness(b);
BusManager::setBrightness(scaledBri(b));
if (!direct) {
unsigned long t = millis();
if (_segments[0].next_time > t + 22 && t - _lastShow > MIN_SHOW_DELAY) trigger(); //apply brightness change immediately if no refresh soon