Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 29 additions & 20 deletions wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8127,7 +8127,7 @@ uint16_t mode_particlepit(void) {
PartSys->particles[i].sat = ((SEGMENT.custom3) << 3) + 7;
// set particle size
if (SEGMENT.custom1 == 255) {
PartSys->setParticleSize(1); // set global size to 1 for advanced rendering
PartSys->setParticleSize(1); // set global size to 1 for advanced rendering (no single pixel particles)
PartSys->advPartProps[i].size = hw_random16(SEGMENT.custom1); // set each particle to random size
} else {
PartSys->setParticleSize(SEGMENT.custom1); // set global size
Expand Down Expand Up @@ -9085,7 +9085,6 @@ uint16_t mode_particlePinball(void) {
PartSys->sources[0].sourceFlags.collide = true; // seeded particles will collide (if enabled)
PartSys->sources[0].source.x = PS_P_RADIUS_1D; //emit at bottom
PartSys->setKillOutOfBounds(true); // out of bounds particles dont return
PartSys->setUsedParticles(255); // use all available particles for init
SEGENV.aux0 = 1;
SEGENV.aux1 = 5000; //set out of range to ensure uptate on first call
}
Expand Down Expand Up @@ -9308,6 +9307,7 @@ uint16_t mode_particleFireworks1D(void) {
uint8_t *forcecounter;

if (SEGMENT.call == 0) { // initialization
if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system
if (!initParticleSystem1D(PartSys, 4, 150, 4, true)) // init advanced particle system
return mode_static(); // allocation failed or is single pixel
PartSys->setKillOutOfBounds(true);
Expand All @@ -9321,9 +9321,7 @@ uint16_t mode_particleFireworks1D(void) {
// Particle System settings
PartSys->updateSystem(); // update system properties (dimensions and data pointers)
forcecounter = PartSys->PSdataEnd;
PartSys->setParticleSize(SEGMENT.check3); // 1 or 2 pixel rendering
PartSys->setMotionBlur(SEGMENT.custom2); // anable motion blur

int32_t gravity = (1 + (SEGMENT.speed >> 3)); // gravity value used for rocket speed calculation
PartSys->setGravity(SEGMENT.speed ? gravity : 0); // set gravity

Expand All @@ -9337,17 +9335,17 @@ uint16_t mode_particleFireworks1D(void) {
SEGENV.aux0 = 0;

PartSys->sources[0].sourceFlags.custom1 = 0; //flag used for rocket state
PartSys->sources[0].source.hue = hw_random16();
PartSys->sources[0].source.hue = hw_random16(); // different color for each launch
PartSys->sources[0].var = 10; // emit variation
PartSys->sources[0].v = -10; // emit speed
PartSys->sources[0].minLife = 30;
PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 40;
PartSys->sources[0].maxLife = SEGMENT.check2 ? 400 : 60;
PartSys->sources[0].source.x = 0; // start from bottom
uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame
PartSys->sources[0].source.vx = min(speed, (uint32_t)127);
PartSys->sources[0].source.ttl = 4000;
Comment on lines 9344 to 9346
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Heavy floating-point sqrt() on MCU – replace with an integer approximation

sqrt() promotes the argument to double, incurring ~40 µs on ESP8266 and ~15 µs on ESP32. As this path runs every launch, that overhead is avoidable.
A fast 16-bit integer square-root (e.g. ic_sqrt16() used elsewhere in WLED) or a bit-shift approximation is sufficient:

-uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4);
+uint32_t speed = isqrt16((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4);

(Replace isqrt16 with any existing integer sqrt helper.)
This saves CPU cycles and power without visible changes in the effect.

📝 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
uint32_t speed = sqrt((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame
PartSys->sources[0].source.vx = min(speed, (uint32_t)127);
PartSys->sources[0].source.ttl = 4000;
uint32_t speed = isqrt16((gravity * ((PartSys->maxX >> 2) + hw_random16(PartSys->maxX >> 1))) >> 4); // set speed such that rocket explods in frame
PartSys->sources[0].source.vx = min(speed, (uint32_t)127);
PartSys->sources[0].source.ttl = 4000;

PartSys->sources[0].sat = 30; // low saturation exhaust
PartSys->sources[0].size = 0; // default size
PartSys->sources[0].size = SEGMENT.check3; // single or double pixel rendering
PartSys->sources[0].sourceFlags.reversegrav = false ; // normal gravity

if (SEGENV.aux0) { // inverted rockets launch from end
Expand All @@ -9360,17 +9358,17 @@ uint16_t mode_particleFireworks1D(void) {
}
else { // rocket is launched
int32_t rocketgravity = -gravity;
int32_t speed = PartSys->sources[0].source.vx;
int32_t currentspeed = PartSys->sources[0].source.vx;
if (SEGENV.aux0) { // negative speed rocket
rocketgravity = -rocketgravity;
speed = -speed;
currentspeed = -currentspeed;
}
PartSys->applyForce(PartSys->sources[0].source, rocketgravity, forcecounter[0]);
PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags);
PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase speed by calling the move function twice, also ages twice
PartSys->particleMoveUpdate(PartSys->sources[0].source, PartSys->sources[0].sourceFlags); // increase rocket speed by calling the move function twice, also ages twice
uint32_t rocketheight = SEGENV.aux0 ? PartSys->maxX - PartSys->sources[0].source.x : PartSys->sources[0].source.x;

if (speed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee
if (currentspeed < 0 && PartSys->sources[0].source.ttl > 50) // reached apogee
PartSys->sources[0].source.ttl = min((uint32_t)50, rocketheight >> (PS_P_RADIUS_SHIFT_1D + 3)); // alive for a few more frames

if (PartSys->sources[0].source.ttl < 2) { // explode
Expand All @@ -9379,19 +9377,32 @@ uint16_t mode_particleFireworks1D(void) {
PartSys->sources[0].minLife = 600;
PartSys->sources[0].maxLife = 1300;
PartSys->sources[0].source.ttl = 100 + hw_random16(64 - (SEGMENT.speed >> 2)); // standby time til next launch
PartSys->sources[0].sat = 7 + (SEGMENT.custom3 << 3); //color saturation TODO: replace saturation with something more useful?
PartSys->sources[0].size = hw_random16(SEGMENT.intensity); // random particle size in explosion
PartSys->sources[0].sat = SEGMENT.custom3 < 16 ? 10 + (SEGMENT.custom3 << 4) : 255; //color saturation
PartSys->sources[0].size = SEGMENT.check3 ? hw_random16(SEGMENT.intensity) : 0; // random particle size in explosion
uint32_t explosionsize = 8 + (PartSys->maxXpixel >> 2) + (PartSys->sources[0].source.x >> (PS_P_RADIUS_SHIFT_1D - 1));
explosionsize += hw_random16((explosionsize * SEGMENT.intensity) >> 8);
for (uint32_t e = 0; e < explosionsize; e++) { // emit explosion particles
if (SEGMENT.check1) // colorful mode
PartSys->sources[0].source.hue = hw_random16(); //random color for each particle
PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
int idx = PartSys->sprayEmit(PartSys->sources[0]); // emit a particle
if(SEGMENT.custom3 > 23) {
if(SEGMENT.custom3 == 31) { // highest slider value
PartSys->setColorByAge(SEGMENT.check1); // color by age if colorful mode is enabled
PartSys->setColorByPosition(!SEGMENT.check1); // color by position otherwise
}
else { // if custom3 is set to high value (but not highest), set particle color by initial speed
PartSys->particles[idx].hue = map(abs(PartSys->particles[idx].vx), 0, PartSys->sources[0].var, 0, 16 + hw_random16(200)); // set hue according to speed, use random amount of palette width
PartSys->particles[idx].hue += PartSys->sources[0].source.hue; // add hue offset of the rocket (random starting color)
}
}
else {
if (SEGMENT.check1) // colorful mode
PartSys->sources[0].source.hue = hw_random16(); //random color for each particle
}
}
}
}
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false) // every second frame and not in standby
if ((SEGMENT.call & 0x01) == 0 && PartSys->sources[0].sourceFlags.custom1 == false && PartSys->sources[0].source.ttl > 50) // every second frame and not in standby and not about to explode
PartSys->sprayEmit(PartSys->sources[0]); // emit exhaust particle

if ((SEGMENT.call & 0x03) == 0) // every fourth frame
PartSys->applyFriction(1); // apply friction to all particles

Expand All @@ -9401,10 +9412,9 @@ uint16_t mode_particleFireworks1D(void) {
if (PartSys->particles[i].ttl > 10) PartSys->particles[i].ttl -= 10; //ttl is linked to brightness, this allows to use higher brightness but still a short spark lifespan
else PartSys->particles[i].ttl = 0;
}

return FRAMETIME;
}
static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Saturation,Colorful,Trail,Smooth;,!;!;1;sx=150,c2=30,c3=31,o1=1,o2=1";
static const char _data_FX_MODE_PS_FIREWORKS1D[] PROGMEM = "PS Fireworks 1D@Gravity,Explosion,Firing side,Blur,Color,Colorful,Trail,Smooth;,!;!;1;c2=30,o1=1";

/*
Particle based Sparkle effect
Expand Down Expand Up @@ -9925,7 +9935,6 @@ uint16_t mode_particle1DGEQ(void) {
PartSys->sources[i].maxLife = 240 + SEGMENT.intensity;
PartSys->sources[i].sat = 255;
PartSys->sources[i].size = SEGMENT.custom1;
PartSys->setParticleSize(SEGMENT.custom1);
PartSys->sources[i].source.x = (spacing >> 1) + spacing * i; //distribute evenly
}

Expand Down
58 changes: 37 additions & 21 deletions wled00/FXparticleSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ ParticleSystem2D::ParticleSystem2D(uint32_t width, uint32_t height, uint32_t num
for (uint32_t i = 0; i < numSources; i++) {
sources[i].source.sat = 255; //set saturation to max by default
sources[i].source.ttl = 1; //set source alive
sources[i].sourceFlags.asByte = 0; // all flags disabled
}

}
Expand Down Expand Up @@ -559,6 +560,10 @@ void ParticleSystem2D::pointAttractor(const uint32_t particleindex, PSparticle &
void ParticleSystem2D::render() {
CRGB baseRGB;
uint32_t brightness; // particle brightness, fades if dying
TBlendType blend = LINEARBLEND; // default color rendering: wrap palette
if (particlesettings.colorByAge) {
blend = LINEARBLEND_NOWRAP;
}

if (motionBlur) { // motion-blurring active
for (int32_t y = 0; y <= maxYpixel; y++) {
Expand All @@ -581,11 +586,11 @@ void ParticleSystem2D::render() {
if (fireIntesity) { // fire mode
brightness = (uint32_t)particles[i].ttl * (3 + (fireIntesity >> 5)) + 20;
brightness = min(brightness, (uint32_t)255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, brightness, 255, LINEARBLEND_NOWRAP);
}
else {
brightness = min((particles[i].ttl << 1), (int)255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend);
if (particles[i].sat < 255) {
CHSV32 baseHSV;
rgb2hsv((uint32_t((byte(baseRGB.r) << 16) | (byte(baseRGB.g) << 8) | (byte(baseRGB.b)))), baseHSV); // convert to HSV
Expand All @@ -598,14 +603,15 @@ void ParticleSystem2D::render() {
renderParticle(i, brightness, baseRGB, particlesettings.wrapX, particlesettings.wrapY);
}

// apply global size rendering
if (particlesize > 1) {
uint32_t passes = particlesize / 64 + 1; // number of blur passes, four passes max
uint32_t bluramount = particlesize;
uint32_t bitshift = 0;
for (uint32_t i = 0; i < passes; 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;
blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift);
blur2D(framebuffer, maxXpixel + 1, maxYpixel + 1, bluramount << bitshift, bluramount << bitshift);
bluramount -= 64;
}
}
Expand All @@ -626,7 +632,11 @@ void ParticleSystem2D::render() {

// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
__attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB& color, const bool wrapX, const bool wrapY) {
if (particlesize == 0) { // single pixel rendering
uint32_t size = particlesize;
if (advPartProps && advPartProps[particleindex].size > 0) // use advanced size properties (0 means use global size including single pixel rendering)
size = advPartProps[particleindex].size;

if (size == 0) { // single pixel rendering
uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT;
uint32_t y = particles[particleindex].y >> PS_P_RADIUS_SHIFT;
if (x <= (uint32_t)maxXpixel && y <= (uint32_t)maxYpixel) {
Expand Down Expand Up @@ -667,7 +677,7 @@ __attribute__((optimize("O2"))) void ParticleSystem2D::renderParticle(const uint
pxlbrightness[2] = (dx * precal3) >> PS_P_SURFACE; // top right value equal to (dx * dy * brightness) >> PS_P_SURFACE
pxlbrightness[3] = (precal1 * precal3) >> PS_P_SURFACE; // top left value equal to ((PS_P_RADIUS-dx) * dy * brightness) >> PS_P_SURFACE

if (advPartProps && advPartProps[particleindex].size > 0) { //render particle to a bigger size
if (advPartProps && advPartProps[particleindex].size > 1) { //render particle to a bigger size
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
Expand Down Expand Up @@ -962,15 +972,13 @@ void ParticleSystem2D::updateSystem(void) {
// FX handles the PSsources, need to tell this function how many there are
void ParticleSystem2D::updatePSpointers(bool isadvanced, bool sizecontrol) {
PSPRINTLN("updatePSpointers");
// DEBUG_PRINT(F("*** PS pointers ***"));
// DEBUG_PRINTF_P(PSTR("this PS %p "), this);
// Note on memory alignment:
// a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment.
// The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock.
// by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes.
particleFlags = reinterpret_cast<PSparticleFlags *>(this + 1); // pointer to particle flags
particles = reinterpret_cast<PSparticle *>(particleFlags + numParticles); // pointer to particles
sources = reinterpret_cast<PSsource *>(particles + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D)
particles = reinterpret_cast<PSparticle *>(this + 1); // pointer to particles
particleFlags = reinterpret_cast<PSparticleFlags *>(particles + numParticles); // pointer to particle flags
sources = reinterpret_cast<PSsource *>(particleFlags + numParticles); // pointer to source(s) at data+sizeof(ParticleSystem2D)
framebuffer = reinterpret_cast<CRGB *>(sources + numSources); // pointer to framebuffer
// align pointer after framebuffer
uintptr_t p = reinterpret_cast<uintptr_t>(framebuffer + (maxXpixel+1)*(maxYpixel+1));
Expand Down Expand Up @@ -1155,6 +1163,7 @@ ParticleSystem1D::ParticleSystem1D(uint32_t length, uint32_t numberofparticles,
// initialize some default non-zero values most FX use
for (uint32_t i = 0; i < numSources; i++) {
sources[i].source.ttl = 1; //set source alive
sources[i].sourceFlags.asByte = 0; // all flags disabled
}

if (isadvanced) {
Expand Down Expand Up @@ -1269,7 +1278,7 @@ int32_t ParticleSystem1D::sprayEmit(const PSsource1D &emitter) {
particles[emitIndex].x = emitter.source.x;
particles[emitIndex].hue = emitter.source.hue;
particles[emitIndex].ttl = hw_random16(emitter.minLife, emitter.maxLife);
particleFlags[emitIndex].collide = emitter.sourceFlags.collide;
particleFlags[emitIndex].collide = emitter.sourceFlags.collide; // TODO: could just set all flags (asByte) but need to check if that breaks any of the FX
particleFlags[emitIndex].reversegrav = emitter.sourceFlags.reversegrav;
particleFlags[emitIndex].perpetual = emitter.sourceFlags.perpetual;
if (advPartProps) {
Expand Down Expand Up @@ -1419,6 +1428,10 @@ void ParticleSystem1D::applyFriction(int32_t coefficient) {
void ParticleSystem1D::render() {
CRGB baseRGB;
uint32_t brightness; // particle brightness, fades if dying
TBlendType blend = LINEARBLEND; // default color rendering: wrap palette
if (particlesettings.colorByAge || particlesettings.colorByPosition) {
blend = LINEARBLEND_NOWRAP;
}

#ifdef ESP8266 // no local buffer on ESP8266
if (motionBlur)
Expand All @@ -1442,7 +1455,7 @@ void ParticleSystem1D::render() {

// generate RGB values for particle
brightness = min(particles[i].ttl << 1, (int)255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255);
baseRGB = ColorFromPaletteWLED(SEGPALETTE, particles[i].hue, 255, blend);

if (advPartProps) { //saturation is advanced property in 1D system
if (advPartProps[i].sat < 255) {
Expand Down Expand Up @@ -1489,9 +1502,9 @@ void ParticleSystem1D::render() {
// calculate pixel positions and brightness distribution and render the particle to local buffer or global buffer
__attribute__((optimize("O2"))) void ParticleSystem1D::renderParticle(const uint32_t particleindex, const uint8_t brightness, const CRGB &color, const bool wrap) {
uint32_t size = particlesize;
if (advPartProps) { // use advanced size properties
if (advPartProps) // use advanced size properties (1D system has no large size global rendering TODO: add large global rendering?)
size = advPartProps[particleindex].size;
}

if (size == 0) { //single pixel particle, can be out of bounds as oob checking is made for 2-pixel particles (and updating it uses more code)
uint32_t x = particles[particleindex].x >> PS_P_RADIUS_SHIFT_1D;
if (x <= (uint32_t)maxXpixel) { //by making x unsigned there is no need to check < 0 as it will overflow
Expand Down Expand Up @@ -1736,30 +1749,32 @@ void ParticleSystem1D::updatePSpointers(bool isadvanced) {
// a pointer MUST be 4 byte aligned. sizeof() in a struct/class is always aligned to the largest element. if it contains a 32bit, it will be padded to 4 bytes, 16bit is padded to 2byte alignment.
// The PS is aligned to 4 bytes, a PSparticle is aligned to 2 and a struct containing only byte sized variables is not aligned at all and may need to be padded when dividing the memoryblock.
// by making sure that the number of sources and particles is a multiple of 4, padding can be skipped here as alignent is ensured, independent of struct sizes.
particleFlags = reinterpret_cast<PSparticleFlags1D *>(this + 1); // pointer to particle flags
particles = reinterpret_cast<PSparticle1D *>(particleFlags + numParticles); // pointer to particles
sources = reinterpret_cast<PSsource1D *>(particles + numParticles); // pointer to source(s)
particles = reinterpret_cast<PSparticle1D *>(this + 1); // pointer to particles
particleFlags = reinterpret_cast<PSparticleFlags1D *>(particles + numParticles); // pointer to particle flags
sources = reinterpret_cast<PSsource1D *>(particleFlags + numParticles); // pointer to source(s)
#ifdef ESP8266 // no local buffer on ESP8266
PSdataEnd = reinterpret_cast<uint8_t *>(sources + numSources);
#else
framebuffer = reinterpret_cast<CRGB *>(sources + numSources); // pointer to framebuffer
// align pointer after framebuffer to 4bytes
uintptr_t p = reinterpret_cast<uintptr_t>(framebuffer + (maxXpixel+1));
uintptr_t p = reinterpret_cast<uintptr_t>(framebuffer + (maxXpixel+1)); // maxXpixel is SEGMENT.virtualLength() - 1
p = (p + 3) & ~0x03; // align to 4-byte boundary
PSdataEnd = reinterpret_cast<uint8_t *>(p); // pointer to first available byte after the PS for FX additional data
#endif
if (isadvanced) {
advPartProps = reinterpret_cast<PSadvancedParticle1D *>(PSdataEnd);
PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles);
PSdataEnd = reinterpret_cast<uint8_t *>(advPartProps + numParticles); // since numParticles is a multiple of 4, this is always aligned to 4 bytes. No need to add padding bytes here
}
#ifdef WLED_DEBUG_PS
PSPRINTLN(" PS Pointers: ");
PSPRINT(" PS : 0x");
Serial.println((uintptr_t)this, HEX);
PSPRINT(" Sources : 0x");
Serial.println((uintptr_t)sources, HEX);
PSPRINT(" Particleflags : 0x");
Serial.println((uintptr_t)particleFlags, HEX);
PSPRINT(" Particles : 0x");
Serial.println((uintptr_t)particles, HEX);
PSPRINT(" Sources : 0x");
Serial.println((uintptr_t)sources, HEX);
#endif
}

Expand All @@ -1780,6 +1795,7 @@ uint32_t calculateNumberOfParticles1D(const uint32_t fraction, const bool isadva
numberofParticles = numberofParticles < 20 ? 20 : numberofParticles; // 20 minimum
//make sure it is a multiple of 4 for proper memory alignment (easier than using padding bytes)
numberofParticles = (numberofParticles+3) & ~0x03; // note: with a separate particle buffer, this is probably unnecessary
PSPRINTLN(" calc numparticles:" + String(numberofParticles))
return numberofParticles;
}

Expand Down
Loading
Loading