Skip to content
Open
Changes from all 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
243 changes: 147 additions & 96 deletions wled00/FX.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment on lines +5199 to +5201
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Enforce 1-byte Cell packing at compile time.

Bitfield packing is implementation-defined. Add a static_assert to guarantee 1 byte per cell across toolchains.

 typedef struct Cell {
     uint8_t alive : 1, faded : 1, toggleStatus : 1, edgeCell: 1, oscillatorCheck : 1, spaceshipCheck : 1, unused : 2; 
 } Cell;
+static_assert(sizeof(Cell) == 1, "Cell must be 1 byte to meet memory/perf goals");
🤖 Prompt for AI Agents
In wled00/FX.cpp around lines 5199 to 5201, the Cell bitfield layout is
implementation-defined and may not be 1 byte; add a compile-time check
immediately after the typedef: use a static_assert that sizeof(Cell) == 1 with a
clear message (e.g., "Cell must be 1 byte") so builds fail if packing differs
across toolchains; keep the assert close to the struct definition for clarity.


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(dataSize + sizeof(uint16_t)*crcBufferLen)) return mode_static(); //allocation failed
CRGB *prevLeds = reinterpret_cast<CRGB*>(SEGENV.data);
uint16_t *crcBuffer = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize);

CRGB backgroundColor = SEGCOLOR(1);

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));
if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed

Cell *cells = reinterpret_cast<Cell*> (SEGENV.data);

Comment on lines +5209 to +5212
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix allocation size to match iteration domain (cols*rows).

You iterate maxIndex = cols*rows but allocate SEGMENT.length() cells. In some 2D mappings these can diverge. Allocate exactly maxIndex to avoid OOB writes on atypical mappings.

Apply this diff:

-  if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed
+  if (!SEGENV.allocateData(maxIndex * sizeof(Cell))) return mode_static(); // allocation failed
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!SEGENV.allocateData(SEGMENT.length() * sizeof(Cell))) return mode_static(); // allocation failed
Cell *cells = reinterpret_cast<Cell*> (SEGENV.data);
if (!SEGENV.allocateData(maxIndex * sizeof(Cell))) return mode_static(); // allocation failed
Cell *cells = reinterpret_cast<Cell*>(SEGENV.data);
🤖 Prompt for AI Agents
In wled00/FX.cpp around lines 5209 to 5212, the code allocates SEGMENT.length()
Cell entries but the iteration domain uses maxIndex = cols * rows, which can
differ for 2D mappings and cause out-of-bounds writes; change the allocation to
request maxIndex * sizeof(Cell) (i.e., allocate exactly cols*rows Cells), keep
the existing null-check and fallback to mode_static() on failure, and then cast
SEGENV.data to Cell* as before.

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

uint32_t bgColor = SEGCOLOR(1);
uint32_t birthColor = SEGMENT.color_from_palette(128, false, PALETTE_SOLID_WRAP, 255);

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;
}
Comment on lines +5223 to +5226
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Guard gliderLength against overflow.

gliderLength = LCM(rows,cols)*4 stored in uint16_t can overflow on larger grids. Use 32-bit during calc and clamp/store to aux1.

-    gliderLength = (cols * rows / a) << 2;
+    uint32_t gl = ((uint32_t)cols * (uint32_t)rows / a) << 2;
+    gliderLength = (gl > UINT16_MAX) ? UINT16_MAX : (uint16_t)gl;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
unsigned a = rows, b = cols;
while (b) { unsigned t = b; b = a % b; a = t; }
gliderLength = (cols * rows / a) << 2;
}
unsigned a = rows, b = cols;
while (b) { unsigned t = b; b = a % b; a = t; }
uint32_t gl = ((uint32_t)cols * (uint32_t)rows / a) << 2;
gliderLength = (gl > UINT16_MAX) ? UINT16_MAX : (uint16_t)gl;
}
🤖 Prompt for AI Agents
In wled00/FX.cpp around lines 5223 to 5226, the gliderLength calculation
(LCM(rows,cols)*4) can overflow uint16_t; compute using 32-bit temporaries: cast
rows and cols to uint32_t, compute gcd then lcm as (rows / gcd) * cols to avoid
intermediate overflow, multiply the 32-bit lcm by 4, clamp the result to the
maximum uint16_t value (0xFFFF) and then store the clamped value into aux1 (or
the uint16_t gliderLength), ensuring the assignment uses the clamped 16-bit
value.


if (abs(long(strip.now) - long(SEGENV.step)) > 2000) SEGENV.step = 0; // Timebase jump fix
bool paused = SEGENV.step > strip.now;

// 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));

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

SEGMENT.setPixelColorXY(x, y, isAlive ? SEGMENT.color_from_palette(hw_random8(), false, PALETTE_SOLID_WRAP, 0) : bgColor);
}
}

if (paused || (strip.now - SEGENV.step < 1000 / map(SEGMENT.speed,0,255,1,42))) {
// 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);
if (cellColor != bgColor) {
uint32_t newColor;
bool needsColor = false;
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);
}
}
}

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;
}

}

// Repeat detection
bool updateOscillator = generation % 16 == 0;
bool updateSpaceship = gliderLength && generation % gliderLength == 0;
bool repeatingOscillator = true, repeatingSpaceship = true, emptyGrid = true;

unsigned cIndex = maxIndex-1;
for (unsigned y = rows; y--; ) for (unsigned x = cols; x--; cIndex--) {
Cell& cell = cells[cIndex];

if (cell.alive) emptyGrid = false;
if (cell.oscillatorCheck != cell.alive) repeatingOscillator = false;
if (cell.spaceshipCheck != cell.alive) repeatingSpaceship = false;
if (updateOscillator) cell.oscillatorCheck = cell.alive;
if (updateSpaceship) cell.spaceshipCheck = cell.alive;

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;
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;
}
}
}

//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);

//calculate new leds
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {

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

// 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
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;
}
} // 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));
}
// 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<crcBufferLen && !repetition; i++) repetition = (crc == crcBuffer[i]); // (Ewowi)
// same CRC would mean image did not change or was repeating itself
if (!repetition) SEGENV.step = strip.now; //if no repetition avoid reset
// remember CRCs across frames
crcBuffer[SEGENV.aux0] = crc;
++SEGENV.aux0 %= crcBufferLen;
}

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";


/////////////////////////
Expand Down