Looking for suggestions for optimization + improving frame rate

Hey. I just started using openframeworks, mainly because I’d seen other people use it for a particular project I’ve been doing in processing. The project is a slime mold simulation, which is an agent/particle-based program. I’ve seen other projects reach millions of particles, but mine starts having framerate issues when reaching the thousands. It’s important to mention that I’m running this on a really poor setup, but even still, I’d expect the project to run at least a little better.

I’m very new to openframeworks and c++ in general, so I’m looking for some suggestions or ways to optimize my code. I’d also like to know if there are ways to allocate more CPU memory to a program. My code is pasted below, and I’d really appreciate anyone taking the time to give me some advice.

//Main

#include "ofMain.h"
#include "ofApp.h"

//========================================================================
int main( ){
	ofSetupOpenGL(400,400,OF_WINDOW);			// <-------- setup the GL context

	// this kicks off the running of my app
	// can be OF_WINDOW or OF_FULLSCREEN
	// pass in width and height too:
	ofRunApp(new ofApp());

}

// ofApp.cpp

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
    ofSetFrameRate(60);
    
    for (int i = 0; i < NUMAGENTS; ++i) {
        float x = ofGetWidth()/2;
        float y = ofGetHeight()/2;
        agents[i].initialize(x, y);
    }
    map.initialize();
}

//--------------------------------------------------------------
void ofApp::update(){
    for (int i = 0; i < NUMAGENTS; ++i) {
        agents[i].move(map);
    }
    map.diffuse();
    map.evaporate();
    map.updateMap();
    std::printf("value: %f\n", ofGetFrameRate());
}

//--------------------------------------------------------------
void ofApp::draw(){
    map.render();
}

//ofApp.hpp

#pragma once

#include "ofMain.h"
#include "Map.hpp"
#include "Slime.hpp"

#define NUMAGENTS 10000

class ofApp : public ofBaseApp{

	public:
		void setup();
		void update();
		void draw();

		void keyPressed(int key);
		void keyReleased(int key);
		void mouseMoved(int x, int y );
		void mouseDragged(int x, int y, int button);
		void mousePressed(int x, int y, int button);
		void mouseReleased(int x, int y, int button);
		void mouseEntered(int x, int y);
		void mouseExited(int x, int y);
		void windowResized(int w, int h);
		void dragEvent(ofDragInfo dragInfo);
		void gotMessage(ofMessage msg);
    
        Map map;
    
        Slime agents[NUMAGENTS];
		
};

//Map.cpp

#include "Map.hpp"

Map::Map(){
}

void Map::initialize() {
    diffusionRate = 0.98;
    
    for (int x = 0; x < ofGetWidth(); ++x) {
        for (int y = 0; y < ofGetHeight(); ++y) {
            grid[x][y] = 0;
            buffer[x][y] = 0;
        }
    }
}

void Map::setVal(int x, int y, float val) {
    grid[x][y] += val;
}

float Map::getVal(int x, int y) {
    return grid[x][y];
}

void Map::evaporate() {
    for (int x = 0; x < ofGetWidth(); ++x) {
        for (int y = 0; y < ofGetHeight(); ++y) {
            if (grid[x][y] > 1) {
                grid[x][y] = grid[x][y]*diffusionRate;
            }
        }
    }
}

void Map::diffuse() {
    for (int x = 0; x < ofGetWidth(); ++x) {
        for (int y = 0; y < ofGetHeight(); ++y) {
            float sum = 0;
            float avg = 0;
            
            for (int ix = -1; ix < 2; ++ix) {
                for (int iy = -1; iy < 2; ++iy) {
                    int currentX, currentY;
                    if (x==ofGetWidth()-1) {
                        currentX = 0;
                    } else if (x==0) {
                        currentX = ofGetWidth()-1;
                    } else {
                        currentX = ix+x;
                    }
                    if (y==ofGetHeight()-1) {
                        currentY = 0;
                    } else if (y==0) {
                        currentY = ofGetHeight()-1;
                    } else {
                        currentY = iy+y;
                    }
                    
                    sum += grid[currentX][currentY];
                }
            }
            
            avg = sum/9;
            float diffusedVal = ofLerp(grid[x][y], avg, diffusionRate);
            buffer[x][y] = diffusedVal;
        }
    }
    
    for (int x = 0; x < ofGetWidth(); ++x) {
        for (int y = 0; y < ofGetHeight(); ++y) {
            grid[x][y] = buffer[x][y];
            //buffer[x][y] = 0;
        }
    }
}

void Map::updateMap(){
    map.allocate(ofGetWidth(), ofGetHeight(), OF_IMAGE_COLOR); // RGB
    // loop thru and set a random color for each pixel
    for(int y = 0; y < map.getHeight(); ++y){
        for(int x = 0; x < map.getWidth(); ++x){
            float r = grid[x][y];
            float g = grid[x][y];
            float b = grid[x][y];
            if (r > 255) {
                r = 255;
            }
            if (g > 255) {
                g = 255;
            }
            if (b > 255) {
                b = 255;
            }
            ofColor color(r, g, b);
            map.setColor(x, y, color);
        }
    }
    // update the ofTexture in image with its ofPixels
    map.update();
}

void Map::render(){
    map.draw(0.f, 0.f);
}

//Map.hpp

#include <stdio.h>

#ifndef _MAP 
#define _MAP
#include "ofMain.h"


class Map {

    public:

    void initialize();
    void diffuse();
    void evaporate();

    void setVal(int x, int y, float val);
    float getVal(int x, int y);
    
    void updateMap();
    void render();


    ofImage map;
    float grid[400][400];
    float buffer[400][400];
    float diffusionRate;
    
    Map();
    
    private:
};
#endif

//Slime.cpp

#include "Slime.hpp"
#include "ofApp.h"
#include "Map.hpp"

Slime::Slime(){
}

void Slime::initialize(float x, float y) {
    int xP = int(x);
    int yP = int(y);
    position.set(xP, yP);
    angle = ofDegToRad(ofRandom(360));
    speed = 1;
    DEPOSITVALUE = 170;
    
    maxSpeed = 0.1;
    minSpeed = 0.1;
    
    turnSpeed = 45;
    sensorOffsetDst = 9;
    sensorAngleSpacing = ofDegToRad(22.5);
    sensorSize = 1;
}

void Slime::move(Map& map) {
    detect(map);
    
    direction = ofVec2f(cosf(angle), sinf(angle));
    direction.normalize();
    direction.x = direction.x*speed;
    direction.y = direction.y*speed;
    position.x = position.x + direction.x;
    position.y = position.y + direction.y;
    
    wrap();
    
    map.setVal(floor(position.x), floor(position.y), DEPOSITVALUE);
}

float Slime::sense(float sensorAngleOffset, Map& map) {
    float sum = 0;
    float sensorAngle = angle + sensorAngleOffset;
    ofVec2f sensorDir = ofVec2f(cos(sensorAngle), sin(sensorAngle));
    sensorDir.x = sensorDir.x*sensorOffsetDst;
    sensorDir.y = sensorDir.y*sensorOffsetDst;
    ofVec2f sensorCenter = ofVec2f(position.x + sensorDir.x, position.y + sensorDir.y);

    for (int offsetX = -sensorSize; offsetX <= sensorSize; ++offsetX) {
      for (int offsetY = -sensorSize; offsetY <= sensorSize; ++offsetY) {
          ofVec2f pos = ofVec2f(sensorCenter.x + offsetX, sensorCenter.y + offsetY);
        pos.x = floor(pos.x);
        pos.y = floor(pos.y);

        if (pos.x >= 0 && pos.x < ofGetWidth() && pos.y >= 0 && pos.y < ofGetHeight()) {
          sum += map.getVal(floor(pos.x), floor(pos.y));
        }
      }
    }
    return sum;
}

void Slime::detect(Map& map) {
    float forwardWeight = sense(0, map);
    float leftWeight = sense(sensorAngleSpacing, map);
    float rightWeight = sense(-sensorAngleSpacing, map );
    
    if (forwardWeight > leftWeight && forwardWeight > rightWeight) {
        angle += 0;
    } else if (forwardWeight < leftWeight && forwardWeight < rightWeight) {
        angle += ofDegToRad(turnSpeed);
    } else if (rightWeight > leftWeight) {
        angle -= ofDegToRad(turnSpeed);
    } else if (leftWeight > rightWeight) {
        angle += ofDegToRad(turnSpeed);
    }

}

void Slime::wrap() {
    if(position.x < 0 ){
        position.x = ofGetWidth()-1;
    } else if(position.x > ofGetWidth()-1){
        position.x = 0;
    }

    if(position.y < 0 ){
        position.y = ofGetHeight()-1;
    } else if(position.y > ofGetHeight()-1){
        position.y = 0;
    }
}

void Slime::show() {
    ofSetColor(255);
    ofDrawCircle(position.x, position.y, 5);
}

//Slime.hpp

#include <stdio.h>

#ifndef _SLIME
#define _SLIME
#include "ofMain.h"
#include "Map.hpp"

class Slime {

    public:

    void initialize(float x, float y);
    void move(Map& map);
    void wrap();
    void show();
    float sense(float sensorAngleOffset, Map& map);
    void detect(Map& map);

        
    ofVec2f position;
    ofVec2f direction;
    float angle;
    float speed;
    
    float maxSpeed;
    float minSpeed;
    
    float turnSpeed;
    float sensorOffsetDst;
    float sensorAngleSpacing;
    float sensorSize;
    
    float DEPOSITVALUE;    
    
    Slime();
    
    private:
};
#endif

Quick Explanation: the TrailMap is a 2d grid that contains a value corresponding to every pixel. As the agents move around the map, they deposit a value onto the pixels that correspond with their position (meaning they increase the 2d grid value by x amount, function setVal () in Map). Each agent has three sensors, one in front, one to its front and right, and one to its front and left. These sensors detect the value of the the TrailMap in that location of the sensor, and then the agent turns toward the highest value and deposits an amount onto the TrailMap. Every frame the TrailMap diffuses(), meaning each on the map is set to the average of the sum of its 8 neighboring values. Then, the TrailMap decays(), meaning each of its values are multiplicatively decreased.

An awesome blog post about the project in case you wanted to read more: [physarum - Sage Jenson]

Thanks

Hey @KingstownBar , try removing the line above. Printing to terminal every cycle can be slow. I’ll often draw the frame rate in the ofApp::draw():

    ofDrawBitmapString("frameRate: " + ofToString(ofGetFrameRate()), 20.f, 20.f);

Also you may have found this physarum thread in the forum. There might be a way to do (some of) this on a gpu with shaders, which is discussed in the thread.

I’ve refactored your code a little and optimized a few things and that nearly tripled the framerate.
If you want to make it real snappy, you need to involved shaders, like mentioned above.

ofApp.cpp:

#include "ofApp.h"

//--------------------------------------------------------------
void ofApp::setup(){
  ofSetFrameRate(60);
  
  agents.reserve(NUMAGENTS);
  for (int i = 0; i < NUMAGENTS; i++) {
    agents.emplace_back(ofGetWidth() / 2.0f, ofGetHeight() / 2.0f);
  }
    
  map = new Map(ofGetWidth(), ofGetHeight());
  
}

//--------------------------------------------------------------
void ofApp::update(){
  for (auto& agent : agents) {
    agent.move(map);
  }
  
  map->diffuse();
  map->evaporate();
  map->update();
//  std::printf("value: %f\n", ofGetFrameRate());
}

//--------------------------------------------------------------
void ofApp::draw(){
  map->draw();
  
}

//--------------------------------------------------------------
void ofApp::exit(){
  delete map;
}

ofApp.h:

pragma once

#include "ofMain.h"
#include "Map.hpp"
#include "Slime.hpp"

#define NUMAGENTS 10000

class ofApp : public ofBaseApp{

  public:
    void setup();
    void update();
    void draw();
    void exit();

  Map* map;
  std::vector<Slime> agents;
    
};

Map.cpp:

#include "Map.hpp"

Map::Map(int _width, int _height)
: width(_width), height(_height) {
  
  grid.reserve(width);
  buffer.reserve(width);
  for (int x = 0; x < width; x++) {
    std::vector<float> column(height, 0.0f);
    grid.emplace_back(column);
    buffer.emplace_back(column);
  }
  
  diffusion_rate = 0.98;
  
  image.allocate(width, height, OF_IMAGE_COLOR);
}

void Map::setVal(int x, int y, float val) {
  grid[x][y] += val;
}

float Map::getVal(int x, int y) const {
  return grid[x][y];
}

void Map::evaporate() {
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      if (grid[x][y] > 1.0f)
        grid[x][y] *= diffusion_rate;
    }
  }
}

void Map::diffuse() {
  float sum, avg;
  int current_x, current_y;
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      sum = 0.0f;
      avg = 0.0f;
      
      for (int ix = -1; ix < 2; ix++) {
        for (int iy = -1; iy < 2; iy++) {
      
          if (x == width - 1)
            current_x = 0;
          else if (x == 0)
            current_x = width - 1;
          else
            current_x = ix + x;
    
          if (y == height - 1)
            current_y = 0;
          else if (y == 0)
            current_y = height - 1;
          else
            current_y = iy + y;
          
          sum += grid[current_x][current_y];
         }
      }
            
      avg = sum / 9.0f;
      float diffused_val = ofLerp(grid[x][y], avg, diffusion_rate);
      buffer[x][y] = diffused_val;
    }
  }
    
  for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
      grid[x][y] = buffer[x][y];
    }
  }
}

void Map::update(){
  // loop thru and set a random color for each pixel
  float r, g, b;
  ofColor color;
  for(int y = 0; y < height; y++) {
    for(int x = 0; x < width; x++) {
      r = grid[x][y];
      if (r > 255)
        r = 255;
      g = r;
      b = g;
      color = {r, g, b};
      image.setColor(x, y, color);
    }
  }
  // update the ofTexture in image with its ofPixels
  image.update();
}

void Map::draw() const {
  image.draw(0.0f, 0.0f);
}

Map.hpp:

#ifndef Map_hpp
#define Map_hpp

#include <stdio.h>
#include <vector>
#include "ofMain.h"

struct Map {
  
  Map(int _width, int _height);
  
  void initialize();
  void diffuse();
  void evaporate();

  void setVal(int x, int y, float val);
  float getVal(int x, int y) const;
    
  void update();
  void draw() const;


  int width, height;
  std::vector< std::vector<float> > grid;
  std::vector< std::vector<float> > buffer;
  float diffusion_rate;
  
  ofImage image;
};

#endif

Slime.cpp:

#include "Slime.hpp"
#include "ofApp.h"
#include "Map.hpp"

Slime::Slime() {}

Slime::Slime(int _x, int _y)
: position(_x, _y) {
  angle = ofRandom(M_PI * 2);
  speed = 1.0f;
  DEPOSIT_VALUE = 170.0f;
  
  max_speed = 0.1f;
  min_speed = 0.1f;
  
  turn_speed = M_PI / 4.0f;
  sensor_offset_dst = 9.0f;
  sensor_angle_spacing = ofDegToRad(22.5);
  sensor_size = 1.0f;
}

void Slime::move(Map* map) {
    detect(map);
    
    direction = glm::vec2(std::cos(angle), std::sin(angle));
    direction = glm::normalize(direction) * speed;
    position += direction;

    wrap();
    
    map->setVal(std::floor(position.x), std::floor(position.y), DEPOSIT_VALUE);
}

float Slime::sense(const Map* map, float sensor_angle_offset) {
  float sum = 0.0f;
  float sensor_angle = angle + sensor_angle_offset;
  glm::vec2 sensor_dir = glm::vec2(std::cos(sensor_angle), std::sin(sensor_angle));
  sensor_dir *= sensor_offset_dst;
  glm::vec2 sensor_center = position + sensor_dir;

  for (int offset_x = -sensor_size; offset_x <= sensor_size; offset_x++) {
    for (int offset_y = -sensor_size; offset_y <= sensor_size; offset_y++) {
      glm::vec2 pos = glm::vec2(sensor_center.x + offset_x, sensor_center.y + offset_y);
      pos = glm::vec2(std::floor(pos.x), std::floor(pos.y));
      if (pos.x >= 0.0f && pos.x < ofGetWidth() && pos.y >= 0.0f && pos.y < ofGetHeight()) {
        sum += map->getVal(pos.x, pos.y);
      }
    }
  }
  return sum;
}

void Slime::detect(Map* map) {
  float forward_weight = sense(map, 0.0f);
  float left_weight = sense(map, sensor_angle_spacing);
  float right_weight = sense(map, -sensor_angle_spacing);
  
  if (forward_weight > left_weight && forward_weight > right_weight)
    angle += 0.0f;
  else if (forward_weight < left_weight && forward_weight < right_weight)
    angle += turn_speed;
  else if (right_weight > left_weight)
    angle -= turn_speed;
  else if (left_weight > right_weight)
    angle += turn_speed;
}

void Slime::wrap() {
  if (position.x < 0)
    position.x = ofGetWidth() - 1;
  else if (position.x > ofGetWidth() - 1)
    position.x = 0.0f;

  if (position.y < 0)
    position.y = ofGetHeight() - 1;
  else if (position.y > ofGetHeight() - 1)
    position.y = 0.0f;
}

void Slime::draw() {
  ofSetColor(255);
  ofDrawCircle(position.x, position.y, 5);
}

Slime.hpp:

#ifndef Slime_hpp
#define Slime_hpp

#include <stdio.h>
#include "ofMain.h"
#include "Map.hpp"

struct Slime {
  Slime();
  Slime(int _x, int _y);
  
  void move(Map* map);
  void wrap();
  void draw();
  float sense(const Map* map, float sensor_angl_offset);
  void detect(Map* map);
      
  glm::vec2 position;
  glm::vec2 direction;
  float angle;
  
  float speed;
  float max_speed;
  float min_speed;
  
  float turn_speed;
  float sensor_offset_dst;
  float sensor_angle_spacing;
  float sensor_size;
  
  float DEPOSIT_VALUE;
};

#endif

Further code optimizations might still be possible. I’m only a mediocre C++ hobby programmer. :slight_smile:

1 Like

Hey @diff-arch wow! a 3x speedup is awesome! If you get a chance, what changes do you think had the most impact? After quickly looking at the code you posted, I found:

  1. use std::vector<> instead of c-style arrays
  2. std::vector::reserve() to allocate lots of memory and keep the vectors from moving around as they get bigger
  3. use .emplace_back() to construct new elements in place in the vectors (nice, no copy!)
  4. use Map* to pass map, instead of passing map by reference

I wonder if the pointer is faster than passing by reference for some reason.

1 Like

You did a great job listing important changes.
I always prefer vectors, but I got rid of the two-dimensional arrays, because they posed a problem when I introduced parametrized constructors.

I doubt it, but the Map struct has no default constructor thus it can’t get declared unless you pass it arguments. It thus needs to be a pointer and heap allocated, but a default constructor could be defined as well.

Here are some more changes that I remember:

  • introduced parametrized constructors instead of the setup() and instantiate() methods, which saves a step, since otherwise you need to instantiate plus call a function
  • initially a new ofImage was allocated with each update I believe and now there’s a single one that gets iteratively updated
  • got rid of a couple of redundant if statements and other code snippets
  • moved most variable declarations out of loops to prevent re-declarations
  • changed many redundant float to int, double to float, and vis versa type castings
  • got rid of 5 or 6 ofDegToRad() conversions, which are pointless for ease fractions of Pi
  • got rid of the depreciated ofVec2fs and used glm::vec2s instead

I guess it all adds up.

2 Likes

Hey thanks so much posting all of this!

This statement really helped to clear up some confusion I’ve had for a long time about (non-default) constructors, and also why some code seems to rely heavily on pointers, heap, etc. Now it makes perfect sense!

Also I missed that an ofImage was getting allocated each cycle in the original post. So recycling that image would help improve the frame rate.

1 Like

It took me also long to understand that when I started learning C++ about two years ago.
When you have a header- and cpp-file with a class or struct that you want to declare a member instance of another class or struct in, the latter either has to have a default constructor (with no parameter) or you have to make the declaration a pointer.
The very simple structs might be an exception, since they can be instantiated differently still.

I think you can instantiate structs by passing parameters in the correct order like this

outDraw outs[2] = {
	{ glm::vec2(32, 64), &out1, &scenes, ui, uiColors },
	{ glm::vec2(84,84*2), &out2, &scenes2, ui2, uiColors2 }
};
2 Likes