Skip to content

Commit 39958c9

Browse files
committed
Merge branch 'stage'
2 parents 99c0af8 + 1c225b5 commit 39958c9

7 files changed

Lines changed: 109 additions & 17 deletions

File tree

components/cfx_effect/CFXRunner.cpp

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -646,13 +646,20 @@ static const uint32_t PaletteTwilight[16] CFX_PROGMEM = {
646646
0xFF00FF, 0xDD33FF, 0xBB66FF, 0x9999FF // Magenta -> Lighter
647647
};
648648

649-
// Palette 255: Solid Color - filled dynamically from segment.colors[0]
650-
static uint32_t PaletteSolid[16] = {0}; // Will be filled at runtime
649+
// Palette 255: Solid Color - filled dynamically from segment.colors[0].
650+
// Runners can be serviced on both ESP32 cores at the same time, so keep the
651+
// mutable solid palette per-core instead of sharing one write-heavy buffer.
652+
static uint32_t PaletteSolidPerCore[2][16] = {};
653+
654+
static uint32_t *activeSolidPalette() {
655+
return PaletteSolidPerCore[xPortGetCoreID()];
656+
}
651657

652658
// Fill PaletteSolid with current color
653659
static void fillSolidPalette(uint32_t color) {
660+
uint32_t *palette = activeSolidPalette();
654661
for (int i = 0; i < 16; i++) {
655-
PaletteSolid[i] = color;
662+
palette[i] = color;
656663
}
657664
}
658665

@@ -704,7 +711,7 @@ static const uint32_t *getPaletteByIndex(uint8_t palette_index) {
704711
case 20:
705712
return PaletteSunnyGold;
706713
case 21:
707-
return PaletteSolid;
714+
return activeSolidPalette();
708715
case 22:
709716
return PaletteFairy;
710717
case 23:
@@ -718,7 +725,7 @@ static const uint32_t *getPaletteByIndex(uint8_t palette_index) {
718725
case 255:
719726
// Solid color mode - caller must call fillSolidPalette first
720727
// 21 = selector position, 255 = internal constant
721-
return PaletteSolid;
728+
return activeSolidPalette();
722729
default:
723730
return PaletteRainbow; // Fallback to Rainbow (most generic)
724731
}
@@ -2234,7 +2241,7 @@ uint16_t mode_dissolve(void) {
22342241
if (!use_rainbow) {
22352242
if (instance->_segment.palette == 255) {
22362243
fillSolidPalette(instance->_segment.colors[0]);
2237-
active_palette = PaletteSolid;
2244+
active_palette = activeSolidPalette();
22382245
} else {
22392246
active_palette = getPaletteByIndex(instance->_segment.palette);
22402247
}
@@ -3229,7 +3236,7 @@ uint16_t mode_percent(void) {
32293236

32303237
const uint32_t *active_palette =
32313238
(instance->_segment.palette == 0)
3232-
? PaletteSolid // Default to Solid
3239+
? activeSolidPalette() // Default to Solid
32333240
: getPaletteByIndex(instance->_segment.palette);
32343241

32353242
// Behavior:
@@ -3238,7 +3245,7 @@ uint16_t mode_percent(void) {
32383245

32393246
if (instance->_segment.palette == 0 || instance->_segment.palette == 255) {
32403247
fillSolidPalette(instance->_segment.colors[0]);
3241-
active_palette = PaletteSolid;
3248+
active_palette = activeSolidPalette();
32423249
}
32433250

32443251
for (int i = 0; i < len; i++) {
@@ -3285,12 +3292,12 @@ uint16_t mode_percent_center(void) {
32853292

32863293
const uint32_t *active_palette =
32873294
(instance->_segment.palette == 0)
3288-
? PaletteSolid
3295+
? activeSolidPalette()
32893296
: getPaletteByIndex(instance->_segment.palette);
32903297

32913298
if (instance->_segment.palette == 0 || instance->_segment.palette == 255) {
32923299
fillSolidPalette(instance->_segment.colors[0]);
3293-
active_palette = PaletteSolid;
3300+
active_palette = activeSolidPalette();
32943301
}
32953302

32963303
for (int i = 0; i < len; i++) {
@@ -5811,7 +5818,7 @@ uint16_t mode_bouncing_balls(void) {
58115818
if (instance->_segment.palette == 255 || instance->_segment.palette == 0) {
58125819
// Default (0) or Explicit Solid (255) -> Use Primary Color
58135820
fillSolidPalette(instance->_segment.colors[0]);
5814-
active_palette = PaletteSolid;
5821+
active_palette = activeSolidPalette();
58155822
} else {
58165823
active_palette = getPaletteByIndex(instance->_segment.palette);
58175824
}
@@ -6245,7 +6252,7 @@ uint16_t color_wipe(bool rev, bool useRandomColors) {
62456252
if (useRandomColors) {
62466253
active_palette = PaletteRainbow;
62476254
} else if (instance->_segment.palette == 255) {
6248-
active_palette = PaletteSolid;
6255+
active_palette = activeSolidPalette();
62496256
} else {
62506257
active_palette = getPaletteByIndex(instance->_segment.palette);
62516258
}

components/cfx_effect/cfx_addressable_light_effect.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2645,6 +2645,12 @@ bool CFXAddressableLightEffect::try_batch_steady_virtual_segments_(
26452645
return false;
26462646
}
26472647
auto *parent = my_seg->get_parent();
2648+
if (parent->is_spi_transport()) {
2649+
// SPI parents already use the CFXLightOutput segment coordinator/coalesced
2650+
// flush path. The legacy steady batcher can be entered once per virtual
2651+
// segment apply and queue duplicate full-strip SPI frames.
2652+
return false;
2653+
}
26482654

26492655
CFXAddressableLightEffect *effects[MAX_CFX_SEGMENTS]{};
26502656
CFXRunner *runners[MAX_CFX_SEGMENTS]{};

components/cfx_effect/cfx_scheduler.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ bool CFXScheduler::service_runners(std::vector<CFXRunner *> &runners,
126126
if (this->last_batch_diag_ms_ == 0 ||
127127
(now_ms - this->last_batch_diag_ms_) >= 2000) {
128128
this->last_batch_diag_ms_ = now_ms;
129-
ESP_LOGD(TAG,
129+
ESP_LOGV(TAG,
130130
"CFX sched_batch total=%u mode=sequential force=%u global=%u "
131131
"core1=%u core0=0 cost1=0 cost0=0 dispatch_us=%u ok=1",
132132
static_cast<unsigned>(total),
@@ -226,7 +226,7 @@ bool CFXScheduler::service_runners(std::vector<CFXRunner *> &runners,
226226
if (this->last_batch_diag_ms_ == 0 ||
227227
(now_ms - this->last_batch_diag_ms_) >= 2000) {
228228
this->last_batch_diag_ms_ = now_ms;
229-
ESP_LOGD(TAG,
229+
ESP_LOGV(TAG,
230230
"CFX sched_batch total=%u mode=dual force=0 global=%u "
231231
"core1=%u core0=%u cost1=%u cost0=%u dispatch_us=%u "
232232
"ok=%u timeout_ms=%u",
@@ -259,7 +259,7 @@ bool CFXScheduler::service_runners(std::vector<CFXRunner *> &runners,
259259
if (this->last_batch_diag_ms_ == 0 ||
260260
(now_ms - this->last_batch_diag_ms_) >= 2000) {
261261
this->last_batch_diag_ms_ = now_ms;
262-
ESP_LOGD(TAG,
262+
ESP_LOGV(TAG,
263263
"CFX sched_batch total=%u mode=fallthrough force=%u global=%u "
264264
"core1=%u core0=0 cost1=0 cost0=0 dispatch_us=%u ok=1",
265265
static_cast<unsigned>(total),

components/cfx_light/cfx_light.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -664,6 +664,11 @@ void CFXLightOutput::release_outro_callback_storage_() {
664664
}
665665
}
666666

667+
void CFXLightOutput::add_outro_callback(OutroCallback cb) {
668+
this->outro_cbs_.push_back(cb);
669+
this->update_high_frequency_loop_request_();
670+
}
671+
667672
void CFXLightOutput::drain_outro_callbacks() {
668673
if (this->outro_cbs_.empty()) {
669674
return;
@@ -2473,6 +2478,7 @@ void CFXLightOutput::flush_parent_owned_segment_epoch_direct_(uint8_t mask,
24732478
// all subsequent socket() calls to fail. close() is available via
24742479
// lwip/sockets.h.
24752480
CFXLightOutput::~CFXLightOutput() {
2481+
this->high_freq_loop_requester_.stop();
24762482
if (this->socket_fd_ >= 0) {
24772483
::close(this->socket_fd_);
24782484
this->socket_fd_ = -1;
@@ -2502,6 +2508,7 @@ void CFXLightOutput::on_shutdown() {
25022508
if (this->transport_ == TRANSPORT_PARALLEL) {
25032509
this->force_parallel_shutdown_blackout_();
25042510
}
2511+
this->high_freq_loop_requester_.stop();
25052512
}
25062513

25072514
// --- Timing Configuration ---
@@ -5460,6 +5467,51 @@ static uint8_t active_parallel_group_mask_() {
54605467
return active_mask;
54615468
}
54625469

5470+
bool CFXLightOutput::should_request_high_frequency_loop_() {
5471+
if (!this->setup_completed_ || this->is_failed()) {
5472+
return false;
5473+
}
5474+
5475+
// Governor: request high-frequency loop only while transport/service work is
5476+
// pending. Active effects alone can monopolize the main loop on dense nodes.
5477+
if (this->has_outro() || this->rmt_flush_pending_ ||
5478+
this->seg_flush_pending_) {
5479+
return true;
5480+
}
5481+
5482+
// SPI and Classic RMT need active-effect cadence restoration under ESPHome
5483+
// 2026.5+. Parallel remains governed by pending group work to preserve the
5484+
// validated S3 behavior and avoid monopolizing dense parallel nodes.
5485+
if ((this->is_spi_transport() || this->is_rmt_transport()) &&
5486+
has_active_rendering_cfx_effect(this)) {
5487+
return true;
5488+
}
5489+
5490+
if (this->is_parallel_transport()) {
5491+
auto &group = *parallel_group_for_output_(this);
5492+
if (group.pending_mask != 0 || group.pending_first_ms != 0) {
5493+
return true;
5494+
}
5495+
#ifdef CFX_PARALLEL_I80_ENABLED
5496+
const uint8_t group_bit =
5497+
static_cast<uint8_t>(1u << group.group_index);
5498+
if ((g_parallel_i80.pending_group_mask & group_bit) != 0) {
5499+
return true;
5500+
}
5501+
#endif
5502+
}
5503+
5504+
return false;
5505+
}
5506+
5507+
void CFXLightOutput::update_high_frequency_loop_request_() {
5508+
if (this->should_request_high_frequency_loop_()) {
5509+
this->high_freq_loop_requester_.start();
5510+
} else {
5511+
this->high_freq_loop_requester_.stop();
5512+
}
5513+
}
5514+
54635515
static bool parallel_shared_whole_group_mode_enabled_() {
54645516
#if !defined(CONFIG_IDF_TARGET_ESP32) && defined(CFX_PARALLEL_I80_ENABLED)
54655517
if (!g_parallel_i80.ready || parallel_configured_group_count_() < 2) {
@@ -5537,6 +5589,7 @@ bool CFXLightOutput::request_parallel_group_flush_() {
55375589
active_parallel_lanes_mask_(g_parallel_group, lane_bit);
55385590

55395591
if ((g_parallel_group.pending_mask & active_lanes_mask) != active_lanes_mask) {
5592+
this->update_high_frequency_loop_request_();
55405593
return false;
55415594
}
55425595

@@ -5623,6 +5676,7 @@ bool CFXLightOutput::request_parallel_shared_group_flush_() {
56235676
}
56245677
this->flush_parallel_shared_groups_(flush_mask);
56255678
}
5679+
this->update_high_frequency_loop_request_();
56265680
return false;
56275681
#else
56285682
return true;
@@ -6034,6 +6088,7 @@ void CFXLightOutput::on_segment_update() {
60346088
// --- Component Loop (Intercepts Outro Playback) ---
60356089

60366090
void CFXLightOutput::loop() {
6091+
this->update_high_frequency_loop_request_();
60376092
this->record_parallel_completed_led_frames_();
60386093

60396094
if (runtime_debug_enabled_for_output(this)) {
@@ -6173,6 +6228,7 @@ void CFXLightOutput::loop() {
61736228
}
61746229

61756230
segment_flush_done:
6231+
this->update_high_frequency_loop_request_();
61766232
#ifdef USE_CFX_EVENTS
61776233
chimera_fx::CFXEventManager::get().flush_pending();
61786234
#endif
@@ -6403,6 +6459,10 @@ void CFXLightOutput::request_segment_flush(light::LightState *state) {
64036459
this->log_segment_coordinator_diag_();
64046460
return;
64056461
}
6462+
if (state != nullptr && this->segment_coordinator_owns(state)) {
6463+
this->note_segment_coord_write_skip();
6464+
return;
6465+
}
64066466

64076467
if (state != nullptr) {
64086468
for (size_t i = 0; i < this->segment_light_states_.size() && i < 8; i++) {
@@ -6425,6 +6485,7 @@ void CFXLightOutput::request_segment_flush(light::LightState *state) {
64256485
if (!this->seg_flush_pending_) {
64266486
this->seg_flush_pending_ = true;
64276487
this->seg_flush_first_ms_ = esphome::millis();
6488+
this->update_high_frequency_loop_request_();
64286489
}
64296490

64306491
const size_t segment_count = this->segment_light_states_.size();
@@ -6511,6 +6572,7 @@ void CFXLightOutput::write_state(light::LightState *state) {
65116572
const bool rmt_cadence_diag_enabled =
65126573
this->is_rmt_transport() && runtime_debug_enabled_for_output(this);
65136574
const uint32_t write_start_us = micros();
6575+
this->update_high_frequency_loop_request_();
65146576
this->perf_diag_last_flush_valid_ = false;
65156577
this->perf_diag_last_flush_total_us_ = 0;
65166578
this->perf_diag_last_flush_tx_us_ = 0;
@@ -6545,6 +6607,15 @@ void CFXLightOutput::write_state(light::LightState *state) {
65456607
if (state != nullptr && !this->outro_cbs_.empty()) {
65466608
return; // Block Master during outro on non-segmented lights
65476609
}
6610+
if (state == nullptr && this->is_spi_transport() &&
6611+
this->has_active_parent_owned_segments_() && !this->has_outro()) {
6612+
// Parent-coordinated SPI segments flush through
6613+
// flush_parent_owned_segment_epoch_direct_(). A generic nullptr write can
6614+
// still be queued by ESPHome/legacy segment paths in the same visual frame,
6615+
// causing a second identical SPI DMA frame and inflated LedFPS.
6616+
this->note_segment_coord_write_skip();
6617+
return;
6618+
}
65486619

65496620
// 1.2 Prevent Master paint from bleeding into OFF segments.
65506621
// Skip during outro — the outro renders into the OFF segment's range;
@@ -6774,6 +6845,7 @@ void CFXLightOutput::write_state(light::LightState *state) {
67746845
this->perf_diag_last_log_ms_ = now_ms;
67756846
}
67766847
}
6848+
this->update_high_frequency_loop_request_();
67776849
}
67786850

67796851
// --- RMT Transport Flush ---
@@ -6814,6 +6886,7 @@ void CFXLightOutput::flush_rmt_() {
68146886
this->rmt_flush_pending_ = true;
68156887
this->perf_diag_total_rmt_coalesced_flushes_++;
68166888
this->perf_diag_last_flush_valid_ = false;
6889+
this->update_high_frequency_loop_request_();
68176890
return;
68186891
}
68196892
}

components/cfx_light/cfx_light.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ class CFXLightOutput : public light::AddressableLight {
206206
int32_t size() const override { return this->num_leds_; }
207207
std::unique_ptr<light::LightTransformer> create_default_transition() override;
208208

209-
void add_outro_callback(OutroCallback cb) { this->outro_cbs_.push_back(cb); }
209+
void add_outro_callback(OutroCallback cb);
210210
void drain_outro_callbacks();
211211
bool has_outro() const { return !this->outro_cbs_.empty(); }
212212
void note_show_request();
@@ -498,6 +498,8 @@ class CFXLightOutput : public light::AddressableLight {
498498
bool include_reset);
499499
bool flush_parallel_shared_groups_(uint8_t group_mask);
500500
void bind_force_white_switch_();
501+
bool should_request_high_frequency_loop_();
502+
void update_high_frequency_loop_request_();
501503
void maybe_apply_turn_on_defaults_(light::LightState *state, bool &prev_on_state);
502504
void repaint_force_white_solid_(bool state);
503505
void release_outro_callback_storage_();
@@ -562,6 +564,7 @@ class CFXLightOutput : public light::AddressableLight {
562564
// Pixel data buffer (written by effects via ESPColorView)
563565
uint8_t *buf_{nullptr};
564566
bool setup_completed_{false};
567+
HighFrequencyLoopRequester high_freq_loop_requester_{};
565568

566569
// Callbacks used to execute Outro animations after ESPHome turns the light
567570
// off

docs/Troubleshooting.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ ESP-IDF doesn't always play well with RGB lights. `ChimeraFX` tries to set the b
6464
- **ESP32-S3**: 192 total symbols, 48-symbol blocks.
6565
- **ESP32-C3**: 96 total symbols, 48-symbol blocks.
6666

67+
On ESP32 Classic, automatic allocation is intentionally conservative and caps each RMT light at `128` symbols. This avoids the higher completion latency seen with larger automatic blocks on long strips. If you have a tested layout that benefits from larger buffers, set `rmt_symbols` manually: for example `512` for one RMT light, `256` each for two RMT lights, or `128` each for four RMT lights.
68+
6769
For platform-specific output limits, see [`cfx_light`](cfx_light.md#hardware-architecture--performance-limits).
6870

6971
Example configuration:

docs/cfx_light.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ light:
6262
* **is_wrgb** (*boolean*, default: `false`): Sets the white byte position to the front of the data packet. Required for some rare SK6812 variant clones.
6363
* **sacrificial_pixel** (*boolean*, default: `false`): RMT-only option. Transmits one extra black pixel before logical LED `0` to boost data signals on long wire runs.
6464
* **spi_speed** (*Frequency*): SPI clock speed for 2-wire strips.
65-
* **rmt_symbols** (*int*, default: `0`): Manual RMT symbol allocation. Leave at `0` for dynamic safe allocation.
65+
* **rmt_symbols** (*int*, default: `0`): Manual RMT symbol allocation. Leave at `0` for dynamic safe allocation. On ESP32 Classic, auto mode intentionally caps each RMT light at `128` symbols for the lowest-latency stable path; set this manually if a tested install should use more of the 512-symbol hardware pool.
6666
* **default_transition_length** (*Time*, default: `0s`): Standard ESPHome transition duration for solid-color mode and eligible effects.
6767
* **controls** (*boolean*, default: `true`): Automatically generate ChimeraFX control entities for this light.
6868
* **ctrl_exclude** (*list[int]*): Exclude specific auto-generated control groups by ID. See [Controls](Controls.md).
@@ -191,6 +191,7 @@ Rules of thumb:
191191
* **Stress Tests, Not Averages:** The numbers below represent the suggested maximum limits of the hardware. A device is intentionally pushed to find the maximum number of LEDs you can run while maintaining a *minimum* of ~30 FPS and keeping the device stable.
192192
* **Higher FPS:** These are not the best performances you can get! By simply reducing the number of LEDs to normal room-scale amounts, performance will scale up smoothly to **~60 FPS**.
193193
* **Benchmark Details:** Every test ran for at least 20 minutes using the `Energy` effect, chosen because it represents one of the heaviest mathematical loads in the library. This guarantees real-world stability for even the most demanding setups.
194+
* **Strip Type:** The matrix was measured with SK-class RGBW strips. Simpler 3-byte WS-class RGB strips can reduce heap pressure and may run roughly **10-15% faster** because each frame carries less pixel data.
194195
* **Segment Sizing:** In the tables below, segmented lights were tested using equal-sized segments (e.g., 4x200 or 8x175). Please note that **this is not a limitation**. You can customize your segments to any size; equal sizes were used purely to establish a consistent testing baseline.
195196
!!! note "Result labels"
196197
`PASS` means Heap WiFi >= 75kB.

0 commit comments

Comments
 (0)