-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
Segment layering/blending #4550
Comments
Your proposed pipeline is more-or-less how I'd've expected WLED to be implemented, before I got in to the actual code. I'd thought of it more of "segment" renders to a texture buffer; textures are then rendered to the frame buffer (with grouping, spacing, any other stretching/scaling transformations going on here); and finally the frame buffer is mapped to pixels on the outputs bus(es). Memory costs aside: we've observed in prototype implementations that having the FX write directly to local texture buffers ( Lastly I think the pipelined approach will also make the code simpler and easier to follow. The key is breaking "Segment" down into the component parts -- the FX renderer, the texture->frame renderer, and the physical mapper can all be pulled into individual stages and components instead of packing everything into one giant class. |
Thanks. This is an interesting approach which indeed simplifies single segment processing (and reduces memory requirements) but will inevitably create blending into "frame buffer" more complex (imagine two partially overlapping segments with spacing and grouping set). I would leave frame buffer to physical mapping in the bus level logic but that would make anything but Cartesian coordinate system difficult to implement (mapping arbitrary 3D location of pixels into frame rendering logic, especially sparse set-up). But we can leave this mapping out of the scope of this discussion. |
In general I think this is a good approach!
just an idea: use a "mask" checkmark in segment settings to treat an FX as a transparency mask instead of using its colors, the mask could be R+G+B or even something more elaborate. |
By "frame buffer", I mean what you are calling a "canvas buffer": a single global buffer of all LEDs in the virtual space to which all segments are rendered, top to bottom, for each output frame (eg. show() call). I would expect that segment spacing and grouping would be best implemented as part of the segment->canvas render process -- if output canvas coordinates overlap from one segment to the next, you blend; if not, they'll end up interleaved as expected. Mostly I was trying to highlight that I had expected an implementation with the same sequence of concepts, but I'd've used different names -- I don't think there's any significant difference between what I described and what you proposed. |
RGBA at the FX level would be very practical if we're serious about considering blending, I think... If we really want to get radical, I'd also float the idea of struct-of-arrays instead of array-of-structs, eg.
This is the true crux of this approach. If we move to this architecture, the solution might be to explicitly develop a sparse canvas buffer abstraction. Sparse matrices have a lot of prior art in numeric processing code, I'm sure there's some insights there that could be helpful. |
If we introduce alpha channel (instead of W in effect functions) I would assume it only applies to that segment in regards to blending it with lower segment. Right? I do admit that it will open new possibilities when writing new effects, but none of current effects are written to utilise it so there will be no immediate gain. So, if I summarize what we've defined so far:
Caveats:
|
Yes -- I'd expect segment-level opacity to apply "on top" of the FX computed alpha channel, much the same way segment-level brightness applies "on top" of the FX computed colors. IIRC the computation is something like
Maybe
Bikeshedding a bit, but I'd probably put the canvas to bus functionality in its own free function(s) to start off with. Some
I think it's reasonable to flush everything (segments, transitions, etc.) and start clean if the bus configuration changes. If we can't re-allocate everything from a clean slate, the config is in trouble anyways...
One neat feature of this architecture is that transitions can be implemented as part of canvas composition -- each segment need only contain enough information for the compositor to know which pixels to draw. So I'd suggest trying a broad approach of "don't re-use segments or buffers at all" as the place to start.
From there we can explore progressively more sophisticated fallbacks to handle cases where memory is tight. Some ideas:
The segment object itself shouldn't be big enough to matter (compared to the pixel buffers), so we can just allocate new ones whenever it's convenient.
This is on my todo list already -- the render loop really needs to be mutexed with any source of segment state changes. Every platform we support has multiple tasks, even ESP8266! I believe this may be responsible for some crash cases in some configurations in the current code. I haven't had time to look at it yet -- either we want a new "render state lock", or we can expand the scope of the current JSON buffer lock to cover any case of reading/writing the core state. |
Just a thought that sparked: We have to distinguish effect transitions (i.e. change form one effect to another), color transition and palette transition. Each of those apply to one segment only. With transition styles two effect runs may be necessary (this part needs tweaking). @willmmiles I will read your post again when I have more time and properly reply. |
I pushed a brief 1D untested POC. 2D shouldn't be more difficult but that was all I managed to conjure today. |
I think blending is now completely implemented. It may need a review from @DedeHai as raw setPixelColorXY had his optimisation which I moved into Segment's @willmmiles if you have the time please take a look. There are 3 relevant commits. |
I wish I had more time to spend - code would speak more precisely than words. :( I'm suggesting implementing all transitions as running the effect twice with different settings, and using dynamic blending to actually effect the visible transition. For example, for a brightness change: the old copy keeps running with the old brightness, the new copy runs with the new brightness, and the alpha used for blending shifts over time from one to the other. The basic implementation is some "struct transition_instructions" on each segment that the blending layer uses to modify the segment parameters before blending is executed. Traditional blending is done by altering the blending alpha; push/swipe/etc. alter the target canvas coordinates; and so forth. It makes the code simple and the transitions easy to implement, no matter what the actual transition style is. (In fact you also get clever things like transitions that work even if segment boundaries change!) Each transition style is implemented as a time-varying change to the blending, where "old" and "new" segments get opposite transition states (eg. one counts down, the other counts up). Then, once that's working well, allow FX to opt in to optimized transitions when the right conditions match. Each FX gets to decide what transition optimizations they support, eg. single FX call for color/palette blending, smooth settings changes, etc. -- only for certain blending styles, or only if segment size hasn't changed, and so on and so forth. The neat implemention trick is that these optimized blendings can be "just another bit of state" in the transition_instructions that adjusts the segment parameters before the FX renderer is called. You may note I am glossing over what happens to FX state data during transition. Truthfully, we need to move past
Thanks, I'll take a look when I've got time to do a proper review - probably won't be until later in the week. I'm rather behind at the moment. :( |
That's why all my replies tend to be short. 😄
That's how it is implemented in my branch.
Clipping pixels happen in All of the magic happens in Caveat: This approach may interfere with live data (i.e. DDP, Art-Net, E1.31) as that still directly writes to NPB. |
And ... it works!!!! |
I tested the layer blending today, awesome work!
We briefly discussed the white channel issue on Discord: the question is still if the buffers should be 32bit or 24bit. From my point of view, using RGB instead or RGBW buffers:
|
@DedeHai mirroring has (hopefully) been fixed. |
Freeze & double effect run fixed. |
@willmmiles @DedeHai @TripleWhy my recent changes (to prevent running effect twice in FADE transition mode if it didn't change) prompted me to a new thinking about temporary/transition data. Since the blending of segments is now relatively simple and quick (in my POC) it would make sense to completely ditch The workflow would be as follows:
The drawback is increased RAM usage as entire segment is copied, including its data and pixels (this may allow some really funny transitions though) but the benefit is simplified and quick drawing process. Did I write this clearly enough? Does it make sense? What are your thoughts? |
My understanding is that the temporary segment already does most of that right? at least in the PS, I can run two systems in parallel during transitions, meaning the data is already copied and all its properties too, so the big difference of your proposal is to not name it temporary but copy and also copy its buffer, which I thought is also needed for transitions. |
There is an issue when painting segment while in transition and the transition mode is push style. With segment copy each iteration of effect runs normally as it would if there were no transitions. It should paint its own pixel buffer, not needing to care what lies underneath (or above) or if it is shifted or not. Effect should not be aware if it is in transition as your current PS is (which I opposed). Once segment blending is invoked (during
This is already in place, check the code above. |
Yes, this is what I was proposing upthread! Transitions can be implemented as temporary
Consider a function: void WS2812FX::renderPixelBuffer(
uint32_t* pixels, size_t rows, size_t cols, /* source buffer; rows/cols is # of pixels to render from buffer */
size_t row_size, /* of source buffer only; allows arbitrary start point selection */
uint16_t x_start, int x_stride, size_t x_grouping, /* spacing implemented by stride; can be negative for reverse */
uint16_t y_start, int y_stride, size_t y_grouping,
bool transpose,
blend_mode_t blend_mode,
uint8_t opacity
bool* mask /* or equivalent, for pixel_dust; could use a deterministic calculation instead */
);
// NOTE! mirroring is not an argument to the above
// Mirroring is implemented by rendering the same source buffer more than once,
// with opposite start and reversed sign on stride Given this function, you can implement FADE by passing in modified opacities, while SWIPE and PUSH are done by computing modified output coordinate arguments and/or supplying Does this make sense?
It's not just json.cpp, all of the network interfaces should get checked. Really IMO any calls to update the FX state ought to be explicitly mutexed against the render loop. We've gotten away with a lot thus far because only the network interface runs on a separate thread, but I think it'd offer a lot more flexibility and reliability to be rigorous about mutexing state access. Taking a look at this has been on my list for a while but I haven't had much time - got caught up trying to upstream my AsyncTCP work. :( (In fact, I'd like to give serious thought to moving the render loop to its own task in the future. Yes, this can be done on 8266, too, through a couple of mechanisms; not all of which require a separate stack.) |
I need to read this a few more times and think about it, I am not sure I understand the old or the new proposed way correctly, but I trust your judgment on it.
Effects are actually not aware of transitions, just the particle system is and for a good reason: each effect takes up to 20k or RAM, so if you run transitions that means 40k for a single (large) segment. Sharing the buffer halfs the RAM use enabling more segments to run PS effects. The PS can be easily adjusted to render each FX to its segment buffer so same behaviour but each effect having its own particle buffer will crash an ESP quite quickly. |
Untested POC: https://github.com/blazoncek/WLED/tree/unified-blend EDIT: I think the POC is ready for testing. |
Awesome!
Not so awesome..? ^^ I believe segment copies are currently only used to detect if something changed after applying a json command. So I'd be careful as (I assume) the copy support has never been tested to a functional degree. Also note that, segments contain a lot of information.
Is it though? You do create a copy already when applying changes via json (which I believe are all changes?). What if you extended the lifetime of this copy that already exists? Btw. where would you store such a copy? In the segment I assume? If you do that, make sure you do not copy the copy when making a copy, otherwise you could end up with a lot of copies. I have actually considered the opposite before: Making Segment uncopyable; in my modular effect classes research. I did not get rid of TemporarySegmentData, but I did get rid of the swap functions. If you were to move the complete state of an effect into the effect (instead of reading it from the config each frame in each effect),
If only there was a solution that made this simple ;) |
With 0.14 we've got proper blending of color and palettes across all segments, 0.15 improved on that and allowed effects to be blended as well. Recent blending styles (#4158) introduced effect transitions with different styles.
What is missing IMO is correct segment blending/layering with different blending modes known to Photoshop users, i.e. lighten, darken, multiply, add, subtract, difference, etc.
I've recently come across a snippet that I find useful and relatively simple to implement in WLED. Unfortunately it would require re-implementation of pixel drawing functions.
Let's first see the code and then discuss the nuances:
This assumes that
Segment::getPixelColor()
returns unmodified value set bySegment::setPixelColor()
during effect execution. To achieve that, each segment must maintain its own pixel drawing buffer (also known in the past assetUpLeds()
)Next it also assumes
WS2812FX
instance will maintain its entire canvas buffer (calledpixels[]
; similar to global buffer).The process with which segments/layers would be blended is:
This process does not handle color/palette blending (which does not change from current) or effect transition (from one effect to another) but only allows users to stack segments one atop another which would allow mixing of content of two segments even if effect function does not support layering.
The price is of course memory and (possibly) speed degradation as there will be more operations per pixel. However, segment's
setPixelColor()
/getPixelColor()
could be simplified and there would be no need forWS2812FX::setPixelColor()
.I would like to get some feedback and thoughts about layering and the implementation. An if it is worth the effort even if speed would be impaired.
The text was updated successfully, but these errors were encountered: