Skip to content

Conversation

@DedeHai
Copy link

@DedeHai DedeHai commented Apr 18, 2025

  • port from AC with minor adaptations
  • also adding hardware random functions

Summary by CodeRabbit

  • New Features

    • Introduced a comprehensive particle system supporting both 2D and 1D LED animations with advanced physics and rendering.
    • Added over 30 new particle-based lighting effects including fire, volcano, fireworks, vortex, and more.
    • Expanded the total number of lighting modes to 228.
    • Integrated hardware-based true random number generation for improved randomness in effects.
  • Improvements

    • Enhanced visual complexity with particle collision physics, customizable size control, motion and smear blur effects, and flexible particle emission settings.

- port from AC
- also adding hardware random functions
@coderabbitai
Copy link

coderabbitai bot commented Apr 18, 2025

Walkthrough

This update introduces a comprehensive particle system framework for both 2D and 1D LED matrix effects, adding new classes, memory management, and rendering utilities. The effect mode enumeration is expanded to include a wide range of particle and particle system modes, increasing the total mode count from 198 to 228. A new hardware-based random number generator API is added, providing fast, platform-specific random number functions in various bit widths and ranges. The utility file implements bounded random number functions using the hardware RNG. All changes are declarative, focusing on new features and expanded capabilities for effect rendering and randomness.

Changes

File(s) Change Summary
wled00/FX.h Expanded effect mode enumeration: added 30 new particle/PS modes starting at ID 198 through 227, updated MODE_COUNT from 198 to 228.
wled00/FXparticleSystem.cpp
wled00/FXparticleSystem.h
Added new 2D and 1D particle system classes with extensive particle management, physics, collision detection, rendering, memory allocation, and utility functions for advanced LED matrix and strip effects.
wled00/fcn_declare.h Added hardware RNG API: platform-dependent RNG register macros, inline and declared functions for 8/16/32-bit random numbers (bounded and unbounded), and macro to replace Arduino random() with hardware RNG calls.
wled00/util.cpp Implemented bounded hardware random number functions: hw_random(uint32_t upperlimit) and hw_random(int32_t lowerlimit, int32_t upperlimit), scaling raw hardware RNG output to specified ranges.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ParticleSystem2D
    participant ParticleSystem1D
    participant HardwareRNG

    User->>ParticleSystem2D: Initialize & configure
    User->>ParticleSystem2D: Emit particles (spray/flame/angle)
    ParticleSystem2D->>ParticleSystem2D: Update particles (move, collide, render)
    ParticleSystem2D->>HardwareRNG: Request random values (for emission, physics)
    HardwareRNG-->>ParticleSystem2D: Return random numbers

    User->>ParticleSystem1D: Initialize & configure
    User->>ParticleSystem1D: Emit particles (spray)
    ParticleSystem1D->>ParticleSystem1D: Update particles (move, collide, render)
    ParticleSystem1D->>HardwareRNG: Request random values
    HardwareRNG-->>ParticleSystem1D: Return random numbers
Loading

Poem

A flurry of particles, swirling with light,
New modes abound—what a dazzling sight!
Hardware random, fast as a breeze,
Sprays and collisions, effects sure to please.
With 2D and 1D, the matrix ignites—
Bouncing and blurring through magical nights.
🐇✨ Let’s hop into code, where the rainbow delights!

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 9

♻️ Duplicate comments (1)
wled00/FXparticleSystem.h (1)

82-96: Particle flag unions repeat the same pitfall

The same packing caveat applies to PSparticleFlags, PSparticleFlags1D.
Consider the same remedy or at least a compile‑time size assertion.

🧹 Nitpick comments (5)
wled00/util.cpp (2)

657-663: Division‑by‑zero / degenerate upper‑limit

hw_random(uint32_t upperlimit) returns 0 when upperlimit == 0.
While harmless, silently returning a constant defeats the caller’s entropy expectation.
A defensive early‑exit with return 0; (or even an assert(upperlimit) in debug builds) would make the behaviour explicit.


664-670: Inclusive vs exclusive upper bound – please document

hw_random(lower, upper) returns values in [lower, upper) (exclusive upper bound).
Several Arduino utilities (e.g. random()) follow the same rule, but WLED helpers sometimes use inclusive upper bounds.
Adding a short doxygen note will save developers from off‑by‑one errors.

wled00/FXparticleSystem.h (2)

221-224: blur2D/blur1D signatures expose large uint32_t params – risk of accidental overflow

xblur, yblur, blur are compared with pixel counts (uint16_t).
Passing a too‑large value because of a mismatched type can allocate huge stack buffers.
Recommend narrowing to uint16_t or clamping inside the function.


242-249: Magic constants deserve a comment

PS_P_MINHARDRADIUS_1D 32, PS_P_MINSURFACEHARDNESS_1D 120 – the header notes “do not change or hourglass effect will be broken” but not why.

Add a short rationale (collision bin size? fixed‑point maths?) so future maintainers don’t “clean up” the constants.

wled00/FXparticleSystem.cpp (1)

1844-1853: Modulo on negative numbers is implementation‑defined

checkBoundsAndWrap() uses position % (max + 1) while position can be negative.
For signed operands the % result is implementation‑defined (§8.3.9/2 C++20), so behaviour may change between compilers/optimisations.

A portable substitute:

auto wrap = [&](int32_t pos) {
  int32_t m = max + 1;
  pos %= m;
  return pos < 0 ? pos + m : pos;
};
position = wrap(position);

Not urgent for the currently supported GCC/XTensa toolchains, but worth hardening for future portability.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7cb8eeb and 66e2e4d.

📒 Files selected for processing (5)
  • wled00/FX.h (1 hunks)
  • wled00/FXparticleSystem.cpp (1 hunks)
  • wled00/FXparticleSystem.h (1 hunks)
  • wled00/fcn_declare.h (1 hunks)
  • wled00/util.cpp (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
wled00/util.cpp (1)
wled00/fcn_declare.h (4)
  • hw_random (414-414)
  • hw_random (414-414)
  • hw_random (415-415)
  • hw_random (416-416)
wled00/FXparticleSystem.cpp (3)
wled00/FXparticleSystem.h (31)
  • force (153-153)
  • force (173-173)
  • force (337-337)
  • ParticleSystem2D (136-136)
  • isadvanced (198-198)
  • isadvanced (362-362)
  • size (172-172)
  • size (336-336)
  • emitter (143-143)
  • emitter (144-144)
  • emitter (145-145)
  • emitter (317-317)
  • part (141-141)
  • part (147-147)
  • part (148-148)
  • part (151-151)
  • part (154-154)
  • part (318-318)
  • part (320-320)
  • part (322-322)
  • limitSpeed (34-36)
  • limitSpeed (34-34)
  • blur2D (222-222)
  • calculateNumberOfParticles2D (225-225)
  • calculateNumberOfSources2D (226-226)
  • allocateParticleSystemMemory2D (227-227)
  • ParticleSystem1D (312-312)
  • blur1D (387-387)
  • calculateNumberOfParticles1D (384-384)
  • calculateNumberOfSources1D (385-385)
  • allocateParticleSystemMemory1D (386-386)
wled00/FX.cpp (2)
  • wrap (5401-5417)
  • wrap (5401-5401)
wled00/fcn_declare.h (13)
  • hw_random16 (417-417)
  • hw_random16 (417-417)
  • hw_random16 (418-418)
  • hw_random16 (418-418)
  • hw_random16 (419-419)
  • hw_random16 (419-419)
  • cos16_t (453-453)
  • sin16_t (452-452)
  • r (434-434)
  • r (434-434)
  • b (312-312)
  • b (312-312)
  • b (343-343)
🪛 Cppcheck (2.10-2)
wled00/FXparticleSystem.cpp

[warning] 445-445: Uninitialized variable

(uninitvar)

🔇 Additional comments (3)
wled00/fcn_declare.h (1)

401-407: Nice addition – clear documentation & sensible defaults

The introductory comment block precisely documents timing, entropy caveats and inlining trade‑offs.
Helps future maintainers a lot – thanks!

wled00/FX.h (1)

370-371: MODE_COUNT is consistent – good catch

Highest ID is 227, MODE_COUNT set to 228 → off‑by‑one avoided.

wled00/FXparticleSystem.cpp (1)

441-446: Potential race on forcecounter inside loop

tempcounter = forcecounter; ... forcecounter = tempcounter; means that for usedParticles > 1 the last particle processed dictates the new global counter.
If you intended each particle to keep its own delay counter you’ll need a per‑particle array (similar to advPartProps[i].forcecounter) or at least increment forcecounter outside the loop once.

Please confirm the desired behaviour; otherwise subtle timing differences occur when usedParticles changes.

🧰 Tools
🪛 Cppcheck (2.10-2)

[warning] 445-445: Uninitialized variable

(uninitvar)

Comment on lines +407 to +415
#ifdef ESP8266
#define HW_RND_REGISTER RANDOM_REG32
#else // ESP32 family
#include "soc/wdev_reg.h"
#define HW_RND_REGISTER REG_READ(WDEV_RND_REG)
#endif
#define random hw_random // replace arduino random()
inline uint32_t hw_random() { return HW_RND_REGISTER; };
uint32_t hw_random(uint32_t upperlimit); // not inlined for code size
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Risk of globally replacing random() with a macro

#define random hw_random silently overrides every free‑standing call to random() across the whole code‑base (and third‑party libraries that might be compiled together with WLED).
If any TU

  • calls Arduino’s randomSeed() but expects Arduino’s random() implementation
  • has a namespace‑qualified random overload
  • contains a class method called random()

the pre‑processor substitution will break compilation or change semantics in hard‑to‑trace ways.

Consider limiting the override to the files that actually need it, e.g.

#ifdef WLED_REPLACE_RANDOM
  #define random hw_random
#endif

or wrap it in #ifndef ARDUINO_ARCH_AVR#endif if only ESP chips are affected.

Comment on lines +414 to +423
inline uint32_t hw_random() { return HW_RND_REGISTER; };
uint32_t hw_random(uint32_t upperlimit); // not inlined for code size
int32_t hw_random(int32_t lowerlimit, int32_t upperlimit);
inline uint16_t hw_random16() { return HW_RND_REGISTER; };
inline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t)
inline int16_t hw_random16(int32_t lowerlimit, int32_t upperlimit) { int32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random16(range); }; // signed limits, use int16_t ranges
inline uint8_t hw_random8() { return HW_RND_REGISTER; };
inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255
inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Out‑of‑range handling for 8‑/16‑bit helpers

hw_random8(lower, upper) and hw_random16(lower, upper) assume lower < upper.
If the caller accidentally swaps the limits, upper - lower underflows (unsigned!) and the helper returns almost the full 0‑255/0‑65535 range – a silent logic bug.

A minimal guard costs two instructions and avoids hours of debugging:

-inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); };
+inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) {
+  if (upperlimit <= lowerlimit) return lowerlimit;
+  uint32_t range = upperlimit - lowerlimit;
+  return lowerlimit + hw_random8(range);
+}

(Analogous change for the 16‑bit overload.)

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inline uint32_t hw_random() { return HW_RND_REGISTER; };
uint32_t hw_random(uint32_t upperlimit); // not inlined for code size
int32_t hw_random(int32_t lowerlimit, int32_t upperlimit);
inline uint16_t hw_random16() { return HW_RND_REGISTER; };
inline uint16_t hw_random16(uint32_t upperlimit) { return (hw_random16() * upperlimit) >> 16; }; // input range 0-65535 (uint16_t)
inline int16_t hw_random16(int32_t lowerlimit, int32_t upperlimit) { int32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random16(range); }; // signed limits, use int16_t ranges
inline uint8_t hw_random8() { return HW_RND_REGISTER; };
inline uint8_t hw_random8(uint32_t upperlimit) { return (hw_random8() * upperlimit) >> 8; }; // input range 0-255
inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) { uint32_t range = upperlimit - lowerlimit; return lowerlimit + hw_random8(range); }; // input range 0-255
inline uint8_t hw_random8(uint32_t lowerlimit, uint32_t upperlimit) {
if (upperlimit <= lowerlimit) return lowerlimit;
uint32_t range = upperlimit - lowerlimit;
return lowerlimit + hw_random8(range);
}

Comment on lines +57 to +70
typedef union {
struct{ // one byte bit field for 2D settings
bool wrapX : 1;
bool wrapY : 1;
bool bounceX : 1;
bool bounceY : 1;
bool killoutofbounds : 1; // if set, out of bound particles are killed immediately
bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top
bool useCollisions : 1;
bool colorByAge : 1; // if set, particle hue is set by ttl value in render function
};
byte asByte; // access as a byte, order is: LSB is first entry in the list above
} PSsettings2D;

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Bit‑field layout may differ between ESP8266 & ESP32

PSsettings2D and PSsettings1D rely on an 8‑bit bool bit‑field inside a union.
ABI differences (alignment, endian packing) can yield:

  • sizeof(PSsettings2D) == 2 instead of 1 on one platform
  • unexpected ordering of bits (wrapX≠LSB)

When the struct is transmitted over the wire or compared as asByte, bugs appear.

Safer alternatives:

using PSsettings2D = uint8_t;
constexpr uint8_t PS_WRAP_X = 0x01;
constexpr uint8_t PS_WRAP_Y = 0x02;
// ...

or std::bitset<8>.

At minimum, add static_assert(sizeof(PSsettings2D) == 1, "Unexpected packing");.

Comment on lines +135 to +143
public:
ParticleSystem2D(const uint32_t width, const uint32_t height, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false, const bool sizecontrol = false); // constructor
// note: memory is allcated in the FX function, no deconstructor needed
void update(void); //update the particles according to set options and render to the matrix
void updateFire(const uint8_t intensity, const bool renderonly); // update function for fire, if renderonly is set, particles are not updated (required to fix transitions with frameskips)
void updateSystem(void); // call at the beginning of every FX, updates pointers and dimensions
void particleMoveUpdate(PSparticle &part, PSparticleFlags &partFlags, PSsettings2D *options = NULL, PSadvancedParticle *advancedproperties = NULL); // move function
// particle emitters
int32_t sprayEmit(const PSsource &emitter);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Constructor parameter mismatch guard

ParticleSystem2D(width, height, numParticles, numSources …) takes raw counts, but upstream macros (ESP32_MAXPARTICLES, …) cap them.
If the caller passes a bigger value the constructor silently proceeds, later allocators may overflow.

Add:

if (numberofparticles > MAX_ALLOWED_PARTICLES)
  numberofparticles = MAX_ALLOWED_PARTICLES;

(or an assert) to fail fast.

Comment on lines +788 to +792
uint16_t binIndices[maxBinParticles]; // creat array on stack for indices, 2kB max for 1024 particles (ESP32_MAXPARTICLES/2)
uint32_t binParticleCount; // number of particles in the current bin
uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)
uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame

Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Large automatic array may overflow the tiny MCU stack

uint16_t binIndices[maxBinParticles]; is allocated on the stack.
With the default limits (usedParticles/2 for 2‑D, /4 for 1‑D) this can be several kilobytes (e.g. 2 kB for 1024 particles).
On ESP8266 the default task stack is only 4 kB, leaving little head‑room and causing random resets.

Consider:

  • allocating the array once in PSdataEnd memory, or
  • capping maxBinParticles to a conservative constant (e.g. 256), or
  • switching to a small StaticVector/ring‑buffer allocated in IRAM_ATTR.

Comment on lines +38 to +44
setParticleSize(1); // 2x2 rendering size by default
motionBlur = 0; //no fading by default
smearBlur = 0; //no smearing by default
emitIndex = 0;
collisionStartIdx = 0;

//initialize some default non-zero values most FX use
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Several internal counters start uninitialised

Members such as forcecounter, gforcecounter (and their 1‑D counterparts) are later read and incremented inside applyForce / applyGravity, but they are never initialised in the constructor, resulting in undefined behaviour.

Please add explicit zero‑initialisation in both constructors, e.g.:

   smearBlur = 0; //no smearing by default
   emitIndex = 0;
   collisionStartIdx = 0;
+  forcecounter   = 0;
+  gforcecounter  = 0;

Do the same for the 1‑D class.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
setParticleSize(1); // 2x2 rendering size by default
motionBlur = 0; //no fading by default
smearBlur = 0; //no smearing by default
emitIndex = 0;
collisionStartIdx = 0;
//initialize some default non-zero values most FX use
setParticleSize(1); // 2x2 rendering size by default
motionBlur = 0; // no fading by default
smearBlur = 0; // no smearing by default
emitIndex = 0;
collisionStartIdx = 0;
forcecounter = 0;
gforcecounter = 0;
// initialize some default non-zero values most FX use

- removed local buffer for ESP8266 in 1D system to save on RAM
- increased particle brightness in PS Impact
- minor tweak in collision binning (might improve speed)
- removed comments and some other unused stuff
- fixed a few compiler wranings
Copy link

@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: 3

♻️ Duplicate comments (6)
wled00/FXparticleSystem.h (2)

57-69: Bit-field layout may differ between ESP8266 & ESP32

PSsettings2D relies on an 8-bit bool bit-field inside a union. ABI differences (alignment, endian packing) can cause unexpected behavior when accessing as asByte.

At minimum, add a static assertion to verify structure size:

  byte asByte; // access as a byte, order is: LSB is first entry in the list above
} PSsettings2D;
+static_assert(sizeof(PSsettings2D) == 1, "PSsettings2D size mismatch");

135-143: Constructor parameter mismatch guard needed for safety

ParticleSystem2D(width, height, numParticles, numSources …) takes raw counts, but those values should be capped by platform-specific macros to prevent memory issues.

In the constructor implementation, add:

if (numberofparticles > ESP32_MAXPARTICLES)
  numberofparticles = ESP32_MAXPARTICLES;
if (numberofsources > ESP32_MAXSOURCES)
  numberofsources = ESP32_MAXSOURCES;
wled00/FXparticleSystem.cpp (4)

25-38: Constructor initializes PS pointers before knowing matrix size

In the ParticleSystem2D constructor, pointers are calculated by updatePSpointers() before setMatrixSize() sets maxXpixel and maxYpixel. This causes incorrect memory layout and potential buffer overflows.

Fix the ordering issue in both 2D and 1D constructors:

  numParticles = numberofparticles; // number of particles allocated in init
  usedParticles = numParticles; // use all particles by default
  advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
  advPartSize = nullptr;
- updatePSpointers(isadvanced, sizecontrol); // set the particle and sources pointer
- setMatrixSize(width, height);
+ setMatrixSize(width, height);              // make size‑dependent members valid first
+ updatePSpointers(isadvanced, sizecontrol); // now compute the correct pointer layout

38-44: Several internal counters start uninitialised

Members like forcecounter and gforcecounter are used in methods like applyForce and applyGravity but are never initialized in the constructor.

Add zero-initialization in both constructors:

  motionBlur = 0; //no fading by default
  smearBlur = 0; //no smearing by default
  emitIndex = 0;
  collisionStartIdx = 0;
+ forcecounter = 0;
+ gforcecounter = 0;

787-790: Large automatic array may overflow the MCU stack

uint16_t binIndices[maxBinParticles] is allocated on the stack, which can be several kilobytes. On ESP8266 with a 4KB task stack, this could cause random resets.

Consider one of these approaches:

  1. Allocate the array in PSdataEnd memory
  2. Cap maxBinParticles to a conservative constant (e.g., 256)
  3. Use a static buffer to avoid stack allocation

1137-1148: Same constructor ordering issue in 1D particle system

The ParticleSystem1D constructor has the same pointer initialization issue as the 2D version.

Apply the same ordering fix:

  numParticles = numberofparticles; // number of particles allocated in init
  usedParticles = numParticles; // use all particles by default
  advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
  //advPartSize = nullptr;
- updatePSpointers(isadvanced); // set the particle and sources pointer
- setSize(length);
+ setSize(length);              // make size‑dependent members valid first
+ updatePSpointers(isadvanced); // pointers now calculated with correct size
🧹 Nitpick comments (5)
wled00/FXparticleSystem.h (3)

1-9: Copyright year might be incorrect

The copyright year is set to 2025, which is either a year ahead or might indicate a long-term copyright. Please verify if this is intentional.

-  Copyright (c) 2025  Damian Schneider
+  Copyright (c) 2024  Damian Schneider

279-284: Remove unused flag declared as TODO

The forcedirection flag is marked with a TODO comment indicating it's not used anymore.

    bool outofbounds : 1; // out of bounds flag, set to true if particle is outside of display area
    bool collide : 1; // if set, particle takes part in collisions
    bool perpetual : 1; // if set, particle does not age (TTL is not decremented in move function, it still dies from killoutofbounds)
    bool reversegrav : 1; // if set, gravity is reversed on this particle
-   bool forcedirection : 1; // direction the force was applied, 1 is positive x-direction (used for collision stacking, similar to reversegrav) TODO: not used anymore, can be removed
+   bool reserved : 1; // reserved for future use
    bool fixed : 1; // if set, particle does not move (and collisions make other particles revert direction),
    bool custom1 : 1; // unused custom flags, can be used by FX to track particle states
    bool custom2 : 1;

50-55: Consider using static_cast for shifts to improve clarity

Magic numbers in shift operations can be confusing. Consider using static_cast or explicit comments.

-#define PS_P_RADIUS_SHIFT 6 // shift for RADIUS
+#define PS_P_RADIUS_SHIFT static_cast<int>(log2(PS_P_RADIUS)) // shift for RADIUS, equals 6
wled00/FXparticleSystem.cpp (2)

1822-1842: Missing optimizer attribute on blur1D function

The blur1D function could benefit from the same optimization attribute used on other performance-critical functions.

-void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start)
+__attribute__((optimize("O2"))) void blur1D(CRGB *colorbuffer, uint32_t size, uint32_t blur, uint32_t start)

7-8: Copyright year might be incorrect (same issue as in header)

The copyright year is set to 2025, which is likely incorrect.

-  Copyright (c) 2025  Damian Schneider
+  Copyright (c) 2024  Damian Schneider
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66e2e4d and 56d7d84.

📒 Files selected for processing (2)
  • wled00/FXparticleSystem.cpp (1 hunks)
  • wled00/FXparticleSystem.h (1 hunks)
🧰 Additional context used
🪛 Cppcheck (2.10-2)
wled00/FXparticleSystem.cpp

[warning] 445-445: Uninitialized variable

(uninitvar)

🔇 Additional comments (1)
wled00/FXparticleSystem.cpp (1)

183-204:

Details

✅ Verification successful

hw_random16 calls without checking for availability

The code calls hw_random16() which is likely a hardware random function, but there's no guard to ensure this function exists or falls back to a software implementation if hardware RNG is unavailable.

Check if there's a fallback mechanism defined elsewhere in the code or consider adding one:


🏁 Script executed:

#!/bin/bash
# Check for declarations of hw_random16 and any fallback mechanisms
grep -r "hw_random16" --include="*.h" --include="*.cpp" .

Length of output: 12399


No additional fallback needed for hw_random16 – hardware register is wrapped with inline definitions

I’ve confirmed that hw_random16 is defined in wled00/fcn_declare.h as three inline overloads (no‑arg, upper‑limit, and lower‑/upper‑limit) all ultimately reading from HW_RND_REGISTER. The comment in wled00/util.cpp refers to a 32‑bit hardware RNG implementation (used by other APIs) and does not provide a software fallback for hw_random16. Since all calls to hw_random16 already resolve to the inline functions and will fail to compile if HW_RND_REGISTER isn’t defined for your target, no further guard or fallback exists at present.

If you intend to support platforms without a hardware RNG, you’ll need to add a software fallback in one place—ideally in fcn_declare.h or a new header—by #ifdef’ing on a target‑specific macro (e.g. #ifdef HAS_HW_RNG) and providing a PRNG for the else case. Otherwise, the existing inline definitions suffice.

– No changes required unless software‐RNG fallback is desired.

Comment on lines +312 to +313
ParticleSystem1D(const uint32_t length, const uint32_t numberofparticles, const uint32_t numberofsources, const bool isadvanced = false); // constructor
// note: memory is allcated in the FX function, no deconstructor needed
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Apply same constructor parameter safety checks for 1D system

Similar to the 2D constructor, add parameter validation to the ParticleSystem1D constructor.

In the constructor implementation, add:

if (numberofparticles > ESP32_MAXPARTICLES_1D)
  numberofparticles = ESP32_MAXPARTICLES_1D;
if (numberofsources > ESP32_MAXSOURCES_1D)
  numberofsources = ESP32_MAXSOURCES_1D;

Comment on lines +251 to +264
typedef union {
struct{
// one byte bit field for 1D settings
bool wrap : 1;
bool bounce : 1;
bool killoutofbounds : 1; // if set, out of bound particles are killed immediately
bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top
bool useCollisions : 1;
bool colorByAge : 1; // if set, particle hue is set by ttl value in render function
bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment
bool unused : 1;
};
byte asByte; // access as a byte, order is: LSB is first entry in the list above
} PSsettings1D;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Same bit-field layout issue affects PSsettings1D

Like PSsettings2D, the PSsettings1D union has the same potential for inconsistent bit layout across platforms.

Add a static assertion and consider using explicit bit flags:

  byte asByte; // access as a byte, order is: LSB is first entry in the list above
} PSsettings1D;
+static_assert(sizeof(PSsettings1D) == 1, "PSsettings1D size mismatch");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
typedef union {
struct{
// one byte bit field for 1D settings
bool wrap : 1;
bool bounce : 1;
bool killoutofbounds : 1; // if set, out of bound particles are killed immediately
bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top
bool useCollisions : 1;
bool colorByAge : 1; // if set, particle hue is set by ttl value in render function
bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment
bool unused : 1;
};
byte asByte; // access as a byte, order is: LSB is first entry in the list above
} PSsettings1D;
typedef union {
struct{
// one byte bit field for 1D settings
bool wrap : 1;
bool bounce : 1;
bool killoutofbounds : 1; // if set, out of bound particles are killed immediately
bool useGravity : 1; // set to 1 if gravity is used, disables bounceY at the top
bool useCollisions : 1;
bool colorByAge : 1; // if set, particle hue is set by ttl value in render function
bool colorByPosition : 1; // if set, particle hue is set by its position in the strip segment
bool unused : 1;
};
byte asByte; // access as a byte, order is: LSB is first entry in the list above
} PSsettings1D;
static_assert(sizeof(PSsettings1D) == 1, "PSsettings1D size mismatch");

Comment on lines +444 to +447
}
forcecounter = tempcounter; // save value back
}

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Uninitialized variable warning on tempcounter

The variable tempcounter might be uninitialized if the loop is never entered (when usedParticles is 0), but is later assigned to forcecounter.

Initialize the variable before the loop:

- uint8_t tempcounter;
+ uint8_t tempcounter = forcecounter;
  // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough
  for (uint32_t i = 0; i < usedParticles; i++) {
    tempcounter = forcecounter;
    applyForce(particles[i], xforce, yforce, tempcounter);
  }
  forcecounter = tempcounter; // save value back
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
}
forcecounter = tempcounter; // save value back
}
uint8_t tempcounter = forcecounter;
// note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough
for (uint32_t i = 0; i < usedParticles; i++) {
tempcounter = forcecounter;
applyForce(particles[i], xforce, yforce, tempcounter);
}
forcecounter = tempcounter; // save value back
}
🧰 Tools
🪛 Cppcheck (2.10-2)

[warning] 445-445: Uninitialized variable

(uninitvar)

Copy link

@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: 2

♻️ Duplicate comments (4)
wled00/FXparticleSystem.cpp (4)

440-447: ⚠️ Potential issue

Uninitialized variable issue with tempcounter

The tempcounter variable might be used uninitialized if the loop is never entered (when usedParticles is 0), but is later assigned to forcecounter.

Initialize the variable before the loop:

- uint8_t tempcounter;
+ uint8_t tempcounter = forcecounter;
  // note: this is not the most computationally efficient way to do this, but it saves on duplicate code and is fast enough
  for (uint32_t i = 0; i < usedParticles; i++) {
    tempcounter = forcecounter;
    applyForce(particles[i], xforce, yforce, tempcounter);
  }
  forcecounter = tempcounter; // save value back
🧰 Tools
🪛 Cppcheck (2.10-2)

[warning] 445-445: Uninitialized variable

(uninitvar)


788-792: ⚠️ Potential issue

Large automatic array may overflow the tiny MCU stack

uint16_t binIndices[maxBinParticles]; is allocated on the stack. With the default limits (usedParticles/2 for 2D, /4 for 1D) this can be several kilobytes (e.g., 2 kB for 1024 particles).

On ESP8266 the default task stack is only 4 kB, leaving little headroom and potentially causing random resets.

Consider:

  • Allocating the array once in PSdataEnd memory
  • Capping maxBinParticles to a conservative constant (e.g., 256)
  • Using a small StaticVector/ring-buffer allocated in IRAM_ATTR

1148-1151: ⚠️ Potential issue

Missing initialization of counters in 1D system

Similar to the 2D system, internal counters in the 1D system constructor are not properly initialized:

  motionBlur = 0; //no fading by default
  smearBlur = 0; //no smearing by default
  emitIndex = 0;
  collisionStartIdx = 0;
+ forcecounter = 0;
+ gforcecounter = 0;

This could lead to undefined behavior when using applyForce() and applyGravity() methods.


38-44: ⚠️ Potential issue

Several internal counters remain uninitialized

In the ParticleSystem2D constructor, members like forcecounter and gforcecounter are not initialized before they're read and incremented in methods like applyForce and applyGravity. This results in undefined behavior.

Please add explicit zero-initialization in the constructor:

  smearBlur = 0; //no smearing by default
  emitIndex = 0;
  collisionStartIdx = 0;
+ forcecounter = 0;
+ gforcecounter = 0;
🧹 Nitpick comments (3)
wled00/FXparticleSystem.cpp (3)

624-666: Optimize memory access pattern in particle rendering function

The render function uses a nested structure pixco with x,y coordinates, but the memory access pattern isn't optimal.

The current approach creates a struct of arrays, when an array of structs would be more cache-friendly:

- struct {
-   int32_t x,y;
- } pixco[4]; // particle pixel coordinates
+ struct PixelCoord {
+   int32_t x, y;
+ };
+ PixelCoord pixco[4]; // particle pixel coordinates

Also, consider using constant indices when accessing the array elements for better compiler optimization.


1850-1870: Optimize force calculation for common case

The calcForce_dv function can be optimized for the common case where force is a power of 16:

static int32_t calcForce_dv(const int8_t force, uint8_t &counter) {
  if (force == 0)
    return 0;
  // for small forces, need to use a delay counter
  int32_t force_abs = abs(force); // absolute value (faster than lots of if's only 7 instructions)
  int32_t dv = 0;
  
+ // Fast path for powers of 16
+ if ((force_abs & 0xF) == 0) {
+   return force / 16; // Powers of 16 can skip counter logic
+ }
+
  // for small forces, need to use a delay counter, apply force only if it overflows
  if (force_abs < 16) {
    counter += force_abs;
    if (counter > 15) {
      counter -= 16;
      dv = force < 0 ? -1 : 1; // force is either 1 or -1 if it is small (zero force is handled above)
    }
  }
  else
    dv = force / 16; // MSBs, note: cannot use bitshift as dv can be negative

  return dv;
}

This optimization would improve performance for common force values that are multiples of 16.


7-7: Copyright year is set to 2025

The copyright year in the file header is set to 2025, which is in the future. Update it to reflect the actual year:

- Copyright (c) 2025  Damian Schneider
+ Copyright (c) 2024  Damian Schneider
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56d7d84 and 4f688a4.

📒 Files selected for processing (1)
  • wled00/FXparticleSystem.cpp (1 hunks)
🧰 Additional context used
🪛 Cppcheck (2.10-2)
wled00/FXparticleSystem.cpp

[warning] 445-445: Uninitialized variable

(uninitvar)

🔇 Additional comments (1)
wled00/FXparticleSystem.cpp (1)

143-144: ⚠️ Potential issue

Constructor initializes PS pointers before knowing strip size – memory layout is wrong

In the 1D particle system constructor, updatePSpointers() is called after setSize(), but the ordering should be reversed.

  advPartProps = nullptr; //make sure we start out with null pointers (just in case memory was not cleared)
  //advPartSize = nullptr;
- setSize(length);
- updatePSpointers(isadvanced); // set the particle and sources pointer (call this before accessing sprays or particles)
+ updatePSpointers(isadvanced); // set the particle and sources pointer
+ setSize(length);

This ordering issue would cause memory offsets to be calculated incorrectly, potentially leading to memory corruption.

Likely an incorrect or invalid review comment.

Comment on lines +1604 to +1606
uint32_t binParticleCount; // number of particles in the current bin
uint16_t nextFrameStartIdx = hw_random16(usedParticles); // index of the first particle in the next frame (set to fixed value if bin overflow)
uint32_t pidx = collisionStartIdx; //start index in case a bin is full, process remaining particles next frame
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Same stack overflow risk in 1D particle system

Like the 2D system, the 1D particle system also uses a large stack-allocated array:

uint16_t binIndices[maxBinParticles];

With the default limit of usedParticles/4, this can still consume significant stack space, especially on memory-constrained devices like ESP8266.

Apply the same mitigation strategies recommended for the 2D system:

  • Allocate the array in data memory rather than on the stack
  • Cap maxBinParticles to a conservative constant
  • Consider a more memory-efficient data structure

Comment on lines +668 to +733
CRGB renderbuffer[100]; // 10x10 pixel buffer
memset(renderbuffer, 0, sizeof(renderbuffer)); // clear buffer
//particle size to pixels: < 64 is 4x4, < 128 is 6x6, < 192 is 8x8, bigger is 10x10
//first, render the pixel to the center of the renderbuffer, then apply 2D blurring
fast_color_add(renderbuffer[4 + (4 * 10)], color, pxlbrightness[0]); // oCrder is: bottom left, bottom right, top right, top left
fast_color_add(renderbuffer[5 + (4 * 10)], color, pxlbrightness[1]);
fast_color_add(renderbuffer[5 + (5 * 10)], color, pxlbrightness[2]);
fast_color_add(renderbuffer[4 + (5 * 10)], color, pxlbrightness[3]);
uint32_t rendersize = 2; // initialize render size, minimum is 4x4 pixels, it is incremented int he loop below to start with 4
uint32_t offset = 4; // offset to zero coordinate to write/read data in renderbuffer (actually needs to be 3, is decremented in the loop below)
uint32_t maxsize = advPartProps[particleindex].size;
uint32_t xsize = maxsize;
uint32_t ysize = maxsize;
if (advPartSize) { // use advanced size control
if (advPartSize[particleindex].asymmetry > 0)
getParticleXYsize(&advPartProps[particleindex], &advPartSize[particleindex], xsize, ysize);
maxsize = (xsize > ysize) ? xsize : ysize; // choose the bigger of the two
}
maxsize = maxsize/64 + 1; // number of blur passes depends on maxsize, four passes max
uint32_t bitshift = 0;
for (uint32_t i = 0; i < maxsize; i++) {
if (i == 2) //for the last two passes, use higher amount of blur (results in a nicer brightness gradient with soft edges)
bitshift = 1;
rendersize += 2;
offset--;
blur2D(renderbuffer, rendersize, rendersize, xsize << bitshift, ysize << bitshift, offset, offset, true);
xsize = xsize > 64 ? xsize - 64 : 0;
ysize = ysize > 64 ? ysize - 64 : 0;
}

// calculate origin coordinates to render the particle to in the framebuffer
uint32_t xfb_orig = x - (rendersize>>1) + 1 - offset;
uint32_t yfb_orig = y - (rendersize>>1) + 1 - offset;
uint32_t xfb, yfb; // coordinates in frame buffer to write to note: by making this uint, only overflow has to be checked (spits a warning though)

//note on y-axis flip: WLED has the y-axis defined from top to bottom, so y coordinates must be flipped. doing this in the buffer xfer clashes with 1D/2D combined rendering, which does not invert y
// transferring the 1D buffer in inverted fashion will flip the x-axis of overlaid 2D FX, so the y-axis flip is done here so the buffer is flipped in y, giving correct results

// transfer particle renderbuffer to framebuffer
for (uint32_t xrb = offset; xrb < rendersize + offset; xrb++) {
xfb = xfb_orig + xrb;
if (xfb > (uint32_t)maxXpixel) {
if (wrapX) { // wrap x to the other side if required
if (xfb > (uint32_t)maxXpixel << 1) // xfb is "negative", handle it
xfb = (maxXpixel + 1) + (int32_t)xfb; // this always overflows to within bounds
else
xfb = xfb % (maxXpixel + 1); // note: without the above "negative" check, this works only for powers of 2
}
else
continue;
}

for (uint32_t yrb = offset; yrb < rendersize + offset; yrb++) {
yfb = yfb_orig + yrb;
if (yfb > (uint32_t)maxYpixel) {
if (wrapY) {// wrap y to the other side if required
if (yfb > (uint32_t)maxYpixel << 1) // yfb is "negative", handle it
yfb = (maxYpixel + 1) + (int32_t)yfb; // this always overflows to within bounds
else
yfb = yfb % (maxYpixel + 1); // note: without the above "negative" check, this works only for powers of 2
}
else
continue;
}
fast_color_add(framebuffer[xfb + (maxYpixel - yfb) * (maxXpixel + 1)], renderbuffer[xrb + yrb * 10]);
}
Copy link

@coderabbitai coderabbitai bot Apr 19, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Large temporary buffer allocation on stack in renderParticle

The function allocates a 100-element CRGB array on the stack:

CRGB renderbuffer[100]; // 10x10 pixel buffer

Each CRGB is typically 3 bytes, resulting in a 300-byte stack allocation. For most platforms this is manageable, but on ESP8266 with limited stack space, this could contribute to stack overflows, especially when combined with other stack allocations.

Consider:

  1. Moving this buffer to heap allocation at initialization time
  2. Reducing buffer size if full 10x10 particles are not needed
  3. Using a smaller buffer and multi-pass rendering

Copy link
Author

Choose a reason for hiding this comment

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

there is enough stack memory available, using heap causes heap fragmentation.

Copy link

Choose a reason for hiding this comment

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

@DedeHai I understand your reasoning. Stack allocation is indeed preferable in this case if you've verified there's sufficient stack space available. You're right that heap allocations can lead to fragmentation over time, especially in embedded systems with frequent allocations/deallocations in tight rendering loops.

Thanks for sharing this insight about your implementation decision.


✏️ Learnings added
Learnt from: DedeHai
PR: MoonModules/WLED-MM#237
File: wled00/FXparticleSystem.cpp:678-743
Timestamp: 2025-05-04T10:32:30.438Z
Learning: In the Particle System implementation for WLED, stack allocation is preferred over heap allocation for temporary rendering buffers because there's enough stack memory available and heap allocations can cause memory fragmentation.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@DedeHai
Copy link
Author

DedeHai commented Apr 28, 2025

Here is what is still needed to make this PR work as intended:

  • envs in platformio.ini need to be adjusted such that the PS is disabled by default on chips where it does not fit the flash by adding one or both
    • -D WLED_DISABLE_PARTICLESYSTEM2D
    • -D WLED_DISABLE_PARTICLESYSTEM1D
  • removing some FX that have a PS replacement is also an option to save on flash (see FX list in FX.cpp in AC)
  • For larger setups, more FX memory is needed. MAX_SEGMENT_DATA needs to be increased in that case, the question is just how much.
  • during FX transitions, double the RAM is needed if two PS effects are blended. disabling mode-blending on larger setups may be required.
  • PS creates a local CRGB rendering buffer of the size of the segment in FX-Data, if any other CRGB buffer is available for rendering (HUB75?) that buffer could also be used instead. Rendering to global LED buffer is also possible but very slow, about 30% FPS drop.

Since I am not too familiar with MM I will not update this PR but leave these changes to someone with better code knowledge.

DedeHai added 2 commits May 4, 2025 12:21
- fixed inconsitencies in size rendering
- fixed palette being wrapped in color by position and color by age modes
- Fixed bug in memory layout: for some unknown reason, if flags come before particles, last flag is sometimes overwritten, changing memory laout seems to fix that
- New color modes in PS Fireworks 1D:
 - custom3 slider < 16: lower saturation (check1: single color or multi-color explosions)
 - custom3 slider <= 23: full saturation (check1: single color or multi-color explosions)
 - custom3 slider > 23: color by speed (check 1 has not effect here)
 - custom slider = max: color by age or color by position (depends on check1)
Copy link

@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

♻️ Duplicate comments (7)
wled00/FXparticleSystem.h (1)

57-69: Bit-field packing still platform-dependent – risk of ABI mismatch

Previous feedback pointed out that the anonymous bit-fields inside PSsettings2D may be packed differently between ESP8266 and ESP32, breaking asByte-style comparisons and transmissions.
The structure is still unchanged, so the problem remains. Consider switching to explicit bit-flags or add a static_assert(sizeof(PSsettings2D)==1) at minimum.

wled00/FXparticleSystem.cpp (6)

439-447: tempcounter may carry garbage on first use

tempcounter is declared but not initialised before being passed to applyForce, reproducing the cppcheck warning.
Initialise it with the current forcecounter value.

-  uint8_t tempcounter;
+  uint8_t tempcounter = forcecounter;
🧰 Tools
🪛 Cppcheck (2.10-2)

[warning] 446-446: Uninitialized variable

(uninitvar)


678-684: 100-element stack buffer still risks ESP8266 overflow

CRGB renderbuffer[100] (~300 B) is allocated on the stack each call to renderParticle().
On ESP8266 the default task stack is only 4 kB and this function is called for every particle ‑ stack blow-ups and resets are still likely.

Move the buffer to:

  • a static thread_local array, or
  • heap / PSdataEnd, or
  • reuse a shared scratch buffer.

798-803: Large binIndices[] on stack can still exhaust MCU stack

uint16_t binIndices[maxBinParticles] may be >2 kB (ESP8266 stack is 4 kB). The earlier recommendation to pre-allocate this array in PSdataEnd or cap its size has not been implemented.


1616-1617: Stack allocation of binIndices repeats the overflow hazard

The 1-D collision routine still allocates binIndices on the stack (up to ~1 kB). Reuse a pre-allocated buffer or cap maxBinParticles to a safe constant.


38-43: ⚠️ Potential issue

Counters forcecounter / gforcecounter start uninitialised

Both counters are read and incremented later (e.g. applyForce, applyGravity) but are never initialised here, leading to undefined behaviour and sporadic velocity spikes.

   smearBlur = 0; //no smearing by default
   emitIndex = 0;
   collisionStartIdx = 0;
+  forcecounter  = 0;
+  gforcecounter = 0;

1151-1160: ⚠️ Potential issue

Same uninitialised counters in 1-D constructor

ParticleSystem1D also misses zero-initialisation for forcecounter and gforcecounter.

   smearBlur = 0; //no smearing by default
   emitIndex = 0;
   collisionStartIdx = 0;
+  forcecounter  = 0;
+  gforcecounter = 0;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d640654 and f7eba55.

📒 Files selected for processing (2)
  • wled00/FXparticleSystem.cpp (1 hunks)
  • wled00/FXparticleSystem.h (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
wled00/FXparticleSystem.cpp (3)
wled00/FXparticleSystem.h (27)
  • force (153-153)
  • force (173-173)
  • force (337-337)
  • ParticleSystem2D (136-136)
  • hardness (159-159)
  • hardness (160-160)
  • hardness (326-326)
  • x (162-162)
  • x (327-327)
  • size (172-172)
  • size (336-336)
  • part (141-141)
  • part (147-147)
  • part (148-148)
  • part (151-151)
  • part (154-154)
  • part (318-318)
  • part (320-320)
  • part (322-322)
  • limitSpeed (34-36)
  • limitSpeed (34-34)
  • blur2D (222-222)
  • calculateNumberOfParticles2D (225-225)
  • calculateNumberOfSources2D (226-226)
  • allocateParticleSystemMemory2D (227-227)
  • ParticleSystem1D (312-312)
  • blur1D (389-389)
wled00/FX.cpp (2)
  • wrap (5401-5417)
  • wrap (5401-5401)
wled00/fcn_declare.h (13)
  • hw_random16 (417-417)
  • hw_random16 (417-417)
  • hw_random16 (418-418)
  • hw_random16 (418-418)
  • hw_random16 (419-419)
  • hw_random16 (419-419)
  • cos16_t (453-453)
  • sin16_t (452-452)
  • r (434-434)
  • r (434-434)
  • b (312-312)
  • b (312-312)
  • b (343-343)
🪛 Cppcheck (2.10-2)
wled00/FXparticleSystem.cpp

[warning] 446-446: Uninitialized variable

(uninitvar)

Comment on lines +241 to +248
if (advancedproperties) { //using individual particle size?
setParticleSize(particlesize); // updates default particleHardRadius
if (advancedproperties->size > PS_P_MINHARDRADIUS) {
particleHardRadius += (advancedproperties->size - PS_P_MINHARDRADIUS); // update radius
renderradius = particleHardRadius;
}
}
// note: if wall collisions are enabled, bounce them before they reach the edge, it looks much nicer if the particle does not go half out of view
Copy link

@coderabbitai coderabbitai bot May 4, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Calling setParticleSize() inside the per-particle loop is expensive and global

setParticleSize() recalculates global members (particleHardRadius, motionBlur) every time it is invoked.
Invoking it for every particle in particleMoveUpdate() (hot path) causes:

  1. O(n²) overhead on size-controlled effects.
  2. Global state oscillation – later particles see a different particleHardRadius than earlier ones.

Extract radius computation into a small inline helper that operates on local variables instead of mutating global settings, and call it once per frame.

Copy link
Author

Choose a reason for hiding this comment

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

this is intentional, not using the function leads to unnecessary code-duplication

Copy link

Choose a reason for hiding this comment

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

@DedeHai I understand your reasoning. The call to setParticleSize() within the loop does keep the code DRY by reusing the logic for calculating the particle radius. While there is a theoretical performance cost from recalculating global state for each particle, you've made a conscious design decision prioritizing code maintainability over this potential overhead. Makes sense given the complexity of the particle system implementation.


✏️ Learnings added
Learnt from: DedeHai
PR: MoonModules/WLED-MM#237
File: wled00/FXparticleSystem.cpp:241-248
Timestamp: 2025-05-04T10:31:58.827Z
Learning: In the particle system (FXparticleSystem.cpp), calling `setParticleSize()` inside the particle update loop is intentional despite potential performance implications. This design decision prioritizes code reuse and maintainability over performance optimization.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@netmindz
Copy link
Collaborator

One blocker for merge is that PS isn't optional but merging in will break any builds that use animartrix as this would take us over 254 effects

@DedeHai
Copy link
Author

DedeHai commented May 11, 2025

If I counted right, the currently used effect number is 220 (with PS effects).
wled#4573 could be pulled into MM, removing 13 effect numbers by consolidating.
Animartrix uses 52 effect numbers, so without consolidating those the max number of standard effects can not be larger than 102.
this leaves us with 220-13 = 207, so 5 effects too many.
If effects that have a PS replacement are removed (see WLED_PS_DONT_REPLACE_FX in AC) it removes another 9 effects from the list.
since animartrix is 2D only: could also add WLED_DISABLE_PARTICLESYSTEM1D instead of WLED_PS_DONT_REPLACE_FX and remove the 15 1D PS effects (although some also look nice in 2D)
In the long run: Animartrix effects could be consolidated to save more IDs.

@netmindz
Copy link
Collaborator

Given MoonModules focus on 2D and larger more complex setups Vs WLED, I'm much more comfortable ditching basic 1D legacy effects to free to resources for PS

@netmindz netmindz merged commit 23f9860 into MoonModules:mdev Jun 29, 2025
1 check passed
softhack007 pushed a commit that referenced this pull request Oct 24, 2025
Adding Particle System (port from AC)
@softhack007 softhack007 added this to the 14.7.1 milestone Dec 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants