[light] Avoid addressable transition stall at low gamma-corrected values

When a uniform-colored addressable strip transitions from one color to
another, interpolate math-only against a cached start color instead of
reading each LED's current value back through the 8-bit stored byte.

The old algorithm used led.get_red()/etc. every step as the source for
the delta, which round-tripped through gamma uncorrect/correct and the
8-bit stored byte. At gamma 2.8, any pre-gamma value below ~27 rounds
to stored byte 0, so small early-transition steps produced stored 0 and
the next step read back 0, stalling progress until ~90% of the transition
before a single step produced a large-enough pre-gamma value to clear
the gamma threshold. Result: dark for the first 9s of a 10s fade, then
jump on in the final 1s.

Detect uniform start state in start() and take a cheap math-only lerp
path when true, so the stored byte advances through each gamma threshold
as smoothed_progress crosses it. Falls back to the existing per-LED
read-back algorithm when the buffer is non-uniform (e.g. when
transitioning out of an addressable effect).
This commit is contained in:
J. Nick Koston
2026-04-13 14:17:21 -10:00
parent 21df5d9bf6
commit 3f56e0255a
7 changed files with 263 additions and 6 deletions
+42 -6
View File
@@ -58,6 +58,26 @@ void AddressableLightTransformer::start() {
// our transition will handle brightness, disable brightness in correction.
this->light_.correction_.set_local_brightness(255);
this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state());
// When every LED starts at the same color (the common case: plain turn_on/turn_off on a uniform
// strip), interpolate math-only against a single start color. Avoiding the per-step read-back
// through the 8-bit stored byte prevents gamma round-trip quantization from stalling the fade
// at low values (e.g. gamma 2.8 pre-gamma values <27 round to stored 0, freezing progress).
this->uniform_start_ = false;
if (this->light_.size() > 0) {
Color first = this->light_[0].get();
bool uniform = true;
for (int32_t i = 1; i < this->light_.size(); i++) {
if (this->light_[i].get() != first) {
uniform = false;
break;
}
}
if (uniform) {
this->uniform_start_ = true;
this->start_color_ = first;
}
}
}
inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) {
@@ -97,12 +117,28 @@ optional<LightColorValues> AddressableLightTransformer::apply() {
// non-linear when applying small deltas.
if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) {
int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
if (this->uniform_start_) {
// All LEDs started at the same color: compute the interpolated value once and write it to
// every LED. No read-back, so each LED's stored byte advances through every gamma threshold
// as smoothed_progress crosses it, instead of stalling at 0 for low pre-gamma values.
// lerp(start, target, progress) via existing helper: target - (target-start)*(1-progress).
int32_t remaining = int32_t(256.f * (1.f - smoothed_progress));
uint8_t r = subtract_scaled_difference(this->target_color_.red, this->start_color_.red, remaining);
uint8_t g = subtract_scaled_difference(this->target_color_.green, this->start_color_.green, remaining);
uint8_t b = subtract_scaled_difference(this->target_color_.blue, this->start_color_.blue, remaining);
uint8_t w = subtract_scaled_difference(this->target_color_.white, this->start_color_.white, remaining);
for (auto led : this->light_) {
led.set_rgbw(r, g, b, w);
}
} else {
int32_t scale =
int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f));
for (auto led : this->light_) {
led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale),
subtract_scaled_difference(this->target_color_.green, led.get_green(), scale),
subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale),
subtract_scaled_difference(this->target_color_.white, led.get_white(), scale));
}
}
this->last_transition_progress_ = smoothed_progress;
this->light_.schedule_show();