Skip to content

Conversation

@DedeHai
Copy link
Collaborator

@DedeHai DedeHai commented Oct 26, 2025

  • added proper config parameters to allow multiple panels
  • added config checks
  • fixed crashes on S3
  • changed constant variables to constexpr
  • added "wled.h" to bus_manager.cpp and removed local function prototypes (needed for buffer allocations)
  • speed optimisations: yields about 10% higher FPS
  • proper brightness handling
  • updated platformio_override.sample.ini
  • some code cleanup

I do not own any quarter-scan HUB75 panels nor a suitable ESP32, so please test if this is working, I tested this on 2x half-scan 64x32 panels using an S3 only.

Summary by CodeRabbit

  • Bug Fixes

    • Improved HUB75 matrix display handling and pixel color accuracy.
    • Enhanced memory management for large display configurations.
  • New Features

    • Added validation for HUB75 panel configuration to prevent invalid setups.
    • Expanded HUB75 configuration UI with improved row/column tracking.
  • Documentation

    • Added brightness guidance notes for HUB75 displays to prevent ghosting artifacts.

- added proper config parameters to allow multiple panels
- added config checks
- fixed crashes on S3
- changed constant variables to constexpr
- added "wled.h" to bus_manager.cpp and removed local function prototypes (needed for buffer allocations)
- speed optimisations: yields about 10% higher FPS
- proper brightness handling
- updated platformio_override.sample.ini
- some code cleanup
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 26, 2025

Walkthrough

This pull request refactors the HUB75 matrix display handling with memory management improvements, bitwise operation optimizations, and virtual display support in the bus manager. API signature changes update pin counts, configuration validation is added to the HUB75 UI settings, and documentation adjustments are made to platformio configuration and color processing.

Changes

Cohort / File(s) Summary
Configuration & Documentation
platformio_override.sample.ini
Minor documentation and formatting adjustments to Hub75 environment blocks; added notes about maximum brightness, board file placement, and environment-specific URLs.
Core Bus Manager Implementation
wled00/bus_manager.cpp
Significant refactoring: replaced integer division/modulo with bitwise operations; introduced memory allocation wrappers (d_malloc/d_free); added virtual/physical HUB75 display handling with virtualDisp member, panel geometry aliases, and per-pixel dirty-bit tracking; updated setPixelColor/getPixelColor for virtual display paths; added IRAM_ATTR to setPixelColor; expanded getPins to return 5 values; reworked show() for virtual display height handling.
Core Bus Manager Headers
wled00/bus_manager.h
Updated getNumberOfPins() public static method: Hub75-type branch now returns 5 pins instead of 3; other bus types unchanged.
Color Processing
wled00/colors.cpp
Minor update to color_fade: replaced literal 0 comparison with named BLACK constant for edge case handling.
UI Settings & Validation
wled00/data/settings_leds.htm
Added HUB75 configuration validation (panel count, height restrictions); updated UI labels for panel size and row/column counts; increased HUB75-specific pin contribution from 2 to 4; extended conflict/availability logic to prevent multiple HUB75 buses and enforce defaults.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • bus_manager.cpp virtual display logic: Initialization, rendering, and memory management of VirtualMatrixPanel require careful validation of rotation settings, brightness application, and display height calculations.
  • Memory allocation patterns: Transition to d_malloc/d_free wrappers and cleanup paths—verify all allocation/deallocation pairs are correctly matched and handle edge cases.
  • getPins signature change: Return value changed from 3 to 5 for Hub75; verify all callers properly handle the expanded array and new return semantics.
  • Bitwise optimization accuracy: Integer division/modulo replacements (>> 3 for /8, & 0x07 for %8) should be audited for correctness across different input ranges.
  • HUB75 UI validation logic: Configuration constraints and defaults in settings_leds.htm need verification against actual panel layout and device memory limits.

Possibly related PRs

Suggested reviewers

  • netmindz
  • softhack007
  • willmmiles

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The PR title "Improvements & fixes for HUB75" is fully related to the changeset, which encompasses multiple HUB75-related enhancements across configuration, performance, crash fixes, UI updates, and general code cleanup. The title correctly identifies HUB75 as the primary focus and accurately conveys that the PR contains both improvements and bug fixes. While the title could be more specific about particular changes (such as multi-panel support or ESP32-S3 crash fixes), it is sufficiently clear and specific enough that a teammate scanning the history would immediately understand this is a comprehensive HUB75 enhancement effort.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
wled00/colors.cpp (1)

66-66: Consider using BLACK constant consistently.

For consistency, the return statement should also use BLACK instead of the literal 0:

-  if (c1 == BLACK || amount == 0) return 0; // black or full fade
+  if (c1 == BLACK || amount == 0) return BLACK; // black or full fade
wled00/data/settings_leds.htm (1)

115-128: Address TODO: conditional reboot warning.

The reboot warning currently shows for all S3 HUB75 form submissions. The TODO comment suggests checking if config actually changed before warning.

Consider tracking the initial HUB75 configuration and only showing the reboot alert when values actually change:

// At page load, store initial config
let initialHub75Config = {};

// In trySubmit, compare and warn only if changed
if(isS3() && configChanged(n, initialHub75Config)) {
  alert("HUB75 changes require a reboot");
}

This improves UX by avoiding unnecessary reboot warnings when users haven't modified HUB75 settings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b60313e and 5552c05.

📒 Files selected for processing (5)
  • platformio_override.sample.ini (3 hunks)
  • wled00/bus_manager.cpp (11 hunks)
  • wled00/bus_manager.h (2 hunks)
  • wled00/colors.cpp (1 hunks)
  • wled00/data/settings_leds.htm (5 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
wled00/**/*.cpp

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for C++ source files (.cpp)

Files:

  • wled00/colors.cpp
  • wled00/bus_manager.cpp
wled00/data/**/*.{htm,html,css,js}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use tabs for indentation in web files (.htm/.html/.css/.js) under wled00/data

Files:

  • wled00/data/settings_leds.htm
wled00/data/**

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

wled00/data/**: When modifying web UI files, run npm run build to regenerate embedded headers before any firmware build
For web UI changes, edit files only under wled00/data (not firmware or generated files)

Files:

  • wled00/data/settings_leds.htm
wled00/data/settings*.htm

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Name settings pages as settings*.htm within the web UI

Files:

  • wled00/data/settings_leds.htm
wled00/**/!(html_*)*.h

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use 2-space indentation for non-generated C++ header files (.h)

Files:

  • wled00/bus_manager.h
🧠 Learnings (2)
📚 Learning: 2025-09-13T13:13:35.817Z
Learnt from: netmindz
PR: wled/WLED#3777
File: wled00/bus_manager.cpp:941-947
Timestamp: 2025-09-13T13:13:35.817Z
Learning: For HUB75 matrix panels in WLED, pins are physically dedicated to the matrix hardware and don't need to be deallocated on driver initialization failure since they cannot be reused for other purposes without hardware changes.

Applied to files:

  • wled00/bus_manager.cpp
  • wled00/bus_manager.h
📚 Learning: 2025-09-13T13:13:36.092Z
Learnt from: netmindz
PR: wled/WLED#3777
File: wled00/bus_manager.cpp:982-989
Timestamp: 2025-09-13T13:13:36.092Z
Learning: In the WLED HUB75 implementation, when HUB75 matrix initialization fails after pin allocation, pins are intentionally not deallocated because they're physically connected to the HUB75 panel and cannot be reused for other purposes. This design choice prioritizes simplicity over strict software resource management consistency.

Applied to files:

  • wled00/bus_manager.cpp
  • wled00/data/settings_leds.htm
  • wled00/bus_manager.h
  • platformio_override.sample.ini
🧬 Code graph analysis (2)
wled00/colors.cpp (1)
usermods/Analog_Clock/Analog_Clock.cpp (2)
  • c1 (110-117)
  • c1 (110-110)
wled00/bus_manager.cpp (3)
wled00/fcn_declare.h (2)
  • d_free (446-446)
  • d_free (448-448)
wled00/util.cpp (6)
  • d_malloc (658-662)
  • d_malloc (658-658)
  • d_malloc (690-707)
  • d_malloc (690-690)
  • allocate_buffer (750-811)
  • allocate_buffer (750-750)
wled00/bus_manager.h (5)
  • show (248-254)
  • show (307-308)
  • show (334-335)
  • show (355-359)
  • show (381-389)
🔇 Additional comments (21)
platformio_override.sample.ini (5)

532-532: Added helpful ghosting guidance for HUB75 configurations.

This new comment documents a common HUB75 issue and provides a concrete solution (max brightness setting), which improves user experience and reduces support questions.


565-566: Added board file placement guidance with reference URL.

The comment about manual board file placement and the link to the official board definition is helpful for users encountering missing board file errors. This reduces friction during environment setup.


573-574: Improved USB configuration flag organization and clarity.

Separating ARDUINO_USB_CDC_ON_BOOT and ARDUINO_USB_MODE into distinct lines with explanatory comments makes the ESP32-S3 USB troubleshooting guidance clearer and easier to debug if users need to adjust these settings. The mention of possible boot issues is valuable context.


576-576: Clarified TX power reduction in LOLIN_WIFI_FIX comment.

The updated comment now explicitly documents that this flag sets lower TX power, which explains why it helps with WiFi stability on ESP32-S3 boards with active HUB75 panels. This is more informative than the previous wording.


602-602: Consistent TX power clarification in second HUB75 environment.

The same LOLIN_WIFI_FIX comment update applied here maintains consistency across both ESP32-S3 HUB75 configurations.

wled00/bus_manager.h (2)

398-406: LGTM - Well-structured additions for virtual display support.

The new member variables and constants are properly typed and support the expanded HUB75 functionality:

  • _rows and _cols enable multi-panel configurations
  • _ledBuffer type change from uint32_t* to CRGB* is more semantically appropriate for FastLED integration
  • Constants use proper constexpr with explicit typing

169-169: API change correctly implements expanded HUB75 pin configuration.

The change from 3 to 5 pins for HUB75 type is properly implemented and consistent throughout the codebase:

  • BusHub75Matrix::getPins() returns exactly 5 values (width, height, chain_length, rows, cols)
  • All callers treat the pin count as a dynamic value via loops—no hardcoded assumptions
  • OUTPUT_MAX_PINS is already defined as 5, supporting the expanded configuration
  • Call sites in cfg.cpp and xml.cpp scale correctly with the returned pin count
wled00/data/settings_leds.htm (4)

121-123: LGTM - Robust validation with sensible defaults.

The validation logic correctly enforces:

  1. Panel count must equal rows × cols
  2. Panels with height ≥ 64 are limited to single panel (memory constraint)

The ||1 defaults handle missing values gracefully.


263-268: LGTM - Clear and descriptive UI labels.

The updated labels correctly reflect the HUB75 configuration model with width, height, panel count, rows, and columns.


275-275: LGTM - Pin calculation aligns with API change.

The change from 2*isHub75(t) to 4*isHub75(t) correctly reflects the expanded HUB75 configuration from 3 to 5 "pins" (width, height, chain_length, rows, cols).


385-391: LGTM - Sensible defaults and constraints.

The field validation correctly:

  • Limits chain length, rows, and cols to 1-4 (matching C++ limits)
  • Defaults empty values to 1
  • Prevents invalid configurations
wled00/bus_manager.cpp (10)

26-26: LGTM - Explicit dependency inclusion.

Including "wled.h" makes buffer allocation dependencies (d_malloc, d_free, allocate_buffer) explicit and supports the memory management improvements in this PR.


34-54: LGTM - Micro-optimization using bit operations.

Replacing division and modulo with bit shifts is a classic optimization:

  • position / 8position >> 3
  • position % 8position & 0x07

While modern compilers typically optimize these automatically, the explicit bit operations make the intent clearer and may provide minor performance benefits on embedded targets.


762-768: LGTM - Clear initialization and readable aliases.

Explicitly initializing virtualDisp = nullptr prevents potential use-after-free issues, and the pin aliases (panelWidth, panelHeight, chainLength, rows, cols) improve code readability.


781-787: LGTM - Defensive memory constraints.

The chain length validation correctly:

  1. Clamps to maximum of 4 panels (prevents excessive memory allocation)
  2. Forces single panel for height ≥ 64 (prevents memory exhaustion)
  3. Provides clear debug output

This aligns with the UI validation in settings_leds.htm.


927-946: LGTM - Careful memory management with proper cleanup.

The memory allocation logic demonstrates good practices:

  • Defensive freeing of existing buffers (Lines 927-928)
  • Proper error handling with cleanup on allocation failure
  • Using allocate_buffer with appropriate flags (BFRALLOC_PREFER_DRAM | BFRALLOC_CLEAR)
  • Clear debug messages for troubleshooting

949-980: LGTM - Comprehensive virtual display initialization.

The virtual display setup correctly:

  1. Determines when virtualization is needed (multi-panel chains or quarter-scan)
  2. Creates VirtualMatrixPanel with proper geometry
  3. Configures scan rates for quarter-scan panels based on height
  4. Handles unsupported configurations with cleanup

998-998: LGTM - Appropriate IRAM placement for hot path.

Adding IRAM_ATTR to setPixelColor is appropriate as this is a frequently-called hot-path function. Placing it in IRAM reduces instruction cache misses and improves performance, especially during LED updates.


1045-1061: LGTM - Efficient show() with proper brightness handling.

The implementation correctly:

  1. Selects virtual or physical display based on configuration
  2. Applies brightness using FastLED's nscale8_video() (video scaling)
  3. Updates only dirty pixels for performance
  4. Clears dirty flags after display update

1063-1078: LGTM - Careful cleanup with S3 workaround.

The cleanup implementation demonstrates proper resource management:

  • Stops DMA output before cleanup
  • Deallocates pins
  • Works around ESP32-S3 DMA deletion crash (Lines 1070-1075)
  • Properly frees buffers using d_free

The S3 workaround is well-documented and aligns with the "requests additional testing" note in the PR objectives.

Based on learnings: "For HUB75 matrix panels in WLED, pins are physically dedicated to the matrix hardware and don't need to be deallocated on driver initialization failure."


1093-1102: LGTM - getPins correctly returns 5 configuration values.

The implementation returns the expanded HUB75 configuration:

  1. Width (mxconfig.mx_width)
  2. Height (mxconfig.mx_height)
  3. Chain length (mxconfig.chain_length)
  4. Rows (_rows)
  5. Columns (_cols)

This aligns with the API change in bus_manager.h (Line 169) and the UI implementation in settings_leds.htm.

Comment on lines 789 to 796
if (bc.type == TYPE_HUB75MATRIX_HS) {
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]);
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]);
// Disable chains of panels for now, incomplete UI changes
// if(bc.pins[2] > 1 && bc.pins[3] != 0 && bc.pins[4] != 0 && bc.pins[3] != 255 && bc.pins[4] != 255) {
// virtualDisp = new VirtualMatrixPanel((*display), bc.pins[3], bc.pins[4], mxconfig.mx_width, mxconfig.mx_height, CHAIN_BOTTOM_LEFT_UP);
// }
mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64
mxconfig.mx_height = min((uint8_t) 64, panelHeight);
} else if (bc.type == TYPE_HUB75MATRIX_QS) {
mxconfig.mx_width = min((uint8_t) 64, bc.pins[0]) * 2;
mxconfig.mx_height = min((uint8_t) 64, bc.pins[1]) / 2;
virtualDisp = new VirtualMatrixPanel((*display), 1, 1, bc.pins[0], bc.pins[1]);
virtualDisp->setRotation(0);
switch(bc.pins[1]) {
case 16:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_16PX_HIGH);
break;
case 32:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_32PX_HIGH);
break;
case 64:
virtualDisp->setPhysicalPanelScanRate(FOUR_SCAN_64PX_HIGH);
break;
default:
DEBUGBUS_PRINTLN("Unsupported height");
return;
}
_isVirtual = true;
mxconfig.mx_width = min((uint8_t) 64, panelWidth) * 2;
mxconfig.mx_height = min((uint8_t) 64, panelHeight) / 2;
} else {
Copy link
Contributor

@coderabbitai coderabbitai bot Oct 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Address dimension limit inconsistency.

The code limits panel dimensions to 64 pixels, but Line 790's TODO notes "UI limit is 128, this limits to 64". This creates a mismatch between what the UI allows and what the backend accepts.

Verify the correct maximum dimension for HUB75 panels. If 64 is the correct limit, update the UI validation in settings_leds.htm. If 128 is correct, adjust the code here.


🏁 Script executed:

#!/bin/bash
# Check UI field limits for HUB75 width/height
rg -n "LC.*max.*128" wled00/data/settings_leds.htm -A 2 -B 2

Length of output: 164



And check for any related constants or configuration:



🏁 Script executed:

#!/bin/bash
# Search for more context about HUB75 matrix dimensions and type handling
rg -n "TYPE_HUB75MATRIX" wled00/bus_manager.cpp -B 2 -A 5

Length of output: 1805


🏁 Script executed:

#!/bin/bash
# Look for HUB75-related constants or limits in the codebase
rg -n "HUB75|mx_width|mx_height" wled00/bus_manager.cpp -B 1 -A 1 | head -100

Length of output: 4927


Update the UI to enforce type-specific dimension limits for HUB75 panels.

The code correctly handles different limits for panel types:

  • Half-Scan (HS): limited to 64×64 via min((uint8_t) 64, panelWidth)
  • Quarter-Scan (QS): can reach 128×128 via min(64, panelWidth) * 2

However, the UI allows 128 for both types without differentiation. This means users can set invalid 128×128 dimensions for Half-Scan panels through the UI, which the code silently clamps to 64×64. Update settings_leds.htm to set LC.max = 64 when isHub75(t) AND the panel type is Half-Scan, or dynamically adjust based on selected panel type. Remove or resolve the TODO comment at line 790 once fixed.

🤖 Prompt for AI Agents
In wled00/bus_manager.cpp around lines 789-796 the code clamps HS panels to
64×64 but the UI still allows 128×128 causing silent clamping; update
settings_leds.htm to enforce type-specific limits: when isHub75(t) is true and
panel type is Half-Scan set LC.max = 64, when panel type is Quarter-Scan set
LC.max = 128 (or compute from selected type), add a change handler to
dynamically adjust LC.max when the panel type dropdown changes and
validate/submit to prevent values above the type limit, and remove or resolve
the TODO comment at the C++ line once the UI enforces the correct limit.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct, this is what the TODO sais.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DedeHai, understood! Since this is a known issue already documented in the TODO comment, would you like to:

  1. Address it in this PR - I can help update the UI validation logic in settings_leds.htm to enforce type-specific limits (64 for Half-Scan, 128 for Quarter-Scan)
  2. Defer to a follow-up - If this is out of scope for the current PR, I can create a tracking issue that references this PR and the TODO comment for future work

Which would you prefer?

@softhack007
Copy link
Member

softhack007 commented Nov 2, 2025

@DedeHai and @netmindz one thing you might want to try - currently chained hub75 panels are not compatible with the wled 2D layout settings. The reason is that HUB75 is scan line oriented, and adding a second panel means that the physical width (scanline) is increased, for example from width=64 (1panel) to width=128 (2 panels). This is not foreseen in the 2D setup of wled (at least it was not foreseen in the code from 6 months ago 😉 ).

The solution might be to tell the driver to use "vertical" panel layout, which means that chained panels are stacked in one column, instead of putting them into a row next to each other.

This "vertical layout" should make multi-panels (chained panels) compatible with what WLED expects: first pixel = top left, no serpentine, each panel having a separate (not interleaved) pixels range.

Copy link
Member

@softhack007 softhack007 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some comments on the core HUB75 driver parts.

  • not sure if removing display->setBrightness(_bri); is a good idea - what was the reason?
    -> OK, clarified.
  • consider to add user settings for mxconfig.driver options. panels with FM6124 might mis-behave with driver=default.
    -> possible enhancement, still open.
  • Think about removing the fallback code for "if (! ledBuffer)", to optimize speed in the critical path. Downside would be that the HUB75 driver will refuse to start if it cannot get its ledBuffer.
    -> OK, clarified. Keep the fallback for low RAM boards like HD-WF2.


mxconfig.double_buff = false; // Use our own memory-optimised buffer rather than the driver's own double-buffer

// mxconfig.driver = HUB75_I2S_CFG::ICN2038S; // experimental - use specific shift register driver
Copy link
Member

@softhack007 softhack007 Nov 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DedeHai, @netmindz actually the shift register driver should be user-configurable.
Especially panels with FM6124 will mis-behave with driver=default. In the MM code we made an assumption for simplicity, that "outdoor" panels are always 4-scan FM6124, while "indoor" is always 2-scan with "default" shift register driver -> https://github.com/MoonModules/WLED-MM/blob/d9e542663634812e9136b66bd1d53f187979d33a/wled00/bus_manager.cpp#L684

This assumption works in 90% of all cases, but for a proper solution the user should also have an option to select the shift register chip type.

NB: panel brightness control via display->setbrightness() works better with the correct shift register driver.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is the settings UI is a total pain in the ass and needs complete rewrite really. It's a similar issue with the virtual display params, we need the ability for the bus config and UI to allow for an enum type, there is an old issue/pr I started a while back for that, but even getting the basic changes to delegate the led type list to the bus type was slow progress for adoption

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@softhack007 logic done (including I2S bus frequency). @netmindz not PITA when you know what to do, however it is a hack really. 😉
See my fork (it even compiles now but I have no way of knowing if it works 😂 ).

// if(bc.pins[2] > 1 && bc.pins[3] != 0 && bc.pins[4] != 0 && bc.pins[3] != 255 && bc.pins[4] != 255) {
// virtualDisp = new VirtualMatrixPanel((*display), bc.pins[3], bc.pins[4], mxconfig.mx_width, mxconfig.mx_height, CHAIN_BOTTOM_LEFT_UP);
// }
mxconfig.mx_width = min((uint8_t) 64, panelWidth); // TODO: UI limit is 128, this limits to 64
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This toDo is a bit of a pity ... I have a 128x64 2-scan panel, and it can be used with width=128 height=64... but yes this was never supported in upstream, so ok to address 128x64 panels later in another PR.
Hint: many 128x64 can also be used as 64x64 chain_lenght=2.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is code from @netmindz, I simply added the TODO as I noticed the discrepancy and did not know the reason for this limit.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm also not sure any more why the limit was added ... maybe just to catch user errors, because 64x64 used to be the max supported panel size. I can confirm that 128x64 works here, too. So it might be safe to increase the "width" limit to 128.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was when i was battling issues with the settings management and so never wanted to use width 255, height 255 or chain length 255 as you were then in a memory based bootloop, so just adding some sanity

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This toDo is a bit of a pity ... I have a 128x64 2-scan panel, and it can be used with width=128 height=64... but yes this was never supported in upstream, so ok to address 128x64 panels later in another PR. Hint: many 128x64 can also be used as 64x64 chain_lenght=2.

I believe all 128s are really a pair of 64s. Not sure if was the width of 64 or the height of 64 that needed the extra pin in hub75e, so there might be similar reasons why 128s are built this way

@softhack007 softhack007 added good job optimization re-working an existing feature to be faster, or use less memory labels Nov 2, 2025

void BusHub75Matrix::cleanup() {
if (display && _valid) display->stopDMAoutput(); // terminate DMA driver (display goes black)
delay(30); // give some time to finish DMA
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(minor) it might be better for thread-safety to change the order of _valid=false and delay(30) so that _valid=false; comes before delay.

Copy link
Member

@softhack007 softhack007 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just a minor comment

@netmindz
Copy link
Member

netmindz commented Nov 7, 2025

Had a quick test with a basics single panel setup. UI improvements look good. Need to test multiple panels setups.
Need to double check but I think the brightness looked lower, I need to revert to compare

@DedeHai
Copy link
Collaborator Author

DedeHai commented Nov 8, 2025

I think the brightness looked lower, I need to revert to compare

brightness should not change, its still set to 255 at driver level. New gamma correction handling may play a role.

@blazoncek
Copy link
Collaborator

@netmindz @DedeHai & @softhack007 I've backported @netmindz's Hub75 to my fork (0.15) where I made some similar changes as @DedeHai made (no coordination) here. 😄
However, I've also enhanced UI to be aware of GPIO used by Hub75 driver and allowed for dropdown use in LED GPIO.
I do not have any hardware to test so my coding was pure guesswork and logic analysis of flow.

One thing I'd comment here is the removal of HW assisted brightness scaling in this PR. It is a waste of CPU cycles to do any scaling on bus level IMO. For NPB it makes sense as that was purely SW.

The other thing is getPixelColor(). Bus level gPC() is no longer needed since the introduction of my segment blending. Any and all getPixelColor() happen on segment buffers and is not affected by final or segment brightness/opacity. Realtime functions probe WS2812FX buffer instead of bus driver.
This means that drivers no longer need to maintain pixel data and any "double" buffering can be removed as it is done in WS2812FX class.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Nov 12, 2025

One thing I'd comment here is the removal of HW assisted brightness scaling in this PR. It is a waste of CPU cycles to do any scaling on bus level IMO. For NPB it makes sense as that was purely SW.

I did that to be consistent with NPB handling, as I just learned that HUB75 actually support HW-brightness. I need to check that but if it is true hardware scaling, we should definitely NOT do software scaling. so thanks for pointing this out.

The other thing is getPixelColor(). Bus level gPC() is no longer needed since the introduction of my segment blending.

IIRC there may be some usermods that need bus-level gPC(), at least thats what I remember from the top of my head, because I was close to removing them when I updated the ABL & brightness scaling code. Not sure this is actually true or just coming from some outdated, lingering inline comment.

@blazoncek
Copy link
Collaborator

IIRC there may be some usermods that need bus-level gPC(),

None should do that IMO. I would remove bus level gPC/sPC from public API.

There are benefits of having bus level ABL and strip (AKA WS2812FX) level ABL. I would keep both.

@softhack007
Copy link
Member

softhack007 commented Nov 13, 2025

. I need to check that but if it is true hardware scaling,

i think it is "kind of" hardware scaling. the hub75 driver will PWM-modulate the "enable" signal, instead of scaling individual pixels. But this feature depends on having the correct "shift register" driver in HUB75 mxconfig.

@softhack007
Copy link
Member

softhack007 commented Nov 13, 2025

This means that drivers no longer need to maintain pixel data and any "double" buffering can be removed as it is done in WS2812FX class.

Hi @blazoncek, hope you are doing well :-)

About your comments:

  • It might be that in general, busses.getPixelColor() could be removed if its no longer needed. I'm not going to interfere with any design decision on this, it has to be decided by the team. However I'd like to keep the scope of PRs clean, meaning that "remove gPC from bus level" should be a separate PR.
  • "drivers no longer need to maintain pixel data and any "double" buffering can be removed" -> this is partially true. One reason that HUB75 has its own double buffer was the need to support getPixelColor on bus level.
    • There is also a second purpose - buffering all pixel writes until "busses.show()" is necessary to avoid flickering. HUB75 works similar to a TV screen, and the content is continuously refreshed. So if we forward each "setPixelcolor" directly to the HUB75 hardware driver, it results in flickering and tearing. The HUB75 driver offers its own double-buffer solution for this, but it needs a lot more RAM for this buffer - so having a CRGB "FastLED" style buffer is actually removing flickering (all pixels pushed at once), and it reduces the RAM needed by a large amount (like 24Kb saved for one 64x64 panel).
    • An idea for upstream WLED - which I haven't tried yet - is to directly give the busHUB75 driver access to the main "frame buffer" at strip level. It means that hub75.show() can directly push all pixels from the upper level framebuffer (and yes, the "pixel pusher" must be at busHUB75 level !), without needing a buffer of its own. In this scenario, hub75.sPC() will only manage the "dirty bits" for optimization, but we could avoid the full double buffering by accessing the already existing one on upper level.
  • Finally - you should try a HUB75 panel, these things are really fun 😀 .
    I would suggest to start with a 64x64 "indoors" P2 or P3 module - make sure to look at brightness (nits) and contrast range (1:800 or more). These panels are quite affordable (~25€ on ali). Then get the "Moonhub" adapter and the LiliGo "T7-S3" with PSRAM, connect and have fun. I'm sure that @lost-hope would be happy to help you select a panel and get started :-)

Cheers, and take care,
Frank.

Edit: just for the fun of it - run the below .gif in image effect on HUB75, once you have the hardware
ghostbusters64

@DedeHai
Copy link
Collaborator Author

DedeHai commented Nov 14, 2025

  • There is also a second purpose - buffering all pixel writes until "busses.show()" is necessary to avoid flickering. HUB75 works similar to a TV screen, and the content is continuously refreshed. So if we forward each "setPixelcolor" directly to the HUB75 hardware driver, it results in flickering and tearing. The HUB75 driver offers its own double-buffer solution for this, but it needs a lot more RAM for this buffer - so having a CRGB "FastLED" style buffer is actually removing flickering (all pixels pushed at once), and it reduces the RAM needed by a large amount (like 24Kb saved for one 64x64 panel).

I did not check on how the HUB75 driver uses its internal buffer(s?), the additional CRGB buffer is needed for determining the "dirty" pixels, if we could access the HUB75 driver's internal buffer for that, we would not need it. I ran quite a few experiments when writing this PR, like removing the buffer and the "dirty" bit buffer and makeing the CRGB buffer a uint32_t buffer (slight seed increase) and even using the highest byte of that to mark "dirty" pixels but came back to the way it was i.e. seperate dirty pixel buffer (1 bit per pixel) and CRGB as it yielded highest throughput at lowest RAM cost. Fastest would be: seperate dirty pixel buffer with uint32_t colorbuffer. Best for RAM use is to grant access to the busmanager's _pixels[] buffer and not use dirty.

just FYI: here are my raw notes about those experiments (translated by chatGPT):

PS Fire and Black Hole speed test:
With CRGB buffer and dirty: 48 FPS (fire) and 92 FPS (black hole)
Without CRGB buffer and without dirty: 47 FPS and 64 FPS
With uint32_t buffer (non-PSRAM) and without dirty: fire 45 FPS, black hole 63 FPS, palette 45 FPS (huh?) → Probably the loop makes access to the panel much (much) faster.

With uint32_t buffer + dirty (and show(), i.e. inside the loop):

fire: 52 FPS
black hole: 108 FPS
palette: 47 FPS (this is without color_scale!)
with color_scale it’s about 1 FPS lower
DNA: 66 FPS

With dirty stored in the high byte instead of a separate array:

black hole: 58 FPS
DNA: 46 FPS
fire: 44 FPS
→ massively worse.

With dirty in the high byte (not a separate array) and IRAMATTR on SpC: about +1–2 FPS.

With 32-bit buffer, with dirty, with color scale:

fire: 47 FPS
black hole: 102 FPS
palette: 40 FPS
DNA: 89 FPS

With else-if in color fade instead of if, if: 47 FPS / 102 FPS / 88 FPS → no change.

What if you drop all the dirty-tracking stuff and SpC entirely, and just write directly to the array inside the loop?
For that the array must be accessible → you can do it with getPixelRaw.

→ Hack with direct access to _pixels[], including scaling:

black hole: 63 FPS
DNA: 48 FPS
fire: 46 FPS
palette: 45 FPS

Conclusion:
Dirty tracking helps. The fastest is with dirty + draw loop.
So basically exactly like now, just without CRGB.
CRGB uses less RAM, and it’s not that much slower… only a few FPS.
But for sparse scenes it does make a difference (92 FPS → 108 FPS).

Maybe there’s an even better variant, but for now it’s fine as it is.

@blazoncek
Copy link
Collaborator

@softhack007 you are missing my point. 😉

  • With current version of 0.16 there are no calls to BusManager::getPixelColor(), hence no calls to any of bus' getPixelColor(). This doesn't mean it has to be stripped right away (about 1k of code). It's the team's decision to do that, as you say. I'm merely pointing out the fact.
  • The same goes to updating actual driver's buffer. It is only updated in the WS2812FX::show() and nowhere else. Flickering will only occur if you call show() midframe while driver is still updating LEDs.
  • IMO giving driver access to frame buffer (WS2812FX::_pixels[]) may not work as desired as realtime functions write directly into that buffer and expect show() to do proper blending. And the buffer is not properly updated until blendSegment() is finished and all realtime calls are done.
  • As for the panel, I have started to clean my desk and I do not intend to spend additional money on something I would only use for development (but I would not refuse a gift). I'm purely enjoying logical challenges associated with tough coding choices. However my code is free for anyone to use as a basis for their own improvements. That's why I'm posting here.

The last statement includes improvements to LED settings page where Hub75's GPIO are taken into account (and all LED outputs get GPIO dropdowns) and ability to select shift register via dropdown. I will not make a PR for that due to lack of time, but you are free to cherry pick and adapt as needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Awaiting testing good job optimization re-working an existing feature to be faster, or use less memory

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants