[light] Reduce validate_ clamp code size and speed up unit-range clamps (#15728)

This commit is contained in:
J. Nick Koston
2026-04-22 06:34:26 +02:00
committed by GitHub
parent 699cf9690a
commit 9c80cbf19c
3 changed files with 117 additions and 65 deletions
+39 -24
View File
@@ -10,13 +10,10 @@ namespace esphome::light {
static const char *const TAG = "light";
// Helper functions to reduce code size for logging
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
float max = 1.0f) {
if (value < min || value > max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
value = clamp(value, min, max);
}
// Cold-path logger; caller handles the clamp so the in-range hot path avoids
// the spill/reload around the call.
static void log_value_out_of_range(const char *name, float value, const LogString *param_name, float min, float max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
@@ -57,6 +54,12 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature",
"Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white");
// Indices 0-7 match FieldFlags bits 0-7; index 8 is color_temperature.
// PROGMEM_STRING_TABLE is constexpr-init (no RAM guard variable).
PROGMEM_STRING_TABLE(ValidateFieldNames, "Brightness", "Color brightness", "Red", "Green", "Blue", "White",
"Cold white", "Warm white", "Color temperature");
static constexpr uint8_t VALIDATE_CT_INDEX = 8;
static const LogString *color_mode_to_human(ColorMode color_mode) {
return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0);
}
@@ -277,25 +280,37 @@ LightColorValues LightCall::validate_() {
if (this->has_state())
v.set_state(this->state_);
// clamp_and_log_if_invalid already clamps in-place, so assign directly
// to avoid redundant clamp code from the setter being inlined.
#define VALIDATE_AND_APPLY(field, name_str, ...) \
if (this->has_##field()) { \
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
v.field##_ = this->field##_; \
// FieldFlags bits 0-7 must match unit_fields_ array indices.
static_assert(FLAG_HAS_BRIGHTNESS == 1u << 0 && FLAG_HAS_COLOR_BRIGHTNESS == 1u << 1 && FLAG_HAS_RED == 1u << 2 &&
FLAG_HAS_GREEN == 1u << 3 && FLAG_HAS_BLUE == 1u << 4 && FLAG_HAS_WHITE == 1u << 5 &&
FLAG_HAS_COLD_WHITE == 1u << 6 && FLAG_HAS_WARM_WHITE == 1u << 7,
"FieldFlags bits 0-7 must match unit_fields_ indices");
// Iterate set bits only (ctz + clear-lowest) — HA can drive perform()
// at high frequency so the hot path is O(popcount).
unsigned active = this->flags_ & CLAMP_FLAGS_MASK;
while (active != 0) {
unsigned bit = __builtin_ctz(active);
active &= active - 1; // clear lowest set bit
float &value = this->unit_fields_[bit];
if (float_out_of_unit_range(value)) {
log_value_out_of_range(name, value, ValidateFieldNames::get_log_str(bit, 0), 0.0f, 1.0f);
value = clamp_unit_float(value);
}
v.unit_fields_[bit] = value;
}
VALIDATE_AND_APPLY(brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, "Red")
VALIDATE_AND_APPLY(green, "Green")
VALIDATE_AND_APPLY(blue, "Blue")
VALIDATE_AND_APPLY(white, "White")
VALIDATE_AND_APPLY(cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
#undef VALIDATE_AND_APPLY
// color_temperature: runtime range from traits.
if (this->has_color_temperature()) {
const float ct_min = traits.get_min_mireds();
const float ct_max = traits.get_max_mireds();
if (this->color_temperature_ < ct_min || this->color_temperature_ > ct_max) {
log_value_out_of_range(name, this->color_temperature_, ValidateFieldNames::get_log_str(VALIDATE_CT_INDEX, 0),
ct_min, ct_max);
this->color_temperature_ = clamp(this->color_temperature_, ct_min, ct_max);
}
v.color_temperature_ = this->color_temperature_;
}
v.normalize_color();
+16 -23
View File
@@ -195,25 +195,26 @@ class LightCall {
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_(const LightTraits &traits);
// Bitfield flags - each flag indicates whether a corresponding value has been set.
// Bits 0-7 index unit_fields_[] in validate_(); don't reorder (asserts in light_call.cpp).
enum FieldFlags : uint16_t {
FLAG_HAS_STATE = 1 << 0,
FLAG_HAS_TRANSITION = 1 << 1,
FLAG_HAS_FLASH = 1 << 2,
FLAG_HAS_EFFECT = 1 << 3,
FLAG_HAS_BRIGHTNESS = 1 << 4,
FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5,
FLAG_HAS_RED = 1 << 6,
FLAG_HAS_GREEN = 1 << 7,
FLAG_HAS_BLUE = 1 << 8,
FLAG_HAS_WHITE = 1 << 9,
FLAG_HAS_COLOR_TEMPERATURE = 1 << 10,
FLAG_HAS_COLD_WHITE = 1 << 11,
FLAG_HAS_WARM_WHITE = 1 << 12,
FLAG_HAS_BRIGHTNESS = 1 << 0,
FLAG_HAS_COLOR_BRIGHTNESS = 1 << 1,
FLAG_HAS_RED = 1 << 2,
FLAG_HAS_GREEN = 1 << 3,
FLAG_HAS_BLUE = 1 << 4,
FLAG_HAS_WHITE = 1 << 5,
FLAG_HAS_COLD_WHITE = 1 << 6,
FLAG_HAS_WARM_WHITE = 1 << 7,
FLAG_HAS_COLOR_TEMPERATURE = 1 << 8,
FLAG_HAS_STATE = 1 << 9,
FLAG_HAS_TRANSITION = 1 << 10,
FLAG_HAS_FLASH = 1 << 11,
FLAG_HAS_EFFECT = 1 << 12,
FLAG_HAS_COLOR_MODE = 1 << 13,
FLAG_PUBLISH = 1 << 14,
FLAG_SAVE = 1 << 15,
};
static constexpr uint16_t CLAMP_FLAGS_MASK = 0x00FFu; // bits 0-7
inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
@@ -239,19 +240,11 @@ class LightCall {
LightState *parent_;
// Light state values - use flags_ to check if a value has been set.
// Group 4-byte aligned members first
uint32_t transition_length_;
uint32_t flash_length_;
uint32_t effect_;
float brightness_;
float color_brightness_;
float red_;
float green_;
float blue_;
float white_;
ESPHOME_LIGHT_UNIT_FIELDS_UNION();
float color_temperature_;
float cold_white_;
float warm_white_;
// Smaller members at the end for better packing
uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set
+62 -18
View File
@@ -3,11 +3,62 @@
#include "esphome/core/helpers.h"
#include "color_mode.h"
#include <cmath>
#include <cstdint>
#include <limits>
namespace esphome::light {
inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(roundf(x * 255.0f)); }
// IEEE 754 bit patterns. Values in [0.0f, 1.0f] have bits <= ONE_F_BITS;
// negatives have the sign bit set (→ huge unsigned). A single unsigned compare
// replaces two soft-float __ltsf2/__gtsf2 calls on ESP8266.
static constexpr uint32_t ONE_F_BITS = 0x3F800000u; // 1.0f
static constexpr uint32_t NEG_ZERO_F_BITS = 0x80000000u; // -0.0f / sign-bit mask
static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
static_assert(std::numeric_limits<float>::is_iec559, "IEEE 754 float required");
// Union pun — memcpy/bit_cast don't fold on xtensa-gcc (see api/proto.h).
// -0.0f is numerically zero so it's reported in range (no warning, no clamp).
inline bool float_out_of_unit_range(float x) {
union {
float f;
uint32_t u;
} pun;
pun.f = x;
return pun.u > ONE_F_BITS && pun.u != NEG_ZERO_F_BITS;
}
// Clamps to [0.0f, 1.0f] without float compares. Out of range: sign bit set
// (negatives, -NaN, -Inf) → 0.0f; sign bit clear (>1, +NaN, +Inf) → 1.0f.
inline float clamp_unit_float(float x) {
union {
float f;
uint32_t u;
} pun;
pun.f = x;
if (pun.u <= ONE_F_BITS)
return x;
return (pun.u & NEG_ZERO_F_BITS) ? 0.0f : 1.0f; // sign bit → negative → clamp to 0
}
// Shared anonymous union: eight unit-range floats alias unit_fields_[8] so
// LightCall::validate_() can iterate them as a real array. GCC/Clang ext.
#define ESPHOME_LIGHT_UNIT_FIELDS_UNION() \
union { \
struct { \
float brightness_; \
float color_brightness_; \
float red_; \
float green_; \
float blue_; \
float white_; \
float cold_white_; \
float warm_white_; \
}; \
float unit_fields_[8]; \
}
/** This class represents the color state for a light object.
*
* The representation of the color state is dependent on the active color mode. A color mode consists of multiple
@@ -52,9 +103,9 @@ class LightColorValues {
green_(1.0f),
blue_(1.0f),
white_(1.0f),
color_temperature_{0.0f},
cold_white_{1.0f},
warm_white_{1.0f},
color_temperature_{0.0f},
color_mode_(ColorMode::UNKNOWN) {}
LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green,
@@ -220,39 +271,39 @@ class LightColorValues {
/// Get the binary true/false state of these light color values.
bool is_on() const { return this->get_state() != 0.0f; }
/// Set the state of these light color values. In range from 0.0 (off) to 1.0 (on)
void set_state(float state) { this->state_ = clamp(state, 0.0f, 1.0f); }
void set_state(float state) { this->state_ = clamp_unit_float(state); }
/// Set the state of these light color values as a binary true/false.
void set_state(bool state) { this->state_ = state ? 1.0f : 0.0f; }
/// Get the brightness property of these light color values. In range 0.0 to 1.0
float get_brightness() const { return this->brightness_; }
/// Set the brightness property of these light color values. In range 0.0 to 1.0
void set_brightness(float brightness) { this->brightness_ = clamp(brightness, 0.0f, 1.0f); }
void set_brightness(float brightness) { this->brightness_ = clamp_unit_float(brightness); }
/// Get the color brightness property of these light color values. In range 0.0 to 1.0
float get_color_brightness() const { return this->color_brightness_; }
/// Set the color brightness property of these light color values. In range 0.0 to 1.0
void set_color_brightness(float brightness) { this->color_brightness_ = clamp(brightness, 0.0f, 1.0f); }
void set_color_brightness(float brightness) { this->color_brightness_ = clamp_unit_float(brightness); }
/// Get the red property of these light color values. In range 0.0 to 1.0
float get_red() const { return this->red_; }
/// Set the red property of these light color values. In range 0.0 to 1.0
void set_red(float red) { this->red_ = clamp(red, 0.0f, 1.0f); }
void set_red(float red) { this->red_ = clamp_unit_float(red); }
/// Get the green property of these light color values. In range 0.0 to 1.0
float get_green() const { return this->green_; }
/// Set the green property of these light color values. In range 0.0 to 1.0
void set_green(float green) { this->green_ = clamp(green, 0.0f, 1.0f); }
void set_green(float green) { this->green_ = clamp_unit_float(green); }
/// Get the blue property of these light color values. In range 0.0 to 1.0
float get_blue() const { return this->blue_; }
/// Set the blue property of these light color values. In range 0.0 to 1.0
void set_blue(float blue) { this->blue_ = clamp(blue, 0.0f, 1.0f); }
void set_blue(float blue) { this->blue_ = clamp_unit_float(blue); }
/// Get the white property of these light color values. In range 0.0 to 1.0
float get_white() const { return white_; }
/// Set the white property of these light color values. In range 0.0 to 1.0
void set_white(float white) { this->white_ = clamp(white, 0.0f, 1.0f); }
void set_white(float white) { this->white_ = clamp_unit_float(white); }
/// Get the color temperature property of these light color values in mired.
float get_color_temperature() const { return this->color_temperature_; }
@@ -277,26 +328,19 @@ class LightColorValues {
/// Get the cold white property of these light color values. In range 0.0 to 1.0.
float get_cold_white() const { return this->cold_white_; }
/// Set the cold white property of these light color values. In range 0.0 to 1.0.
void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); }
void set_cold_white(float cold_white) { this->cold_white_ = clamp_unit_float(cold_white); }
/// Get the warm white property of these light color values. In range 0.0 to 1.0.
float get_warm_white() const { return this->warm_white_; }
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
void set_warm_white(float warm_white) { this->warm_white_ = clamp_unit_float(warm_white); }
friend class LightCall;
protected:
float state_; ///< ON / OFF, float for transition
float brightness_;
float color_brightness_;
float red_;
float green_;
float blue_;
float white_;
ESPHOME_LIGHT_UNIT_FIELDS_UNION();
float color_temperature_; ///< Color Temperature in Mired
float cold_white_;
float warm_white_;
ColorMode color_mode_;
};