2
\$\begingroup\$

I am developing a game that uses SDL2, SDL2 Image & SDL2 TTF, while using clang compiler under Linux. When running it in release mode(compiler flags: -Wall -O3) there is a constant usage of about 52MB, I've checked with Valgrind for memory leaks and there are none.

My Game class:

#include "Game.h"

Game::Game(Configurations configurations) {
  _graphics = std::make_unique<Graphics>(configurations.windowWidth,
                                         configurations.windowHeight,
                                         configurations.flags);
  _gameState = std::make_unique<GameState>(configurations.windowWidth,
                                           configurations.windowHeight);
  _mouseInput = std::make_unique<MouseInput>(MouseInput{-1, -1, 0});
  _fpsCounter = std::make_unique<FPSCounter>();
  _worldWidth = configurations.worldWidth;
  _worldHeight = configurations.worldHeight;
  _fpsCap = configurations.fpsCap;
}

auto Game::start() -> void {
  _fpsCounter->fpsInit();
  gameLoop(_fpsCap);
}

auto Game::convertStateToGraphicsMap()
    -> std::vector<std::pair<TileType, SDL_Point>> {
  std::vector<std::pair<TileType, SDL_Point>> convertedVector;

  for (auto o : _gameState->getGameObjects()) {
    convertedVector.push_back(
        std::make_pair(o.getTileType(), SDL_Point{(int)o.getPosition().x,
                                                  (int)o.getPosition().y}));
  }

  std::remove_if(convertedVector.begin(), convertedVector.end(),
                 [this](std::pair<TileType, SDL_Point> o) {
                   return o.second.x - (int)_gameState->getCamera().x < 0 ||
                          o.second.y - (int)_gameState->getCamera().y < 0 ||
                          o.second.x - (int)_gameState->getCamera().x >
                              _graphics->getWindowWidth() ||
                          o.second.y - (int)_gameState->getCamera().y >
                              _graphics->getWindowHeight();
                 });

  for (auto &o : convertedVector) {
    o.second.x -= _gameState->getCamera().x;
    o.second.y -= _gameState->getCamera().y;
  }
  return convertedVector;
}

auto Game::checkForSDLQuitEvents() -> bool {
  SDL_Event event;
  while (SDL_PollEvent(&event)) {
    switch (event.type) {
    case SDL_QUIT:
      std::cout << "closeRequested! quiting\n";
      return true;
    }
  }
  return false;
}

auto Game::validatePlayerPosition() -> void {
  Vector2d<float> &playerPosition = _gameState->getPlayer()->getPosition();
  if (playerPosition.x < 0) {
    playerPosition.x = 0;
  }
  if (playerPosition.y < 0) {
    playerPosition.y = 0;
  }
  if (playerPosition.x > _worldWidth) {
    playerPosition.x = (float)_worldWidth;
  }
  if (playerPosition.y > _worldHeight) {
    playerPosition.y = (float)_worldHeight;
  }
}

auto Game::handleMouseState(float fps) -> void {
  _mouseInput->mouseState =
      SDL_GetMouseState(&_mouseInput->mouseX, &_mouseInput->mouseY);
  if (_mouseInput->mouseState & SDL_BUTTON(SDL_BUTTON_LEFT)) {
    // printf("SDL_MOUSEBUTTONDOWN\n");
    auto halfWindowWidth = _graphics->getWindowWidth() / 2;
    auto halfWindowHeight = _graphics->getWindowHeight() / 2;
    _gameState->setCamera(
        {_gameState->getPlayer()->getPosition().x - halfWindowWidth,
         _gameState->getPlayer()->getPosition().y - halfWindowHeight});
    _gameState->getPlayer()->onDestinationSelected(
        {(float)_mouseInput->mouseX + _gameState->getCamera().x,
         (float)_mouseInput->mouseY + _gameState->getCamera().y},
        fps);

    validatePlayerPosition();
  }
}

auto Game::gameLoop(float fpsCap) -> void {
  float minFrameRateDelay = MILLISECOND_IN_SECOND / fpsCap;
  while (true) {
    // process events
    if (checkForSDLQuitEvents())
      return;

    _graphics->clearRender();
    float averageFPS = _fpsCounter->getAverageFramesPerSecond();
    handleMouseState(averageFPS);
    _graphics->renderGrid(convertStateToGraphicsMap());
    _graphics->renderText("FPS: " + std::to_string(averageFPS),
                          {255, 255, 0, 255}, 0, 0);
    _graphics->presentRender();

    if (fpsCap < averageFPS) {
      SDL_Delay(minFrameRateDelay);
    }
    _fpsCounter->fpsThink();
  }
}

auto Game::loadMap(const char *filename) -> void {
  int current, mx, my, mw, mh;
  std::ifstream in(filename);
  if (!in.is_open()) {
    std::cout << "Failed to open map file." << std::endl;
    return;
  }
  in >> mw;
  in >> mh;
  in >> mx;
  in >> my;
  for (int i = 0; i < mh; i++) {
    for (int j = 0; j < mw; j++) {
      if (in.eof()) {
        std::cout << "Reached end of map file too soon." << std::endl;
        return;
      }
      in >> current;
      if (current != 0) {
        GameObject tmp(DEFAULT_OBJECT_SIZE, 0, {0, 0}, HUMAN_FEMALE);
        if (current == 2 || current == 4) {
        }
        _map.push_back(tmp);
      }
    }
  }
  in.close();
}

Game::~Game() { std::cout << "Game destructor\n"; }

My Graphics class:

#include "Graphics.h"

Graphics::Graphics(Uint32 windowWidth, Uint32 windowHeight, Uint32 flags)
    : _windowWidth(windowWidth), _windowHeight(windowHeight), _flags(flags) {
  if (initializeSdl()) {
    _window = std::unique_ptr<SDL_Window, std::function<void(SDL_Window *)>>(
        createWindow(), SDL_DestroyWindow);
    _renderer =
        std::unique_ptr<SDL_Renderer, std::function<void(SDL_Renderer *)>>(
            createRenderer(), SDL_DestroyRenderer);
    _textures = std::unique_ptr<
        std::map<TileType, SDL_Texture *>,
        std::function<void(std::map<TileType, SDL_Texture *> *)>>(
        loadAllTextures(), Graphics::destroyAllTextures);
    _baseTile =
        std::unique_ptr<RectAndTexture, std::function<void(RectAndTexture *)>>(
            createBaseRect(), destroyRectAndTexture);
    _globalFont = std::unique_ptr<TTF_Font, std::function<void(TTF_Font *)>>(
        createRegularFont(), destroyFont);
    std::cout << "Graphics created\n";
  }
}

auto Graphics::initializeSdl() -> bool {
  // attempt to initialize graphics and timer system
  if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
    std::cout << "error initializing SDL: \n" << SDL_GetError();
    return false;
  }
  if (!(IMG_Init(IMG_INIT_PNG) & IMG_INIT_PNG)) {
    std::cout << "could not initialize sdl2_image: \n" << IMG_GetError();
    return false;
  }
  if (TTF_Init() == -1) {
    std::cout << "SDL_ttf could not initialize! SDL_ttf Error: \n"
              << TTF_GetError();
    return false;
  }
  return true;
}

auto Graphics::createWindow() -> SDL_Window * {
  SDL_Window *window = SDL_CreateWindow(
      "InvasiveSpecies", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
      _windowWidth, _windowHeight, _flags);
  if (!window) {
    std::cout << "error creating window: \n" << SDL_GetError();
    SDL_Quit();
    return nullptr;
  }
  return window;
}

auto Graphics::createRenderer() -> SDL_Renderer * {
  // create a renderer, which sets up the graphics hardware
  Uint32 render_flags = SDL_RENDERER_ACCELERATED;

  SDL_Renderer *renderer = SDL_CreateRenderer(_window.get(), -1, render_flags);
  if (!renderer) {
    std::cout << "error creating renderer: \n" << SDL_GetError();
    quitSdl();
    return nullptr;
  }

  return renderer;
}

auto Graphics::loadTexture(const char *imagePath) -> SDL_Texture * {
  SDL_Texture *texture = IMG_LoadTexture(_renderer.get(), imagePath);
  if (!texture) {
    std::cout << "error creating texture\n";
    quitSdl();
    return nullptr;
  }
  return texture;
}

auto Graphics::createRectFromTexture(SDL_Texture *texture) -> RectAndTexture * {
  auto *dest = new SDL_Rect();
  SDL_QueryTexture(texture, nullptr, nullptr, &dest->w, &dest->h);
  auto *rectAndTexture = new RectAndTexture{dest, texture};
  return rectAndTexture;
}

auto Graphics::createBaseRect() -> RectAndTexture * {
  RectAndTexture *baseTile = createRectFromTexture((*_textures)[SOIL]);
  return baseTile;
}

auto Graphics::clearRender() -> void { SDL_RenderClear(_renderer.get()); }

auto Graphics::presentRender() -> void { SDL_RenderPresent(_renderer.get()); }

auto Graphics::renderGrid(
    const std::vector<std::pair<TileType, SDL_Point>> &gameObjectsPositionsMap)
    -> void {
  renderGridBackground();
  renderGameObjects(gameObjectsPositionsMap);
}

auto Graphics::renderGameObjects(
    const std::vector<std::pair<TileType, SDL_Point>> &gameObjectsPositionsMap)
    -> void {
  for (const auto &current : gameObjectsPositionsMap) {
    _baseTile->texture = (*_textures)[current.first];
    _baseTile->rect->x = current.second.x;
    _baseTile->rect->y = current.second.y;
    renderTexture(_baseTile.get());
  }
}

auto Graphics::renderGridBackground() -> void {
  // printf("renderGridBackground\n");
  int tileWidth = _baseTile->rect->w;
  int tileHeight = _baseTile->rect->h;
  _baseTile->texture = (*_textures)[SOIL];
  for (int i = 0; i < _windowWidth; i += tileWidth) {
    for (int j = 0; j < _windowHeight; j += tileHeight) {
      _baseTile->rect->x = i;
      _baseTile->rect->y = j;
      renderTexture(_baseTile.get());
    }
  }
}

auto Graphics::renderTexture(RectAndTexture *rectAndTexture) -> void {
  SDL_RenderCopy(_renderer.get(), rectAndTexture->texture, nullptr,
                 rectAndTexture->rect);
}

auto Graphics::getImagePathStringByTileType(TileType tileType) -> const char * {
  switch (tileType) {
  case TileType::SOIL:
    return SOIL_IMAGE_PATH;
  case TileType::GRASS:
    return GRASS_IMAGE_PATH;
  case TileType::STONES:
    return STONES_IMAGE_PATH;
  case TileType::HUMAN_MALE:
    return HUMAN_MALE_IMAGE_PATH;
  case TileType::HUMAN_FEMALE:
    return HUMAN_FEMALE_IMAGE_PATH;
  default:
    break;
  }
  return nullptr;
}

auto Graphics::loadAllTextures() -> std::map<TileType, SDL_Texture *> * {
  auto texturesMap = new std::map<TileType, SDL_Texture *>();
  for (auto currentTileType : tileTypeVector) {
    (*texturesMap)[currentTileType] =
        loadTexture(getImagePathStringByTileType(currentTileType));
  }
  return texturesMap;
}

auto Graphics::destroyAllTextures(
    std::map<TileType, SDL_Texture *> *texturesMap) -> void {
  for (auto const &p : *texturesMap) {
    SDL_DestroyTexture(p.second);
  }

  texturesMap->clear();
  delete texturesMap;
  std::cout << "destroyAllTextures done\n";
}

auto Graphics::destroyFont(TTF_Font *font) -> void {
  TTF_CloseFont(font);
  std::cout << "destroyFont done\n";
}

auto Graphics::destroyRectAndTexture(RectAndTexture *rectAndTexture) -> void {
  delete rectAndTexture->rect;
  //    SDL_DestroyTexture(rectAndTexture->texture); //already freed at
  //    destroyAllTextures()
  delete rectAndTexture;
}

auto Graphics::renderText(const std::string &textureText, SDL_Color textColor,
                          int x, int y) -> void {
  SDL_Surface *textSurface =
      TTF_RenderText_Solid(_globalFont.get(), textureText.c_str(), textColor);
  SDL_Texture *textTexture = nullptr;
  if (textSurface == nullptr) {
    std::cout << "Unable to render text surface! SDL_ttf Error: \n",
        TTF_GetError();
  } else {
    // Create texture from surface pixels
    textTexture = SDL_CreateTextureFromSurface(_renderer.get(), textSurface);
    if (textTexture == nullptr) {
      std::cout << "Unable to create texture from rendered text! SDL Error: \n"
                << SDL_GetError();
    } else {
      std::unique_ptr<RectAndTexture, std::function<void(RectAndTexture *)>>
          baseTile = std::unique_ptr<RectAndTexture,
                                     std::function<void(RectAndTexture *)>>(
              createRectFromTexture(textTexture), destroyRectAndTexture);
      baseTile->rect->x = x;
      baseTile->rect->y = y;
      renderTexture(baseTile.get());
      baseTile.reset();
      SDL_DestroyTexture(textTexture);
    }

    // Get rid of old surface
    SDL_FreeSurface(textSurface);
  }
}

auto Graphics::getFontFromFile(const char *file, int ptsize) -> TTF_Font * {
  TTF_Font *gFont = TTF_OpenFont(file, ptsize);

  if (gFont == nullptr) {
    std::cout << "Failed to load lazy font! SDL_ttf Error: \n"
              << TTF_GetError();
  }

  return gFont;
}

auto Graphics::createRegularFont() -> TTF_Font * {
  return getFontFromFile(FONT_PATH, FONT_SIZE);
}

auto Graphics::quitSdl() -> void {
  // these must be here or else they will get called after SDL_Quit()
  _globalFont.reset();
  _textures.reset();
  _baseTile.reset();
  _renderer.reset();
  _window.reset();
  TTF_Quit();
  IMG_Quit();
  SDL_Quit();
}

Graphics::~Graphics() {
  std::cout << "Graphics destructor\n";
  quitSdl();
}

The assets for the game contains .png and .tff files with a total size of about 34KiB.

The rest of the code is in My GitHub repo

Is the usage of about 50MB RAM at runtime is normal under these conditions?

\$\endgroup\$
3
  • 3
    \$\begingroup\$ If the memory stays constantly at roughly 50MB, then this is not an issue with memory leaks. A good way to test if 50MB is a lot or not, would be to create a "Hello World" program using SDL2, and see how much memory that consumes. If it consumes around 30-40MB, then you can assume your game uses the rest. Given to how modern hardware has evolved, a lot of programs prefer to use more memory, instead of CPU, so I wouldn't be surprised if SDL or another library uses that much memory. \$\endgroup\$ – TomTsagk Apr 7 '20 at 14:52
  • \$\begingroup\$ @TomTsagk I've done what you've suggested, an empty sdl2 app with sdl2 window and with a loop for sdl_quit event without any rendering; takes about 50MB, so I'm guessing it's how the OS using SDL2. \$\endgroup\$ – Yan.F Apr 7 '20 at 15:34
  • 1
    \$\begingroup\$ I would say that means SDL2 is made that way. Not much you can do about that. If you think that's an issue with your project, the only option is to experiment with other libraries. \$\endgroup\$ – TomTsagk Apr 7 '20 at 15:46
1
\$\begingroup\$

I've tested the application under Windows 7, and it takes about 8MB RAM (under linux it was about 50MB) moreover the FPS is higher: about 600, while in linux it was about 370. I presume the performance drop in linux is due to poor GPU driver support, that can result in higher memory consumption and lower FPS, furthermore I am printing some output to a separated terminal window, so it is possible that Windows CMD is more lightweight than the terminal used under my linux distro(Manjaro).

Makes me think that developing games under linux is maybe not such a good idea especially since my GPU driver is not supported. So I guess ill continue the development under Windows.

\$\endgroup\$
1
  • \$\begingroup\$ The Windows 10 console host for a simple stdio test program I've just put together takes about 6.5mb - the program itself is a further 0.9 mb. I wouldn't be surprised if the additional memory you're observing under Linux was preallocating some large pools to satisfy potential future smaller allocations, rather than having to do lots of small allocations, which can be a total performance killer. \$\endgroup\$ – Maximus Minimus Apr 8 '20 at 15:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.