a-simple-triangle / Part 8 - Refactor foundation

Now that we have onboarded all our platform targets we can start concentrating more on authoring our C++ application code. As hinted at in the previous article, the code we have written so far will get our app up and running, but some parts are actually a bit smelly.

The goals of this article are:

Note: I will use the term class a lot, but in fact most of the objects we will write will syntactically be structs.

So, crack open Visual Studio Code, open our a-simple-triangle-workspace and hop into the CMake view. We can make all of these changes using our console application which is one of the main reasons we wrote it. Using our console application will make the C++ coding quite fast to iterate compared to other platforms which have heavy startup overheads to see code changes.

Note: Don’t forget, whenever we add new source files we need to press the small CMake icon (next to the little hammer icon) in Visual Studio Code so it refreshes and picks up the new files. Burn this action into your grey matter because I will be assuming that you do this as we add source files. Remember also when revisiting the iOS and MacOS platforms that we need to run their setup.sh scripts to pick up file additions or removals as well. Android and Emscripten don’t need to do this because their CMakeLists.txt rules should automatically pick up new code.


Logging class

In our main.cpp class, we had a simple std::cout statement to print some output to the console. There are a couple of problems with this:

  1. We don’t really want to print output in release builds - only debug builds; and
  2. std::cout on its own doesn’t even show up by default on Android.

Let’s add a new class that does the following:

Add the following new files:

Remember in Visual Studio Code: File -> New then Save.

Edit the log.hpp header:

#pragma once

#include <string>

namespace ast
{
    void log(const std::string& tag, const std::string& message);

    void log(const std::string& tag, const std::string& message, const std::exception& error);
} // namespace ast

This header gives us two logging methods in the ast namespace. They are free functions (are not member functions of any object) so we don’t need an instance of any type to call them. For example, any code that has imported the header file would log a message like this:

ast::log("TAG", "Some output message");

Edit the ‘log.cpp’ source:

#ifndef NDEBUG
#ifdef __ANDROID__
#include <android/log.h>
#endif
#endif

#include "log.hpp"
#include <iostream>

void ast::log(const std::string& tag, const std::string& message)
{
#ifndef NDEBUG
#ifdef __ANDROID__
    __android_log_print(ANDROID_LOG_DEBUG, "a-simple-triangle", "%s: %s", tag.c_str(), message.c_str());
#else
    std::cout << tag << ": " << message << std::endl;
#endif
#endif
}

void ast::log(const std::string& tag, const std::string& message, const std::exception& error)
{
#ifndef NDEBUG
    std::string output = message + " Exception message was: " + std::string{error.what()};
    ast::log(tag, output);
#endif
}

Consider the conditional we are using in this code:

#ifndef NDEBUG
...
#endif

The conditional will only evaluate if we are in a debug build. By wrapping blocks of code with this conditional, it won’t even be compiled into the application if it is not in debug mode.

As you can see, by using this conditional the log methods actually don’t do anything if we are not in a debug build.

This code also detects __ANDROID__ and routes the logging text through the Android logging system instead of std::cout, because by default std::cout messages don’t appear in Logcat.


Adopt the PIMPL pattern

For some of the non trivial classes, I will be using a basic form of the Pointer to IMPLementation, or PIMPL pattern. Here are some articles about it:

We will write a simple move semantic only PIMPL implementation that is similar to one demonstrated in the last web link above. We will name this internal_ptr - I just really can’t stand seeing the word pimpl in my code.

Using this pattern allows us to have an internal object that forms the private implementation of a class, hiding all the private guts of a class from its public API. Our approach will use the basic functionality of the std::unique_ptr type - so only move semantics will be possible. Add a new file main/src/core/internal_ptr.hpp with the following code:

#pragma once

#include <memory>

namespace ast
{
    namespace internal_ptr_deleter
    {
        // 1. Custom deleter.
        template <class T>
        void deleter(T* victim)
        {
            delete victim;
        }
    } // namespace internal_ptr_deleter

    // 2. Template definition, alias of std::unique_ptr.
    template <class T, class Deleter = void (*)(T*)>
    using internal_ptr = std::unique_ptr<T, Deleter>;

    // 3. Factory to create an internal_ptr instance.
    template <class T, class... Args>
    inline internal_ptr<T> make_internal_ptr(Args&&... args)
    {
        return internal_ptr<T>(new T(std::forward<Args>(args)...), &internal_ptr_deleter::deleter<T>);
    }
} // namespace ast
  1. The custom deleter template is needed to ensure that an internal_ptr based object will automatically self destroy upon going out of scope.
  2. The template definition that gives us the internal_ptr type. Basically it is just an alias to a std::unique_ptr with a custom deleter signature.
  3. The factory method is used to create a new instance of an internal_ptr type. The factory method will forward all its arguments on to the target object constructor.

We’ll make use of the internal_ptr quite liberally as we go forward.


Decouple OpenGL code

By the end of this series we would like to use OpenGL or Vulkan depending on which technology is available at run time. At the moment, our code base is fairly tightly coupled to use OpenGL. This won’t be too helpful when we want to use Vulkan down the track.

We will model a basic application which can then have different implementations - an OpenGL implementation, and later when we get to it, a Vulkan implementation. Once we have these applications, our code can try to pick the best one it can. For now, we will focus on moving our OpenGL code into an OpenGL application, starting with a base application class.

Create two new source files in main/src/application (you will need to create the application folder under main/src):

Edit the application.hpp file with:

#pragma once

namespace ast
{
    struct Application
    {
        Application() = default;

        virtual ~Application() = default;

        void startApplication();

        bool runMainLoop();

        virtual void render() = 0;
    };
} // namespace ast

The header defines some basic functions that would be common to both OpenGL or Vulkan (or probably other technologies too). By declaring an abstract render() function (because it is marked virtual and is assigned a value of 0), we have effectively made this a base class.

Notice also that we are declaring the startApplication() and runMainLoop() functions - we are going to shift all the non OpenGL code in our main.cpp file into the implementation of the application class. This will include the conditional logic to correctly start Emscripten, as well as the main event looping code. We will also make a tweak to how we start the Emscripten main loop.

Edit the application.cpp file with:

#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif

#include "../core/sdl-wrapper.hpp"
#include "application.hpp"

using ast::Application;

namespace
{
#ifdef EMSCRIPTEN
    void emscriptenMainLoop(ast::Application* application)
    {
        application->runMainLoop();
    }
#endif
} // namespace

void Application::startApplication()
{
#ifdef __EMSCRIPTEN__
    emscripten_set_main_loop_arg((em_arg_callback_func) ::emscriptenMainLoop, this, 60, 1);
#else
    while (runMainLoop())
    {
        // Just waiting for the main loop to end.
    }
#endif
}

bool Application::runMainLoop()
{
    SDL_Event event;

    // Each loop we will process any events that are waiting for us.
    while (SDL_PollEvent(&event))
    {
        switch (event.type)
        {
            case SDL_QUIT:
                return false;

            case SDL_KEYDOWN:
                if (event.key.keysym.sym == SDLK_ESCAPE)
                {
                    return false;
                }
                break;
            default:
                break;
        }
    }

    // Perform our rendering for this frame.
    render();

    return true;
}

Points of interest:

Notice that we have the conditional Emscripten header include at the top.

We have declared the emscriptenMainLoop function as a free function in an anonymous namespace private to this file. Notice also that it now takes an argument of type ast::Application* application. This is how we can give our Emscripten main loop something contextual, in this case the instance of the application that should be controlled by the main loop.

To invoke the emscriptenMainLoop we are now using a slightly different variant of the emscripten_set_main_loop function:

emscripten_set_main_loop_arg((em_arg_callback_func) ::emscriptenMainLoop, this, 60, 1);

The first argument specifies which method Emscripten should invoke on every main loop iteration, cast to the em_arg_callback_func type which is defined as:

typedef void (*em_arg_callback_func)(void*)

This means that the function we pass in, must take one argument of type void* which pretty much means it can be a pointer to whatever we like.

Note: The :: preceeding the ::emscriptenMainLoop argument is a way to explicitly call a method in the anonymous namespace in the current file. It isn’t strictly needed but I like using it to make it clear about where the function is located and disambiguate it from a class member function.

The second argument represents the argument we want to pass into the function, so we specify this to send our current Application instance to the function. This is how we are allowed to pass the argument ast::Application* application then call the runMainLoop() method upon it:

void emscriptenMainLoop(ast::Application* application)
{
    application->runMainLoop();
}

The third and fourth arguments are the same as before.

Read this Emscripten documentation to learn more.


OpenGL application

Now that we have a base application we can create our OpenGL application variant and move all our tightly coupled OpenGL code into it.

Create the following new files in main/src/application/opengl (you will need to create the opengl folder):

The opengl-application will subclass our new application class, meaning it will inherit the existing main loop code and basic event handling. The only thing our OpenGL variant needs to do is implement the ability to render something each frame.

Edit opengl-application.hpp to:

Note: This will be our first use of the internal_ptr we made earlier.

#pragma once

#include "../../core/internal-ptr.hpp"
#include "../application.hpp"

namespace ast
{
    struct OpenGLApplication : public ast::Application
    {
        OpenGLApplication();
        void render() override;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

The header is fairly straight forward, we are extending the ast::Application base class, declaring that we are implementing the render function and applying our internal_ptr pattern to hide the implementation from the public API.

Edit opengl-application.cpp to:

#include "opengl-application.hpp"
#include "../../core/graphics-wrapper.hpp"
#include "../../core/log.hpp"
#include "../../core/sdl-wrapper.hpp"
#include <string>

using ast::OpenGLApplication;

namespace
{
    SDL_GLContext createContext(SDL_Window* window)
    {
        static const std::string logTag{"ast::OpenGLApplication::createContext"};

        SDL_GLContext context{SDL_GL_CreateContext(window)};

#ifdef WIN32
        glewInit();
#endif

        int viewportWidth;
        int viewportHeight;
        SDL_GL_GetDrawableSize(window, &viewportWidth, &viewportHeight);
        ast::log(logTag, "Created OpenGL context with viewport size: " + std::to_string(viewportWidth) + " x " + std::to_string(viewportHeight));

        glClearDepthf(1.0f);
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LEQUAL);
        glEnable(GL_CULL_FACE);
        glViewport(0, 0, viewportWidth, viewportHeight);

        return context;
    }
} // namespace

struct OpenGLApplication::Internal
{
    SDL_Window* window;
    SDL_GLContext context;

    Internal() : window(ast::sdl::createWindow(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI)),
                 context(::createContext(window)) {}

    void render()
    {
        SDL_GL_MakeCurrent(window, context);

        glClearColor(0.3f, 0.7f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        SDL_GL_SwapWindow(window);
    }

    ~Internal()
    {
        SDL_GL_DeleteContext(context);
        SDL_DestroyWindow(window);
    }
};

OpenGLApplication::OpenGLApplication() : internal(ast::make_internal_ptr<Internal>()) {}

void OpenGLApplication::render()
{
    internal->render();
}

Let’s step through this implementation a piece at a time. A lot of it looks very similar to the code in our main.cpp file so parts of it should feel familiar.

Create OpenGL context function

We have a free function createContext that simply creates the OpenGL context we need and does some basic OpenGL setup. This code is somewhat similar to the code in the runApplication function of main.cpp, except that it returns the context to the caller and we have changed the approach to computing the viewport size.

The viewport size calculation has changed because on some platforms such as iOS the actual renderable size might be at a higher DPI than the reported display size. Typically on iOS retina devices the renderable size would be twice the size of the display size. If we didn’t change this then our application would only render at a quarter of the screen size on iOS retina devices. The SDL_GL_GetDrawableSize function gives us the true size which our viewport needs in order to fill the screen.

namespace
{
    SDL_GLContext createContext(SDL_Window* window)
    {
        static const std::string logTag{"ast::OpenGLApplication::createContext"};

        SDL_GLContext context{SDL_GL_CreateContext(window)};

#ifdef WIN32
        glewInit();
#endif

        int viewportWidth;
        int viewportHeight;
        SDL_GL_GetDrawableSize(window, &viewportWidth, &viewportHeight);
        ast::log(logTag, "Created OpenGL context with viewport size: " + std::to_string(viewportWidth) + " x " + std::to_string(viewportHeight));

        glClearDepthf(1.0f);
        glEnable(GL_DEPTH_TEST);
        glDepthFunc(GL_LEQUAL);
        glEnable(GL_CULL_FACE);
        glViewport(0, 0, viewportWidth, viewportHeight);

        return context;
    }
} // namespace

Internal implementation

This is the implementation of our Internal struct that was forward declared in the header file and encapsulated within our internal_ptr mechanism. Notice that it has the window and context member fields which means they are now nicely tucked away instead of sitting in global scope like we had them in main.cpp.

The constructor will initialise both the window and the context. Note we have added a new window flag named SDL_WINDOW_ALLOW_HIGHDPI. If we didn’t add this flag then iOS retina devices would only render at half their actual renderable size which results in an upscaled low resolution image. This flag is related to the change we made to calculate the viewport correctly and is necessary on high DPI devices.

The destructor will clean up our OpenGL window and context.

The render function will simply do the same thing we were already doing - displaying a (yes very boring, I promise we will start showing something more interesting soon enough!) green screen.

struct OpenGLApplication::Internal
{
    SDL_Window* window;
    SDL_GLContext context;

    Internal() : window(ast::sdl::createWindow(SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI)),
                 context(::createContext(window)) {}

    void render()
    {
        SDL_GL_MakeCurrent(window, context);

        glClearColor(0.3f, 0.7f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        SDL_GL_SwapWindow(window);
    }

    ~Internal()
    {
        SDL_GL_DeleteContext(context);
        SDL_DestroyWindow(window);
    }
};

Public API implementation

The public functions in our header that we must implement are:

  1. The constructor, whose job it is to create the instance of our private internal member field.
  2. The render function, which will simply defer to our internal implementation.
OpenGLApplication::OpenGLApplication() : internal(ast::make_internal_ptr<Internal>()) {}

void OpenGLApplication::render()
{
    internal->render();
}

Notice how the constructor is initialising the internal member field like this:

internal(ast::make_internal_ptr<Internal>())

The make_internal_ptr is our factory method we wrote in the internal_ptr template file. In this class our Internal struct doesn’t need any constructor arguments, but if it did, they would be passed into the make_internal_ptr function which would forward them on.


Engine wrapper

So we have a new OpenGL application class, but we haven’t yet touched our main.cpp file to use it. Instead of our main.cpp file directly creating a new instance of an OpenGLApplication, we will write another class whose responsibility is to selectively bootstrap the correct application, then run it and own its lifecycle including its destruction.

For this we will write a new class named Engine, after which we will clean up our main.cpp file such that all it does is create an instance of our Engine and ask it to run.

Create new source files under main/src/core named:

First, the header file. There isn’t much in the header, which if nothing else illustrates the internal_ptr pattern we are using. We just want a constructor and one public method named run:

Edit the engine.hpp with the following:

#pragma once

#include "internal-ptr.hpp"

namespace ast
{
    struct Engine
    {
        Engine();

        void run();

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

Edit the engine.cpp with the following:

#include "engine.hpp"
#include "../application/application.hpp"
#include "../application/opengl/opengl-application.hpp"
#include "log.hpp"
#include "sdl-wrapper.hpp"
#include <stdexcept>
#include <string>

using ast::Engine;

struct Engine::Internal
{
    const std::string classLogTag;

    Internal() : classLogTag("ast::Engine::") {}

    void run()
    {
        static const std::string logTag{classLogTag + "run"};

        ast::log(logTag, "Starting engine ...");
        SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
        ast::log(logTag, "SDL2 initialized successfully ...");
        resolveApplication()->startApplication();
    }

    std::unique_ptr<ast::Application> resolveApplication()
    {
        static const std::string logTag{classLogTag + "resolveApplication"};

        try
        {
            ast::log(logTag, "Creating OpenGL application ...");
            return std::make_unique<ast::OpenGLApplication>();
        }
        catch (const std::exception& error)
        {
            ast::log(logTag, "OpenGL application failed to initialize.", error);
        }

        throw std::runtime_error(logTag + " No applications can run in the current environment");
    }

    ~Internal()
    {
        SDL_Quit();
    }
};

Engine::Engine() : internal(ast::make_internal_ptr<Internal>()) {}

void Engine::run()
{
    internal->run();
}

Let’s walk through the interesting parts of the struct Engine::Internal:

Run function

The run function simply initialises the basic SDL system, then proceeds to obtain an instance of an application to start by invoking the resolveApplication function.

void run()
{
    static const std::string logTag{classLogTag + "run"};

    ast::log(logTag, "Starting engine ...");
    SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS);
    ast::log(logTag, "SDL2 initialized successfully ...");
    resolveApplication()->startApplication();
}

Resolve application function

The resolveApplication function will attempt to initialise each application that we want to support - to begin with we only have OpenGL but later we will introduce the Vulkan application as well.

We are using exception handling to give us a non fatal control flow if a desired application cannot be initialised. This will become far more important when we integrate Vulkan, where it is generally not possible to know ahead of time if the myriad of Vulkan configuration steps will succeed.

Of course, if none of our supported application variants can be initialised then we don’t have a lot of options so we will throw a meaningful exception instead and bomb out.

Note also that we are using a unique_ptr smart pointer as a return value so it will self destruct upon leaving its containing scope. We are also returning a type of ast::Application although the actual selected implementation is ast::OpenGLApplication (and in the future could be a Vulkan application).

There is also a smattering of logging with our logging mechanism, including the use case of passing an exception through for logging.

std::unique_ptr<ast::Application> resolveApplication()
{
    static const std::string logTag{classLogTag + "resolveApplication"};

    try
    {
        ast::log(logTag, "Creating OpenGL application ...");
        return std::make_unique<ast::OpenGLApplication>();
    }
    catch (const std::exception& error)
    {
        ast::log(logTag, "OpenGL application failed to initialize.", error);
    }

    throw std::runtime_error(logTag + " No applications can run in the current environment");
}

Internal destructor

The internal destructor is pretty simple, it just tries to be a good citizen and shut down the SDL system gracefully.

~Internal()
{
    SDL_Quit();
}

Public API - constructor

The public constructor is quite basic and you will see this pattern repeated a lot. It’s pretty much same as before - our public constructor simply creates a new instance of our Internal struct via our internal_ptr mechanism.

Engine::Engine() : internal(ast::make_internal_ptr<Internal>()) {}

Public API - run function

The public run function simply defers to the internal implementation. This is how most of our public functions will operate - by deferring to the internal implementation.

void Engine::run()
{
    internal->run();
}

Clean up time!

After all that we can finally revisit the main.cpp file and remove almost everything from it, leaving it with the simple responsibility of creating an instance of our Engine and running it.

Edit the main.cpp file to this:

#include "core/engine.hpp"
#include "core/sdl-wrapper.hpp"

int main(int, char* [])
{
    ast::Engine().run();
    return 0;
}

Pretty massive change yeah? An instance of our engine is both created and run with this line:

ast::Engine().run();

There is one very important detail in this new main.cpp file: the #include "core/sdl-wrapper.hpp" MUST be included here, as it will reconfigure the main entry point of the application to actually bootstrap the SDL foundation automatically on certain platforms. If the header is not included here, you will find that platforms such as Android will fail. The related code can be found in the SDL_main.h from the SDL library and looks like this:

#if defined(SDL_MAIN_NEEDED) || defined(SDL_MAIN_AVAILABLE)
#define main    SDL_main
#endif

Drum roll please!

Run your application again and you should see the familiar window filled with green. In the DEBUG CONSOLE area you will see our logging statements (amongst all the framework logs) that would look something like:

ast::Engine::run: Starting engine ...
ast::Engine::run: SDL2 initialized successfully ...
ast::Engine::resolveApplication: Creating OpenGL application ...
ast::OpenGLApplication::createContext: Created OpenGL context with display size: 640 x 480

At this point you could go and run each of our target platforms to see that they are all still working. As a reminder, here is how to do each (of course we just ran the console application so no need to explain that).

Emscripten

Android

MacOS

iOS

Windows


Summary

The refactored code feels a bit cleaner than the mash of code we had before and gives us a few handy mechanisms to start building out some more interesting parts of our engine.

I’m not sure about you but I am getting rather tired of the plain green screen, so next up we will learn a way to load a 3D model from storage and with the goal of rendering it.

The code for this article can be found here.

Continue to Part 9: Load a 3D model.

End of part 8