[light] Collapse 8 clamp-and-copy blocks in LightCall::validate_ into a loop

Reorder FieldFlags so the eight [0.0, 1.0]-clamped float fields occupy
bits 0-7 in the same order as they appear in LightCall and
LightColorValues, and move color_temperature_ to the end of both
structs. Under that layout the LightCall offset for clamp field i is
`offsetof(LightCall, brightness_) + i * 4`, and the LightColorValues
offset is exactly 12 bytes lower for every field. validate_() now
iterates the active clamp bits in a small loop that computes these
offsets from the bit position instead of expanding eight nearly
identical inline blocks via macro.

The field-name PROGMEM pointer is passed to clamp_and_log_if_invalid as
`const LogString *const *`; progmem_read_ptr only runs on the cold
(out-of-range) path, so the hot path performs no flash reads for the
name. The eight invariants the loop relies on (flag-bit layout,
field contiguity, and the constant 12-byte delta) are enforced by
static_asserts so any future reshuffle fails loudly at compile time.

Size deltas for the isolated light component build (vs dev):
  ESP32-IDF:  -118 B code, +32 B PROGMEM name table = -86 B net
  ESP8266:     -56 B code, +32 B PROGMEM name table = -24 B net
0 B RAM impact on both targets.
This commit is contained in:
J. Nick Koston
2026-04-13 15:55:50 -10:00
parent 21df5d9bf6
commit edb2145aca
3 changed files with 100 additions and 37 deletions
+64 -20
View File
@@ -10,10 +10,19 @@ 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) {
// Helper functions to reduce code size for logging.
//
// `param_name_progmem` is a pointer to a flash-resident `const LogString *`
// slot (e.g. into the FIELD_NAMES table below). We only dereference it via
// `progmem_read_ptr` on the cold path that actually emits the log message,
// so the hot path (value in range) performs no flash reads at all. On
// non-ESP8266 platforms `progmem_read_ptr` is a plain `*addr` inline, so
// there is no cost there either.
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *const *param_name_progmem,
float min = 0.0f, float max = 1.0f) {
if (value < min || value > max) {
const auto *param_name = reinterpret_cast<const LogString *>(
progmem_read_ptr(reinterpret_cast<const char *const *>(param_name_progmem)));
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);
}
@@ -277,25 +286,60 @@ 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##_; \
// Clamp the eight [0.0, 1.0] fields and copy them from `this` into `v`.
//
// LightCall and LightColorValues both declare the same eight float fields in
// the same order (brightness_, color_brightness_, red_, green_, blue_,
// white_, cold_white_, warm_white_), and their corresponding flag bits are
// also 0-7 in that order. Under that layout the LightCall offset for field i
// is `offsetof(LightCall, brightness_) + i * 4`, and the LightColorValues
// offset is exactly 12 bytes lower (enforced by the static_asserts below).
// Iterating via bit-position arithmetic lets us collapse eight inlined
// clamp/copy blocks into a single loop.
static_assert(FLAG_HAS_BRIGHTNESS == 1u << 0, "clamp loop assumes bit 0");
static_assert(FLAG_HAS_WARM_WHITE == 1u << 7, "clamp loop assumes bit 7");
static_assert(offsetof(LightCall, warm_white_) - offsetof(LightCall, brightness_) == 7 * sizeof(float),
"LightCall clamp fields must be contiguous");
static_assert(offsetof(LightColorValues, warm_white_) - offsetof(LightColorValues, brightness_) == 7 * sizeof(float),
"LightColorValues clamp fields must be contiguous");
static_assert(offsetof(LightCall, brightness_) - offsetof(LightColorValues, brightness_) == 12,
"LightCall and LightColorValues clamp fields must have constant byte-offset delta");
static const LogString *const FIELD_NAMES[8] PROGMEM = {
LOG_STR("Brightness"), // FLAG_HAS_BRIGHTNESS (bit 0)
LOG_STR("Color brightness"), // FLAG_HAS_COLOR_BRIGHTNESS (bit 1)
LOG_STR("Red"), // FLAG_HAS_RED (bit 2)
LOG_STR("Green"), // FLAG_HAS_GREEN (bit 3)
LOG_STR("Blue"), // FLAG_HAS_BLUE (bit 4)
LOG_STR("White"), // FLAG_HAS_WHITE (bit 5)
LOG_STR("Cold white"), // FLAG_HAS_COLD_WHITE (bit 6)
LOG_STR("Warm white"), // FLAG_HAS_WARM_WHITE (bit 7)
};
constexpr size_t SRC_BASE = offsetof(LightCall, brightness_);
constexpr size_t SRC_TO_DST_DELTA = SRC_BASE - offsetof(LightColorValues, brightness_);
uint8_t active = this->flags_ & CLAMP_FLAGS_MASK;
if (active != 0) {
auto *self = reinterpret_cast<uint8_t *>(this);
auto *out = reinterpret_cast<uint8_t *>(&v);
for (uint8_t bit = 0; bit < 8; bit++) {
if (!(active & (1u << bit)))
continue;
const size_t src_off = SRC_BASE + bit * sizeof(float);
float &f = *reinterpret_cast<float *>(self + src_off);
clamp_and_log_if_invalid(name, f, &FIELD_NAMES[bit]);
*reinterpret_cast<float *>(out + src_off - SRC_TO_DST_DELTA) = f;
}
}
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 uses a dynamic range from the light's traits and is
// handled separately.
if (this->has_color_temperature()) {
static const LogString *const CT_NAME PROGMEM = LOG_STR("Color temperature");
clamp_and_log_if_invalid(name, this->color_temperature_, &CT_NAME, traits.get_min_mireds(),
traits.get_max_mireds());
v.color_temperature_ = this->color_temperature_;
}
v.normalize_color();
+28 -15
View File
@@ -196,24 +196,31 @@ class LightCall {
void transform_parameters_(const LightTraits &traits);
// Bitfield flags - each flag indicates whether a corresponding value has been set.
//
// Bits 0-7 are the eight float fields that share the [0.0, 1.0] clamp range,
// in member declaration order. The validate_() clamp loop relies on this
// layout to index into LightCall/LightColorValues via bit-position arithmetic
// without a per-field offset table. Do not reorder without updating the
// static_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,7 +246,13 @@ class LightCall {
LightState *parent_;
// Light state values - use flags_ to check if a value has been set.
// Group 4-byte aligned members first
// Group 4-byte aligned members first.
//
// The eight [0.0, 1.0]-clamped float fields (brightness_ ... warm_white_)
// are declared in the same order as their flag bits (0-7) and the matching
// fields in LightColorValues. validate_() exploits this to iterate them via
// bit-position arithmetic. color_temperature_ has a custom range and lives
// outside that block.
uint32_t transition_length_;
uint32_t flash_length_;
uint32_t effect_;
@@ -249,9 +262,9 @@ class LightCall {
float green_;
float blue_;
float white_;
float color_temperature_;
float cold_white_;
float warm_white_;
float color_temperature_;
// Smaller members at the end for better packing
uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set
@@ -52,9 +52,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,
@@ -287,6 +287,12 @@ class LightColorValues {
friend class LightCall;
protected:
// The eight [0.0, 1.0]-clamped float fields are declared in the same order
// as their flag bits (0-7) in LightCall::FieldFlags and the matching fields
// in LightCall. LightCall::validate_() exploits this layout to iterate and
// copy them via bit-position arithmetic with a constant delta of 12 bytes
// between matching LightCall and LightColorValues members. color_temperature_
// has a different range and is placed after the clamp block.
float state_; ///< ON / OFF, float for transition
float brightness_;
float color_brightness_;
@@ -294,9 +300,9 @@ class LightColorValues {
float green_;
float blue_;
float white_;
float color_temperature_; ///< Color Temperature in Mired
float cold_white_;
float warm_white_;
float color_temperature_; ///< Color Temperature in Mired
ColorMode color_mode_;
};