Temporary Effects - AlbertGBarber/PixelSpork GitHub Wiki

Jump to:

Intro to Temporary Effects:

For a working example of temporary effects, see the "Temporary Effects" example in the library code.

In most of my programs I have a set of effects that I cycle through, only using one or two effects at a time. However, in the library's example code, I always create effects statically at the start of the program. Since I don't need all of the effects at all times, creating them all at once is technically a waste of memory. Instead, I could create effects on the fly, use them, and then de-allocate them as temporary effects.

Think of your micro-controller's memory as a stage. As I add more acts (effects), the stage gets more and more crowded -- there's only so much space! Meanwhile, lets say I only have one act active at one time. There's no point in having everyone else up there all the time. Instead it would be better to have people wait back stage (where there's more room) and only come on as needed, making them "temporary". This is the idea behind temporary effects.

In practice it is more complicated that this metaphor, but if this method saves memory, why don't I always do it?

Because:

  • Temporary effects are far more likely to cause bugs, especially if you don't know what you're doing. While a temporary effect is de-allocated, trying to update or change it in any way will crash your program, so you need to be careful to only adjust an effect when it is running (this can be trickier than it sounds).

  • You have to track your memory usage more closely. The compiler will not report any RAM used by temporary effects. So your program may compile just fine, but then find it doesn't have enough memory to create one of your temporary effects. To be more certain that you have enough memory, you should compile you code with each temporary effect replaced with a pre-allocated version (like in the library examples), doing this for one effect at a time and noting the memory usage. Even if you do this, you still may run into problems because some other piece of code may use dynamic memory that you were not aware of (particularly if you are using other libraries in addition to Pixel Spork)

  • They can create memory leaks. In C/C++, once a piece of memory is allocated, it is never de-allocated unless asked. So if you create a temporary effect, but forget to de-allocate it, the memory becomes locked. If you repeat this, eventually the micro-controller will run out of memory. This can be quite easy to do, because usually you access a temporary effect by tying a pointer to it. If you change the pointer's address before de-allocating the effect, then the effect becomes "lost", and you cannot de-allocate it.

  • They can cause memory fragmentation. When you create temporary effect, you allocate memory for its variables. When you remove the effect, you de-allocate the memory. This can leave holes in your memory (see fragmentation). There's no real way around this other than to program carefully. Temporary effects should only exist for as long as they need to. If you have multiple effects at once, they should be allocated and the de-allocated at the same time. MAKE SURE that nothing is being dynamically allocated while your effect(s) is active. This is not such a worry if you are using Pixel Spork as your only library and are following the fragmentation guidelines linked above, but I cannot speak for other libraries (although most shouldn't randomly re-allocate memory).

  • It only saves you RAM. Using temporary effects only saves you the memory allocated for an effect's static and dynamic variables (stored in RAM), it will not save you any programming memory because the compiler still has to store the instructions for the effect even when it's not active. If you are running out of programming space, temporary effects will not help you.

  • Creating and de-allocating effects takes time. The micro-controller has to do work to create and de-allocate an effect. If you need to rapidly swap between effects, temporary effects may slow your program down.

Overall, you should avoid using temporary effects unless you absolutely need them.

Creating, Using, and Destroying Temporary Effects:

I'll demonstrate temporary effects using an example below. In the example, I'll use an Effect set (see here for an effect set example), which is a utility class that helps manage groups of effects and/or other utilities. The class also includes some features that make it easier to work with temporary effects.

In the example, I'll create a static palette blender utility and a temporary streamer (SL) effect that will be re-created with each Arduino loop() cycle. To keep the example concise, I'll omit most of the common setup code that defines the segment set, assigns the LEDs to a pin, etc.

The palette blender is created like normal, before the Arduino setup() function:

//<Before Arduino setup()>
//Create two random palettes for the palette blender
CRGB palette1_arr[] = { colorUtilsPS::randColor(), colorUtilsPS::randColor() };
palettePS palette1 = {palette1_arr, SIZE(palette1_arr)};

CRGB palette2_arr[] = { colorUtilsPS::randColor(), colorUtilsPS::randColor() };
palettePS palette2 = {palette2_arr, SIZE(palette2_arr)};

//Create a palette blender utility, named "PB"
//Note that this is not a temporary effect/utility
PaletteBlenderPS PB(palette1, palette2, true, 30, 100);

We'll then create the effect set to store our palette blender and streamer:

//<Before Arduino setup()>
//Create an effect array for the effect set, it is length 2 to store our palette blender and streamer effect
//Since we already have out palette blender, we can add it to the array.
//However, our streamer doesn't exist yet, so we'll set its array entry as nullptr
EffectBasePS *effArray[2] = {&PB, nullptr};

//Create an effect set, named "effectSet" using the effect array, with a destruct limit of 1, and a default runtime of 10000
EffectSetPS effectSet(effectArr, SIZE(effectArr), 1, 10000);

In the code above, we first create a two entry effect array (one entry for the palette blender and streamer). Note that, since we already have our palette blender, we can add it to the effect array (via its address) as &PB. We'll leave the second array index as nullptr, since we'll be using it for our streamer effect.

We then use the array to create our effect set, named "effectSet". The effect set's constructor is slightly different from those in other examples, because we include an extra "destructLimit" argument, set to 1. This helps us de-allocate our streamer effect. The effect set utility includes a function, destructEffsAftLim(), which calls the destructor of all effects/utilities in the set's effect array at and after the destructLimit, de-allocating them all at once. This is incredibly helpful for handling multiple temporary effects, because you can easily de-allocated them without having to manage them individually.

However, typically, you'll have a set of utilities and/or effects, defined statically, as we've done with the palette blender, that you never want to de-allocate. By placing those utilities at the beginning of the effect set array, you can use the destructLimit to prevent them from being destructed. In our example, the palette blender is placed in the 0th index of our effect array, while the temporary streamer effect is placed in the 1 index. So, we set the destructLimit to 1 so that we only ever de-allocate the temporary effect.

Note that you must manage the destruct limit manually. Tying to de-allocate a static effect/utility will cause a crash.

With the effect set created, we need to create a pointer for our streamer effect. This will be explained later:

//<Before Arduino setup()>
//Create a pointer for our future streamer effect
StreamerSL *strem;

//Some extra variables we'll need
bool effectSetup = false;
uint8_t effectNum = 0;

Finally, we get to our main loop() function: (Note that I've wrapped the temporary effect in switch statement, which isn't needed with one effect, but will help if you wanted to cycle through multiple effects)

void loop(){
   
   //This switch statement isn't needed for our single streamer effect,
   //but typically you'd need one to cycle through multiple effects. 
   switch (effectNum) {
      case 0: default: {
          //If we are just starting the effect, we need to create it, add it to the effect set, and set a run time
          if ( !effectSetup ) {
            //Create the temporary streamer effect using new, and assign it to the strem pointer so we can access it later
            strem = new StreamerSL(mainSegments, PB.blendPalette, 4, 4, 0, 10, 50); 

            //Add the streamer to our effect set and set its runtime
            effectSet.runTime = 10000;
            effectSet.setEffect(strem, 1);
          } else {
            //for doing stuff while the effect is active
          }
       }
       break;
   }
   
   //In the switch statement above, we always setup an effect if needed.
   //So we can move the effectSetup flag handling code out of each switch case statement.
   //The code below is run once an effect is setup, so that we flag it not to setup again.
   //This is very important when using temporary effects, because creating a new effect before de-allocating the old will create a memory leak.
   if (!effectSetup) {
      def = true;
   }
   
   //update all the effects and utilities in the effect set
   effectSet.update();
   
   //The effect set will set its "done" flag to true once the "runTime" has passed, ie the effect has run for its set runTime
   //At this point we want to de-allocate the temporary effect, and swap to the next one.
   //In order, we:
   //Set the effectSetup flag, so we know to setup the next effect in the switch statement above,
   //call destructEffsAftLim() to de-allocate any temporary effects,
   //and reset the effect set, which tells the set to start tracking the time for the next effect,
   if (effectSet.done) {
      //since we only have one effect, the effectNum will stay 0
      effectNum = 0;  //(effectNum + 1) % numEffects; use this for multiple effects
      effectSetup = false;
      effectSet.destructEffsAftLim();
      effectSet.reset();
   }

}

Overall, the code is almost identical to the effect set cycling example. In each loop cycle we create an effect if we need to, update the effects/utilities in the effect set, then check if we need de-allocate our temporary effect and create a new one.

There are two notable sections:

  1. Creating the temporary effect:
if ( !effectSetup ) {
   strem = new StreamerSL(mainSegments, PB.blendPalette, 0, 4, 4, 10, 50);
   effectSet.runTime = 10000;
   effectSet.setEffect(strem, 1);
} ... etc
Here we are creating the temporary streamer effect using `new`. **Remember that this allocates the effect using dynamic memory, which must be de-allocated once the effect is done, or you'll create a memory leak.** Note that anything created with in the switch statement's `if()` is local to the statement. This means that any variables we create inside the `if()` only exist in the `if()`, including our streamer effect. To find the streamer from the rest of the code, we tie it to the `strem` pointer. **The pointer is our only access point to the temporary effect; it must not be re-assigned before the effect is de-allocated.** 

After creating the effect, we add it to the effect array at index 1. (The palette blender is at index 0).
  1. De-allocating the temporary effect:
if (effectSet.done) {
    //since we only have one effect, the effectNum will stay 0
    effectNum = 0;  //(effectNum + 1) % numEffects; use this for multiple effects
    effectSetup = false;
    effectSet.destructEffsAftLim();
    effectSet.reset();
 }

In the code above, we are cycling to the next effect once the current effect has reached its runtime (effectSet.done is true). Note the line effectSet.destructEffsAftLim();. This line is critical, in that it de-allocates all the sets temporary effects past the destructLimit (as discussed above). If we don't call destructEffsAftLim() then the memory for the previous effects will never be de-allocated, creating a memory leak.

That's all there is to using temporary effects, while they are more error prone than normal effects, as long as you are mindful of memory fragmentation, and allocate and de-allocate your effects correctly, they can allow you to really cram as many effects as possible onto your micro-controller.