Merge pull request #133 from Brandon502/mdev

Game of Life Memory Leak Fix + Other Changes

Fixed memory leak using new. Removed gameoflife struct and reverted back to using pointers to segenv.data (similar to starLeds version).

Added Blur slider. Blurs dead cells slowly to bgColor depending on slider instead of immediately dying. Doesn't effect game logic.

Removed confusing pauseFrames, uses .step to pause now.

Keeps track of last palette used and immediately recolors cells if new palette/color selected. Can be improved.

Changes gameSpeed slider to be more consistent. Speed = updates/second now in increments of 4.
This commit is contained in:
Frank
2024-06-23 00:17:15 +02:00
committed by GitHub

View File

@@ -5164,66 +5164,61 @@ static void setBitValue(uint8_t* byteArray, size_t n, bool value) {
else else
byteArray[byteIndex] &= ~(1 << bitIndex); byteArray[byteIndex] &= ~(1 << bitIndex);
} }
// create game of life struct to hold cells and future cells
struct gameOfLife {
uint8_t* cells;
uint8_t* futureCells;
uint8_t gliderLength;
uint16_t oscillatorCRC;
uint16_t spaceshipCRC;
};
uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https://natureofcode.com/book/chapter-7-cellular-automata/ 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 // and https://github.com/DougHaber/nlife-color , Modified By: Brandon Butler
if (!strip.isMatrix) return mode_static(); // not a 2D set-up if (!strip.isMatrix) return mode_static(); // not a 2D set-up
const uint16_t cols = SEGMENT.virtualWidth(); const uint16_t cols = SEGMENT.virtualWidth();
const uint16_t rows = SEGMENT.virtualHeight(); const uint16_t rows = SEGMENT.virtualHeight();
const size_t dataSize = (SEGMENT.length() / 8) + (((SEGMENT.length() % 8) != 0) ? 1 : 0); // add one byte when extra bits needed (length not a multiple of 8) const size_t dataSize = ((SEGMENT.length() + 7) / 8); // round up to nearest byte
const size_t totalSize = dataSize*2 + sizeof(gameOfLife); const size_t detectionSize = sizeof(uint16_t) * 3; // 2 CRCs, gliderLength
const size_t totalSize = dataSize * 2 + detectionSize + sizeof(uint8_t); // detectionSize + prevPalette
if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed if (!SEGENV.allocateData(totalSize)) return mode_static(); //allocation failed
gameOfLife* gol = reinterpret_cast<gameOfLife*>(SEGENV.data); byte *cells = reinterpret_cast<byte*>(SEGENV.data);
byte *futureCells = reinterpret_cast<byte*>(SEGENV.data + dataSize);
uint16_t *gliderLength = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize * 2);
uint16_t *oscillatorCRC = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize * 2 + sizeof(uint16_t));
uint16_t *spaceshipCRC = reinterpret_cast<uint16_t*>(SEGENV.data + dataSize * 2 + sizeof(uint16_t) * 2);
uint8_t *prevPalette = reinterpret_cast<uint8_t*>(SEGENV.data + dataSize * 2 + detectionSize);
if (gol->cells == nullptr) { uint16_t &generation = SEGENV.aux0; //Rename SEGENV/SEGMENT variables for readability
gol->cells = new uint8_t[dataSize]; bool allColors = SEGMENT.check1;
gol->futureCells = new uint8_t[dataSize]; bool overlayBG = SEGMENT.check2;
} bool wrap = SEGMENT.check3;
byte blur = map(SEGMENT.custom1, 0, 255, 255, 0);
uint16_t &generation = SEGENV.aux0; //rename aux0 and aux1 for readability (not needed) bool bgBlendMode = SEGMENT.custom1 > 220 && !overlayBG; // if blur is high and not overlaying, use bg blend mode
uint16_t &pauseFrames = SEGENV.aux1; byte bgBlur = map(SEGMENT.custom1 - 220, 0, 35, 255, 128);
CRGB backgroundColor = SEGCOLOR(1); uint32_t bgColor = SEGCOLOR(1);
CRGB color; uint32_t color = allColors ? random16() * random16() : SEGMENT.color_from_palette(0, false, PALETTE_SOLID_WRAP, 0);
uint16_t cIndex;
if (SEGENV.call == 0) { if (SEGENV.call == 0) {
SEGMENT.setUpLeds(); SEGMENT.setUpLeds();
SEGMENT.fill(BLACK); // to make sure that segment buffer and physical leds are aligned initially SEGMENT.fill(BLACK); // to make sure that segment buffer and physical leds are aligned initially
} }
//start new game of life // Setup New Game of Life
if ((SEGENV.call == 0 || generation == 0) && pauseFrames == 0) { if ((SEGENV.call == 0 || generation == 0) && SEGENV.step < strip.now) {
SEGENV.step = strip.now; // .step = previous call time SEGENV.step = strip.now + 1250; // show initial state for 1.25 seconds
generation = 1; generation = 1;
pauseFrames = 75; // show initial state for longer *prevPalette = SEGMENT.palette;
random16_set_seed(strip.now>>2); //seed the random generator random16_set_seed(strip.now>>2); //seed the random generator
//Setup Grid //Setup Grid
memset(cells, 0, dataSize);
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
uint8_t state = (random8() < 82) ? 1 : 0; // ~32% chance of being alive cIndex = y * cols + x;
if (state == 0) { if (random8(100) < 32) { // ~32% chance of being alive
setBitValue(gol->cells, y * cols + x, false); setBitValue(cells, cIndex, true);
setBitValue(gol->futureCells, y * cols + x, false); if (!overlayBG) SEGMENT.setPixelColorXY(x,y, bgColor); // Initial color set in redraw loop
if (SEGMENT.check2) continue;
SEGMENT.setPixelColorXY(x,y, !SEGMENT.check1?backgroundColor : RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0));
}
else {
setBitValue(gol->cells, y * cols + x, true);
setBitValue(gol->futureCells, y * cols + x, true);
color = SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
SEGMENT.setPixelColorXY(x,y,!SEGMENT.check1?color : RGBW32(color.r, color.g, color.b, 0));
} }
} }
memcpy(futureCells, cells, dataSize);
//Clear CRCs //Set CRCs
gol->oscillatorCRC = 0; uint16_t crc = crc16((const unsigned char*)cells, dataSize);
gol->spaceshipCRC = 0; *oscillatorCRC = crc;
*spaceshipCRC = crc;
//Calculate glider length LCM(rows,cols)*4 //Calculate glider length LCM(rows,cols)*4
uint8_t a = rows; uint8_t a = rows;
@@ -5233,108 +5228,125 @@ uint16_t mode_2Dgameoflife(void) { // Written by Ewoud Wijma, inspired by https:
b = a % b; b = a % b;
a = t; a = t;
} }
gol->gliderLength = cols * rows / a * 4; *gliderLength = cols * rows / a * 4;
return FRAMETIME;
} }
//Redraw immediately if overlay to avoid flicker
if (SEGMENT.check2) { int aliveCount = 0; // Solo glider detection
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { bool blurDead = SEGENV.step > strip.now && blur && !bgBlendMode && !overlayBG;
//redraw foreground/alive bool palChanged = SEGMENT.palette != *prevPalette && !allColors;
if (getBitValue(gol->cells, y * cols + x)) { if (palChanged) *prevPalette = SEGMENT.palette;
color = SEGMENT.getPixelColorXY(x,y);
SEGMENT.setPixelColorXY(x,y, !SEGMENT.check1?color : RGBW32(color.r, color.g, color.b, 0)); // Redraw Loop
} // Redraw if paused (remove blur), palette changed, overlaying background (avoid flicker)
// Always redraw dead cells if not overlaying background. Allows overlayFG by default.
// Generation 1 draws alive cells randomly and fades dead cells
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
cIndex = y * cols + x;
bool alive = getBitValue(cells, cIndex);
if (alive) aliveCount++;
uint32_t cellColor = SEGMENT.getPixelColorXY(x,y);
bool aliveBgColor = (alive && !overlayBG && generation == 1 && cellColor == bgColor);
if ( alive && (palChanged || (aliveBgColor && !random(12)))) { // Palette change or spawn initial colors randomly
uint32_t randomColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
SEGMENT.setPixelColorXY(x,y, randomColor); // Recolor alive cells
aliveBgColor = false;
} }
else if ( alive && overlayBG) SEGMENT.setPixelColorXY(x,y, cellColor); // Redraw alive cells for overlayBG
if (!alive && palChanged && !overlayBG) SEGMENT.setPixelColorXY(x,y, bgColor); // Remove blurred cells from previous palette
else if (!alive && blurDead) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, blur));// Blur dead cells (paused)
else if ((!alive || aliveBgColor) && !overlayBG && !bgBlendMode) SEGMENT.setPixelColorXY(x,y, cellColor); // Redraw dead cells/alive off cells for default overlayFG
else if (!alive && !overlayBG && generation == 1) SEGMENT.setPixelColorXY(x,y, color_blend(cellColor, bgColor, 16)); // Fade dead cells (bgBlendMode) on generation 1
} }
if (pauseFrames || strip.now - SEGENV.step < FRAMETIME_FIXED * (uint32_t)map(SEGMENT.speed,0,255,64,2)) {
if(pauseFrames) pauseFrames--; if (SEGENV.step > strip.now || strip.now - SEGENV.step < 1000 / (uint32_t)map(SEGMENT.speed,0,255,1,64)) return FRAMETIME; //skip if not enough time has passed (1-64 updates/sec)
return FRAMETIME; //skip if not enough time has passed
}
//Update Game of Life //Update Game of Life
bool cellChanged = false; // Detect still live and dead grids bool cellChanged = false; // Detect still live and dead grids
//cell index and coordinates //cell coordinates
uint16_t cIndex;
uint16_t cX; uint16_t cX;
uint16_t cY; uint16_t cY;
//Loop through all cells. Count neighbors, apply rules, setPixel //Loop through all cells. Count neighbors, apply rules, setPixel
for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) { for (int x = 0; x < cols; x++) for (int y = 0; y < rows; y++) {
byte neighbors = 0; byte neighbors = 0;
byte colorCount = 0; //track number of valid colors byte colorCount = 0; //track number of valid colors
CRGB nColors[3]; // track 3 colors, dying cells may overwrite but this wont be used uint32_t nColors[3]; // track 3 colors, dying cells may overwrite but this wont be used
for (int i = -1; i <= 1; i++) for (int j = -1; j <= 1; j++) { // iterate through 3*3 matrix 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 if (i == 0 && j == 0) continue; // ignore itself
if (!SEGMENT.check3 || generation % 1500 == 0) { //no wrap disable wrap every 1500 generations to prevent undetected repeats if (!wrap || generation % 1500 == 0 || aliveCount == 5) { //no wrap, disable wrap every 1500 generations to prevent undetected repeats
cX = x+i; cX = x + i;
cY = y+j; cY = y + j;
if (cX < 0 || cY < 0 || cX >= cols || cY >= rows) continue; //skip if out of bounds if (cX < 0 || cY < 0 || cX >= cols || cY >= rows) continue; //skip if out of bounds
} else { //wrap around } else { //wrap around
cX = (x+i+cols) % cols; cX = (x + i + cols) % cols;
cY = (y+j+rows) % rows; cY = (y + j + rows) % rows;
} }
cIndex = cY * cols + cX; cIndex = cY * cols + cX; //neighbor cell index
// count neighbors and store upto 3 neighbor colors // count neighbors and store upto 3 neighbor colors
if (getBitValue(gol->cells, cIndex)) { //if alive if (getBitValue(cells, cIndex)) { //if alive
neighbors++; neighbors++;
if (!getBitValue(futureCells, cIndex) || SEGMENT.getPixelColorXY(cX,cY) == bgColor) continue; //parent just died, color lost
color = SEGMENT.getPixelColorXY(cX, cY); color = SEGMENT.getPixelColorXY(cX, cY);
if (color == backgroundColor) continue; //parent just died, color lost nColors[colorCount % 3] = color;
nColors[colorCount%3] = color;
colorCount++; colorCount++;
} }
} }
// Rules of Life // Rules of Life
bool cellValue = getBitValue(gol->cells, y * cols + x); cIndex = y * cols + x; //current cell index
bool cellValue = getBitValue(cells, cIndex);
if ((cellValue) && (neighbors < 2 || neighbors > 3)) { if ((cellValue) && (neighbors < 2 || neighbors > 3)) {
// Loneliness or overpopulation // Loneliness or Overpopulation
cellChanged = true; cellChanged = true;
setBitValue(gol->futureCells, y * cols + x, false); setBitValue(futureCells, cIndex, false);
if (!SEGMENT.check2) SEGMENT.setPixelColorXY(x,y, !SEGMENT.check1?backgroundColor : RGBW32(backgroundColor.r, backgroundColor.g, backgroundColor.b, 0)); // Blur/turn off dying cells
if (!overlayBG) SEGMENT.setPixelColorXY(x,y, color_blend(SEGMENT.getPixelColorXY(x,y), bgColor, bgBlendMode ? bgBlur : blur));
} }
else if (!(cellValue) && (neighbors == 3)) { else if (!(cellValue) && (neighbors == 3)) {
// Reproduction // Reproduction
setBitValue(gol->futureCells, y * cols + x, true); setBitValue(futureCells, cIndex, true);
cellChanged = true; cellChanged = true;
// find dominant color and assign it to a cell // find dominant color and assign it to a new born cell no longer storing colors, if parent dies the color is lost
// no longer storing colors, if parent dies the color is lost uint32_t dominantColor;
CRGB dominantColor;
if (colorCount == 3) { //All parents survived if (colorCount == 3) { //All parents survived
if ((nColors[0] == nColors[1]) || (nColors[0] == nColors[2])) dominantColor = nColors[0]; if ((nColors[0] == nColors[1]) || (nColors[0] == nColors[2])) dominantColor = nColors[0];
else if (nColors[1] == nColors[2]) dominantColor = nColors[1]; else if (nColors[1] == nColors[2]) dominantColor = nColors[1];
else dominantColor = nColors[random8()%3]; else dominantColor = nColors[random8(3)];
} }
else if (colorCount == 2) dominantColor = nColors[random8()%2]; // 1 leading parent died else if (colorCount == 2) dominantColor = nColors[random8(2)]; // 1 leading parent died
else if (colorCount == 1) dominantColor = nColors[0]; // 2 leading parents died else if (colorCount == 1) dominantColor = nColors[0]; // 2 leading parents died
else dominantColor = color; // all parents died last used color else dominantColor = color; // all parents died use last seen color
// mutate color chance // mutate color chance
if (random8() < SEGMENT.intensity) dominantColor = !SEGMENT.check1?SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0): random16()*random16(); if (random8() < SEGMENT.intensity || dominantColor == bgColor) dominantColor = allColors ? random16() * random16() : SEGMENT.color_from_palette(random8(), false, PALETTE_SOLID_WRAP, 0);
if (SEGMENT.check1) dominantColor = RGBW32(dominantColor.r, dominantColor.g, dominantColor.b, 0); //WLEDMM support all colors
SEGMENT.setPixelColorXY(x,y, dominantColor); SEGMENT.setPixelColorXY(x,y, dominantColor);
} }
else { // blur dead cells
if (!cellValue && !overlayBG && !bgBlendMode) SEGMENT.setPixelColorXY(x,y, color_blend(SEGMENT.getPixelColorXY(x,y), bgColor, blur));
}
} }
//update cell values //update cell values
memcpy(gol->cells, gol->futureCells, dataSize); memcpy(cells, futureCells, dataSize);
// Get current crc value // Get current crc value
uint16_t crc = crc16((const unsigned char*)gol->cells, dataSize); uint16_t crc = crc16((const unsigned char*)cells, dataSize);
bool repetition = false; bool repetition = false;
if (!cellChanged || crc == gol->oscillatorCRC || crc == gol->spaceshipCRC) repetition = true; //check if cell changed this gen and compare previous stored crc values if (!cellChanged || crc == *oscillatorCRC || crc == *spaceshipCRC) repetition = true; //check if cell changed this gen and compare previous stored crc values
if (repetition) { if (repetition) {
generation = 0; // reset on next call generation = 0; // reset on next call
pauseFrames = 50; SEGENV.step += 1000; // pause final generation for 1 second
return FRAMETIME; return FRAMETIME;
} }
// Update CRC values // Update CRC values
if (generation % 16 == 0) gol->oscillatorCRC = crc; if (generation % 16 == 0) *oscillatorCRC = crc;
if (generation % gol->gliderLength == 0) gol->spaceshipCRC = crc; if (*gliderLength && generation % *gliderLength == 0) *spaceshipCRC = crc;
generation++; generation++;
SEGENV.step = strip.now; SEGENV.step = strip.now;
return FRAMETIME; return FRAMETIME;
} // mode_2Dgameoflife() } // mode_2Dgameoflife()
static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,Color Mutation ☾,,,,All Colors ☾,Overlay ☾,Wrap ☾,;!,!;!;2;sx=200,ix=12,c1=0,o3=1"; static const char _data_FX_MODE_2DGAMEOFLIFE[] PROGMEM = "Game Of Life@!,Color Mutation ☾,Blur ☾,,,All Colors ☾,Overlay BG ☾,Wrap ☾,;!,!;!;2;sx=56,ix=2,c1=128,o1=0,o2=0,o3=1";
///////////////////////// /////////////////////////
// 2D Hiphotic // // 2D Hiphotic //