Skip to content
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

Open
wants to merge 5 commits into
base: 0_15
Choose a base branch
from
Open
Changes from 2 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
139 changes: 57 additions & 82 deletions wled00/FX_fcn.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Collaborator Author

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

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.

Copy link
Collaborator

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).

}
// 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)
Copy link
Collaborator

@softhack007 softhack007 Oct 7, 2024

Choose a reason for hiding this comment

The 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.
And for me, "1 << bits" is harder to read than "32768" which is a well-known power of two.

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: constexpr int fixed_scale = 1U << fixed_shift;. The "U" is important here to stay within "well-defined behaviour".

Copy link
Collaborator

@softhack007 softhack007 Oct 7, 2024

Choose a reason for hiding this comment

The 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

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I read shifts easier than large numbers.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Then maybe constexpr int fixed_scale = 1U << fixed_shift;.  is a compromise that keeps both of us happy 😉 sorry again for me going into "lecture mode".

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

Expand All @@ -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
Expand Down Expand Up @@ -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;
Copy link
Collaborator

@softhack007 softhack007 Oct 7, 2024

Choose a reason for hiding this comment

The 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).
gcc is able to detect that mult/div by a constant with power of two can be done with a shift. But the compiler will pick the proper (arithmetic) shift.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator

Choose a reason for hiding this comment

The 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.....

Copy link
Collaborator

Choose a reason for hiding this comment

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

. I did notice a difference in negative and positive values (negative having less pixel holes)

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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@Brandon502 can you try with requiredsteps set to 9 instead of 8?

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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;
Expand Down Expand Up @@ -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;
Expand Down