From a71d1c8867a726d05dd9324601148121a75ff5be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Feb 2026 14:09:40 -0600 Subject: [PATCH] [light] Replace powf gamma correction with Python-generated PROGMEM LUTs Replace runtime powf() calls in light gamma correction with pre-computed uint16[256] PROGMEM lookup tables generated at Python codegen time. The gamma value is a compile-time constant, so the tables can be computed once and shared across all lights with the same gamma value. This eliminates powf/ieee754_powf (~2.3KB) from the binary. Two 512-byte PROGMEM tables (forward + reverse) are shared per unique gamma value, for a net savings of ~1.3KB flash and zero RAM impact. The LUT uses linear interpolation between table entries, achieving zero PWM errors at both 8-bit and 16-bit resolution compared to the old powf-based approach. Breaking change: gamma parameter removed from LightColorValues::as_*() methods since gamma correction is now applied externally via LightState::gamma_correct_lut(). gamma_correct() and gamma_uncorrect() in helpers.h are deprecated (removal in 2026.12.0). --- esphome/components/esp32/core.cpp | 1 + esphome/components/esp8266/core.cpp | 3 + esphome/components/host/core.cpp | 1 + esphome/components/libretiny/core.cpp | 1 + esphome/components/light/__init__.py | 41 ++++++++++- esphome/components/light/addressable_light.h | 2 +- .../light/addressable_light_wrapper.h | 9 ++- .../components/light/esp_color_correction.cpp | 36 +++++----- .../components/light/esp_color_correction.h | 44 ++++++++---- esphome/components/light/light_call.cpp | 5 +- esphome/components/light/light_color_values.h | 48 ++++++------- esphome/components/light/light_state.cpp | 72 ++++++++++++++++--- esphome/components/light/light_state.h | 14 ++++ esphome/components/rp2040/core.cpp | 1 + esphome/core/hal.h | 1 + esphome/core/helpers.h | 4 ++ 16 files changed, 207 insertions(+), 76 deletions(-) diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index 202d929ab9..df98678c45 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -47,6 +47,7 @@ void arch_init() { void HOT arch_feed_wdt() { esp_task_wdt_reset(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 236d3022be..24f9a51046 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -32,6 +32,9 @@ void HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +uint16_t progmem_read_uint16(const uint16_t *addr) { + return pgm_read_word(addr); // NOLINT +} uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } diff --git a/esphome/components/host/core.cpp b/esphome/components/host/core.cpp index c20a33fa37..689277e360 100644 --- a/esphome/components/host/core.cpp +++ b/esphome/components/host/core.cpp @@ -53,6 +53,7 @@ void HOT arch_feed_wdt() { } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t arch_get_cpu_cycle_count() { struct timespec spec; clock_gettime(CLOCK_MONOTONIC, &spec); diff --git a/esphome/components/libretiny/core.cpp b/esphome/components/libretiny/core.cpp index 4dda7c3856..ded4db1a52 100644 --- a/esphome/components/libretiny/core.cpp +++ b/esphome/components/libretiny/core.cpp @@ -34,6 +34,7 @@ void HOT arch_feed_wdt() { lt_wdt_feed(); } uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); } uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } } // namespace esphome diff --git a/esphome/components/light/__init__.py b/esphome/components/light/__init__.py index f1089ad64f..f6bb627a4e 100644 --- a/esphome/components/light/__init__.py +++ b/esphome/components/light/__init__.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field import enum import esphome.automation as auto @@ -37,7 +38,7 @@ from esphome.const import ( CONF_WEB_SERVER, CONF_WHITE, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, HexInt, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity from esphome.cpp_generator import MockObjClass @@ -66,6 +67,42 @@ from .types import ( # noqa CODEOWNERS = ["@esphome/core"] IS_PLATFORM_COMPONENT = True +DOMAIN = "light" + + +@dataclass +class LightData: + gamma_tables: dict = field( + default_factory=dict + ) # gamma_value -> (fwd_arr, rev_arr) + + +def _get_data() -> LightData: + if DOMAIN not in CORE.data: + CORE.data[DOMAIN] = LightData() + return CORE.data[DOMAIN] + + +def _get_or_create_gamma_table(gamma_correct): + data = _get_data() + if gamma_correct in data.gamma_tables: + return data.gamma_tables[gamma_correct] + + if gamma_correct > 0: + forward = [ + HexInt(min(65535, int(round((i / 255.0) ** gamma_correct * 65535)))) + for i in range(256) + ] + else: + forward = [HexInt(int(round(i / 255.0 * 65535))) for i in range(256)] + + gamma_str = f"{gamma_correct}".replace(".", "_") + fwd_id = ID(f"gamma_{gamma_str}_fwd", is_declaration=True, type=cg.uint16) + fwd_arr = cg.progmem_array(fwd_id, forward) + data.gamma_tables[gamma_correct] = fwd_arr + return fwd_arr + + LightRestoreMode = light_ns.enum("LightRestoreMode") RESTORE_MODES = { "RESTORE_DEFAULT_OFF": LightRestoreMode.LIGHT_RESTORE_DEFAULT_OFF, @@ -239,6 +276,8 @@ async def setup_light_core_(light_var, output_var, config): cg.add(light_var.set_flash_transition_length(flash_transition_length)) if (gamma_correct := config.get(CONF_GAMMA_CORRECT)) is not None: cg.add(light_var.set_gamma_correct(gamma_correct)) + fwd_arr = _get_or_create_gamma_table(gamma_correct) + cg.add(light_var.set_gamma_table(fwd_arr)) effects = await cg.build_registry_list( EFFECTS_REGISTRY, config.get(CONF_EFFECTS, []) ) diff --git a/esphome/components/light/addressable_light.h b/esphome/components/light/addressable_light.h index fcaf07f578..643ec962ba 100644 --- a/esphome/components/light/addressable_light.h +++ b/esphome/components/light/addressable_light.h @@ -66,7 +66,7 @@ class AddressableLight : public LightOutput, public Component { Color(to_uint8_scale(red), to_uint8_scale(green), to_uint8_scale(blue), to_uint8_scale(white))); } void setup_state(LightState *state) override { - this->correction_.calculate_gamma_table(state->get_gamma_correct()); + this->correction_.set_gamma_table(state->get_gamma_table()); this->state_parent_ = state; } void update_state(LightState *state) override; diff --git a/esphome/components/light/addressable_light_wrapper.h b/esphome/components/light/addressable_light_wrapper.h index cd83482248..cc6aa57905 100644 --- a/esphome/components/light/addressable_light_wrapper.h +++ b/esphome/components/light/addressable_light_wrapper.h @@ -74,11 +74,10 @@ class AddressableLightWrapper : public light::AddressableLight { return; } - float gamma = this->light_state_->get_gamma_correct(); - float r = gamma_uncorrect(this->wrapper_state_[0] / 255.0f, gamma); - float g = gamma_uncorrect(this->wrapper_state_[1] / 255.0f, gamma); - float b = gamma_uncorrect(this->wrapper_state_[2] / 255.0f, gamma); - float w = gamma_uncorrect(this->wrapper_state_[3] / 255.0f, gamma); + float r = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[0] / 255.0f); + float g = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[1] / 255.0f); + float b = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[2] / 255.0f); + float w = this->light_state_->gamma_uncorrect_lut(this->wrapper_state_[3] / 255.0f); auto call = this->light_state_->make_call(); diff --git a/esphome/components/light/esp_color_correction.cpp b/esphome/components/light/esp_color_correction.cpp index 1b511a94b2..9d731a2bd5 100644 --- a/esphome/components/light/esp_color_correction.cpp +++ b/esphome/components/light/esp_color_correction.cpp @@ -1,25 +1,25 @@ #include "esp_color_correction.h" -#include "light_color_values.h" -#include "esphome/core/log.h" namespace esphome::light { -void ESPColorCorrection::calculate_gamma_table(float gamma) { - for (uint16_t i = 0; i < 256; i++) { - // corrected = val ^ gamma - auto corrected = to_uint8_scale(gamma_correct(i / 255.0f, gamma)); - this->gamma_table_[i] = corrected; - } - if (gamma == 0.0f) { - for (uint16_t i = 0; i < 256; i++) - this->gamma_reverse_table_[i] = i; - return; - } - for (uint16_t i = 0; i < 256; i++) { - // val = corrected ^ (1/gamma) - auto uncorrected = to_uint8_scale(powf(i / 255.0f, 1.0f / gamma)); - this->gamma_reverse_table_[i] = uncorrected; - } +uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const { + if (this->gamma_table_ == nullptr) + return value; + return static_cast((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257); +} + +uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const { + if (this->gamma_table_ == nullptr) + return value; + if (value == 0) + return 0; + uint16_t target = value * 257; // Scale 0-255 to 0-65535 + uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target); + if (lo >= 255) + return 255; + uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]); + uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]); + return (target - a <= b - target) ? lo : lo + 1; } } // namespace esphome::light diff --git a/esphome/components/light/esp_color_correction.h b/esphome/components/light/esp_color_correction.h index d275e045b7..2905a9f9c5 100644 --- a/esphome/components/light/esp_color_correction.h +++ b/esphome/components/light/esp_color_correction.h @@ -1,15 +1,29 @@ #pragma once #include "esphome/core/color.h" +#include "esphome/core/hal.h" namespace esphome::light { +/// Binary search a monotonically increasing uint16[256] PROGMEM table. +/// Returns the largest index where table[index] <= target. +inline uint8_t gamma_table_reverse_search(const uint16_t *table, uint16_t target) { + uint8_t lo = 0, hi = 255; + while (lo < hi) { + uint8_t mid = (lo + hi + 1) / 2; + if (progmem_read_uint16(&table[mid]) <= target) + lo = mid; + else + hi = mid - 1; + } + return lo; +} + class ESPColorCorrection { public: - ESPColorCorrection() : max_brightness_(255, 255, 255, 255) {} void set_max_brightness(const Color &max_brightness) { this->max_brightness_ = max_brightness; } void set_local_brightness(uint8_t local_brightness) { this->local_brightness_ = local_brightness; } - void calculate_gamma_table(float gamma); + void set_gamma_table(const uint16_t *table) { this->gamma_table_ = table; } inline Color color_correct(Color color) const ESPHOME_ALWAYS_INLINE { // corrected = (uncorrected * max_brightness * local_brightness) ^ gamma return Color(this->color_correct_red(color.red), this->color_correct_green(color.green), @@ -17,19 +31,19 @@ class ESPColorCorrection { } inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); - return this->gamma_table_[res]; + return this->gamma_correct_(res); } inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { // uncorrected = corrected^(1/gamma) / (max_brightness * local_brightness) @@ -39,36 +53,40 @@ class ESPColorCorrection { inline uint8_t color_uncorrect_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.red == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[red] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(red) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.red) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.green == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[green] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(green) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.green) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.blue == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[blue] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(blue) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.blue) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } inline uint8_t color_uncorrect_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { if (this->max_brightness_.white == 0 || this->local_brightness_ == 0) return 0; - uint16_t uncorrected = this->gamma_reverse_table_[white] * 255UL; + uint16_t uncorrected = this->gamma_uncorrect_(white) * 255UL; uint16_t res = ((uncorrected / this->max_brightness_.white) * 255UL) / this->local_brightness_; return (uint8_t) std::min(res, uint16_t(255)); } protected: - uint8_t gamma_table_[256]; - uint8_t gamma_reverse_table_[256]; - Color max_brightness_; + /// Forward gamma: read uint16 PROGMEM table, convert to uint8 + uint8_t gamma_correct_(uint8_t value) const; + /// Reverse gamma: binary search the forward PROGMEM table + uint8_t gamma_uncorrect_(uint8_t value) const; + + const uint16_t *gamma_table_{nullptr}; + Color max_brightness_{255, 255, 255, 255}; uint8_t local_brightness_{255}; }; diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 0291b2c3c6..14cd0e92f6 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -389,9 +389,8 @@ void LightCall::transform_parameters_() { const float ww_fraction = (color_temp - min_mireds) / range; const float cw_fraction = 1.0f - ww_fraction; const float max_cw_ww = std::max(ww_fraction, cw_fraction); - const float gamma = this->parent_->get_gamma_correct(); - this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma); - this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma); + this->cold_white_ = this->parent_->gamma_uncorrect_lut(cw_fraction / max_cw_ww); + this->warm_white_ = this->parent_->gamma_uncorrect_lut(ww_fraction / max_cw_ww); this->set_flag_(FLAG_HAS_COLD_WHITE); this->set_flag_(FLAG_HAS_WARM_WHITE); } diff --git a/esphome/components/light/light_color_values.h b/esphome/components/light/light_color_values.h index dc23263312..3a9ca8c8c2 100644 --- a/esphome/components/light/light_color_values.h +++ b/esphome/components/light/light_color_values.h @@ -111,60 +111,54 @@ class LightColorValues { } } - // Note that method signature of as_* methods is kept as-is for compatibility reasons, so not all parameters - // are always used or necessary. Methods will be deprecated later. - /// Convert these light color values to a binary representation and write them to binary. void as_binary(bool *binary) const { *binary = this->state_ == 1.0f; } /// Convert these light color values to a brightness-only representation and write them to brightness. - void as_brightness(float *brightness, float gamma = 0) const { - *brightness = gamma_correct(this->state_ * this->brightness_, gamma); - } + void as_brightness(float *brightness) const { *brightness = this->state_ * this->brightness_; } /// Convert these light color values to an RGB representation and write them to red, green, blue. - void as_rgb(float *red, float *green, float *blue, float gamma = 0, bool color_interlock = false) const { + void as_rgb(float *red, float *green, float *blue) const { if (this->color_mode_ & ColorCapability::RGB) { float brightness = this->state_ * this->brightness_ * this->color_brightness_; - *red = gamma_correct(brightness * this->red_, gamma); - *green = gamma_correct(brightness * this->green_, gamma); - *blue = gamma_correct(brightness * this->blue_, gamma); + *red = brightness * this->red_; + *green = brightness * this->green_; + *blue = brightness * this->blue_; } else { *red = *green = *blue = 0; } } /// Convert these light color values to an RGBW representation and write them to red, green, blue, white. - void as_rgbw(float *red, float *green, float *blue, float *white, float gamma = 0, - bool color_interlock = false) const { - this->as_rgb(red, green, blue, gamma); + void as_rgbw(float *red, float *green, float *blue, float *white) const { + this->as_rgb(red, green, blue); if (this->color_mode_ & ColorCapability::WHITE) { - *white = gamma_correct(this->state_ * this->brightness_ * this->white_, gamma); + *white = this->state_ * this->brightness_ * this->white_; } else { *white = 0; } } /// Convert these light color values to an RGBWW representation with the given parameters. - void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, float gamma = 0, + void as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness = false) const { - this->as_rgb(red, green, blue, gamma); - this->as_cwww(cold_white, warm_white, gamma, constant_brightness); + this->as_rgb(red, green, blue); + this->as_cwww(cold_white, warm_white, constant_brightness); } /// Convert these light color values to an RGB+CT+BR representation with the given parameters. void as_rgbct(float color_temperature_cw, float color_temperature_ww, float *red, float *green, float *blue, - float *color_temperature, float *white_brightness, float gamma = 0) const { - this->as_rgb(red, green, blue, gamma); - this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness, gamma); + float *color_temperature, float *white_brightness) const { + this->as_rgb(red, green, blue); + this->as_ct(color_temperature_cw, color_temperature_ww, color_temperature, white_brightness); } /// Convert these light color values to an CWWW representation with the given parameters. - void as_cwww(float *cold_white, float *warm_white, float gamma = 0, bool constant_brightness = false) const { + void as_cwww(float *cold_white, float *warm_white, bool constant_brightness = false) const { if (this->color_mode_ & ColorCapability::COLD_WARM_WHITE) { - const float cw_level = gamma_correct(this->cold_white_, gamma); - const float ww_level = gamma_correct(this->warm_white_, gamma); - const float white_level = gamma_correct(this->state_ * this->brightness_, gamma); + const float cw_level = this->cold_white_; + const float ww_level = this->warm_white_; + const float white_level = this->state_ * this->brightness_; if (!constant_brightness) { *cold_white = white_level * cw_level; *warm_white = white_level * ww_level; @@ -184,13 +178,13 @@ class LightColorValues { } /// Convert these light color values to a CT+BR representation with the given parameters. - void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, float *white_brightness, - float gamma = 0) const { + void as_ct(float color_temperature_cw, float color_temperature_ww, float *color_temperature, + float *white_brightness) const { const float white_level = this->color_mode_ & ColorCapability::RGB ? this->white_ : 1; if (this->color_mode_ & ColorCapability::COLOR_TEMPERATURE) { *color_temperature = (this->color_temperature_ - color_temperature_cw) / (color_temperature_ww - color_temperature_cw); - *white_brightness = gamma_correct(this->state_ * this->brightness_ * white_level, gamma); + *white_brightness = this->state_ * this->brightness_ * white_level; } else { // Probably won't get here but put this here anyway. *white_brightness = 0; } diff --git a/esphome/components/light/light_state.cpp b/esphome/components/light/light_state.cpp index ed86bf58da..42b996b2c7 100644 --- a/esphome/components/light/light_state.cpp +++ b/esphome/components/light/light_state.cpp @@ -1,4 +1,5 @@ #include "light_state.h" +#include "esp_color_correction.h" #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" @@ -204,31 +205,86 @@ void LightState::add_effects(const std::initializer_list &effects void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } void LightState::current_values_as_brightness(float *brightness) { - this->current_values.as_brightness(brightness, this->gamma_correct_); + this->current_values.as_brightness(brightness); + *brightness = this->gamma_correct_lut(*brightness); } void LightState::current_values_as_rgb(float *red, float *green, float *blue, bool color_interlock) { - this->current_values.as_rgb(red, green, blue, this->gamma_correct_, false); + this->current_values.as_rgb(red, green, blue); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); } void LightState::current_values_as_rgbw(float *red, float *green, float *blue, float *white, bool color_interlock) { - this->current_values.as_rgbw(red, green, blue, white, this->gamma_correct_, false); + this->current_values.as_rgbw(red, green, blue, white); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *white = this->gamma_correct_lut(*white); } void LightState::current_values_as_rgbww(float *red, float *green, float *blue, float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, this->gamma_correct_, constant_brightness); + this->current_values.as_rgbww(red, green, blue, cold_white, warm_white, constant_brightness); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); } void LightState::current_values_as_rgbct(float *red, float *green, float *blue, float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); this->current_values.as_rgbct(traits.get_min_mireds(), traits.get_max_mireds(), red, green, blue, color_temperature, - white_brightness, this->gamma_correct_); + white_brightness); + *red = this->gamma_correct_lut(*red); + *green = this->gamma_correct_lut(*green); + *blue = this->gamma_correct_lut(*blue); + *white_brightness = this->gamma_correct_lut(*white_brightness); } void LightState::current_values_as_cwww(float *cold_white, float *warm_white, bool constant_brightness) { - this->current_values.as_cwww(cold_white, warm_white, this->gamma_correct_, constant_brightness); + this->current_values.as_cwww(cold_white, warm_white, constant_brightness); + *cold_white = this->gamma_correct_lut(*cold_white); + *warm_white = this->gamma_correct_lut(*warm_white); } void LightState::current_values_as_ct(float *color_temperature, float *white_brightness) { auto traits = this->get_traits(); - this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness, - this->gamma_correct_); + this->current_values.as_ct(traits.get_min_mireds(), traits.get_max_mireds(), color_temperature, white_brightness); + *white_brightness = this->gamma_correct_lut(*white_brightness); +} + +float LightState::gamma_correct_lut(float value) const { + if (value <= 0.0f) + return 0.0f; + if (value >= 1.0f) + return 1.0f; + if (this->gamma_table_ == nullptr) + return value; + float scaled = value * 255.0f; + auto idx = static_cast(scaled); + if (idx >= 255) + return progmem_read_uint16(&this->gamma_table_[255]) / 65535.0f; + float frac = scaled - idx; + float a = progmem_read_uint16(&this->gamma_table_[idx]); + float b = progmem_read_uint16(&this->gamma_table_[idx + 1]); + return (a + frac * (b - a)) / 65535.0f; +} +float LightState::gamma_uncorrect_lut(float value) const { + if (value <= 0.0f) + return 0.0f; + if (value >= 1.0f) + return 1.0f; + if (this->gamma_table_ == nullptr) + return value; + uint16_t target = static_cast(value * 65535.0f); + uint8_t lo = gamma_table_reverse_search(this->gamma_table_, target); + // Interpolate between lo and lo+1 + uint16_t a = progmem_read_uint16(&this->gamma_table_[lo]); + if (lo >= 255) + return 1.0f; + uint16_t b = progmem_read_uint16(&this->gamma_table_[lo + 1]); + if (b == a) + return lo / 255.0f; + float frac = static_cast(target - a) / static_cast(b - a); + return (lo + frac) / 255.0f; } bool LightState::is_transformer_active() { return this->is_transformer_active_; } diff --git a/esphome/components/light/light_state.h b/esphome/components/light/light_state.h index 83b9226d03..e2ae17460a 100644 --- a/esphome/components/light/light_state.h +++ b/esphome/components/light/light_state.h @@ -11,6 +11,7 @@ #include "light_traits.h" #include "light_transformer.h" +#include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include #include @@ -166,6 +167,17 @@ class LightState : public EntityBase, public Component { void set_gamma_correct(float gamma_correct); float get_gamma_correct() const { return this->gamma_correct_; } + /// Set pre-computed gamma forward lookup table (256-entry uint16 PROGMEM array) + void set_gamma_table(const uint16_t *forward) { this->gamma_table_ = forward; } + + /// Get the forward gamma lookup table + const uint16_t *get_gamma_table() const { return this->gamma_table_; } + + /// Apply gamma correction using the pre-computed forward LUT + float gamma_correct_lut(float value) const; + /// Reverse gamma correction by binary-searching the forward LUT + float gamma_uncorrect_lut(float value) const; + /// Set the restore mode of this light void set_restore_mode(LightRestoreMode restore_mode); @@ -297,6 +309,8 @@ class LightState : public EntityBase, public Component { uint32_t flash_transition_length_{}; /// Gamma correction factor for the light. float gamma_correct_{}; + const uint16_t *gamma_table_{nullptr}; + /// Whether the light value should be written in the next cycle. bool next_write_{true}; // for effects, true if a transformer (transition) is active. diff --git a/esphome/components/rp2040/core.cpp b/esphome/components/rp2040/core.cpp index 37378d88bb..53fff9ac77 100644 --- a/esphome/components/rp2040/core.cpp +++ b/esphome/components/rp2040/core.cpp @@ -32,6 +32,7 @@ void HOT arch_feed_wdt() { watchdog_update(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } +uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; } uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return ulMainGetRunTimeCounterValue(); } uint32_t arch_get_cpu_freq_hz() { return RP2040::f_cpu(); } diff --git a/esphome/core/hal.h b/esphome/core/hal.h index 1a4230e421..22840901fd 100644 --- a/esphome/core/hal.h +++ b/esphome/core/hal.h @@ -41,5 +41,6 @@ void arch_feed_wdt(); uint32_t arch_get_cpu_cycle_count(); uint32_t arch_get_cpu_freq_hz(); uint8_t progmem_read_byte(const uint8_t *addr); +uint16_t progmem_read_uint16(const uint16_t *addr); } // namespace esphome diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 298b93fbc4..1e90c21c76 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1350,8 +1350,12 @@ bool base64_decode_int32_vector(const std::string &base64, std::vector ///@{ /// Applies gamma correction of \p gamma to \p value. +// Remove before 2026.12.0 +ESPDEPRECATED("Use LightState::gamma_correct_lut() instead. Removed in 2026.12.0.", "2026.6.0") float gamma_correct(float value, float gamma); /// Reverts gamma correction of \p gamma to \p value. +// Remove before 2026.12.0 +ESPDEPRECATED("Use LightState::gamma_uncorrect_lut() instead. Removed in 2026.12.0.", "2026.6.0") float gamma_uncorrect(float value, float gamma); /// Convert \p red, \p green and \p blue (all 0-1) values to \p hue (0-360), \p saturation (0-1) and \p value (0-1).