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

Copy Segment FX #4124

Open
wants to merge 11 commits into
base: 0_15
Choose a base branch
from
Open

Copy Segment FX #4124

wants to merge 11 commits into from

Conversation

DedeHai
Copy link
Collaborator

@DedeHai DedeHai commented Sep 1, 2024

Added an FX that copies the selected source segment:

  • 1D and 2D supported
  • brightness of segment is relative to source segment
  • optionally shifts the color hue
  • invert, transpose, mirror etc. work
  • if source and target do not match in size, smallest size is copied
  • unused pixels fade to black (allows overlapping segments)
  • if invalid source ID is set, segment just fades to black
  • added a rgb2hsv conversion function as the fastled variant is inaccurate and buggy

note: 1D to 2D and vice versa is not supported

- copies the source segment
- brightness of segment is relative to source segment
- optionally shifts the color hue
- invert, transpose, mirror work
- if source or targets do not match in size, smallest size is copied
- unused pixels fade to black (allows overlapping segments)
- if invalid source ID is set, segment just fades to black
- added a rgb2hsv conversion function as the fastled variant is inaccurate and buggy

note: 1D to 2D and vice versa is not supported
@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 1, 2024

just realised: if the source segment is inverted, the targed does not invert (which is ok) but if the source is mirrored, only half of the pixels are copied unless the mirror settings in the target segment are the same.
Two options:
-this is a feature, i.e. leave it up to the user to make the settings identical
-silently copy the 'mirror' and 'transpose' setting from the source segment (without updating the UI)

@willmmiles
Copy link
Collaborator

just realised: if the source segment is inverted, the targed does not invert (which is ok) but if the source is mirrored, only half of the pixels are copied unless the mirror settings in the target segment are the same.

My quick suggestion would be to use .width(), etc. on the source segment instead of .virtualWidth(). This should essentially copy the rendered output, including invert, mirror, transpose, etc. Continue to use virtualWidth() for sizing the destination segment, though, and let the destination segment options be applied as normal.

I don't have a good setup for testing this but I'll give it a go when I can.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 4, 2024

I did not know about this distinction, that is actually what I was looking for :)
will change and test.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 4, 2024

that does not seem to work as intended. the numbers are correct (i.e. the width is not halfed if mirrored) but one half still stays black.
I am not sure what the best way to proceed is.
From a user perspective, I would expect that 'copy' means 'do the same as the source segment' meaning also the mirroring and grouping/spacing should be copied.

@willmmiles
Copy link
Collaborator

that does not seem to work as intended.

We are being foiled by getPixelColor()'s internal processing, I think, it's still doing some logical to physical pixel conversion. Possibly we'll need to do our own iteration using the raw segment coordinates (start, stop, spacing, group).

wled00/FX.cpp Outdated
Comment on lines 115 to 121
if(SEGMENT.custom2 > 0) // color shifting enabled
{
CHSV pxHSV = rgb2hsv(sourcecolor); //convert to HSV
pxHSV.h += SEGMENT.custom2; // shift hue
hsv2rgb_spectrum(pxHSV, sourcecolor); // convert back to RGB

}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This gets copy and pasted below - probably it wants to be a little static function instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

indeed, can you help out? I do not know what the best way to implement it is. just adding a function?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yup, pretty much. Something like:

static CRGB shift_hue(CRGB sourcecolor, uint8_t amount) {
  if (amount > 0) {
    CHSV pxHSV = rgb2hsv(sourcecolor); //convert to HSV
    pxHSV.h += amount; // shift hue
    hsv2rgb_spectrum(pxHSV, sourcecolor); // convert back to RGB       
   }
  return sourcecolor;
}

My inclination would be to leave it here in FX.cpp (hence static), to allow the compiler to inline it if it wants to. Organizationally though it might be a better fit for colors.cpp.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oh, I though you meant some fancy C++ macro magic ;)
but good point, will add this to color utils

Copy link
Collaborator

Choose a reason for hiding this comment

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

if the shift_hue function is on a critical path (like pixel copy), then it would be smarter to put the static function into fx.cpp so the compiler can inline. Inlined functions are faster (no overhead due to call-and-return).

But the compiler will not inline things that are defined in a different .cpp file.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 4, 2024

I tested just copying the source settings by doing
SEGMENT.mirror = strip._segments[sourceid].mirror; for example. that works, even UI gets updated (though not consistently)
by doing this I found a nasty bug.
if both x and y mirroring are set in a segment, the flipping is done incorrectly. the error is in line 214 of FX_2Dfcn.cpp (sorry, don't know how to reference that here directly). what I have is a 16x16 matrix, two segments, one x= 0-7, one x=8-15, both have y=0-15. if I set x and y mirroring in the right hand segment (the on going x=8-15) one quarter gets transferred to the first segment. that is independend of my copy function. here is what it looks like (line is the split between segments)
image
I lack the underlaying logic to fix it.

@blazoncek
Copy link
Collaborator

@DedeHai looks like start offsets are missing.

        strip.setPixelColorXY(start + width() - xX - 1, startY + height() - yY - 1, tmpCol);

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 4, 2024

looks like start offsets are missing.

correct, can confirm that adding start fixes it.

target segment now also copies the source settings (mirror, grouping, spacing, transpose) so the target looks exactly like the source. inverting can still be set independently.
note: the bug in line 214 of FX_2Dfcn.cpp missing `start` needs to be fixed for it to work properly in 2D.
@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 4, 2024

did some testing on the latest commit. it works fine with one exception: setting spacing other than 0 seems to screw things up. the target segment does weird things and I have no idea how to fix it.

@willmmiles
Copy link
Collaborator

I tested just copying the source settings by doing
SEGMENT.mirror = strip._segments[sourceid].mirror; for example. that works, even UI gets updated (though not consistently)

This feels wrong to me - I want to set my own mirroring or spacing settings on the destination segment and they should be respected, not overridden. However I expect the 'source data' to be the rendered output, including the source's mirroring/reversing/grouping etc.

Spacing is where things get tough, though. Spacing+offset is for creating interleaved segments. My gut feeling is to pretend the gaps don't exist when copying -- leave it to the destination segment to decide how to render those. I think this might be a bit tricky; I'll sketch some code.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 5, 2024

This feels wrong to me - I want to set my own mirroring or spacing settings on the destination segment and they should be respected, not overridden. However I expect the 'source data' to be the rendered output, including the source's mirroring/reversing/grouping etc.

it does feel wrong, but you cannot copy data that just is not there. like if source is mirrored, target cannot be 'unmirrored' without re-calculating the FX. 'reverse' is still possible (and is not copied).
offset is (currently) not copied and seems to work fine.

what I did not test at all yet: what happens if global buffer is disabled.

@willmmiles
Copy link
Collaborator

it does feel wrong, but you cannot copy data that just is not there. like if source is mirrored, target cannot be 'unmirrored' without re-calculating the FX. 'reverse' is still possible (and is not copied).

I don't think that's true - your original logic with virtualLength() and getPixelColor() calls operates on an unmirrored/unreversed/ungrouped "virtual pixel" index space, so that the FX code itself doesn't need to know about those flags. getPixelColor knows how to "undo" all of those features, since they're performed in setPixelColor.

That was the original concern that spawned this discussion, I thought; we wanted the copy to operate on the "rendered" mirrored/reversed/grouped FX output, so if the destination segment had no flags set, it would look completely identical to the source segment. I would then expect that any flags set on the destination are applied normally to the rendered copy. To implement this, it looks like we'd need a new "getRenderedPixelColor()" that has slightly different index calculation logic. I have a working 1D version at home, sorry I didn't get time to publish it last night.

That said: if we don't want to go that way, I think the reasonable alternative is to ask the user to manually harmonize the segment flags they want. For example, if the source segment is 8x16 (mirrored, so the FX calculates 8x8), I think it's reasonable for the copy operation to treat that as an 8x8 source (eg. exactly your original code); and then it's up to the user to set the mirror flag on the destination segment if they also want it mirrored.

To sum up: I think either we should copy from either the "rendered" space, for which we need new getPixelColor flavors; or from the "FX" space, ignoring the source segment flags, as your original code does. In either case the destination segment render flags should be set by the user, I really don't think we should override them.

Note I've completely passed over "spacing" and "offset" here -- IMO these options should be ignored by the copy operation as their purpose is to specify which pixels are /not/ part of the segment, and thus shouldn't be considered for the copy.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 5, 2024

I tried using .length() instead of virtual and confirmed the loop went over the full index range but if mirror of source is active, it sill only gets half of the data, the rest is still black (which is weird).
also if transpose was active, not using virtual did not result in a correct copy.
Anyway: the source buffer will contain the 'mirrored' FX data, so it does not matter whether the user sets the destination to 'unmirrored', 'reverse' will do the same thing (flips one axis).
I also played a little with manual setting of spacing and grouping (which also requires to use virtualLength() to be used) and found that it actually works but did not work if I automatically assigned the value.
I am not sure adding a new 'getPixelColor' will be necessary but I am curios on what that would look like.
I also have a solution for the 'copy source settings': since this is an FX: it has user selectable checkboxes. I added one already but the code to copy the settings is not done yet.
I also found that using fadeToBlackBy() is really slow (and maybe buggy?). It worked in 2D but when I switched to 1D with just 128+128 LEDs the FPS would (sometimes) drop to 8FPS (!). That went away without the fadeToBlackBy() so I am pretty sure that was the cause (I have no explanation on why it would be so slow, and only in 1D).
I also may have fixed the need for that shift_hue() function:
there is no need to distinguish the code between 1D and 2D, a 1D strip can be treated as a nx1 matrix (unless that is really slow for large strips? need to test some more. For WLED_DISABLE_2D there can be an ifdef with different code.

when you say 'rendered space' that is the buffer that is sent out and 'FX space' is that same buffer but with 'mapped' access right? I still don't fully grasp on what is mapped how as in the code it is really hard to grasp where a pixel color actually gets written to (or read from).

@blazoncek
Copy link
Collaborator

we'd need a new "getRenderedPixelColor()"

I did implement "raw" get/setPixelColor (in one of my POCs) that did no translations but it proved wrong and there were always ways that it did not work properly.
I'm not fond of copying segments, but that does not matter. I would suggest you rely on regular getPixelColor() and operate from destination segment. If you will exceed coordinates from source segment you'll get black so you'll copy only "active" pixels from source (however many there are).
"active" are what virtualLength, virtualWidth and virtualHeight are returning. Those are the pixels that effect operated on.
mirroring, reversing and transposing are post rendering activities, hidden from effect functions.

@willmmiles
Copy link
Collaborator

I tried using .length() instead of virtual and confirmed the loop went over the full index range but if mirror of source is active, it sill only gets half of the data, the rest is still black (which is weird).
Anyway: the source buffer will contain the 'mirrored' FX data, so it does not matter whether the user sets the destination to 'unmirrored', 'reverse' will do the same thing (flips one axis).

This is the expected behavior of getPixelColor - it's not accessing the source buffer directly, it's accessing the "virtual pixel" space, described by virtualLength etc. (what I called "FX" space earlier) . Mirrors/reverse/etc. do not apply to getPixelColor, you can think of them as being applied later.

when you say 'rendered space' that is the buffer that is sent out and 'FX space' is that same buffer but with 'mapped' access right? I still don't fully grasp on what is mapped how as in the code it is really hard to grasp where a pixel color actually gets written to (or read from).

I'd defined "render space" as "the pixels that are actually written to" without any gaps. This might just be my idiosyncratic way of of thinking about it though...

The way I think of it is:

Segment.setPixelColor()/Segment.getPixelColor() accept indexes in to "virtual pixel" space for each segment. These are the pixel indexes that are used by FX.

[ 1, 2, 3, 4 ]

FX space is mapped to a "render space" via mirror, transpose, reverse, grouping. (There are currently no functions to access this; I had proposed it as new concept to use as a copy source.)

mirror - note length is doubled from FX space
[ 1, 2, 3, 4, 4, 3, 2 1]

grouping=2, mirror, reverse - length x2 for mirror, x2 again for grouping
[ 4, 4, 3, 3, 2, 2, 1, 1, 1, 1, 2, 2, 3, 3, 4, 4]

"Render space" is then mapped to "strip space" with start, stop, spacing and offset. Note that spacing is applied in between groups. "strip space" can be accessed via strip.setPixelColor() and strip.getPixelColor(). (Xes are pixel indexes that are not part of the example segment.)

start=3, spacing = 1, offset = 0, mirror
[ X, X, X, 1, X, 2, X, 3, X, 4, X, 4, X, 3, X, 2, X, 1, X]

start=0, spacing = 1, offset = 1, mirror
[ X, 1, X, 2, X, 3, X, 4, X, 4, X, 3, X, 2, X, 1]

start=0, spacing = 1, offset = 0, grouping=2, mirror, reverse - length x2 for mirror, x2 again for grouping
[ 4, 4, X, 3, 3, X, 2, 2, X, 1, 1, X, 1, 1, X, 2, 2, X, 3, 3, X, 4, 4, X]

Finally "strip space" is mapped to actual LED addresses using ledmap.json. (Not shown, but I mention for completeness.)


Anyways it sounds like the right thing to copy is FX space. I still feel strongly that we should not be editing the segment properties in the FX function -- the software expects those to be inputs, not outputs, and you'll have a bunch of weird side effects on the UI and config management as the software doesn't expect those to be writable by FX.

there is no need to distinguish the code between 1D and 2D, a 1D strip can be treated as a nx1 matrix (unless that is really slow for large strips? need to test some more. For WLED_DISABLE_2D there can be an ifdef with different code.

Yeah I'd had a feeling we could get away with something there, but I hadn't worked through all the implications yet. There's also the 1D->2D conversion to consider; setPixelColor(index) knows how to do the conversion using the standard 2D expansions.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 5, 2024

Ah get it, if you access outside of 'virtual space' it does not return a valid color, hence getting black.
I agree that operating in 'FX space' is the safe way to do it and all the safety checks are already in place.
Not modifying the properties is probably also a valid point, especially if it does not get properly mapped back to the UI (which is what I saw, sometimes it would, sometimes it would not) it will result in incorrect presets.
a workaround would be to have that checkmark I mentioned and setting the properties just for rendering, then reverting them back. would that work or is data to the UI being processed in parallel (i.e. not just between frames)?

edit:
addendum: I tested to use 1D and 2D (i.e the XY versions) of getPixelColor and setPixelColor on a 2D strip and see no difference in speed (looked at FPS). There may be a tiny advantage on the 1D version but it is negligible.

@softhack007
Copy link
Collaborator

softhack007 commented Sep 5, 2024

did some testing on the latest commit. it works fine with one exception: setting spacing other than 0 seems to screw things up. the target segment does weird things and I have no idea how to fix it.

🤔 I think @willmmiles has described it quite accurate, maybe just a bit over-compilcated.

  • segments are the "drawing canvas" --> reverse, transpose, mirror, grouping and spacing are only applied at the moment when going from segment.setPixelColorXY() to strip.setPixelColorXY() .

  • in the 1D case, SEGLEN (aka Segment::virtualLength()) gives you the size of the active canvas.

  • In 2D, use Segment::virtualWidth() and Segment::virtualHeight().

  • strip is an intermediate layer, which basically performs mapping (ledmaps, gapmaps, 2D->1D) when going to down to busses.setPixelColor() (busses don't have any XY)

  • busses manages double buffering, and forwards pixels to the proper bus driver instance

As you want to copy effects ( = segments), I thinks its enough to copy what segment.getPixelColorXY provides - the raw pixels without grouping and spacing.

The target segment might not be the same size as the source segment -> so maybe implement your own version of "mirror" and "mirrorY", to preserve the overall look. But don't try to duplicate segments settings because the result will not look the same any way. Dimensions might be completely different, so the mirror axis will usually not match with the source segment.


Edit: sorry made a mistake im my original comment -
you'll want segment::virtualWidth() and segment::virtualHeight() for 2D segments, not segment::width() and segment::height().

@softhack007
Copy link
Collaborator

softhack007 commented Sep 5, 2024

PS: if you need a few nasty testcases

  • copy from a 2D segment running "Juggle" with "pinwheel" mapping, into another 2D segment with a different layout
  • copy from a 2D segment running "Pacifica" with pixels mapping, into a small 1D segment with grouping=2

@softhack007
Copy link
Collaborator

edit:
addendum: I tested to use 1D and 2D (i.e the XY versions) of getPixelColor and setPixelColor on a 2D strip and see no difference in speed (looked at FPS). There may be a tiny advantage on the 1D version but it is negligible

I'd say its better to always use the "right" versions - 1D or 2D - if you are on segment level. The "not XY" getPixelColor and setPixelColor versions will perform 1D->2D mapping, which is maybe not what you want.

Actually this function is a good example how to handle 1D and 2D

WLED/wled00/FX_fcn.cpp

Lines 1101 to 1111 in b9080f9

void Segment::fadeToBlackBy(uint8_t fadeBy) {
if (!isActive() || fadeBy == 0) return; // optimization - no scaling to apply
const int cols = is2D() ? virtualWidth() : virtualLength();
const int rows = virtualHeight(); // will be 1 for 1D
for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) {
if (is2D()) setPixelColorXY(x, y, color_fade(getPixelColorXY(x,y), 255-fadeBy));
else setPixelColor(x, color_fade(getPixelColor(x), 255-fadeBy));
}
}

(just except for the if (is2D()) inside the for loop, which costs performance)

@willmmiles
Copy link
Collaborator

willmmiles commented Sep 5, 2024

I think a lot of the challenge here comes from some of the corner cases.

If I have a segment 1 set as1x8, with mirror enabled, I get virtualLength() == 4, values [1 2 3 4], and what is drawn on segment 1 is [1 2 3 4 4 3 2 1]. If I then set "Copy" on Segment 2 (also 1x8) - what should I get?

The simple implementation (434ba3f) will give me [1 2 3 4 X X X X], where X is off. I have to set 'mirror' on segment 2 to make it actually look like a copy of segment 1. This seems counterintuitive, why don't I get a "whole" copy?

Supposing we accept that logic - if segment 1 is mirrored, we should render the mirrored output on segment 2. Mirror, reverse, and transpose all seem reasonable to copy from -- though as @softhack007 notes, copying the flags won't do what we're expecting if the segment sizes don't match. Grouping? Maybe - this is somewhat less clear. Spacing? Definitely not, weird stuff will happen.

What if segment 2 is 1x16, and has the mirror flag set on it? With the naiive code, I'd get [ 1 2 3 4 X X X X X X X X 4 3 2 1]? Or should I expect [1 2 3 4 4 3 2 1 1 2 3 4 4 3 2 1]?

This is why I added that idea of "render space" above - to capture what I think we want as the "copy source".

Anyhow, I suggest https://github.com/willmmiles/WLED/tree/pr/DedeHai/4124 as an example implementation - it's lame but I think it should copy the "obvious" way without disrespecting the target segment settings. Note I've only tested on 1D, I don't have a 2D system on my bench right now. :( Note that we don't need to check the source segment sizes - getRenderedPixelXY checks the coordinate validity and returns black if it's out of range - so we can also omit the fadetoblack, too.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 6, 2024

I tested temporarily setting the target segment settings to the sorce settings (which can be enabled in the FX by a check). This works but is not perfect (does not solve the things mentioned about non-equal sizes, mirroring will leave a gap in the center but that is a separate issue).
When settings in the target are changed, sometimes (with unlucky timing) they get reverted but that shows in the UI. There may be cases where it does not show in the UI and when changing the FX to some other, it may show wrong (unless the UI also sends out a segment setting update when FX is changed).
while not perfect, this is a 'workaround' that would enable users to change the source segment settings and have all targets do the same (I imagin someone having many slave segments, it would be really annoying to have to manually change each one every time).
Making it 'perfect' would require a semaphore or some other rework of code, which I personally think is way overkill for the edge cases.
As for copying the source including the mirror part, that could be done by a new 'getRenderedPixelXY' function or it could be directly handled in the FX (I am not sure which one is the better approach).

Edit:
I thought about this on my way to work and realized that if a getRenderedPixelXY() function is implemented, there is no need to copy the source settings (except maybe for grouping and spacing, those could still be copied the way I mentioned above or completely left to the user. So I am now in favour of using a getRenderedPixelXY(), question is just how robust does it have to be (and how much code we want to spend on edge cases).

@willmmiles
Copy link
Collaborator

I thought about this on my way to work and realized that if a getRenderedPixelXY() function is implemented, there is no need to copy the source settings (except maybe for grouping and spacing, those could still be copied the way I mentioned above or completely left to the user. So I am now in favour of using a getRenderedPixelXY(), question is just how robust does it have to be (and how much code we want to spend on edge cases).

I dug out a 2D panel and tested the implementation I sketched last night: https://github.com/willmmiles/WLED/tree/pr/DedeHai/4124 -- it seems like it handled every case I tried reasonably well, even 1D->2D conversion; though it's still missing the 2D->1D conversion case. Give it a go and let me know what you think?

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 6, 2024

I tested your version, there is a few bugs which I fixed and now it works amazingly well and does exactly what we discussed. There is still some cases I want to run but generally this is the way to go.
One thing that took me quite a while to pinpoint: 2D was all messed up until I figured out that seg.offset is not zero in my 2D config although that is not even an option to set in the UI (it is 6 for some reason). Before I commit my fixes: how is this to be handled? could do an if case (if there is any reason it is not zero) or find out why it is non zero and enforce it there?

@willmmiles
Copy link
Collaborator

Apparently I don't do enough with 2D. ;) Possibly the offset field is carrying some historical value, as it's not used in the standard 2D calculations.

I can think of two ways to approach fixing that: either (a) separate the 1D and 2D getRenderedPixelColor functions, to mirror the 1D and 2D getPixelColor; or (b) make sure that Segment::offset is always set to zero for 2D segments, instead of carrying around an old value. I lean towards the latter - I don't like unused values being copied around in the config - but the more experienced WLED devs might have a different opinion.

- added color adjust function with support for hue, lightness and brightness
- colors can now be adjusted in hue, saturation and brightness
- added  `getRenderedPixelXY()` function (credit @willmmiles)
- source is now accurately copied, even 1D->2D is possible
- fix for segment offset not being zero in 2D in `getRenderedPixelXY()`
- added checkmark to copy source grouping/spacing (may remove again)
- tested many different scenarios, everything seems to work
@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 7, 2024

latest commit now seems to handle all test cases (even the nasty ones). There are a few things that do not work properly though:

  • 2D to 1D copies only one row. since there is not really any good way to map it that all users will like (or makes sense) I left it at that
  • when enabling source grouping / spacing + mirroring, the target is not a 1:1 copy, even if settings are adjusted
  • when using spacing, 'in between' pixels are not always set to black (may be an underlying bug/feature as that also happens in the source segment if spacing is enabled and afterwards 'reverse' or 'mirror')

edit:
forgot to change the FX parameter string to 1D so the UI enables 2D expansion. fixed it locally, will be in the next commit.

edit2:
tested spacing and grouping with mirror again and it is correct if target is set to same settings (apart from the not cleared pixels issue)

@willmmiles
Copy link
Collaborator

when using spacing, 'in between' pixels are not always set to black (may be an underlying bug/feature as that also happens in the source segment if spacing is enabled and afterwards 'reverse' or 'mirror')

The pixels omitted by spacing are not part of the segment at all -- they're expected to belong to some other segment. Spacing+Offset lets you set up "interleaved" segments with different FX, eg with (spacing = 1, seg1: offset=0, seg2: offset=1) you can arrange [1 2 1 2 1 2 1 2] where you can set different FX on 1s and 2s.

@willmmiles
Copy link
Collaborator

Following on by that: I intentionally omitted source grouping and spacing in the getRenderedPixel() sketch. Grouping could perhaps be included, but spacing doesn't make sense -- the pixels skipped over by "spacing" are (meant to be) part of some other segment; including pixels from Segment 2 in a copy of Segment 1 seems wrong to me.

In the end it was a judgement call - mirror, reverse, transpose all feel to me like they're "part of" the segment FX rendering; while grouping and spacing feel more like they're part of the rendered FX->output strip mapping. YMMV, though.

- removed 'copy grouping/spacing' option
- added 'Switch axis' instead (in 2D -> 1D copy this chooses x or y axis to be copied)
- optimized for code size and speed by not using CRGB (where possible) and not returning struct in `rgb2hsv()`
@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 8, 2024

I have it mostly done now (if there are no objections to latest changes).
What remains though: where should the uint32_t getRenderedPixelXY() be put? I think it should go in FX_fcn.cpp but should it be part of the WS2812FX class or just a standalone function? Or just keep it in FX.h?

@willmmiles
Copy link
Collaborator

I have it mostly done now (if there are no objections to latest changes). What remains though: where should the uint32_t getRenderedPixelXY() be put? I think it should go in FX_fcn.cpp but should it be part of the WS2812FX class or just a standalone function? Or just keep it in FX.h?

If we're committed to this approach, I think it should be defined as a member of struct Segment, and implemented in FX_fcn.cpp beside getPixelColor -- as any future changes to getPixelColor or getPixelColorXY might also need to be applied to getRenderedPixelXY.

wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.cpp Outdated Show resolved Hide resolved
@blazoncek
Copy link
Collaborator

@DedeHai please merge blending styles branch (to a new branch) to see how different blending affects your new effect.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 24, 2024

sorry for messing up the blending styles branch...
I was now able to test this and it generally works fine. There is one case that is 'incorrect': if using two segments in 2D and the source segment (the one that is copied) is set to 'transpose' the blending style is of course not transposed on the other segment. For example if I set blending to 'Swipe left' one segment is blended left-right, one up-down, so not an exact copy anymore. This can not be avoided unless special treatment of blending the copy FX is implemented.
Blending style in general does not play so nicely with segments:
-if I have two segments, one running akemi, one crazy bees and I switch to a preset with one segment playing akemi, no blending is done (if the segment playing akemi is segment 0 in both cases). If I then switch back from 1 segment akemi to a different 2-segment preset, the second segment blends from crazy-bees (i.e. the last FX that was set on this segment number)
-I also have had two occasions, where FX parameters were set to 'default' (not the default from the FX but all sliders centered) instead of what was saved in the preset, which with copy segment is obvious as it just goes to black with a wrong source selected (not sure this is related to blending styles but never saw it before)

Copy link
Collaborator

@blazoncek blazoncek left a comment

Choose a reason for hiding this comment

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

I am not in favour of this PR as in my opinion copying segments (as different as they may be) is not the correct approach. It may also lead to user frustration in the long run.

As the underlying issue is "synchronised" segments (i.e. segments that mimic each other) a correct approach would be to execute effect function original segment uses on the destination segment with parameters (and timers and PRNG) of the original segment.

wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.cpp Outdated Show resolved Hide resolved
wled00/FX.h Outdated Show resolved Hide resolved
@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 24, 2024

As the underlying issue is "synchronised" segments (i.e. segments that mimic each other) a correct approach would be to execute effect function original segment uses on the destination segment with parameters (and timers and PRNG) of the original segment.

that would indeed be the best approach BUT: how do you control it? are FX with the same parameters always in sync (if started with the same timebase)? This whole endevour is mostly to sync particle effects, which are frame based (and I have no intention to change that as it makes collisions and speeds way more complex to calculate and therefore a lot slower, the only workaround would be to have fixed frame rates but that poses new issues).
so with each segment running a particle FX, you need to track the state of the PRNG so it can be repeatedly called by each segment with the same outcome (did not think this through all the way, there may be hidden issues with this approach). While this sounds doable, I don't really see the advantage over a 'copy segment' function. And I also do not see why this would lead to frustration on the user side.

@blazoncek
Copy link
Collaborator

Consider the complexity of overlapping segments.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 24, 2024

overlapping segments work fine, but you are correct that it is a bit complex i.e. the segment layering has to be done correctly. This is already the case for overlapping segments, adding copy FX on top does not add much to complexity.
A short description in the KB on how to use the copy FX should be sufficient.
IMHO having a feature that is useful for most users but not perfect for everyone is better than not having it at all.

@blazoncek
Copy link
Collaborator

I disagree.

Consider the following valid example:
Segment 0: spans entire matrix and displays Frizzles (or any asymmetric effect)
Segment 1: spans left half of the matrix and displays Black Hole (or whatever that does not paint every pixel)
Segment 2: spans right half of matrix and copies Segment 1

Segment 2 will show artifacts from underlying Segment 0 which is unwanted. I would consider this a flaw and would report a bug as I only want to copy Segment 1 and not the underlying segment.

If it is to be done, it has to be done correctly or not at all.

@willmmiles
Copy link
Collaborator

I think the implementation of "copy the rendered buffer including all underlays at that segment layer" is probably the correct behavior. It's true there might be some counterintuitive cases, but it offers a lot of opportunity for clever compositions. With some easy extensions, there are some really cool tricks we could use it for.

  • We could add a 'chroma key' feature, so there's a particular color (eg. black) that doesn't get copied
  • We could also add an alpha blended form for specifically for overlaying
  • Segments backed by virtual LEDs can be used as a render canvas that can then be cheaply copied/overlaid/transformed for multiple output segments for expensive fx

As with all powerful features -- like segment overlays themselves! -- there's going to be some sharp edges and cases where it takes a little planning to compose the FX you want to see. Still, I think this feature offers a surprisingly large amount of new capability for a minimum of code.

My $0.02, though!

@blazoncek
Copy link
Collaborator

it offers a lot of opportunity for clever compositions

Yes if that was optional. Now it is not. The solution is to optionally enable individual segment buffer for rendered pixels. So that user has an option if he/she wants underlying segments copied or not.

there are some really cool tricks we could use it for

I do not disagree with that.

To answer @DedeHai regarding "effect blending styles": Yes, not all styles will look great with every effect. I particularly dislike "push" variants as the outcome may be weird to say the least. Yet, they are there to be in line with #3669 . Still, the user has an option to select them or not. The code is also largely untested and there may be bugs still present.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 25, 2024

I fully agree with @willmmiles: while there are some limits in the current state of things, this PR adds a lot of value for the average use case.

Yes if that was optional. Now it is not. The solution is to optionally enable individual segment buffer for rendered pixels. So that user has an option if he/she wants underlying segments copied or not.

I see no problem in adding this later when (or if) individual segment buffers are implemented

Still, the user has an option to select them or not.

same goes for the copy FX :)

edit:
btw I have some changes on the way that will shrink this PR flash use (I hope by about 800bytes compared to current state)

- Bonus: saves over 1.2kB of flash
-also added a struct to handle HSV with 16bit hue better (including some conversions, can be extended easily)
- the functions are optimized for speed and flash use. They are faster and more accurate than what fastled offers (and use much less flash).
- replaced colorHStoRGB() with a call to the new hsv2rgb() function, saving even more flash
- the 16bit hue calculations result in an almost perfect conversion from RGB to HSV and back, the maximum error was 1/255 in the cases I tested.
@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 25, 2024

just FYI: with the latest commit the code size shrinks by 1.2kB, making it 200bytes smaller in total than vanilla 0.15 :)
edit:
I have no means to check if the colorHStoRGB()code is working correctly.

@blazoncek
Copy link
Collaborator

You have started to mix two different things into this PR.
Please reserve optimisations for your other PR.

@DedeHai
Copy link
Collaborator Author

DedeHai commented Sep 26, 2024

that is a good point. the changes are kind of 'required' to improve this PR, but keeping optimizations separated is cleaner. Will move it over to the optimization PR and revert the changes here.

@blazoncek
Copy link
Collaborator

... and revert the changes here.

IMO no need as this PR will be merged after the optimisations one. If you keep source files identical no ill should come out of it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants