back..

Jet Loop

This is a small practice project, started for the purpose of learning OpenGL and the basics of engine-less rendering. The idea of an endless jet flight as well as the stylized visuals was inspired by the incredible work of Calder Moore. The project also features a scope-based profiler (single threaded). For the current state of the source code please visit this link.

For the profiler, timings are collected into a node tree and traversed with a custom iterator, then the current frame is streamed to an ImGui widget through an exposed interface. Snippets of the implementation are provided below.

Profiler header.
namespace Profiler
{
  struct FrameProfileData
  {
    static const size_t headerSize = 128;
    static const size_t bodySize = 1024;
    char header[headerSize];
    char body[bodySize];
  };

  class FrameDataVisualizer 
  {
  public:
    virtual void visualizeFrameData(const FrameProfileData& frameData) = 0;
  };

  class ScopeTimer
  {
  public:
    static int sFrameNumber;
    static bool sIsPaused;

    ScopeTimer(int id, const char* name);

    ~ScopeTimer();

  private:
    int m_id;
    const char* m_name;
    std::chrono::time_point<std::chrono::steady_clock> m_startTimePoint;
  };

  void onStartFrame();
  void onEndFrame(FrameDataVisualizer* pVisualizer);
  void endProfiler();
}

#endif // PROFILER_H

#ifndef PROFILER_MACROS_DEFINED
#define PROFILER_MACROS_DEFINED

#if defined (PROFILE)							
#define PROFILE_SCOPE(nameLiteral)		Profiler::ScopeTimer timer(FILELINE_ID, nameLiteral)
#define PROFILER_START_FRAME()		Profiler::onStartFrame()
#define PROFILER_END_FRAME(visualizer)	Profiler::onEndFrame(visualizer)
#define PROFILER_END()			Profiler::endProfiler()							
#else
#define PROFILER_INIT()			do {} while (0)
#define PROFILE_SCOPE(nameLiteral)		do {} while (0)
#define PROFILER_END_FRAME(visualizer)	do {} while (0)
#define PROFILER_END()			do {} while (0)	
#endif // PROFILE

#endif // PROFILER_MACROS_DEFINED
Profiler's node tree class.
namespace Profiler
{
  static const size_t InvalidNodeIdx = std::numeric_limits<size_t>::max();

  ...

  class ScopeProfileTree
  {
  public:
    class iterator
    {
    public:
      iterator(ScopeProfileNode* pNode, ScopeProfileTree* pTree, int depth) : 
      m_pNode(pNode),
      m_pTree(pTree),
      m_depth(depth) {}

      iterator& operator++()
      {
        advance();
        return *this;
      }

      iterator operator++(int)
      {
        iterator treeIterator = *this;
        advance();
        return treeIterator;
      }

      bool operator==(iterator other) const
      {
        return this->m_pNode == other.m_pNode;
      }

      bool operator!=(iterator other) const
      {
        return this->m_pNode != other.m_pNode;
      }

      ScopeProfileNode& operator*() const
      {
        return *m_pNode;
      }

      ScopeProfileNode* operator->() const
      {
        return m_pNode;
      }

      int depth() const
      {
        return m_depth;
      }

      private:
        ScopeProfileNode* m_pNode;
        ScopeProfileTree* m_pTree;
        int m_depth;
        void advance()
        {
          if (!m_pNode) return;

          if (m_pNode->firstChildIdx != InvalidNodeIdx)
          {
            m_pNode = m_pTree->getNodePtr(m_pNode->firstChildIdx);
            m_depth++;
            return;
          }

          while (true)
          {
            if (m_pNode->nextSiblingIdx != InvalidNodeIdx)
            {
              m_pNode = m_pTree->getNodePtr(m_pNode->nextSiblingIdx);
              return;
            }
            if (m_pNode->parentIdx == InvalidNodeIdx)
            {
              m_pNode = nullptr;
              m_depth = -1;
              return;
            }
            m_pNode = m_pTree->getNodePtr(m_pNode->parentIdx);
            m_depth--;
          }
        }
      };

      ScopeProfileTree() :
        m_nodes(0),
        m_openNodes(0),
        m_stats(0)
      {
        m_nodes.reserve(m_sMaxNodesCount);
        m_openNodes.reserve(m_sMaxNodesCount);
        m_stats.reserve(m_sMaxNodesCount);
        reset();
      }

      ~ScopeProfileTree() {}

      iterator begin()
      {
        if (m_nodes.size() <= m_sStartIdx)
        {
          return end(); // skip empty root
        }
        return iterator{ &m_nodes[getRootIdx()], this, 0 };
      }

      iterator end()
      {
        return iterator{ nullptr, this, -1 };
      }

      void reset()
      {
        m_nodes.clear();
        m_openNodes.clear();

        pushNode(InvalidNodeIdx, -1); // root
        m_openNodes.emplace_back(0);
      }

      void onNodeOpen(int id)
      {
          const size_t parentIdx = m_openNodes.back();
          const size_t thisIdx = m_nodes.size();

#ifdef _DEBUG
        if (thisIdx == m_sMaxNodesCount)
        {
          fprintf(stderr, "Increase the default capacity of the profiler's node storage!\n");
        }
#endif // DEBUG

        pushNode(parentIdx, id);

        ScopeProfileNode& parentNode = m_nodes[parentIdx];
        if (parentNode.firstChildIdx == InvalidNodeIdx)
        {
          parentNode.firstChildIdx = thisIdx;
          parentNode.lastChildIdx = thisIdx;
        }
        else 
        {
          m_nodes[parentNode.lastChildIdx].nextSiblingIdx = thisIdx;
          parentNode.lastChildIdx = thisIdx;
        }

        m_openNodes.emplace_back(thisIdx);
      }

      void onNodeClose(const char* n, double startT, double elapsedT)
      {
        if (m_openNodes.size() <= 1)
        {
          return;
        }
        const size_t lastOpenIdx = m_openNodes.back();
        m_nodes[lastOpenIdx].setData(n, startT, elapsedT);
        m_openNodes.pop_back();

        storeStats(&m_nodes[lastOpenIdx].data, m_nodes[lastOpenIdx].id);
      }

      const ScopeProfileNode& getNode(size_t idx) const
      {
        return m_nodes.at(idx);
      }

      const ScopeStats& getOrCreateStats(int idKey)
      {
        return m_stats[idKey];
      }

      bool hasStatsFor(int idKey) const
      {
        return m_stats.find(idKey) != m_stats.end();
      }

      size_t getNodesCount() const
      {
        return m_nodes.size();
      }

      size_t getRootIdx() const
      {
        return m_sStartIdx;
      }

    private:
      static const size_t m_sStartIdx = 1;
      static const size_t m_sMaxNodesCount = 64;
      std::vector<ScopeProfileNode> m_nodes;
      std::vector<size_t> m_openNodes;
      std::unordered_map<int, ScopeStats> m_stats;
      friend class iterator;

      void pushNode(size_t parent, int id)
      {
        m_nodes.emplace_back(id, parent);
      }

      void storeStats(ScopeProfileData* pData, int id)
      {
        auto it = m_stats.find(id);

        if (it != m_stats.end())
        {
          ScopeStats& nodeStats = it->second;
          nodeStats.maxElapsedTime = std::max(nodeStats.maxElapsedTime, pData->elapsedTime);
          nodeStats.minElapsedTime = std::min(nodeStats.minElapsedTime, pData->elapsedTime);
          nodeStats.pushElapsedTime(pData->elapsedTime);
        }
        else
        {
          m_stats.emplace(id, ScopeStats{ pData->elapsedTime, pData->elapsedTime });
        }
      }

      ScopeProfileNode* getNodePtr(size_t idx)
      {
        return &m_nodes.at(idx);
      }

      ScopeProfileNode& getNodeRef(size_t idx)
      {
        return m_nodes.at(idx);
      }
    };

    ...
}

Function that assembles the tree text:

void getFrame(FrameProfileData* pFrameData)
{
  memset(pFrameData->header, 0, FrameProfileData::headerSize);
  memset(pFrameData->body, 0, FrameProfileData::bodySize);

  snprintf(pFrameData->header, FrameProfileData::headerSize, "Frame %08llu | Duration - real (ms) 
  | - max (ms) | - min (ms) | - avg in %zu fr (ms) | Scope \n", 
  static_cast<unsigned long long>(ScopeTimer::sFrameNumber), ScopeStats::movingAverageWindowSize);

  for (auto it = profileTree.begin(); it != profileTree.end(); it++)
  {
    const ScopeProfileNode& node = *it;
    const ScopeStats& stats = profileTree.getOrCreateStats(node.id);

    formatProfileRowIndented(pFrameData->body, FrameProfileData::bodySize, node.data, stats, it.depth());
  }
}

Profiler Output

Currently, the terrain generation implements simple Fractal Brownian Motion and height-gradient based erosion. Below are some screenshots of different configuration presets.

Patagonia Preset

Patagonia Preset Preset 1

Patagonia Preset

Patagonia Preset Preset 2

The jet model is a free Sketchfab asset.

•   Based on Moonwalk by abhinavs   •
>