From d853fa6af7f46fe88401bf1d297be722cbcf3494 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Sun, 6 Oct 2024 21:26:36 +0200 Subject: [PATCH 1/5] Improved Pinwheel: dynamic step calculation - should now work on larger setups, tested at vaious sizes from 8x8 - 32x32 and on 128x16 - Increased fixed point shift to 15bit - fixed 'pixel holes' on large sizes - speed should be improved too - consolidated code to save flash (about -300 bytes) - reduced 'pixel stepping' to ~60% steps to fix pixel holes (increases number of loops but it has no measurable impact on speed) --- wled00/FX_fcn.cpp | 109 ++++++++++++++++------------------------------ 1 file changed, 38 insertions(+), 71 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 1bbfa365bd..bbf676a92b 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -637,37 +637,31 @@ uint16_t IRAM_ATTR_YN Segment::nrOfVStrips() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D -constexpr int Pinwheel_Steps_Small = 72; // no holes up to 16x16 -constexpr int Pinwheel_Size_Small = 16; // larger than this -> use "Medium" -constexpr int Pinwheel_Steps_Medium = 192; // no holes up to 32x32 -constexpr int Pinwheel_Size_Medium = 32; // larger than this -> use "Big" -constexpr int Pinwheel_Steps_Big = 304; // no holes up to 50x50 -constexpr int Pinwheel_Size_Big = 50; // larger than this -> use "XL" -constexpr int Pinwheel_Steps_XL = 368; -constexpr float Int_to_Rad_Small = (DEG_TO_RAD * 360) / Pinwheel_Steps_Small; // conversion: from 0...72 to Radians -constexpr float Int_to_Rad_Med = (DEG_TO_RAD * 360) / Pinwheel_Steps_Medium; // conversion: from 0...192 to Radians -constexpr float Int_to_Rad_Big = (DEG_TO_RAD * 360) / Pinwheel_Steps_Big; // conversion: from 0...304 to Radians -constexpr float Int_to_Rad_XL = (DEG_TO_RAD * 360) / Pinwheel_Steps_XL; // conversion: from 0...368 to Radians - -constexpr int Fixed_Scale = 512; // fixpoint scaling factor (9bit for fraction) +constexpr int Fixed_Shift = 15; // fixpoint scaling factor (15bit for fraction) -// Pinwheel helper function: pixel index to radians -static float getPinwheelAngle(int i, int vW, int vH) { - int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return float(i) * Int_to_Rad_Small; - if (maxXY <= Pinwheel_Size_Medium) return float(i) * Int_to_Rad_Med; - if (maxXY <= Pinwheel_Size_Big) return float(i) * Int_to_Rad_Big; - // else - return float(i) * Int_to_Rad_XL; -} // Pinwheel helper function: matrix dimensions to number of rays -static int getPinwheelLength(int vW, int vH) { +static inline int getPinwheelSteps(int vW, int vH) { int maxXY = max(vW, vH); - if (maxXY <= Pinwheel_Size_Small) return Pinwheel_Steps_Small; - if (maxXY <= Pinwheel_Size_Medium) return Pinwheel_Steps_Medium; - if (maxXY <= Pinwheel_Size_Big) return Pinwheel_Steps_Big; - // else - return Pinwheel_Steps_XL; + return (maxXY * 9) / 2; // theoretical value sqrt(2) * pi (1 pixel step in the corner) +} +// Pinwheel helper function: pixel index to radians +static float getPinwheelAngle(int i, int vW, int vH) { + int steps = getPinwheelSteps(vW, vH) - 1; // -1 to make the angle larger so the wheel has no gap + return float(i) * (DEG_TO_RAD * 360) / steps; +} + +static void setPinwheelParameters(int i, int vH, int vW, int& posx, int& posy, int& inc_x, int& inc_y, int& maxX, int& maxY) { + float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians + float cosVal = cos_t(angleRad); + float sinVal = sin_t(angleRad); + posx = (vW - 1) << (Fixed_Shift - 1); // X starting position = center (in fixed point) + posy = (vH - 1) << (Fixed_Shift - 1); // Y starting position = center (in fixed point) + inc_x = cosVal * (1 << Fixed_Shift); // X increment per step (fixed point) + inc_y = sinVal * (1 << Fixed_Shift); // Y increment per step (fixed point) + inc_x = (inc_x * 150) >> 8; // reduce to ~60% to avoid pixel holes + inc_y = (inc_y * 150) >> 8; // note: this increases the number of loops to be run but the loops are fast TODO: fine tune this + maxX = vW << Fixed_Shift; // X edge in fixedpoint + maxY = vH << Fixed_Shift; // Y edge in fixedpoint } #endif @@ -689,7 +683,7 @@ uint16_t IRAM_ATTR Segment::virtualLength() const { vLen = sqrt16(vH*vH + vW*vW); // use diagonal break; case M12_sPinwheel: - vLen = getPinwheelLength(vW, vH); + vLen = getPinwheelSteps(vW, vH); break; default: vLen = vW * vH; // use all pixels from segment @@ -763,42 +757,26 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; case M12_sPinwheel: { - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); + int posx, posy, inc_x, inc_y, maxX, maxY; + setPinwheelParameters(i, vH, vW, posx, posy, inc_x, inc_y, maxX, maxY); - // avoid re-painting the same pixel - int lastX = INT_MIN; // impossible position - int lastY = INT_MIN; // impossible position - // draw line at angle, starting at center and ending at the segment edge - // we use fixed point math for better speed. Starting distance is 0.5 for better rounding - // int_fast16_t and int_fast32_t types changed to int, minimum bits commented - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint - - // Odd rays start further from center if prevRay started at center. + // Odd rays start further from center if prevRay started at center (speed optimization) static int prevRay = INT_MIN; // previous ray number if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { - int jump = min(vW/3, vH/3); // can add 2 if using medium pinwheel + int jump = min(vW/3, vH/3); // note: could add more on larger sizes posx += inc_x * jump; posy += inc_y * jump; } prevRay = i; + // avoid re-painting the same pixel + int lastX = INT_MIN; // impossible position + int lastY = INT_MIN; + // draw ray until we hit any edge while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - int x = posx / Fixed_Scale; - int y = posy / Fixed_Scale; - // set pixel + int x = posx >> Fixed_Shift; + int y = posy >> Fixed_Shift; if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different lastX = x; lastY = y; @@ -929,27 +907,16 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const break; case M12_sPinwheel: // not 100% accurate, returns pixel at outer edge - // i = angle --> 0 - 296 (Big), 0 - 192 (Medium), 0 - 72 (Small) - float centerX = roundf((vW-1) / 2.0f); - float centerY = roundf((vH-1) / 2.0f); - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - - int posx = (centerX + 0.5f * cosVal) * Fixed_Scale; // X starting position in fixed point 18 bit - int posy = (centerY + 0.5f * sinVal) * Fixed_Scale; // Y starting position in fixed point 18 bit - int inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) 10 bit - int inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) 10 bit - int32_t maxX = vW * Fixed_Scale; // X edge in fixedpoint - int32_t maxY = vH * Fixed_Scale; // Y edge in fixedpoint + int posx, posy, inc_x, inc_y, maxX, maxY; + setPinwheelParameters(i, vH, vW, posx, posy, inc_x, inc_y, maxX, maxY); // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor int x = INT_MIN; int y = INT_MIN; while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) - x = posx / Fixed_Scale; - y = posy / Fixed_Scale; + // scale down to integer (compiler will replace division with appropriate bitshift) -> not guaranteed + x = posx >> Fixed_Shift; + y = posy >> Fixed_Shift; // advance to next position posx += inc_x; posy += inc_y; From 4b54010986a8f063e651ba21aae6b9f70b81f033 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Mon, 7 Oct 2024 20:05:44 +0200 Subject: [PATCH 2/5] Added dynamic pixel skipping based on "current radius" - tuned parameters for 'no holes' there may be room for improvement, I did not find any. --- wled00/FX_fcn.cpp | 56 +++++++++++++++++++++++++++-------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index bbf676a92b..3d00283747 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -642,7 +642,7 @@ constexpr int Fixed_Shift = 15; // fixpoint scaling factor (15bit for fraction) // Pinwheel helper function: matrix dimensions to number of rays static inline int getPinwheelSteps(int vW, int vH) { int maxXY = max(vW, vH); - return (maxXY * 9) / 2; // theoretical value sqrt(2) * pi (1 pixel step in the corner) + return maxXY * 5; // theoretical value sqrt(2) * pi (1 pixel step in the corner) Note: (maxXY * 9)/2 works on most sizes but not all, there is room for speed optimization here } // Pinwheel helper function: pixel index to radians static float getPinwheelAngle(int i, int vW, int vH) { @@ -650,7 +650,7 @@ static float getPinwheelAngle(int i, int vW, int vH) { return float(i) * (DEG_TO_RAD * 360) / steps; } -static void setPinwheelParameters(int i, int vH, int vW, int& posx, int& posy, int& inc_x, int& inc_y, int& maxX, int& maxY) { +static void setPinwheelParameters(int i, int vW, int vH, unsigned& posx, unsigned& posy, int& inc_x, int& inc_y, int& maxX, int& maxY) { float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians float cosVal = cos_t(angleRad); float sinVal = sin_t(angleRad); @@ -658,8 +658,8 @@ static void setPinwheelParameters(int i, int vH, int vW, int& posx, int& posy, i posy = (vH - 1) << (Fixed_Shift - 1); // Y starting position = center (in fixed point) inc_x = cosVal * (1 << Fixed_Shift); // X increment per step (fixed point) inc_y = sinVal * (1 << Fixed_Shift); // Y increment per step (fixed point) - inc_x = (inc_x * 150) >> 8; // reduce to ~60% to avoid pixel holes - inc_y = (inc_y * 150) >> 8; // note: this increases the number of loops to be run but the loops are fast TODO: fine tune this + inc_x = inc_x >> 1; // reduce to 50% to avoid pixel holes + inc_y = inc_y >> 1; // note: this increases the number of loops to be run but the loops are fast, it appears like a good tradeoff maxX = vW << Fixed_Shift; // X edge in fixedpoint maxY = vH << Fixed_Shift; // Y edge in fixedpoint } @@ -757,31 +757,38 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; case M12_sPinwheel: { - int posx, posy, inc_x, inc_y, maxX, maxY; - setPinwheelParameters(i, vH, vW, posx, posy, inc_x, inc_y, maxX, maxY); - - // Odd rays start further from center if prevRay started at center (speed optimization) - static int prevRay = INT_MIN; // previous ray number - if ((i % 2 == 1) && (i - 1 == prevRay || i + 1 == prevRay)) { - int jump = min(vW/3, vH/3); // note: could add more on larger sizes - posx += inc_x * jump; - posy += inc_y * jump; - } - prevRay = i; - + unsigned posx, posy; // unsigned so negative numbers overflow to > maxXY to save negative checking + int inc_x, inc_y, maxX, maxY; + setPinwheelParameters(i, vW, vH, posx, posy, inc_x, inc_y, maxX, maxY); + unsigned totalSteps = getPinwheelSteps(vW, vH); + static int pixelsdrawn; + /* + Note on the skipping algorithm: + - in the center, the angle is way too small for efficient drawing, pixels get overdrawn a lot, making it slow + - tracking the radius and deciding when to actually draw pixels significantly reduces the number of overdraws + - the number of angular steps required for a certain radius is in theory 2*pi*radius*sqrt(2) (worst case, in the corner) + - we can exploit that and only draw rays that are larger than a minimum step size (need to account for rounding errors) + */ // avoid re-painting the same pixel int lastX = INT_MIN; // impossible position int lastY = INT_MIN; - + unsigned currentR = 0; // current radius in "pixels" // draw ray until we hit any edge - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { + while ((posx < maxX) && (posy < maxY)) { int x = posx >> Fixed_Shift; int y = posy >> Fixed_Shift; - if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different + if (x != lastX || y != lastY) { // only paint if pixel position is different + currentR++; + int requiredsteps = currentR * 8; // empirically found value (R is a rough estimation, always >= actual R) + int skipsteps = totalSteps/requiredsteps; + if(!skipsteps || i % (skipsteps) == 0) // check if pixel would 'overdraw' + { + setPixelColorXY(x, y, col); + } + } lastX = x; lastY = y; - // advance to next position - posx += inc_x; + posx += inc_x; // advance to next position posy += inc_y; } break; @@ -907,13 +914,14 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const break; case M12_sPinwheel: // not 100% accurate, returns pixel at outer edge - int posx, posy, inc_x, inc_y, maxX, maxY; - setPinwheelParameters(i, vH, vW, posx, posy, inc_x, inc_y, maxX, maxY); + unsigned posx, posy; // unsigned so negative numbers overflow to > maxXY to save negative checking + int inc_x, inc_y, maxX, maxY; + setPinwheelParameters(i, vW, vH, posx, posy, inc_x, inc_y, maxX, maxY); // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor int x = INT_MIN; int y = INT_MIN; - while ((posx >= 0) && (posy >= 0) && (posx < maxX) && (posy < maxY)) { + while ((posx < maxX) && (posy < maxY)) { // scale down to integer (compiler will replace division with appropriate bitshift) -> not guaranteed x = posx >> Fixed_Shift; y = posy >> Fixed_Shift; From 0cf98e4cba031d96b2381f1cc7cca7ebf28aab69 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Thu, 10 Oct 2024 06:51:14 +0200 Subject: [PATCH 3/5] updated lots of tweaks, now works from 2x2 up to 512x512 - parameters (constexpr) are a compromise between "no zero pixels on all sizes" and speed - for 8x8, 16x16, 32x32 there are parameters with better optimization, improving speed above current 0.15 state - there is more room to tweak the 'center skipping' on larger sizes --- wled00/FX_fcn.cpp | 125 ++++++++++++++++++++++++++++++---------------- 1 file changed, 83 insertions(+), 42 deletions(-) diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 3d00283747..27ee105c87 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -637,31 +637,36 @@ uint16_t IRAM_ATTR_YN Segment::nrOfVStrips() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D -constexpr int Fixed_Shift = 15; // fixpoint scaling factor (15bit for fraction) +constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) +constexpr float stepFactor = 4.2; // number of angle steps per size (empirically found) +constexpr int incFactor = 220; // Increment is multiplied by INCFACTOR / 256 (scaled with cornerAdd function) +constexpr int cornerAdd = 35; // smaller increments the closer the angle is to 45° +constexpr int smallMatrixIncReduction = 60; // smaller increments on smaller sizes to prevent holes +// note: these parameters work on sizes 2x2 up to 512x512 +// note: for 8x8,16x16,32x32 (and a few other sizes) these parameters work and use less overdraw: +// constexpr float stepFactor = 3.33; // number of angle steps per size (empirically found) +// constexpr int incFactor = 220; // Increment is multiplied by INCFACTOR / 256 (scaled with cornerAdd function) +// constexpr int cornerAdd = 35; // smaller increments the closer the angle is to 45° // Pinwheel helper function: matrix dimensions to number of rays static inline int getPinwheelSteps(int vW, int vH) { int maxXY = max(vW, vH); - return maxXY * 5; // theoretical value sqrt(2) * pi (1 pixel step in the corner) Note: (maxXY * 9)/2 works on most sizes but not all, there is room for speed optimization here -} -// Pinwheel helper function: pixel index to radians -static float getPinwheelAngle(int i, int vW, int vH) { - int steps = getPinwheelSteps(vW, vH) - 1; // -1 to make the angle larger so the wheel has no gap - return float(i) * (DEG_TO_RAD * 360) / steps; + unsigned stepfactor = unsigned(stepFactor * Fixed_Scale); + //if(maxXY < 10) return MIN_STEPS; + return (maxXY * stepfactor) / Fixed_Scale; } static void setPinwheelParameters(int i, int vW, int vH, unsigned& posx, unsigned& posy, int& inc_x, int& inc_y, int& maxX, int& maxY) { - float angleRad = getPinwheelAngle(i, vW, vH); // angle in radians + int steps = getPinwheelSteps(vW, vH); + float angleRad = float(i) * (DEG_TO_RAD * 360) / steps; //angle in radians float cosVal = cos_t(angleRad); float sinVal = sin_t(angleRad); - posx = (vW - 1) << (Fixed_Shift - 1); // X starting position = center (in fixed point) - posy = (vH - 1) << (Fixed_Shift - 1); // Y starting position = center (in fixed point) - inc_x = cosVal * (1 << Fixed_Shift); // X increment per step (fixed point) - inc_y = sinVal * (1 << Fixed_Shift); // Y increment per step (fixed point) - inc_x = inc_x >> 1; // reduce to 50% to avoid pixel holes - inc_y = inc_y >> 1; // note: this increases the number of loops to be run but the loops are fast, it appears like a good tradeoff - maxX = vW << Fixed_Shift; // X edge in fixedpoint - maxY = vH << Fixed_Shift; // Y edge in fixedpoint + posx = (vW - 1) * Fixed_Scale / 2; // X starting position = center (in fixed point) + posy = (vH - 1) * Fixed_Scale / 2; // Y starting position = center (in fixed point) + inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) + inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) + maxX = vW * Fixed_Scale; // X edge in fixedpoint + maxY = vH * Fixed_Scale; // Y edge in fixedpoint } #endif @@ -760,36 +765,72 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) unsigned posx, posy; // unsigned so negative numbers overflow to > maxXY to save negative checking int inc_x, inc_y, maxX, maxY; setPinwheelParameters(i, vW, vH, posx, posy, inc_x, inc_y, maxX, maxY); - unsigned totalSteps = getPinwheelSteps(vW, vH); static int pixelsdrawn; - /* - Note on the skipping algorithm: - - in the center, the angle is way too small for efficient drawing, pixels get overdrawn a lot, making it slow - - tracking the radius and deciding when to actually draw pixels significantly reduces the number of overdraws - - the number of angular steps required for a certain radius is in theory 2*pi*radius*sqrt(2) (worst case, in the corner) - - we can exploit that and only draw rays that are larger than a minimum step size (need to account for rounding errors) - */ + // if(i == 1) { Serial.print("drawn: "); Serial.println(pixelsdrawn); pixelsdrawn = 0;} + + posx = (posx + inc_x/2) ; // X starting position in fixed point + posy = (posy + inc_y/2) ; // Y starting position in fixed point + + unsigned steps = getPinwheelSteps(vW, vH); + int currentangle = (i * Fixed_Scale) / steps; // 360° = Fixed_Scale + int degree45 = Fixed_Scale / 8; // for readability + int degree90 = Fixed_Scale / 4; + int cornerfctr = 1 + cornerAdd * abs((degree45 - ((currentangle + degree45) % degree90))) / degree45; //1FFF is 45°, 3FFF is 90°, so this is 1FFF at 45°, falling off to both sides (triangular function) + int maxXY = vW > vH ? vW : vH; + int incfctr = incFactor; // the smaller the factor, the smaller the increment steps + if(maxXY < 16) + incfctr -= smallMatrixIncReduction; // smaller sizes need a higher increment resolution or pixel holes appear + + // Odd rays start further from center if prevRay started at center. + static int prevRay = INT_MIN; // previous ray number + bool odd = i & 1; + int jump = 0; + if (odd && (i - 1 == prevRay || i + 1 == prevRay)) { + jump = maxXY/3; // can add 2 if using medium pinwheel + } + else // there is room to optimize here + { + // int jumpstep = i % (maxXY/9); + // if (jumpstep > 2) jumpstep = 3; + if(maxXY > 8) + jump = (i % 3); + } + prevRay = i; + + posx += inc_x * jump; + posy += inc_y * jump; + + inc_x = inc_x * (incfctr-cornerfctr)/256; // reduce increments to to avoid pixel holes + inc_y = inc_y * (incfctr-cornerfctr)/256; // avoid re-painting the same pixel int lastX = INT_MIN; // impossible position int lastY = INT_MIN; - unsigned currentR = 0; // current radius in "pixels" - // draw ray until we hit any edge - while ((posx < maxX) && (posy < maxY)) { - int x = posx >> Fixed_Shift; - int y = posy >> Fixed_Shift; - if (x != lastX || y != lastY) { // only paint if pixel position is different - currentR++; - int requiredsteps = currentR * 8; // empirically found value (R is a rough estimation, always >= actual R) - int skipsteps = totalSteps/requiredsteps; - if(!skipsteps || i % (skipsteps) == 0) // check if pixel would 'overdraw' - { - setPixelColorXY(x, y, col); - } - } + + while ((posx < maxX) && (posy < maxY)) { + int cnt = 0; + int x = posx / Fixed_Scale; + if (x != lastX) // Only paint if the pixel is different + {setPixelColorXY(x, lastY, col);pixelsdrawn++;} + else + cnt++; + + int y = posy/(Fixed_Scale); + if(y != lastY) + {setPixelColorXY(x, y, col); pixelsdrawn++;} + else + cnt++; + lastX = x; lastY = y; - posx += inc_x; // advance to next position - posy += inc_y; + if(cnt == 2) { // no pixel drawn + posx += inc_x/3; // advance a bit + posy += inc_y/3; + } + else { + posx += inc_x; // advance to next position + posy += inc_y; + } + } break; } @@ -923,8 +964,8 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const int y = INT_MIN; while ((posx < maxX) && (posy < maxY)) { // scale down to integer (compiler will replace division with appropriate bitshift) -> not guaranteed - x = posx >> Fixed_Shift; - y = posy >> Fixed_Shift; + x = posx / Fixed_Scale; + y = posy / Fixed_Scale; // advance to next position posx += inc_x; posy += inc_y; From c7aa91ff8445ed343e096d45812789305ab22d36 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 11 Oct 2024 18:19:26 +0200 Subject: [PATCH 4/5] complete rework, based on idea from @Brandon502 - using Bresenham's line algorithm and block filling - on-the-fly angle calculation - bit map to avoid overdraw --- wled00/FX.cpp | 2 +- wled00/FX_fcn.cpp | 214 ++++++++++++++++++++++++---------------------- wled00/ws.cpp | 2 +- 3 files changed, 113 insertions(+), 105 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index e7429d19f1..3d14e45ccc 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1111,7 +1111,7 @@ uint16_t mode_running_random(void) { unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (unsigned i=SEGLEN-1; i > 0; i--) { + for (int i=SEGLEN-1; i >= 0; i--) { if (nzone || z >= zoneSize) { unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; diff --git a/wled00/FX_fcn.cpp b/wled00/FX_fcn.cpp index 27ee105c87..1ec334661a 100644 --- a/wled00/FX_fcn.cpp +++ b/wled00/FX_fcn.cpp @@ -637,36 +637,29 @@ uint16_t IRAM_ATTR_YN Segment::nrOfVStrips() const { // Constants for mapping mode "Pinwheel" #ifndef WLED_DISABLE_2D +uint32_t* pinWheelBitMap = NULL; // bit-map to track drawn pixels (managed in service() ) constexpr int Fixed_Scale = 16384; // fixpoint scaling factor (14bit for fraction) -constexpr float stepFactor = 4.2; // number of angle steps per size (empirically found) -constexpr int incFactor = 220; // Increment is multiplied by INCFACTOR / 256 (scaled with cornerAdd function) -constexpr int cornerAdd = 35; // smaller increments the closer the angle is to 45° -constexpr int smallMatrixIncReduction = 60; // smaller increments on smaller sizes to prevent holes -// note: these parameters work on sizes 2x2 up to 512x512 -// note: for 8x8,16x16,32x32 (and a few other sizes) these parameters work and use less overdraw: -// constexpr float stepFactor = 3.33; // number of angle steps per size (empirically found) -// constexpr int incFactor = 220; // Increment is multiplied by INCFACTOR / 256 (scaled with cornerAdd function) -// constexpr int cornerAdd = 35; // smaller increments the closer the angle is to 45° +constexpr float stepFactor = 1.6; // number of angle steps (rays = stepFacor * maxXY) // Pinwheel helper function: matrix dimensions to number of rays -static inline int getPinwheelSteps(int vW, int vH) { +static int getPinwheelSteps(int vW, int vH) { int maxXY = max(vW, vH); unsigned stepfactor = unsigned(stepFactor * Fixed_Scale); - //if(maxXY < 10) return MIN_STEPS; - return (maxXY * stepfactor) / Fixed_Scale; + return (maxXY * stepfactor) / Fixed_Scale; } -static void setPinwheelParameters(int i, int vW, int vH, unsigned& posx, unsigned& posy, int& inc_x, int& inc_y, int& maxX, int& maxY) { +static void setPinwheelParameters(int i, int vW, int vH, int& startx, int& starty, int* cosVal, int* sinVal, bool getPixel = false) { int steps = getPinwheelSteps(vW, vH); - float angleRad = float(i) * (DEG_TO_RAD * 360) / steps; //angle in radians - float cosVal = cos_t(angleRad); - float sinVal = sin_t(angleRad); - posx = (vW - 1) * Fixed_Scale / 2; // X starting position = center (in fixed point) - posy = (vH - 1) * Fixed_Scale / 2; // Y starting position = center (in fixed point) - inc_x = cosVal * Fixed_Scale; // X increment per step (fixed point) - inc_y = sinVal * Fixed_Scale; // Y increment per step (fixed point) - maxX = vW * Fixed_Scale; // X edge in fixedpoint - maxY = vH * Fixed_Scale; // Y edge in fixedpoint + int baseAngle = 0xFFFF / steps; // 360° / steps, in 16 bit scale + int rotate = 0; + if(getPixel) rotate = baseAngle / 2; // rotate by half a ray width when reading pixel color + for(int k = 0; k < 2; k++) // angular steps for two consecutive rays + { + cosVal[k] = (cos16((i + k) * baseAngle + rotate) * Fixed_Scale) >> 15; // step per pixel in fixed point, cos16 output is -0x7FFF to +0x7FFF + sinVal[k] = (sin16((i + k) * baseAngle + rotate) * Fixed_Scale) >> 15; // using explicit bit shifts as dividing negative numbers is not equivalent (rounding error is acceptable) + } + startx = (vW * Fixed_Scale) / 2 + cosVal[0] / 4; // starting position = center + 1/4 pixel (in fixed point) + starty = (vH * Fixed_Scale) / 2 + sinVal[0] / 4; } #endif @@ -762,78 +755,84 @@ void IRAM_ATTR_YN Segment::setPixelColor(int i, uint32_t col) for (int y = 0; y < i; y++) setPixelColorXY(i, y, col); break; case M12_sPinwheel: { - unsigned posx, posy; // unsigned so negative numbers overflow to > maxXY to save negative checking - int inc_x, inc_y, maxX, maxY; - setPinwheelParameters(i, vW, vH, posx, posy, inc_x, inc_y, maxX, maxY); - static int pixelsdrawn; - // if(i == 1) { Serial.print("drawn: "); Serial.println(pixelsdrawn); pixelsdrawn = 0;} - - posx = (posx + inc_x/2) ; // X starting position in fixed point - posy = (posy + inc_y/2) ; // Y starting position in fixed point - - unsigned steps = getPinwheelSteps(vW, vH); - int currentangle = (i * Fixed_Scale) / steps; // 360° = Fixed_Scale - int degree45 = Fixed_Scale / 8; // for readability - int degree90 = Fixed_Scale / 4; - int cornerfctr = 1 + cornerAdd * abs((degree45 - ((currentangle + degree45) % degree90))) / degree45; //1FFF is 45°, 3FFF is 90°, so this is 1FFF at 45°, falling off to both sides (triangular function) - int maxXY = vW > vH ? vW : vH; - int incfctr = incFactor; // the smaller the factor, the smaller the increment steps - if(maxXY < 16) - incfctr -= smallMatrixIncReduction; // smaller sizes need a higher increment resolution or pixel holes appear - - // Odd rays start further from center if prevRay started at center. - static int prevRay = INT_MIN; // previous ray number - bool odd = i & 1; - int jump = 0; - if (odd && (i - 1 == prevRay || i + 1 == prevRay)) { - jump = maxXY/3; // can add 2 if using medium pinwheel - } - else // there is room to optimize here - { - // int jumpstep = i % (maxXY/9); - // if (jumpstep > 2) jumpstep = 3; - if(maxXY > 8) - jump = (i % 3); - } - prevRay = i; - - posx += inc_x * jump; - posy += inc_y * jump; - - inc_x = inc_x * (incfctr-cornerfctr)/256; // reduce increments to to avoid pixel holes - inc_y = inc_y * (incfctr-cornerfctr)/256; - // avoid re-painting the same pixel - int lastX = INT_MIN; // impossible position - int lastY = INT_MIN; - - while ((posx < maxX) && (posy < maxY)) { - int cnt = 0; - int x = posx / Fixed_Scale; - if (x != lastX) // Only paint if the pixel is different - {setPixelColorXY(x, lastY, col);pixelsdrawn++;} - else - cnt++; - - int y = posy/(Fixed_Scale); - if(y != lastY) - {setPixelColorXY(x, y, col); pixelsdrawn++;} - else - cnt++; - - lastX = x; - lastY = y; - if(cnt == 2) { // no pixel drawn - posx += inc_x/3; // advance a bit - posy += inc_y/3; + if(pinWheelBitMap == NULL) break; // safety check + int startX, startY, cosVal[2], sinVal[2]; // in fixed point scale + setPinwheelParameters(i, vW, vH, startX, startY, cosVal, sinVal); + + unsigned maxLineLength = max(vW, vH) + 2; // pixels drawn is always smaller than dx or dy, +1 pair for rounding errors + uint16_t lineCoords[2][maxLineLength]; // uint16_t to save ram + int lineLength[2] = {0}; + + // draw two lines starting at at x0/y0, save the coordinates. based on idea by @Brandon502 + for (int lineNr = 0; lineNr < 2; lineNr++) { + int x0 = startX; // x / y coordinates in fixed scale + int y0 = startY; + int x1 = (startX + (cosVal[lineNr] * vW)); // outside of grid + int y1 = (startY + (sinVal[lineNr] * vH)); + const int dx = abs(x1-x0), sx = x0= vW || unsigned(y0) >= vH) break; // stop if outside of grid (exploit unsigned int overflow) + coordinates[idx++] = x0; + coordinates[idx++] = y0; + (*length)++; + // note: since endpoint is out of grid, no need to check if endpoint is reached + int e2 = 2 * err; + if (e2 >= dy) { err += dy; x0 += sx; } + if (e2 <= dx) { err += dx; y0 += sy; } } - else { - posx += inc_x; // advance to next position - posy += inc_y; + } + + // fill up the shorter line with missing coordinates, so block filling works correctly and efficiently + int diff = lineLength[0] - lineLength[1]; + int longLineIdx = (diff > 0) ? 0 : 1; + int shortLineIdx = longLineIdx ? 0 : 1; + if (diff != 0) { + int idx = (lineLength[shortLineIdx] - 1) * 2; // last valid coordinate index + int lastX = lineCoords[shortLineIdx][idx++]; + int lastY = lineCoords[shortLineIdx][idx++]; + bool keepX = lastX == 0 || lastX == vW - 1; + for (int d = 0; d < abs(diff); d++) { + lineCoords[shortLineIdx][idx] = keepX ? lastX :lineCoords[longLineIdx][idx]; + idx++; + lineCoords[shortLineIdx][idx] = keepX ? lineCoords[longLineIdx][idx] : lastY; + idx++; } + } + // draw and block-fill the line oordinates. Note: block filling only efficient if angle between lines is small + for (int idx = 0; idx < lineLength[longLineIdx] * 2;) { //!! should be long line idx! + int x1 = lineCoords[0][idx]; + int x2 = lineCoords[1][idx++]; + int y1 = lineCoords[0][idx]; + int y2 = lineCoords[1][idx++]; + int minX, maxX, minY, maxY; + (x1 < x2) ? (minX = x1, maxX = x2) : (minX = x2, maxX = x1); + (y1 < y2) ? (minY = y1, maxY = y2) : (minY = y2, maxY = y1); + // fill the block between the two x,y points + for (int x = minX; x <= maxX; x++) { + for (int y = minY; y <= maxY; y++) { + int index = y * vW + x; // calculate bitmap index, int_index, and bitmask for the current (x, y) + uint32_t int_index = index >> 5; // equivalent to index / 32 + uint32_t bitmask = 1U << (index & 31); // equivalent to index % 32 + // Check if the pixel has already been drawn + if (!(pinWheelBitMap[int_index] & bitmask)) { + pinWheelBitMap[int_index] |= bitmask; // set bit in map + setPixelColorXY(x, y, col); + } + } + } } - break; } + break; } return; } else if (Segment::maxHeight!=1 && (width()==1 || height()==1)) { @@ -955,21 +954,17 @@ uint32_t IRAM_ATTR_YN Segment::getPixelColor(int i) const break; case M12_sPinwheel: // not 100% accurate, returns pixel at outer edge - unsigned posx, posy; // unsigned so negative numbers overflow to > maxXY to save negative checking - int inc_x, inc_y, maxX, maxY; - setPinwheelParameters(i, vW, vH, posx, posy, inc_x, inc_y, maxX, maxY); - - // trace ray from center until we hit any edge - to avoid rounding problems, we use the same method as in setPixelColor - int x = INT_MIN; - int y = INT_MIN; - while ((posx < maxX) && (posy < maxY)) { - // scale down to integer (compiler will replace division with appropriate bitshift) -> not guaranteed - x = posx / Fixed_Scale; - y = posy / Fixed_Scale; - // advance to next position - posx += inc_x; - posy += inc_y; + int x, y, cosVal[2], sinVal[2]; + setPinwheelParameters(i, vW, vH, x, y, cosVal, sinVal, true); + int maxX = (vW-1) * Fixed_Scale; + int maxY = (vH-1) * Fixed_Scale; + // trace ray from center until we hit any edge - to avoid rounding problems, we use fixed point coordinates + while ((x < maxX) && (y < maxY) && (x > Fixed_Scale) && (y > Fixed_Scale)) { + x += cosVal[0]; // advance to next position + y += sinVal[0]; } + x /= Fixed_Scale; + y /= Fixed_Scale; return getPixelColorXY(x, y); break; } @@ -1380,6 +1375,15 @@ void WS2812FX::service() { // when cctFromRgb is true we implicitly calculate WW and CW from RGB values if (cctFromRgb) BusManager::setSegmentCCT(-1); else BusManager::setSegmentCCT(seg.currentBri(true), correctWB); + + #ifndef WLED_DISABLE_2D + if (seg.is2D() && seg.map1D2D == M12_sPinwheel) { + int bitArraySize = (seg.virtualHeight() * seg.virtualWidth() + 31) / 32; // 1 bit per pixel + //uint32_t tempmap[bitArraySize] = {0}; // put the bit-map for overdraw tracking on stack + //pinWheelBitMap = tempmap; // assign global pointer Note: using stack leads to crashes on larger sizes (write access error) + pinWheelBitMap = new uint32_t[bitArraySize](); // allocate the bit-map for overdraw tracking on heap and initialize to zero + } + #endif // Effect blending // When two effects are being blended, each may have different segment data, this // data needs to be saved first and then restored before running previous mode. @@ -1408,6 +1412,10 @@ void WS2812FX::service() { seg.next_time = nowUp + delay; } _segment_index++; + #ifndef WLED_DISABLE_2D + if(pinWheelBitMap != NULL) delete[] pinWheelBitMap; // free the memory + pinWheelBitMap = NULL; // invalidate pointer + #endif } _virtualSegmentLength = 0; _isServicing = false; diff --git a/wled00/ws.cpp b/wled00/ws.cpp index 45640b68ce..f233b54035 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -169,7 +169,7 @@ bool sendLiveLedsWs(uint32_t wsClient) #ifdef ESP8266 const size_t MAX_LIVE_LEDS_WS = 256U; #else - const size_t MAX_LIVE_LEDS_WS = 1024U; + const size_t MAX_LIVE_LEDS_WS = 4096U; #endif size_t n = ((used -1)/MAX_LIVE_LEDS_WS) +1; //only serve every n'th LED if count over MAX_LIVE_LEDS_WS size_t pos = 2; // start of data From b81e8c22682d90ca2cd64b87aed2c1c57d6534a1 Mon Sep 17 00:00:00 2001 From: Damian Schneider Date: Fri, 11 Oct 2024 18:23:34 +0200 Subject: [PATCH 5/5] reverting debug changes change in mode_running_random() (FX Stream) may actually be a bugfix, still removing it again --- wled00/FX.cpp | 2 +- wled00/ws.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 3d14e45ccc..1bc0ebdaaf 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -1111,7 +1111,7 @@ uint16_t mode_running_random(void) { unsigned z = it % zoneSize; bool nzone = (!z && it != SEGENV.aux1); - for (int i=SEGLEN-1; i >= 0; i--) { + for (int i=SEGLEN-1; i > 0; i--) { if (nzone || z >= zoneSize) { unsigned lastrand = PRNG16 >> 8; int16_t diff = 0; diff --git a/wled00/ws.cpp b/wled00/ws.cpp index f233b54035..45640b68ce 100644 --- a/wled00/ws.cpp +++ b/wled00/ws.cpp @@ -169,7 +169,7 @@ bool sendLiveLedsWs(uint32_t wsClient) #ifdef ESP8266 const size_t MAX_LIVE_LEDS_WS = 256U; #else - const size_t MAX_LIVE_LEDS_WS = 4096U; + const size_t MAX_LIVE_LEDS_WS = 1024U; #endif size_t n = ((used -1)/MAX_LIVE_LEDS_WS) +1; //only serve every n'th LED if count over MAX_LIVE_LEDS_WS size_t pos = 2; // start of data