Replaces the previous `float *p = &this->brightness_; p[bit]` pattern
(flagged by Copilot as pointer-into-scalar UB) with a shared anonymous
union that exposes brightness_..warm_white_ as unit_fields_[8] in both
LightCall and LightColorValues. validate_() now indexes the real array
directly — defined behavior — while the named members stay accessible
to getters/setters. Code generation is unchanged.
The union is declared once via ESPHOME_LIGHT_UNIT_FIELDS_UNION() in
light_color_values.h and expanded in both structs. The per-field
offsetof static_asserts collapse to a single FieldFlags bit-layout
assert since the union guarantees member ↔ array-index alignment.
Caught by Copilot on PR review.
-0.0f has bit pattern 0x80000000 which exceeds ONE_F_BITS as unsigned,
so the check previously flagged it and emitted a spurious out-of-range
warning. Add an explicit compare against NEG_ZERO_F_BITS (declared as
a named constexpr for readability) so -0.0f takes the in-range fast
path. clamp_unit_float() already returned 0.0f for it via the sign-bit
branch, so behavior on clamp is unchanged.
Caught by Copilot on PR review.
The PROGMEM_STRING_TABLE expansion is entirely constexpr-initialized, so it doesn't require the per-static thread-safe-init guard variables that the previous `static const LogString *const FIELD_NAMES[8] PROGMEM = { LOG_STR(...) }` form emitted inside validate_() (LOG_STR contains a GCC statement-expression, which is not a constant expression, so GCC fell back to dynamic init with an 8-byte guard).
Combined the 8 clamp-field names and "Color temperature" into a single ValidateFieldNames table; index 8 is reserved for CT. log_value_out_of_range_() now takes the resolved `const LogString *` directly, so the helper no longer needs its internal progmem_read_ptr dereference.
Impact vs. prior commit (isolated light build):
ESP8266 .irom0.text: 267912 -> 267784 (-128 B)
ESP8266 .bss guard vars on light symbols: 16 -> 0 (-16 B RAM)
ESP32-IDF .flash.text: 136988 -> 136996 (+8 B, marginal)
Move `float_out_of_unit_range()` / add `clamp_unit_float()` to
light_color_values.h so the nine `set_*(float)` setters can use the
unsigned bit-pattern clamp instead of `std::clamp(x, 0.0f, 1.0f)`.
`std::clamp` expands to two soft-float `__ltsf2`/`__gtsf2` calls per
invocation on ESP8266 — replacing it with a single unsigned compare
saves code across every caller of these setters (StrobeLightEffect,
the 11-arg LightColorValues constructor, external components).
Split the cold-path helper: `log_value_out_of_range_()` now only logs,
and each caller applies the clamp strategy appropriate to its range
(`clamp_unit_float` for the 8-field loop, `std::clamp` for color
temperature's runtime-variable range).
Add layout/format assertions:
- std::is_standard_layout_v on LightCall and LightColorValues so the
offsetof arithmetic in the clamp loop is well-defined.
- sizeof(float) == 4 and is_iec559 so the bit-pattern trick is valid.
A direct __builtin_bit_cast check would be cleaner but is not
available on the ESP8266 xtensa toolchain.
Text-section delta vs. prior commit (isolated light build):
ESP32-IDF: .flash.text 137760 -> 136988 (-772 B)
ESP8266: .irom0.text 268440 -> 267912 (-528 B)
The inlined `value < 0.0f || value > 1.0f` check in the clamp loop costs
~50 B per iteration on ESP8266 (two libgcc soft-float calls with register
spills). IEEE 754 floats in [0.0f, 1.0f] have bit patterns in
[0x00000000, 0x3F800000]; anything out of range — values > 1.0f, negatives
(sign bit set → huge unsigned interpretation), NaN, Infinity — has a
strictly larger unsigned interpretation. A single `pun.u > 0x3F800000u`
covers every case.
Using a union for the type-pun rather than memcpy/bit_cast because those
don't optimize to a no-op on xtensa-gcc (same reason api/proto.h's
float_to_raw() uses a union).
The loop body is now two instructions for the range check:
l32i a2, a9, 0 ; load raw u32
bgeu a10, a2, ... ; compare against pre-hoisted 1.0f bits
Size delta vs. the prior float-compare form:
ESP32-IDF: validate_ -16 B, net -12 B
ESP8266: validate_ -20 B, net -20 B
vs. dev baseline (isolated light build, matched funcs):
ESP32-IDF: -103 B code, +32 B PROGMEM table = -71 B net
ESP8266: -42 B code, +32 B PROGMEM table = -10 B net
Follow-up to edb2145a addressing three points:
1. Hoist the in-range check out of the logging helper. The loop now tests
`value < 0.0f || value > 1.0f` inline and only calls the out-of-line
log_out_of_range_and_clamp_ helper on the cold path. Hot path (value in
range) skips the call8 and the register spill/reload around it, which
matters because HA automations can drive perform() at high frequency.
2. Iterate only set bits with __builtin_ctz + (active & active-1). Common
calls with one or two flags set now exit the loop after one or two
iterations instead of always scanning all eight slots.
3. Replace the uint8_t* pointer arithmetic with typed float arrays aliasing
&brightness_ in each struct. Per-field static_asserts (expanded via a
local macro) now catch reorders of any single member in either struct,
not just reorders at the endpoints. Compiles to the same machine code as
the uint8_t* version.
Size delta vs. prior commit (isolated light build):
ESP32-IDF: validate_ +60 B, helper -29 B, net +31 B
ESP8266: validate_ +72 B, helper -42 B, net +30 B
Still a net win vs. dev on both targets (ESP32-IDF -91 B, ESP8266 -22 B).
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.