-
-
Notifications
You must be signed in to change notification settings - Fork 3.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improvements to pinwheel algorithm, should work on any size and should also be faster #4185
base: 0_15
Are you sure you want to change the base?
Changes from 2 commits
d853fa6
4b54010
0cf98e4
c7aa91f
b81e8c2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 * 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; | ||
} | ||
|
||
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); | ||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as below - generally it's better to avoid shifting signed integers. I would propose to stay with my original implementation, using the scaling factor instead of shifting values around. You could even translate one into the other: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Update: this page from Microsoft explains the problem: https://learn.microsoft.com/en-us/cpp/cpp/left-shift-and-right-shift-operators-input-and-output?view=msvc-170 I think that c++20 (the c++ language definition from 2020) finally ended this by stating "Right-shift on signed integral types is an arithmetic right shift, which performs sign-extension.". But for older compilers, I would still not rely on "what happens when I shift a negative value". Some more explanation is here: https://stackoverflow.com/questions/7522346/right-shift-and-signed-integer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I read shifts easier than large numbers. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Then maybe |
||
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 | ||
} | ||
#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,47 +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: { | ||
// 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); | ||
|
||
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; // 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. | ||
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 | ||
posx += inc_x * jump; | ||
posy += inc_y * jump; | ||
} | ||
prevRay = i; | ||
|
||
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)) { | ||
// scale down to integer (compiler will replace division with appropriate bitshift) | ||
int x = posx / Fixed_Scale; | ||
int y = posy / Fixed_Scale; | ||
// set pixel | ||
if (x != lastX || y != lastY) setPixelColorXY(x, y, col); // only paint if pixel position is different | ||
while ((posx < maxX) && (posy < maxY)) { | ||
int x = posx >> Fixed_Shift; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did you check that shift works properly on signed integer? The reason I initially used multiplication and division is: shifting a signed integer is undefined (or compiler defined) behaviour - depending on architecture you might end with either an arithmetic shift (preserving sign) or with a logical shift (deleting sign). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that is good info. I did notice a difference in negative and positive values (negative having less pixel holes) I will try with multiplications again. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry if I sounded a bit like "lecturing". It's a lesson I've learned some time ago, after spending days to understand why negative values suddenly changed sign..... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This could also be an issue with rounding - negative values "jump" to a different side when cutting away the fraction part. So possibly there are more "off by 1" cases for negative values. |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Brandon502 can you try with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tried 9, 10, and 12. I haven't seen any holes using 12 up to 64x64. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the larger the number, the more overdraws and lower FPS, there are two more parameters to tweak, I tried a few options comparing holes vs FPS and current state was my best finding on 32x32 but did not check many other sizes |
||
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; | ||
|
@@ -929,27 +914,17 @@ 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 | ||
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)) { | ||
// scale down to integer (compiler will replace division with appropriate bitshift) | ||
x = posx / Fixed_Scale; | ||
y = posy / Fixed_Scale; | ||
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; | ||
// advance to next position | ||
posx += inc_x; | ||
posy += inc_y; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Brandon502 you can tweak the 5 here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One thing we learned when we were optimizing before was that Esp32 and S3 performed differently with rounding. I was able to have lower number of rays on my setup with a esp32 but @softhack007 had holes in his S3 setup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah yes, I remember.
Actually I could test with my 128x64 (hub75) -S3 setup once you have good parameters - just tell me where to tweak, and give me a few days (a lot of work@job coming this week).