Skip to content

Conversation

DedeHai
Copy link
Collaborator

@DedeHai DedeHai commented Jul 31, 2025

NPB luminance setting dims dark colors to black, this gives really ugly results at low brightness:
Examples:

  • palette effect with rainbow is just red, green, blue even with black stripes
  • DNA Spiral: parts of the spiral are missing at brightness < 10

The proposed method applies video scaling: any color that is non zero will stay non zero
NPB luminance is not used anymore, so changing to non "Lg" version of NPB, saving flash.
With brightness no longer being handled by NPB, a new approach to brightness limiter was implemented.

fixes #4794

Summary by CodeRabbit

  • New Features

    • Automatic Brightness Limiting (ABL) across digital LED buses to manage current and prevent overloads.
  • Improvements

    • Unified brightness handling — brightness now flows through ABL and per-bus scaling for more consistent dimming.
    • Color/gamma pipeline updates improving palette and color rendering fidelity in many effects and image upscaling.
  • Bug Fixes

    • More consistent brightness behavior during realtime updates and DMX/image-driven changes.

@DedeHai DedeHai requested a review from blazoncek July 31, 2025 05:29
@DedeHai DedeHai added enhancement optimization re-working an existing feature to be faster, or use less memory labels Jul 31, 2025
Copy link
Contributor

coderabbitai bot commented Jul 31, 2025

Walkthrough

Reworks bus/NeoPixelBus API usage, introduces a global Automatic Brightness Limiter (ABL) pipeline, restructures gamma/color utilities into a new colors.h, moves gamma application to the final send stage, and removes several per-bus brightness dispatch paths. Multiple FX and realtime paths updated to use raw brightness values.

Changes

Cohort / File(s) Change Summary
NeoPixelBus / PolyBus modernization
wled00/bus_wrapper.h
Replace NeoPixelBusLg usages with NeoPixelBus variants; remove NeoGammaNullMethod template args; simplify bus macro/type definitions; remove PolyBus::setBrightness dispatch.
Bus management & ABL
wled00/bus_manager.cpp, wled00/bus_manager.h
Add BusManager::initializeABL(), BusManager::applyABL(), BusManager::_useABL; add BusDigital::estimateCurrent(), applyBriLimit(), setCurrentLimit(); per-bus color-sum current estimation and brightness-limiting logic; integrate ABL in show()/off().
Color/gamma API rework
wled00/colors.h, wled00/colors.cpp, wled00/fcn_declare.h
Introduce new colors.h (CRGBW, CHSV32, NeoGammaWLEDMethod, palette utilities); move/modify color_blend/color_fade implementations; add IRAM_ATTR; remove old declarations from fcn_declare.h; remove NeoGammaWLEDMethod::Correct32 implementation.
Gamma application moved to send stage
wled00/FX_fcn.cpp, wled00/FX.cpp, wled00/image_loader.cpp
Apply gamma to a local color copy before BusManager::setPixelColor/send; remove per-pixel gamma8() in mode_2Ddistortionwaves; image upscaling now uses raw RGB (no gamma8).
Brightness scaling / API call changes (scaledBri -> raw bri)
wled00/e131.cpp, wled00/json.cpp, wled00/udp.cpp, wled00/led.cpp, wled00/wled.cpp, wled00/wled.h
Replace or adjust scaledBri usages in several realtime/JSON/DMX/reinit paths (some calls now pass raw bri, others use scaledBri(scaled) via BusManager when reinit); add colors.h include; change scaledBri casting.
Misc color math and helpers
wled00/colors.cpp, wled00/colors.h (continued)
Rework color_fade math (video path preserves dominant channels), adjust gamma table generation (inverse mapping tweak), add debug printf in calcGammaTable.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120+ minutes

Assessment against linked issues

Objective (Issue #) Addressed Explanation
Fix color mixing artifacts at low global brightness (dark color mixing regression) (#4794) Changes move gamma application to final send and remove some per-pixel gamma uses (FX/image), but multiple gamma API changes and removal/addition of gamma helpers make it unclear whether the original low-brightness mixing regression is fully restored across all effects and palette flows.

Assessment against linked issues: Out-of-scope changes

Code Change Explanation
NeoPixelBus API modernization and removal of PolyBus::setBrightness (wled00/bus_wrapper.h) Not related to the regression objective which targets dark color mixing; this is an API modernization affecting bus instantiation and brightness dispatch.
Full ABL pipeline and new BusDigital current estimation (wled00/bus_manager.cpp, wled00/bus_manager.h) Introduces new current-limiting subsystem unrelated to fixing color-mixing artifacts; scope extends beyond the linked issue.
Addition of large color API/header (wled00/colors.h) and removal of declarations from fcn_declare.h Reorganizes public color APIs and types; this structural change is broader than the single-issue regression fix.

Possibly related PRs

Suggested reviewers

  • blazoncek
  • willmmiles

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@DedeHai DedeHai changed the title proof of concept: use video scaling instead of NPB luminance use video scaling instead of NPB luminance Jul 31, 2025
@DedeHai DedeHai marked this pull request as draft July 31, 2025 05:30
Copy link
Contributor

@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

🧹 Nitpick comments (1)
wled00/bus_wrapper.h (1)

8-8: Consider thread safety and encapsulation for global brightness variable

While using a global variable for brightness might be acceptable in this embedded context (ESP8266/ESP32), consider:

  • Making it static within the PolyBus class to improve encapsulation
  • Adding volatile if accessed from interrupts
  • Documenting that this is shared across all bus instances
-uint8_t globalBrightness = 255; // global brightness for digital busses, set by setBrightness()
+class PolyBus {
+  private:
+    static bool _useParallelI2S;
+    static volatile uint8_t _globalBrightness; // shared brightness for all digital buses

And initialize it in the implementation file:

volatile uint8_t PolyBus::_globalBrightness = 255;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b76ef23 and 5be1bc0.

📒 Files selected for processing (1)
  • wled00/bus_wrapper.h (3 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: KrX3D
PR: wled/WLED#4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:430-435
Timestamp: 2025-04-28T20:51:29.773Z
Learning: In WLED, `bri` is a global variable used for brightness control.
wled00/bus_wrapper.h (4)

Learnt from: KrX3D
PR: #4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:430-435
Timestamp: 2025-04-28T20:51:29.773Z
Learning: In WLED, bri is a global variable used for brightness control.

Learnt from: KrX3D
PR: #4664
File: wled00/syslog.cpp:50-51
Timestamp: 2025-04-26T12:51:04.993Z
Learning: In the WLED codebase, syslog configuration variables (syslogHost, syslogPort, syslogProtocol, syslogFacility, syslogSeverity) are defined as global variables in wled.h and are accessed directly in the SyslogPrinter implementation, rather than being class members.

Learnt from: blazoncek
PR: #4658
File: wled00/const.h:140-141
Timestamp: 2025-04-26T19:19:07.600Z
Learning: In WLED, the WLED_MAX_PANELS macro is intentionally defined as a fixed constant value (18) with no redefinition mechanism, making it "unoverridable" - there's no need for a static assertion to check its maximum value.

Learnt from: willmmiles
PR: #4712
File: wled00/FX_fcn.cpp:992-993
Timestamp: 2025-06-07T15:58:42.579Z
Learning: In WLED segments, the backing pixel buffer is always allocated to the full physical segment size, but effect rendering only uses a subset when mirror or grouping are enabled. For 2D segments, this subset is vWidth() * vHeight(), and for 1D segments it's vLength(). Color processing operations like fade_out should iterate over this rendering subset, not the full allocated buffer.

🔇 Additional comments (2)
wled00/bus_wrapper.h (2)

786-798: Well-implemented video scaling algorithm

The brightness scaling implementation correctly addresses the PR objective by ensuring non-zero color values remain visible at low brightness levels. The use of bit manipulation for processing two channels simultaneously is an efficient approach.


919-921: Confirm removal of hardware brightness control

I searched the entire codebase and found no remaining calls to SetLuminance/SetBrightness or other hardware‐driven brightness routines. Shifting all dimming into software video scaling matches the PR’s intent, but please manually verify:

  • Test on every supported bus/LED type to ensure LEDs still initialize and dim correctly.
  • Measure power consumption at very low brightness to catch any unexpected draw.
  • Validate color accuracy and dynamic range across your target chipsets.

@DedeHai DedeHai mentioned this pull request Jul 31, 2025
1 task
@DedeHai
Copy link
Collaborator Author

DedeHai commented Jul 31, 2025

@blazoncek I am not sure this is the proper/best way to implement it, feel free to move stuff around.

@blazoncek
Copy link
Collaborator

It is not. At least not with NeoPixelBusLg.

If you intend to replace luminance (and since 0.16 with segment blending you can) use base class NeoPixelBus instead. And it should be done in show() which outputs segment data to LEDs.

The other way is to move gamma handling into NeoPixelBusLg as it is designed with that in mind. I did intend to do that at some point (it is not very good practice to introduce too many changes at once).

@DedeHai
Copy link
Collaborator Author

DedeHai commented Jul 31, 2025

I thought so :)
Also this draft does not preserve color ratios well, some palettes get ugly hue distortion at low brightness.
How can I test if NPB handles gamma better? That would indeed be the best way.

@blazoncek
Copy link
Collaborator

Base NPB does not handle gamma, nor luminance. It is left for the consumer to do any brightness adjustments on RGB values sent to NPB.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Jul 31, 2025

I am not going to mess with NPB, I want to focus on other things so I will leave that for someone else. I will update this PR with an improved solution that can be used in the meantime.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Jul 31, 2025

@blazoncek I am a bit lost on how to properly implement this as I don't understand all the busses i.e. digitalbus vs polybus and need some guidance: where to set the brightness if not using luminance?

@willmmiles
Copy link
Member

@blazoncek I am a bit lost on how to properly implement this as I don't understand all the busses i.e. digitalbus vs polybus and need some guidance: where to set the brightness if not using luminance?

I think the key question is, "do we get any benefits from letting different bus types use custom luminance or gamma calculations"? Ie. should this be the responsibility of the bus, or should we have a single formula we always use?

IF we want to use a common formula for all buses, the calculation code belongs in WS2812FX::show() before we pass the pixel color to the bus -- same as we're doing for gamma correction. This will give us consistent results across all output types. If we do this, we can replace NeoPixelBusLg<> with plain NeoPixelBus<> and maybe save some cycles and code space.

IF instead we think there's some advantage to letting the bus decide how to apply the brightness calculation, then we should integrate the new calculation (and possibly move gamma correction) directly in to NeoPixelBusLg, which exists specifically to apply a luminance and gamma adjustment to pixel colors on the way through to the hardware buffer.

@willmmiles
Copy link
Member

I think the key question is, "do we get any benefits from letting different bus types use custom luminance or gamma calculations"? Ie. should this be the responsibility of the bus, or should we have a single formula we always use?

At first glance I'd push in favor of using a common formula. It might be that there are special cases (network outputs?) where passing a global brightness separately is preferable -- do we know of any?

@blazoncek
Copy link
Collaborator

@willmmiles is right. IIRC at some point @Makuna considered abandoning NeoPixelBrightnessBus (or whatever it was called) but ultimately replaced it with NeoPixelBusLg.

At the time it was impossible to use NeoPixelBus (non Lg or Brightness) as entire WLED logic relied on NeoPixelBus to do the brightness scaling and WLED also handled gamma in an inappropriate way. Both these issues are resolved and indeed brightness scaling and/or gamma correction can be done in show() (and not in a bus).

We can stick with Lg however and let it do brightness/gamma for digital LEDs. For analog we will need to add gamma adjustment to bus as it already have brightness. For network/virtual bus we'd also need to do the same as it expects gamma to be applied.

@willmmiles
Copy link
Member

We can stick with Lg however and let it do brightness/gamma for digital LEDs. For analog we will need to add gamma adjustment to bus as it already have brightness. For network/virtual bus we'd also need to do the same as it expects gamma to be applied.

I did note that NeoPixelBusLg does brightness before gamma correction. Right now we're doing it the other way around (gamma, then brightness).

I'll also add that, using this branch and doing the brightness ourselves for digital busses, switching from NeoPixelBusLg to NeoPixelBus saves 3.5kb of code space (if that still matters).

To confirm, then, you think we should go the other way around, ie.push the gamma calculation down so each bus type does it? NeoPixelBusLg bakes its brightness algorithm in fairly deeply -- to adopt the new one here, we can't really use NeoPixelBusLg either way.

One up side of doing the brightness and gamma in the bus object is that we could later support different gamma values for each bus -- could be helpful if you're tweaking mismatched hardware or something.

@blazoncek
Copy link
Collaborator

I did note that NeoPixelBusLg does brightness before gamma correction.

That would be the correct way.

switching from NeoPixelBusLg to NeoPixelBus saves 3.5kb of code space (if that still matters)

I think it does and IMO worth doing (as it will also simplify bus wrapper).

you think we should go the other way around, ie.push the gamma calculation down so each bus type does it?

Not yet 100% sure but, to answer your next question, we don't need to use Lg, we can implement brightness and gamma in bus manager (Bus class and derived classes). It is fairly easy implementation and as you say it allows us to have different brightness scaling and/or gamma for each bus.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 1, 2025

actually applying brightness before gamma would be the correct way:
the whole point of gamma correction is to account for the fact that the light output of LEDs is linear, the perceived brightness is non-linear.
with the new correct way of applying gamma after all the color manipulations (in linear color space) is the correct way. since we are using a linear function for brightness, that should also be done while still in linear space: you can see in the code how I had to add a crude hue preservation for lowest light values in order to preserve darkest colors.
Flash usage is always an issue, we currently have some to spare thanks to tasmota core but we will reach a point where it is an issue, so moving away from the NeoPixelBusLg sounds like a good thing if we do not need any of its features.
doing gamma per bus sounds a bit overkill but would give users a means to fine-tune different types of strips, see these measurements. Mixing strips on a single controller with the requirement to match them in color accuracy is an edge case IMHO.
What was requested and I do support is a means to adjust individual color channels to correct mismatched production batches of strips.
My suggestion would therefore be to have this:

  • apply color channel correction
  • apply brightness
  • apply gamma
    if this produces good results needs to be tested, but at least in theory thats how it should be. In order to achieve this, it would need to be per bus, using individual channel correction but global brightness and global gamma.

this is my understanding of how it should be, not claiming this is the best way.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 1, 2025

I ran a test with applying brightness before gamma: it looks awful.

edit:
I did not dig too deep on NPB gamma correction but using NeoGammaTableMethod in bus_wrapper.h and passing uncorrected colors does work to apply gamma, tested without the changes of this PR, plain main with only this change.
Unless I did miss something, this is unusable: when I now set brightness < 15 its just all black. Is this supposed to be that way?

@blazoncek
Copy link
Collaborator

I ran a test with applying brightness before gamma: it looks awful.

The issue IMO is linear versus logarithmic brightness slider.

using NeoGammaTableMethod in bus_wrapper.h

If you haven't noticed, the gamma (functions) rewrite in WLED was with this step in mind. Hence NeoGammaWLEDMethod class.
Unfortunately not yet implemented (until consensus is made regarding where to put corrections).

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 1, 2025

I did see the comment in the code about NeoGammaWLEDMethod and if the NeoGammaTableMethod is not fundamentally different, which I assume its not as the output looks very similar, using gamma and brightness from NPBLg is a no-go.
I will update the code of this PR with a new suggestion in show().

- mproved gamma table calculation for low values
- fixed mismatch in inverting gamma table calculation: inversion should now be as good as it gets
@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 1, 2025

replacing NPB was easier than I thought, did that in the last commit.
@TripleWhy the last commit is probably as good as it gets with regards to color preservation at low brightness.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 2, 2025

Ran some speed comparison:
This PR gives me 2% higher FPS in my scenario (ESP C3, 64x32 DDP pixels at 40FPS)

I deem this PR ready to undraft&merge if this is the route we want to go, which I highly recommend for quality reasons, see #4794

@netmindz what is your opinion on this?
TL;DR:

  • low brightness color accuracy is bad
  • using NPBlg (currently used) allows for gamma correction, but if used, it gets even worse
  • Ditching the lg variant of NPB saves some flash (I tested 1.3k on C3, @willmmiles tested >3.5k)
  • This PR fixes the issue at hand but the question is, do we want to fix it some other way (i.e. per bus and not globally)

@TripleWhy
Copy link
Contributor

As a user, I like the result much better than the current version in main. It even produces some mixed colors at brightness 1, which the old version didn't do.

It doesn't appear to produce even brightness at least until around brightness 30, above it, I can't tell. It looks much more even to me without gamma correction. Even brightness is the point of gamma correction, isn't it?

One minor problem though: Using the power button in the UI doesn't turn off any LEDs.

Did you also want my opinion as a developer? I'd have to get into the gamma topic first.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 11, 2025

alright, I can prototype that.

I think it'll be faster than re-mapping pixel buffers.

not so sure about that, remapping pixel buffer with the correct pre-sorted mapping should be as fast if not faster, its basically a lokup table with minimal checks. Its just more complex to pre-compute that table and harder to hunt bugs.

@blazoncek
Copy link
Collaborator

  • apply global gamma
  • transfer to NPB, summing power usage as we go
  • compute global ABL
  • for each bus, for each pixel: apply brightness (global or local), color correction, any other post-processing
  • tell NPB to send buffer to hardware

AFAIK I've already implemented as such, except points 1, 2 & 3 are swapped (3,1,2).
Applying gamma 1st will indeed provide better results if ABL kicks in since calculated current will be less than without gamma. Low values will still get clipped though. Perhaps a better approach would be to do a logarithmic reduction of brightness instead of linear for ABL. It should preserve lower values better.

@willmmiles
Copy link
Member

I think it'll be faster than re-mapping pixel buffers.

not so sure about that, remapping pixel buffer with the correct pre-sorted mapping should be as fast if not faster, its basically a lokup table with minimal checks. Its just more complex to pre-compute that table and harder to hunt bugs.

It adds all the complexity, it doesn't save us a pass over the pixels, and the random access will be absolutely brutal on performance if any of those buffers end up in PSRAM. One of the nice things about the NPB buffers is that they're almost always in SRAM, since the hardware layer needs to read 'em in ISRs. Another issue is that if we try to re-use the global pixel buffer, there's no guarantee a bus-ordered version will fit -- multiple buses can output the same pixel. I think also no guarantee there's a viable copy order that ensures that we don't have to have an arbitrarily deep scratch pad somewhere else.

(To be fair, my gut reaction "lookup tables slow" is definitely based on experience with big CPUs, where lookup tables are often almost as bad as linked lists in terms of cache management -- very hard to predict which memory needs to be accessed when, so performance tanks unless the table AND data fit in cache. Not so much an issue on these micros main SRAMs, though, but any time PSRAM comes in to play, all the same issues arise.)

  • apply global gamma
  • transfer to NPB, summing power usage as we go
  • compute global ABL
  • for each bus, for each pixel: apply brightness (global or local), color correction, any other post-processing
  • tell NPB to send buffer to hardware

AFAIK I've already implemented as such, except points 1, 2 & 3 are swapped (3,1,2). Applying gamma 1st will indeed provide better results if ABL kicks in since calculated current will be less than without gamma.

Yes, that's correct - the current implementation is pretty close! All that's really needed is moving the global ABL to where it can share the estimate pass with per-bus ABL.

We might get some benefit from inverting the bus mapping and sampling from the global pixel buffer instead of iterating over it -- particularly for sparse setups -- but I think this can be looked at later. Eg.

auto bus_power = std::vector<unsigned>(busses.size());   // FUTURE: small vector optimization - could this fit on the stack?
for(auto bus_index = 0; bus_index < busses.size(); ++bus_index) {
  auto& bus = *busses[bus_index];
  unsigned power = 0;
  for(auto i = 0; i < bus.getLength(); ++i) {
      auto pixel_color = _pixels[getInverseMappedPixelIndex(bus.getStart() + i)];
      pixel_color = calculate_gamma_for(pixel_color);
      power += bus.powerFor(pixel_color);   // Could be merged with bus.setPixelColor if dynamic dispatch is slow
      bus.setPixelColor(i, pixel_color);
  }   
  bus_power[bus_index] = power;
}
// global ABL analysis here

@blazoncek
Copy link
Collaborator

blazoncek commented Aug 12, 2025

All that's really needed is moving the global ABL to where it can share the estimate pass with per-bus ABL.

IMO that makes no sense. Strip-level ABL operates on mapped pixels (pixels that may not exist and may be scattered across multiple buses in undetermined order). It will reduce overall brightness to be within global limit. Strip-level ABL already ignores non-existent pixels (i.e. pixels not belonging to a digital bus).

Per-bus ABL, however, operates on physical pixels and retrieves (modified) pixel values from NPB. This may cause a single bus to be limited while the rest may operate at desired brightness (where no ABL is needed).

EDIT: And ABL is either strip-level or per-bus, not both.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 12, 2025

Strip-level ABL operates on mapped pixels (pixels that may not exist and may be scattered across multiple buses in undetermined order). It will reduce overall brightness to be within global limit. Strip-level ABL already ignores non-existent pixels (i.e. pixels not belonging to a digital bus).

the global ABL will limit too much on sparse mappings, it would be better to first map them to busses and then calculate the total brightness, no matter if global or per bus. This means passing colors to NPB then read them back (any loss here if passed unscaled?), calculate ABL if used, then scale each color in a final pass.
This is however not ideal: when no ABL is used, colors could be scaled before passing to NPB which is faster.
Applying the mapping to _pixels is possible, the mapping does not allow for double-use pixels, at least in the current implementation. There is however the random access during re-mapping which can be slow on PSRAM.
The best approach may be: determine if ABL is used at all, use global scaling if not, use per-bus scaling if it is. This would make normal setups stay fast and make ABL "more correct".

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 12, 2025

RE: PSRAM-Speed
I ran a simple test on an S3 that compares access to a DRAM buffer to PSRAM.
Read access is done like this:

  for (size_t i = 0; i < count; i++) {
    sum += mem[indices[i]];
  }

Write acces like this:

  for (size_t i = 0; i < count; i++) {
    mem[indices[i]] = i;
  }

The first access to PSRAM in the test is always slower, most likely as that is when PSRAM buffer get chached.
Result: buffers up to ~32k will be (mostly) in cache and any access is quite fast (may differ on ESP32). How this changes if using multiple buffers in PSRAM I did not test and it all depends on how clever the caching is.
Partial random access is done in blocks of ~5% buffer size, randomly distributed.

Memory access speed test (6144 elements, ~24 KB)
=== Partial Random Access ===
DRAM           : Write    353 us, Read    411 us
PSRAM          : Write    506 us, Read    411 us
PSRAM vs DRAM Write: 143.3%   -> note: this is slower due to initial caching of PSRAM buffer (first access in test sequence)
PSRAM vs DRAM Read : 100.0%

=== Random Access ===
DRAM           : Write    344 us, Read    418 us
PSRAM          : Write    337 us, Read    411 us
PSRAM vs DRAM Write: 98.0%
PSRAM vs DRAM Read : 98.3%

=== Sequential Access ===
DRAM           : Write    343 us, Read    420 us
PSRAM          : Write    345 us, Read    411 us
PSRAM vs DRAM Write: 100.6%
PSRAM vs DRAM Read : 97.9%



Memory access speed test (8192 elements, ~32 KB)
=== Partial Random Access ===
DRAM           : Write    453 us, Read    554 us
PSRAM          : Write    836 us, Read    562 us
PSRAM vs DRAM Write: 184.5% -> note: this is slower due to initial caching of PSRAM buffer (first access in test sequence)
PSRAM vs DRAM Read : 101.4%

=== Random Access ===
DRAM           : Write    460 us, Read    554 us
PSRAM          : Write    534 us, Read    572 us
PSRAM vs DRAM Write: 116.1%
PSRAM vs DRAM Read : 103.2%

=== Sequential Access ===
DRAM           : Write    463 us, Read    547 us
PSRAM          : Write    529 us, Read    575 us
PSRAM vs DRAM Write: 114.3%
PSRAM vs DRAM Read : 105.1%



From 32k buffer size upward, PSRAM starts getting slower due to cache size of about 32k, this hits random access hard

Memory access speed test (10240 elements, ~40 KB)
=== Partial Random Access ===
DRAM           : Write    572 us, Read    691 us
PSRAM          : Write   1747 us, Read   2905 us
PSRAM vs DRAM Write: 305.4%
PSRAM vs DRAM Read : 420.4%

=== Random Access ===
DRAM           : Write    573 us, Read    691 us
PSRAM          : Write   4911 us, Read   3888 us
PSRAM vs DRAM Write: 857.1%
PSRAM vs DRAM Read : 562.7%

=== Sequential Access ===
DRAM           : Write    565 us, Read    699 us
PSRAM          : Write   1414 us, Read   2498 us
PSRAM vs DRAM Write: 250.3%
PSRAM vs DRAM Read : 357.4%



Memory access speed test (16384 elements, ~64 KB)
=== Partial Random Access ===
DRAM           : Write    904 us, Read   1101 us
PSRAM          : Write   4049 us, Read   4189 us
PSRAM vs DRAM Write: 447.9%
PSRAM vs DRAM Read : 380.5%

=== Random Access ===
DRAM           : Write    906 us, Read   1102 us
PSRAM          : Write  18906 us, Read  11410 us
PSRAM vs DRAM Write: 2086.8%
PSRAM vs DRAM Read : 1035.4%

=== Sequential Access ===
DRAM           : Write    904 us, Read   1101 us
PSRAM          : Write   3171 us, Read   3521 us
PSRAM vs DRAM Write: 350.8%
PSRAM vs DRAM Read : 319.8%

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 12, 2025

Key take-aways from my test:

  • buffers up to 7500 LEDs worth (30kB) are still quite fast in PSRAM, some extra time (~0.3ms) is needed to cache such buffers
  • buffers exceeding this limit will be 3x slower than DRAM buffers if access is somewhat sequential
  • random access on large buffers is painfully slow: 10x-20x slower than DRAM

Assuming reading colors back from the bus is fast enough (which it probably is) doing the mapping in _pixels[] makes little sense. So the approach should be as @willmmiles proposed:

apply global gamma
transfer to NPB, summing power usage as we go
compute global ABL
for each bus, for each pixel: apply brightness (global or local), color correction, any other post-processing
tell NPB to send buffer to hardware

I will try to implement this and check if there are any unforseen complications.

@Arcitec
Copy link
Contributor

Arcitec commented Aug 17, 2025

Thanks for working on improving the gamma handling. I think your plan in the last message makes sense.

But regarding this earlier question:

I had an idea:
since many are used to and seem to like the old way of applying brightness and 0.16 is a breaking change in that regard:
adding a checkmark in settings to make "apply gamma after Effects" optional. If not set, gamma is applied to colors and palettes.

If I understand correctly, 0.16 applies gamma before doing effects/color mixing, whereas 0.15 applied gamma at the end instead? The difference will certainly change how things wrap around or clip during effect calculations.

It only makes sense to apply gamma at the end as the final step. That way, colors have more headroom during effect calculations, and you ensure that effects happen in the correct, linear space (not a scaled/shifted color space). Gamma is a destructive process that causes clipping and shifting of colors. So let's not modify the colors until the end, at the output stage. That is definitely more correct and will ensure more consistent effect calculations.

So in the plan from the previous message, move gamma as the final stage before display on the output device.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 17, 2025

If I understand correctly, 0.16 applies gamma before doing effects/color mixing, whereas 0.15 applied gamma at the end instead?

no, the other way around. 0.16 is doing it the more mathematically correct way to produce better hue accuracy

@Arcitec
Copy link
Contributor

Arcitec commented Aug 17, 2025

Ah okay, yeah doing it last is much better. The quoted plan said "apply global gamma" first so I thought that's what it was doing. :)

Well, anyway, regarding the user checkbox, I personally don't think it makes sense to keep the wrong behavior even as an option. Sure, maybe someone was used to a certain effect being slightly different due to clipping in the old code, but ehhhhhhhhhhhhhhhhhhhh, can't they just... get used to looking at the correct effect. ;)

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
@DedeHai DedeHai marked this pull request as ready for review August 26, 2025 11:29
@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 26, 2025

Finally finished the re-work of the bus brightness handling and the ABL. Please review.
See commit comment for details.

Hope no changes got lost in manual merging of my WIP branch. I reworked the code several times over to arrive at this version which IMHO is the best compromise between speed and code consistency. Global ABL would be faster if done on strip level, only scaling once but it convolutes code and adds computational overhead even if the limit does not apply. @blazoncek took that approach in his dev branch (see table)

I did some testing on the ESP32 C3 but not extensively. Here is a summary of my findings:

Flash usage: only checked for C3 and ESP32

  • C3: saves 1.1k
  • ESP32: saves 5.7k

Test with 2x 256 LEDs (2 outputs) on C3, 32x16 matrix, palette effect

Version Limiter Disabled Global Limit (non-limiting / limiting) Per-Bus Limit (non-limiting / limiting)
PR 76.3 FPS 75.6 FPS (-0.7) / 70.0 FPS (-6.3) 74.8 FPS (-1.5) / 69.3 FPS (-6.3)
Main 75.0 FPS 74.1 FPS (-0.9) / 74.0 FPS (-0.9) 68.0 FPS (-6.1) / 61.3 FPS (-12.8)
Blaz dev 67.0 FPS 64.0 FPS (-3.0) / 64.0 FPS (-3.0) 63.5 FPS (-3.5) / 60.0 FPS (-7.0)

In summary:

  • this PR fixes low brightness color handling for much nicer colors
  • moved brightness scaling from NPB into WLED code, saves us some flash and gives a slight speed boost
  • complete rework of ABL, now also respects white channel and mapping and calculations are more consistent. Also it is done on bus level only, no more ABL code on strip level

Copy link
Contributor

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

67-73: Mandatory refactors required: remove legacy scaledBri() and briMultiplier usage

The grep output shows numerous remaining call sites and declarations of the old brightness-scaling helpers—these must be updated to use the new central briT model (or removed entirely) before dropping scaledBri() and briMultiplier. Please refactor or replace each of the following:

• wled00/led.cpp (lines 57–60):

  • Declaration and implementation of scaledBri(byte) and comment referencing briMultiplier.

• wled00/wled.h (line 433):

  • Global declaration of briMultiplier.

• wled00/xml.cpp (around line 378):

  • printSetFormValue(..., PSTR("BF"), briMultiplier).

• wled00/wled.cpp (line 193):

  • BusManager::setBrightness(scaledBri(bri));

• wled00/set.cpp (line 362):

  • briMultiplier assignment in user-set logic.

• wled00/fcn_declare.h (line 170):

  • Prototype for scaledBri(byte).

• wled00/cfg.cpp (around lines 518 & 1040):

  • JSON serialization/deserialization of briMultiplier.

• wled00/wled_eeprom.cpp (line 134):

  • EEPROM.read into briMultiplier.

• wled00/FX_fcn.cpp (lines 1199 & 1674):

  • bus->setBrightness(scaledBri(...)).

Until all these are migrated to the new briT API (or removed if obsolete), you cannot safely delete scaledBri() or briMultiplier. Ensure each call site uses the updated brightness value so that legacy scaling is fully eradicated before drop.

wled00/colors.cpp (1)

94-104: Bug: bitwise OR used instead of logical OR

This returns early more often than intended when inputs are non-zero bit patterns. Use logical OR.

Apply fix:

-    if (rgb == 0 | hueShift + lighten + brighten == 0) return rgb; // black or no change
+    if ((rgb == 0) || (hueShift + lighten + brighten == 0)) return rgb; // black or no change

Add parentheses to avoid precedence surprises.

♻️ Duplicate comments (1)
wled00/colors.cpp (1)

587-598: Gamma table calc: start at 1, set [0] after loop — OK

This avoids powf(0, …) work and keeps gamma[0] == 0. The inverse table center-bias is acceptable given prior discussion.

🧹 Nitpick comments (10)
wled00/led.cpp (1)

58-63: Cast width is fine; consider explicit fixed-width type for readability

Switching to an unsigned wider intermediate avoids 8-bit overflow. To make intent obvious across platforms, prefer a fixed-width type (e.g., uint32_t) instead of plain unsigned.

Apply this small clarification:

-byte scaledBri(byte in)
+byte scaledBri(byte in)
 {
-  unsigned val = ((unsigned)in*briMultiplier)/100;
+  uint32_t val = (uint32_t(in) * briMultiplier) / 100u;
   if (val > 255) val = 255;
   return (byte)val;
 }
wled00/colors.h (3)

41-48: Index operator returns B,G,R,W order — consider documenting to avoid misuse

operator[] maps 0->B, 1->G, 2->R, 3->W. That’s easy to misuse when people expect RGBW. Consider a short comment or named accessors to reduce footguns.

Example minimal doc:

-    // Access as an array
+    // Access as an array (index order: 0=B, 1=G, 2=R, 3=W)
     inline const uint8_t& operator[] (uint8_t x) const __attribute__((always_inline)) { return raw[x]; }

126-126: Magic number in getPaletteCount()

The base count “13” is a magic number. Define it as a named constant to make intent explicit.

- inline size_t getPaletteCount() { return 13 + GRADIENT_PALETTE_COUNT + customPalettes.size(); }
+ constexpr size_t BASE_PALETTE_COUNT = 13;
+ inline size_t getPaletteCount() { return BASE_PALETTE_COUNT + GRADIENT_PALETTE_COUNT + customPalettes.size(); }

134-135: Prototypes present even when HueSync is disabled

Definitions are under #ifndef WLED_DISABLE_HUESYNC in colors.cpp, but these declarations are unconditional. Guard them to avoid ODR/link errors if someone calls them when HueSync is disabled.

-void colorXYtoRGB(float x, float y, byte* rgb); // only defined if huesync disabled TODO
-void colorRGBtoXY(const byte* rgb, float* xy); // only defined if huesync disabled TODO
+#ifndef WLED_DISABLE_HUESYNC
+void colorXYtoRGB(float x, float y, byte* rgb);
+void colorRGBtoXY(const byte* rgb, float* xy);
+#endif
wled00/colors.cpp (1)

67-87: Video-scaling fade looks solid; preserves dim hues

The “quarter of max” threshold and +1 carry to keep channels alive achieves the PR goal of avoiding black crush at very low brightness. Nice.

Consider making the threshold a named constexpr for easier tuning and experimentation:

-  uint8_t quarterMax = maxc >> 2; // note: using half of max results in color artefacts
+  constexpr uint8_t kVideoHoldShift = 2; // 2 => 1/4 max; tweak if artifacts emerge
+  uint8_t quarterMax = maxc >> kVideoHoldShift;
wled00/e131.cpp (1)

191-197: Avoided double-scaling in DMX MODE_PRESET; confirm transition intent

Switching to strip.setBrightness(bri, false) removes the previous scaledBri duplication — good. One behavioral point: DMX_MODE_SINGLE_DRGB (Line 169) uses direct=true, whereas PRESET now uses direct=false. If the intent is immediate DMX response without fades, consider making both direct=true for consistency. If the fade is intentional only for PRESET, please document the rationale.
Apply if immediate DMX response is desired:

-          strip.setBrightness(bri, false);
+          strip.setBrightness(bri, true);

Please confirm which behavior users expect for the PRESET mode brightness channel: stepped (direct=true) or transition-based (direct=false). A brief comment in code would prevent future flip-flops.

wled00/wled.cpp (1)

193-193: Brightness Usage Audit Passed; Optional Gamma-Preserving Refactor Remains

Audit results confirm that all brightness adjustments follow the intended pattern:

  • strip.setBrightness(...) is only ever called with raw brightness values (no use of scaledBri), and

  • BusManager::setBrightness(...) is only called with scaledBri(...), at two locations:

    • wled00/wled.cpp:193
    • wled00/FX_fcn.cpp:1674

Since the original suggestion is an optional refactor—to route the reinit brightness call through strip.setBrightness(...) and thus invoke gammaCorrectBri immediately—you may apply it if uniform gamma handling is preferred. If you do, please update both instances:

-    BusManager::setBrightness(scaledBri(bri)); // fix re-initialised bus' brightness #4005 and #4824
+    // Use the standard path so gammaCorrectBri and other side-effects remain consistent.
+    strip.setBrightness(bri, true); // fix re-initialised bus' brightness #4005 and #4824

And similarly in FX_fcn.cpp at the BusManager::setBrightness(scaledBri(b)) call.

wled00/FX.cpp (1)

7531-7533: Aligning with centralized gamma: looks good; consider adding a clarifying comment

Switching from gamma8(cos8_t(...)) to just cos8_t(...) removes per-pixel gamma in the effect and is consistent with the PR’s intent to do gamma/brightness at the final output stage. This should also reduce per-frame cycles slightly. To prevent future regressions, add a short comment noting that gamma is intentionally omitted here because it’s handled later in the post-processing/show() pipeline.

       byte valueR = rdistort + ((a- ( ((xoffs - cx)  * (xoffs - cx)  + (yoffs - cy)  * (yoffs - cy))>>7  ))<<1);
       byte valueG = gdistort + ((a2-( ((xoffs - cx1) * (xoffs - cx1) + (yoffs - cy1) * (yoffs - cy1))>>7 ))<<1);
       byte valueB = bdistort + ((a3-( ((xoffs - cx2) * (xoffs - cx2) + (yoffs - cy2) * (yoffs - cy2))>>7 ))<<1);
-
-      valueR = cos8_t(valueR);
-      valueG = cos8_t(valueG);
-      valueB = cos8_t(valueB);
+      // Intentionally no per-pixel gamma here; gamma/brightness is applied in the final output stage.
+      valueR = cos8_t(valueR);
+      valueG = cos8_t(valueG);
+      valueB = cos8_t(valueB);
wled00/bus_manager.h (1)

417-418: Use consistent terminology for ABL milliAmps calculation.

The comment was updated to reflect "milliamps used by ESP (for power estimation)", but the macro name still uses MA_FOR_ESP. Consider using consistent terminology throughout the codebase.

-// milliamps used by ESP (for power estimation)
-// you can set it to 0 if the ESP is powered by USB and the LEDs by external
+// milliamps used by ESP for power estimation
+// set to 0 if ESP is powered via USB and LEDs via external supply
wled00/bus_manager.cpp (1)

928-929: Consider ABL timing optimization.

Calling applyABL() on every show() might be unnecessary if pixel colors haven't changed. Consider adding a dirty flag to optimize this.

Consider tracking whether any pixel colors have changed since the last ABL application to avoid unnecessary brightness recalculations:

 void BusManager::show() {
-  applyABL(); // apply brightness limit, updates _gMilliAmpsUsed
+  if (_needsABL) {
+    applyABL(); // apply brightness limit, updates _gMilliAmpsUsed
+    _needsABL = false;
+  }
   for (auto &bus : busses) {
     bus->show();
   }
 }

Then set _needsABL = true in setPixelColor() when ABL is enabled.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5be1bc0 and 00062b2.

📒 Files selected for processing (15)
  • wled00/FX.cpp (1 hunks)
  • wled00/FX_fcn.cpp (5 hunks)
  • wled00/bus_manager.cpp (10 hunks)
  • wled00/bus_manager.h (5 hunks)
  • wled00/bus_wrapper.h (3 hunks)
  • wled00/colors.cpp (6 hunks)
  • wled00/colors.h (1 hunks)
  • wled00/e131.cpp (1 hunks)
  • wled00/fcn_declare.h (0 hunks)
  • wled00/image_loader.cpp (1 hunks)
  • wled00/json.cpp (1 hunks)
  • wled00/led.cpp (2 hunks)
  • wled00/udp.cpp (2 hunks)
  • wled00/wled.cpp (1 hunks)
  • wled00/wled.h (1 hunks)
💤 Files with no reviewable changes (1)
  • wled00/fcn_declare.h
✅ Files skipped from review due to trivial changes (1)
  • wled00/wled.h
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-04-28T20:51:29.773Z
Learnt from: KrX3D
PR: wled/WLED#4585
File: usermods/seven_segment_display_reloaded_v2/seven_segment_display_reloaded_v2.cpp:430-435
Timestamp: 2025-04-28T20:51:29.773Z
Learning: In WLED, `bri` is a global variable used for brightness control.

Applied to files:

  • wled00/e131.cpp
  • wled00/json.cpp
  • wled00/led.cpp
  • wled00/udp.cpp
📚 Learning: 2025-06-07T15:58:42.579Z
Learnt from: willmmiles
PR: wled/WLED#4712
File: wled00/FX_fcn.cpp:992-993
Timestamp: 2025-06-07T15:58:42.579Z
Learning: In WLED segments, the backing pixel buffer is always allocated to the full physical segment size, but effect rendering only uses a subset when mirror or grouping are enabled. For 2D segments, this subset is vWidth() * vHeight(), and for 1D segments it's vLength(). Color processing operations like fade_out should iterate over this rendering subset, not the full allocated buffer.

Applied to files:

  • wled00/FX_fcn.cpp
🧬 Code graph analysis (7)
wled00/FX.cpp (1)
wled00/wled_math.cpp (2)
  • cos8_t (88-90)
  • cos8_t (88-88)
wled00/image_loader.cpp (1)
wled00/data/pixart/getPixelValues.js (1)
  • red (237-237)
wled00/wled.cpp (2)
wled00/FX_fcn.cpp (2)
  • setBrightness (1667-1679)
  • setBrightness (1667-1667)
wled00/led.cpp (2)
  • scaledBri (58-63)
  • scaledBri (58-58)
wled00/FX_fcn.cpp (2)
wled00/led.cpp (2)
  • scaledBri (58-63)
  • scaledBri (58-58)
wled00/bus_manager.cpp (12)
  • initializeABL (963-997)
  • initializeABL (963-963)
  • setPixelColor (260-297)
  • setPixelColor (260-260)
  • setPixelColor (455-486)
  • setPixelColor (455-455)
  • setPixelColor (633-638)
  • setPixelColor (633-633)
  • setPixelColor (691-700)
  • setPixelColor (691-691)
  • setPixelColor (934-939)
  • setPixelColor (934-934)
wled00/colors.h (1)
wled00/colors.cpp (40)
  • color_blend (11-21)
  • color_blend (11-11)
  • color_add (28-61)
  • color_add (28-28)
  • adjust_color (94-104)
  • adjust_color (94-94)
  • ColorFromPaletteWLED (107-134)
  • ColorFromPaletteWLED (107-107)
  • generateHarmonicRandomPalette (146-241)
  • generateHarmonicRandomPalette (146-146)
  • generateRandomPalette (243-249)
  • generateRandomPalette (243-243)
  • loadCustomPalettes (251-297)
  • loadCustomPalettes (251-251)
  • hsv2rgb (299-328)
  • hsv2rgb (299-299)
  • colorHStoRGB (351-357)
  • colorHStoRGB (351-351)
  • rgb2hsv (330-349)
  • rgb2hsv (330-330)
  • colorKtoRGB (360-382)
  • colorKtoRGB (360-360)
  • colorCTtoRGB (384-404)
  • colorCTtoRGB (384-384)
  • colorXYtoRGB (407-461)
  • colorXYtoRGB (407-407)
  • colorRGBtoXY (463-470)
  • colorRGBtoXY (463-463)
  • colorFromDecOrHexString (474-492)
  • colorFromDecOrHexString (474-474)
  • colorFromHexString (495-513)
  • colorFromHexString (495-495)
  • colorBalanceFromKelvin (529-542)
  • colorBalanceFromKelvin (529-529)
  • approximateKelvinFromRGB (551-581)
  • approximateKelvinFromRGB (551-551)
  • setRandomColor (136-140)
  • setRandomColor (136-136)
  • color_fade (67-87)
  • color_fade (67-67)
wled00/bus_manager.h (1)
wled00/bus_manager.cpp (8)
  • estimateCurrent (195-205)
  • estimateCurrent (195-195)
  • applyBriLimit (207-239)
  • applyBriLimit (207-207)
  • initializeABL (963-997)
  • initializeABL (963-963)
  • applyABL (999-1040)
  • applyABL (999-999)
wled00/bus_manager.cpp (4)
wled00/bus_manager.h (5)
  • hasWhite (165-171)
  • hasCCT (172-177)
  • show (238-244)
  • show (297-298)
  • show (324-325)
wled00/FX_fcn.cpp (8)
  • getPixelColor (896-951)
  • getPixelColor (896-896)
  • setPixelColor (683-859)
  • setPixelColor (683-683)
  • setPixelColor (863-893)
  • setPixelColor (863-863)
  • show (1559-1615)
  • show (1559-1559)
wled00/FX.h (4)
  • getPixelColor (961-961)
  • setPixelColor (693-693)
  • setPixelColor (903-903)
  • setPixelColor (907-907)
wled00/colors.cpp (2)
  • color_fade (67-87)
  • color_fade (67-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: wled_build / Build Environments (usermods)
🔇 Additional comments (20)
wled00/bus_wrapper.h (2)

289-291: TM1914 feature differs between ESP8266 and ESP32 variants

ESP8266 uses NeoRgbTm1914Feature while ESP32 uses NeoGrbTm1914Feature. Unless the protocol/feature mapping intentionally differs per method, this will produce mismatched channel orders across platforms.

Consider unifying to the same feature:

-#define B_32_RN_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, NeoEsp32RmtNTm1914Method>
-#define B_32_I2_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, X1Tm1914Method>
-#define B_32_IP_TM1914_3 NeoPixelBus<NeoGrbTm1914Feature, X8Tm1914Method> // parallel I2S
+#define B_32_RN_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, NeoEsp32RmtNTm1914Method>
+#define B_32_I2_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, X1Tm1914Method>
+#define B_32_IP_TM1914_3 NeoPixelBus<NeoRgbTm1914Feature, X8Tm1914Method> // parallel I2S

If different boards truly require different feature layouts, please add a comment explaining why.


293-295: SM16825 white-channel order mismatch between ESP8266 and ESP32

ESP8266 uses NeoRgbwcSm16825eFeature, ESP32 uses NeoRgbcwSm16825eFeature. The order of WW/CW appears swapped.

If unintentional, align to the same feature:

-#define B_32_RN_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, NeoEsp32RmtNWs2812xMethod>
-#define B_32_I2_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, X1Ws2812xMethod>
-#define B_32_IP_SM16825_5 NeoPixelBus<NeoRgbcwSm16825eFeature, X8Ws2812xMethod> // parallel I2S
+#define B_32_RN_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, NeoEsp32RmtNWs2812xMethod>
+#define B_32_I2_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, X1Ws2812xMethod>
+#define B_32_IP_SM16825_5 NeoPixelBus<NeoRgbwcSm16825eFeature, X8Ws2812xMethod> // parallel I2S

Please confirm against real strips or NPB docs.

wled00/colors.cpp (2)

11-21: IRAM’ing color_blend is good

Tagging color_blend IRAM_ATTR for tight timing paths is appropriate here.


106-134: Palette sampling math cleanup is fine; avoids slower helpers

The inlined blend and brightness scaling are clear and performant. Matches the “video” scaling approach elsewhere.

wled00/image_loader.cpp (1)

54-63: Verified: No premature gamma application detected

A search across the codebase shows that:

  • drawPixelCallback now feeds raw r/g/b values directly into setPixelColorXY, with no inline gamma calls in the GIF path.
  • All occurrences of gamma8(…) are confined to brightness adjustments (e.g. WS2812FX setBrightness, flasher ramps), TV-simulator color expansion, and particle systems—none overlap the GIF–loader’s drawPixelCallback.

No other code applies per-channel gamma before show(), so there’s no risk of double correction.

wled00/json.cpp (1)

315-315: Good fix: pass raw bri to avoid double-scaling

Using strip.setBrightness(bri, true) here is correct since WS2812FX::setBrightness will apply scaledBri(b) internally. This prevents applying the briMultiplier twice in the immediate i-array path. Looks consistent with the PR goal of centralizing brightness handling.

Quick check: with brightness set to 1–5, send a JSON "i" update containing a dim non-zero color and verify no pixels go black spuriously. This specifically guards against the low-brightness regression from #4794.

wled00/udp.cpp (1)

427-427: LGTM!

The direct use of brightness values instead of scaledBri() aligns with the PR's goal of moving gamma correction to the final output stage, ensuring consistent brightness handling across the codebase.

Also applies to: 437-437, 444-444

wled00/bus_manager.h (2)

251-253: LGTM! Well-designed ABL API.

The addition of per-bus current limiting methods provides a clean interface for the new ABL implementation. The separation of concerns between current estimation and brightness limiting is well thought out.


449-450: LGTM! Proper ABL lifecycle management.

The ABL initialization and application methods are well-placed in the BusManager namespace, providing centralized control over automatic brightness limiting.

wled00/FX_fcn.cpp (4)

1199-1201: LGTM! Proper ABL initialization sequence.

The ABL initialization is correctly placed after all buses are created and configured, ensuring proper current limit calculation. The use of scaledBri() for initial brightness maintains consistency with the brightness multiplier system.


1303-1307: LGTM! Architecture-specific optimizations.

The expanded conditional compilation to include ESP8266 alongside ESP32C3 for fixed-point arithmetic paths is appropriate. These platforms benefit from the optimized integer-only calculations due to their lack of hardware floating-point units.

Also applies to: 1314-1318


1595-1598: LGTM! Clean gamma correction implementation.

Moving gamma correction to the final pixel output stage (with proper guards for realtime mode) is the correct approach. This ensures gamma is applied after all color manipulations and brightness adjustments, preventing color distortion.


1674-1674: Brightness scaling with scaledBri(b) is correct and does not conflict with ABL

  • The call to BusManager::setBrightness(scaledBri(b)); applies only the user-configured brightness multiplier (briMultiplier) before handing off to the bus API; the implementation of scaledBri in wled00/led.cpp confirms this is just a linear scale:

    // scales the brightness with the briMultiplier factor  
    byte scaledBri(byte in) {  
      unsigned val = ((unsigned)in * briMultiplier) / 100;  
      return (byte)val;  
    }

    (git.furworks.de)

  • Auto Brightness Limiter (ABL) is applied later in the pixel-show path within bus_manager.cpp, where current draw is estimated and per-bus limits are enforced during show(). This logic does not override or bypass the initial brightness setting but further constrains output if the current limit is reached. Key comments in that file include:

    // if using ABL, sum all color channels to estimate current and limit brightness in show()  
    // ...  
    busd.applyBriLimit(0); // apply per bus ABL limit, updates _milliAmpsTotal if limit reached  

    (git.furworks.de)

Because scaledBri remains the first step in brightness handling and ABL runs independently at show-time, there is no conflict between the two systems—no change required.

wled00/bus_manager.cpp (7)

188-194: Excellent documentation of ABL implementation!

The comment clearly explains the ABL workflow and the special case for buses with LED current set to 0. This will help future maintainers understand the design decisions.


195-205: LGTM! Clean current estimation logic.

The estimateCurrent() implementation properly handles both normal LEDs and the special WS2815 power model. The color sum calculation is efficient and the milliamps conversion is mathematically correct.


207-239: Robust brightness limiting implementation!

The applyBriLimit() method handles edge cases well:

  • Minimum brightness of 1 ensures LEDs remain visible even at very low current limits
  • Video scaling preserves color ratios
  • Proper CCT handling when applicable
  • Efficient pixel repainting with color order correction

The use of video scaling when brightness is limited is particularly clever as it maintains color fidelity.


260-297: LGTM! Proper ABL integration in setPixelColor.

The color sum accumulation for ABL is correctly placed after brightness scaling, and the special handling for WS2815 (using max of RGB channels) properly implements the workaround for issue #549.


300-321: Good color restoration logic.

The getPixelColor() implementation correctly restores colors with proper handling of WS2812_1CH_X3 mapping. The addition of explicit uint8_t type for color order improves type safety.


963-997: Well-designed ABL initialization!

The initializeABL() implementation intelligently handles both global and per-bus current limits:

  • Proper ESP current budget sharing across ABL-enabled buses
  • Minimum 1mA per LED ensures functionality even with very low limits
  • Buses with LED current = 0 are properly excluded from ABL

The logic for distributing the ESP's current budget proportionally across buses is particularly well thought out.


999-1040: Excellent ABL application logic!

The applyABL() method properly orchestrates the brightness limiting process:

  1. Per-bus current estimation
  2. Per-bus limiting (when global limit is 0)
  3. Global limiting with proper minimum handling
  4. Efficient brightness distribution

The two-pass approach (per-bus then global) ensures fair brightness distribution while respecting both local and global constraints.

Comment on lines +179 to 183
#define B_8266_U0_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart0Apa106Method> //3 chan, esp8266, gpio1
#define B_8266_U1_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart1Apa106Method> //3 chan, esp8266, gpio2
#define B_8266_DM_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266DmaApa106Method> //3 chan, esp8266, gpio3
#define B_8266_BB_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)
//FW1906 GRBCW
Copy link
Contributor

@coderabbitai coderabbitai bot Aug 26, 2025

Choose a reason for hiding this comment

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

💡 Verification agent

❓ Verification inconclusive

APA106 color order inconsistent across methods (UART vs DMA/BB)

U0/U1 use NeoRbgFeature, but DMA/BitBang use NeoGrbFeature. This likely flips colors for APA106 on those methods.

Suggest aligning all APA106 variants to the same color order:

-#define B_8266_DM_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266DmaApa106Method>  //3 chan, esp8266, gpio3
-#define B_8266_BB_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)
+#define B_8266_DM_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266DmaApa106Method>  //3 chan, esp8266, gpio3
+#define B_8266_BB_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)

Please validate on hardware; if APA106 is truly GRB for some driver paths, set all four consistently to that feature instead.


Align APA106 definitions to the correct RGB feature (NeoRgbFeature)

The APA106 LED’s native channel order is Red–Green–Blue, so all four method variants should use NeoRgbFeature. The current mix of NeoRbgFeature (R–B–G) and NeoGrbFeature (G–R–B) will produce incorrect colors.

Please update in wled00/bus_wrapper.h:

- #define B_8266_U0_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart0Apa106Method>
- #define B_8266_U1_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart1Apa106Method>
- #define B_8266_DM_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266DmaApa106Method>
- #define B_8266_BB_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBangApa106Method>
+ #define B_8266_U0_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266Uart0Apa106Method>
+ #define B_8266_U1_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266Uart1Apa106Method>
+ #define B_8266_DM_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266DmaApa106Method>
+ #define B_8266_BB_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266BitBangApa106Method>

Validate on hardware; this ensures all APA106 paths use the correct RGB channel mapping.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#define B_8266_U0_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart0Apa106Method> //3 chan, esp8266, gpio1
#define B_8266_U1_APA106_3 NeoPixelBus<NeoRbgFeature, NeoEsp8266Uart1Apa106Method> //3 chan, esp8266, gpio2
#define B_8266_DM_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266DmaApa106Method> //3 chan, esp8266, gpio3
#define B_8266_BB_APA106_3 NeoPixelBus<NeoGrbFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)
//FW1906 GRBCW
#define B_8266_U0_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266Uart0Apa106Method> //3 chan, esp8266, gpio1
#define B_8266_U1_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266Uart1Apa106Method> //3 chan, esp8266, gpio2
#define B_8266_DM_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266DmaApa106Method> //3 chan, esp8266, gpio3
#define B_8266_BB_APA106_3 NeoPixelBus<NeoRgbFeature, NeoEsp8266BitBangApa106Method> //3 chan, esp8266, bb (any pin but 16)
//FW1906 GRBCW
🤖 Prompt for AI Agents
In wled00/bus_wrapper.h around lines 179-183, the APA106 macro definitions use
incorrect feature types (NeoRbgFeature and NeoGrbFeature); update all four
defines so they use NeoRgbFeature instead (i.e., replace NeoRbgFeature and
NeoGrbFeature with NeoRgbFeature for B_8266_U0_APA106_3, B_8266_U1_APA106_3,
B_8266_DM_APA106_3, and B_8266_BB_APA106_3) and rebuild/validate on hardware to
confirm correct color mapping.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@blazoncek is the rabbit right? do you know anything about this?

Copy link
Contributor

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!

Copy link
Collaborator

Choose a reason for hiding this comment

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

Regardless if rabbit is correct or not, users have color order override so it really doesn't matter.

@DedeHai DedeHai changed the title use video scaling instead of NPB luminance use video scaling instead of NPB luminance & new ABL Aug 26, 2025
@blazoncek
Copy link
Collaborator

I do like the idea of BusManager::initializeABL() and the swap of NPB, however I do not like the pre-calculation of sum of all pixels on bus level. In current implementation it assumes a pixel is only ever modified once, which may not be true in the future.

There are a few other erros regarding reverse scaling of PolyBus::getPixelColor() too.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 27, 2025

Thanks for reviewing.

In current implementation it assumes a pixel is only ever modified once, which may not be true in the future.

I don't understand what you mean by that. If a pixel is sent out to the bus, it should have its final value. What manipulation at bus level are you thinking of?

There are a few other erros regarding reverse scaling of PolyBus::getPixelColor() too.

very well possible. any hints?

@blazoncek
Copy link
Collaborator

What manipulation at bus level are you thinking of?

Multiple calls to BusDigital::setPixelColor() for the same pixel. Actually 2 strips may (partially) overlap if you choose so in settings (not relevant in this case, but still).

However, I've dug deeper now and do see the benefit of pre-calculating sum of all channels in the setPixelColor() phase.

any hints?

Check restoreColorLossy() occurrences.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 27, 2025

However, I've dug deeper now and do see the benefit of pre-calculating sum of all channels in the setPixelColor() phase.

it makes it a tad faster :)

Check restoreColorLossy() occurrences.

In the case I use to read back from NPB there is no need to restore the brightness, the ABL calculates on top of the applied brightness as that is applied before summing.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 29, 2025

any objections to merging this @willmmiles?

@willmmiles
Copy link
Member

any objections to merging this @willmmiles?

I haven't done a thorough test, but the code looks good to me. Go for it.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Aug 29, 2025

thanks for reviewing. I did a lot of tests on the actual color scaling, not so many on the new ABL since I do not have all the hardware but the output power values for WS2812 were withing a reasnable tolerance. If you did not see any flaws in logic, this is good to go live.

@DedeHai DedeHai merged commit d5d7fde into wled:main Aug 29, 2025
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement optimization re-working an existing feature to be faster, or use less memory
Projects
None yet
Development

Successfully merging this pull request may close these issues.

dark color mixing regression?
5 participants