[light] Replace soft-float range check with union bit-cast + unsigned compare

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
This commit is contained in:
J. Nick Koston
2026-04-13 16:16:44 -10:00
parent cdde0abec7
commit 36881166a8
+18 -3
View File
@@ -334,8 +334,16 @@ LightColorValues LightCall::validate_() {
// by bit position directly. Iterate only the set bits via __builtin_ctz +
// clear-lowest-bit: HA can drive high-frequency automations through
// perform(), so the hot path runs in O(popcount) instead of always
// scanning all eight slots. The range check is inlined here (cold path
// is the out-of-line helper) so an in-range value skips the call entirely.
// scanning all eight slots.
//
// The range check is done on the IEEE 754 bit pattern as an unsigned int,
// not on the float itself. Values in [0.0f, 1.0f] have bits in
// [0x00000000, 0x3F800000]; anything greater (as unsigned) is out of range:
// values > 1.0f have a larger bit pattern, and negative values have the
// sign bit (0x80000000) set which makes their unsigned interpretation
// enormous. One unsigned compare replaces two soft-float __ltsf2/__gtsf2
// calls on ESP8266 and is essentially free on targets with an FPU too.
constexpr uint32_t ONE_F_BITS = 0x3F800000u; // bit pattern of 1.0f
float *const src_fields = &this->brightness_;
float *const dst_fields = &v.brightness_;
unsigned active = this->flags_ & CLAMP_FLAGS_MASK;
@@ -343,7 +351,14 @@ LightColorValues LightCall::validate_() {
unsigned bit = __builtin_ctz(active);
active &= active - 1; // clear lowest set bit
float &value = src_fields[bit];
if (value < 0.0f || value > 1.0f)
// Union type-pun (GCC/Clang extension): bit_cast/memcpy don't optimize to
// a no-op on xtensa-gcc, same reasoning as api/proto.h float_to_raw().
union {
float f;
uint32_t u;
} pun;
pun.f = value;
if (pun.u > ONE_F_BITS)
log_out_of_range_and_clamp_(name, value, &FIELD_NAMES[bit], 0.0f, 1.0f);
dst_fields[bit] = value;
}