From 65b91762fd91205e2404686d38090713d5a82bc1 Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Wed, 8 Oct 2025 22:48:53 -0400 Subject: [PATCH 1/2] Game of Life Rework RAM and speed optimizations. Better repeat detection. Mutation toggle. Blur option added. --- wled00/FX.cpp | 231 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 141 insertions(+), 90 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index c8b44a18..1b8bfee9 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5196,112 +5196,163 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f /////////////////////////////////////////// // 2D Cellular Automata Game of life // /////////////////////////////////////////// -typedef struct ColorCount { - CRGB color; - int8_t count; -} colorCount; +typedef struct Cell { + uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; +} Cell; -uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ and https://github.com/DougHaber/nlife-color +uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ + // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler if (!strip.isMatrix || !SEGMENT.is2D()) return mode_static(); // not a 2D set-up + const int cols = SEG_W, rows = SEG_H; + const unsigned maxIndex = cols * rows; - const int cols = SEG_W; - const int rows = SEG_H; - const auto XY = [&](int x, int y) { return (x%cols) + (y%rows) * cols; }; - const unsigned dataSize = sizeof(CRGB) * SEGMENT.length(); // using width*height prevents reallocation if mirroring is enabled - const int crcBufferLen = 2; //(SEGMENT.width() + SEGMENT.height())*71/100; // roughly sqrt(2)/2 for better repetition detection (Ewowi) + if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed + + Cell *cells = reinterpret_cast (SEGENV.data); - if (!SEGENV.allocateData(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed - CRGB *prevLeds = reinterpret_cast(SEGENV.data); - uint16_t *crcBuffer = reinterpret_cast(SEGENV.data + dataSize); + uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity + bool mutate = SEGMENT.check3; + uint8_t blur = map(SEGMENT.custom1, 0, 255, 255, 4); - CRGB backgroundColor = SEGCOLOR(1); + uint32_t bgColor = SEGCOLOR(1); + uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255); - if (SEGENV.call == 0 || strip.now - SEGMENT.step > 3000) { - SEGENV.step = strip.now; - SEGENV.aux0 = 0; - - //give the leds random state and colors (based on intensity, colors from palette or all posible colors are chosen) - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { - unsigned state = hw_random8()%2; - if (state == 0) - SEGMENT.setPixelColorXY(x,y, backgroundColor); - else - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); - } - - for (int y = 0; y < rows; y++) for (int x = 0; x < cols; x++) prevLeds[XY(x,y)] = CRGB::Black; - memset(crcBuffer, 0, sizeof(uint16_t)*crcBufferLen); - } else if (strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,4)) { - // update only when appropriate time passes (in 42 FPS slots) - return FRAMETIME; + bool setup = SEGENV.call == 0; + if (setup) { + // Calculate glider length LCM(rows,cols)*4 once + unsigned a = rows, b = cols; + while (b) { unsigned t = b; b = a % b; a = t; } + gliderLength = (cols * rows / a) << 2; } - //copy previous leds (save previous generation) - //NOTE: using lossy getPixelColor() is a benefit as endlessly repeating patterns will eventually fade out causing a reset - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) prevLeds[XY(x,y)] = SEGMENT.getPixelColorXY(x,y); + if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix + bool paused = SEGENV.step > strip.now; - //calculate new leds - for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { + // Setup New Game of Life + if ((!paused && generation == 0) || setup) { + SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds + generation = 1; + paused = true; + //Setup Grid + memset(cells, 0, maxIndex * sizeof(Cell)); - colorCount colorsCount[9]; // count the different colors in the 3*3 matrix - for (int i=0; i<9; i++) colorsCount[i] = {backgroundColor, 0}; // init colorsCount + for (unsigned i = maxIndex; i--; ) { + bool isAlive = !hw_random8(3); // ~33% + cells[i].alive = isAlive; + cells[i].faded = !isAlive; + unsigned x = i % cols, y = i / cols; + cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1); - // iterate through neighbors and count them and their different colors - int neighbors = 0; - for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix - if (i==0 && j==0) continue; // ignore itself - // wrap around segment - int xx = x+i, yy = y+j; - if (x+i < 0) xx = cols-1; else if (x+i >= cols) xx = 0; - if (y+j < 0) yy = rows-1; else if (y+j >= rows) yy = 0; - - unsigned xy = XY(xx, yy); // previous cell xy to check - // count different neighbours and colors - if (prevLeds[xy] != backgroundColor) { - neighbors++; - bool colorFound = false; - int k; - for (k=0; k<9 && colorsCount[k].count != 0; k++) - if (colorsCount[k].color == prevLeds[xy]) { - colorsCount[k].count++; - colorFound = true; - } - if (!colorFound) colorsCount[k] = {prevLeds[xy], 1}; //add new color found in the array - } - } // i,j - - // Rules of Life - uint32_t col = uint32_t(prevLeds[XY(x,y)]) & 0x00FFFFFF; // uint32_t operator returns RGBA, we want RGBW -> cut off "alpha" byte - uint32_t bgc = RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0); - if ((col != bgc) && (neighbors < 2)) SEGMENT.setPixelColorXY(x,y, bgc); // Loneliness - else if ((col != bgc) && (neighbors > 3)) SEGMENT.setPixelColorXY(x,y, bgc); // Overpopulation - else if ((col == bgc) && (neighbors == 3)) { // Reproduction - // find dominant color and assign it to a cell - colorCount dominantColorCount = {backgroundColor, 0}; - for (int i=0; i<9 && colorsCount[i].count != 0; i++) - if (colorsCount[i].count > dominantColorCount.count) dominantColorCount = colorsCount[i]; - // assign the dominant color w/ a bit of randomness to avoid "gliders" - if (dominantColorCount.count > 0 && hw_random8(128)) SEGMENT.setPixelColorXY(x,y, dominantColorCount.color); - } else if ((col == bgc) && (neighbors == 2) && !hw_random8(128)) { // Mutation - SEGMENT.setPixelColorXY(x,y, SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 255)); + SEGMENT.setPixelColorXY(x, y, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); } - // else do nothing! - } //x,y + } - // calculate CRC16 of leds - uint16_t crc = crc16((const unsigned char*)prevLeds, dataSize); - // check if we had same CRC and reset if needed - bool repetition = false; - for (int i=0; i= -1; i--) for (int j = 1; j >= -1; j--) if (i || j) { + int nX = x + j, nY = y + i; + if (cell.edgeCell) { + nX = (nX + cols) % cols; + nY = (nY + rows) % rows; + } + unsigned nIndex = nX + nY * cols; + Cell& neighbor = cells[nIndex]; + if (neighbor.alive) { + if (++neighbors > 3) break; + if (!neighbor.toggleStatus) { // Alive and not dying + parentIdx[aliveParents++] = nIndex; + } + } + } + + uint32_t newColor; + bool needsColor = false; + + if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation + cell.toggleStatus = 1; + if (blur == 255) cell.faded = 1; + newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColorXY(x, y), bgColor, blur); + needsColor = true; + } + else if (!cell.alive) { + if (neighbors == 3 && (!mutate || hw_random8(128)) || // Normal birth with 1/128 failure chance if mutate + (mutate && neighbors == 2 && !hw_random8(128))) { // Mutation birth with 2 neighbors with 1/128 chance if mutate + cell.toggleStatus = 1; + cell.faded = 0; + + if (aliveParents) { + // Set color based on random neighbor + unsigned parentIndex = parentIdx[random8(aliveParents)]; + birthColor = SEGMENT.getPixelColorXY(parentIndex % cols, parentIndex / cols); + } + newColor = birthColor; + needsColor = true; + } + else if (!cell.faded) {// No change, fade dead cells + uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); + uint32_t blended = color_blend(cellColor, bgColor, blur); + if (blended == cellColor) { blended = bgColor; cell.faded = 1; } + newColor = blended; + needsColor = true; + } + } + + if (needsColor) SEGMENT.setPixelColorXY(x, y, newColor); + } + // Loop through cells, if toggle, swap alive status + for (unsigned i = maxIndex; i--; ) { + cells[i].alive ^= cells[i].toggleStatus; + cells[i].toggleStatus = 0; + } + + if (repeatingOscillator || repeatingSpaceship || emptyGrid) { + generation = 0; // reset on next call + SEGENV.step += 1000; // pause final generation for 1 second + } + else { + ++generation; + SEGENV.step = strip.now; + } return FRAMETIME; } // mode_2Dgameoflife() -static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!;!,!;!;2"; +static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,,Blur,,,,,Mutation;!,!;!;2;pal=11,sx=128"; ///////////////////////// From cc0230f83f46a560a079ae2692de11677cc672a8 Mon Sep 17 00:00:00 2001 From: Brandon502 <105077712+Brandon502@users.noreply.github.com> Date: Tue, 14 Oct 2025 22:58:22 -0400 Subject: [PATCH 2/2] Game of Life Optimizations Adjust mutation logic. Use 1D get/set. Reduce code size. --- wled00/FX.cpp | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/wled00/FX.cpp b/wled00/FX.cpp index 1b8bfee9..0b9d0ce5 100644 --- a/wled00/FX.cpp +++ b/wled00/FX.cpp @@ -5197,7 +5197,7 @@ static const char _data_FX_MODE_2DFRIZZLES[] PROGMEM = "Frizzles@X frequency,Y f // 2D Cellular Automata Game of life // /////////////////////////////////////////// typedef struct Cell { - uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; + uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; } Cell; uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ @@ -5207,7 +5207,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: const unsigned maxIndex = cols * rows; if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed - + Cell *cells = reinterpret_cast (SEGENV.data); uint16_t& generation = SEGENV.aux0, &gliderLength = SEGENV.aux1; // rename aux variables for clarity @@ -5230,20 +5230,20 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: // Setup New Game of Life if ((!paused && generation == 0) || setup) { - SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds + SEGENV.step = strip.now + 1280; // show initial state for 1.28 seconds generation = 1; paused = true; //Setup Grid memset(cells, 0, maxIndex * sizeof(Cell)); - for (unsigned i = maxIndex; i--; ) { + for (unsigned i = 0; i < maxIndex; i++) { bool isAlive = !hw_random8(3); // ~33% cells[i].alive = isAlive; cells[i].faded = !isAlive; unsigned x = i % cols, y = i / cols; cells[i].edgeCell = (x == 0 || x == cols-1 || y == 0 || y == rows-1); - SEGMENT.setPixelColorXY(x, y, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); + SEGMENT.setPixelColor(i, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor); } } @@ -5251,22 +5251,21 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: // Redraw if paused or between updates to remove blur for (unsigned i = maxIndex; i--; ) { if (!cells[i].alive) { - uint32_t cellColor = SEGMENT.getPixelColorXY(i % cols, i / cols); + uint32_t cellColor = SEGMENT.getPixelColor(i); if (cellColor != bgColor) { uint32_t newColor; bool needsColor = false; - if (cells[i].faded) { newColor = bgColor; needsColor = true; } + if (cells[i].faded) { newColor = bgColor; needsColor = true; } else { uint32_t blended = color_blend(cellColor, bgColor, 2); if (blended == cellColor) { blended = bgColor; cells[i].faded = 1; } newColor = blended; needsColor = true; } - if (needsColor) SEGMENT.setPixelColorXY(i % cols, i / cols, newColor); + if (needsColor) SEGMENT.setPixelColor(i, newColor); } } } return FRAMETIME; - } // Repeat detection @@ -5286,8 +5285,8 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: unsigned neighbors = 0, aliveParents = 0, parentIdx[3]; // Count alive neighbors - for (int i = 1; i >= -1; i--) for (int j = 1; j >= -1; j--) if (i || j) { - int nX = x + j, nY = y + i; + for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) if (i || j) { + int nX = x + j, nY = y + i; if (cell.edgeCell) { nX = (nX + cols) % cols; nY = (nY + rows) % rows; @@ -5295,8 +5294,8 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: unsigned nIndex = nX + nY * cols; Cell& neighbor = cells[nIndex]; if (neighbor.alive) { - if (++neighbors > 3) break; - if (!neighbor.toggleStatus) { // Alive and not dying + neighbors++; + if (!neighbor.toggleStatus && neighbors < 4) { // Alive and not dying parentIdx[aliveParents++] = nIndex; } } @@ -5304,29 +5303,29 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: uint32_t newColor; bool needsColor = false; - + if (cell.alive && (neighbors < 2 || neighbors > 3)) { // Loneliness or Overpopulation cell.toggleStatus = 1; if (blur == 255) cell.faded = 1; - newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColorXY(x, y), bgColor, blur); + newColor = cell.faded ? bgColor : color_blend(SEGMENT.getPixelColor(cIndex), bgColor, blur); needsColor = true; } else if (!cell.alive) { - if (neighbors == 3 && (!mutate || hw_random8(128)) || // Normal birth with 1/128 failure chance if mutate - (mutate && neighbors == 2 && !hw_random8(128))) { // Mutation birth with 2 neighbors with 1/128 chance if mutate + byte mutationRoll = mutate ? hw_random8(128) : 1; // if 0: 3 neighbor births fail and 2 neighbor births mutate + if ((neighbors == 3 && mutationRoll) || (mutate && neighbors == 2 && !mutationRoll)) { // Reproduction or Mutation cell.toggleStatus = 1; cell.faded = 0; - + if (aliveParents) { // Set color based on random neighbor unsigned parentIndex = parentIdx[random8(aliveParents)]; - birthColor = SEGMENT.getPixelColorXY(parentIndex % cols, parentIndex / cols); + birthColor = SEGMENT.getPixelColor(parentIndex); } newColor = birthColor; needsColor = true; } else if (!cell.faded) {// No change, fade dead cells - uint32_t cellColor = SEGMENT.getPixelColorXY(x, y); + uint32_t cellColor = SEGMENT.getPixelColor(cIndex); uint32_t blended = color_blend(cellColor, bgColor, blur); if (blended == cellColor) { blended = bgColor; cell.faded = 1; } newColor = blended; @@ -5334,7 +5333,7 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: } } - if (needsColor) SEGMENT.setPixelColorXY(x, y, newColor); + if (needsColor) SEGMENT.setPixelColor(cIndex, newColor); } // Loop through cells, if toggle, swap alive status for (unsigned i = maxIndex; i--; ) { @@ -5344,8 +5343,8 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https: if (repeatingOscillator || repeatingSpaceship || emptyGrid) { generation = 0; // reset on next call - SEGENV.step += 1000; // pause final generation for 1 second - } + SEGENV.step += 1024; // pause final generation for ~1 second + } else { ++generation; SEGENV.step = strip.now;