Game Engine, C 1, P 4, Window Setup

About this post

Here we will sneak a quick peek at info logging, dependency injection and window creation with Vulkan and GLFW. The actual useful code after this post resembles this Vulkan tutorial and the code from quick start of spdlog.

I will revisit all these topics in future posts. After creating a window, I will dig further down the Vulkan path, adding functionality along the way. Then multi-thread it all with a job system. When the job system is done, I will come back to logging and profiling (the multi-threaded approach), to dependency injection (not sure about another post, but it will be used in code) and to window creation (different options, etc.).

You can see the full code after this post on my GitHub.

Reviewed Systems

Logging

After the project is a bit bigger, I will rewrite some logging code to access specific logger objects. Right now, it is easier to store the logger globally and access it from everywhere. If you are interested in learning more about spdlog, check it out here. We will set up two logging sinks: a console one for warnings and errors and a text one for all info messages. After that, we combine these sinks in a logger and set it as default.

Dependency Injection

DI is easy to learn. In its most basic form, it is an ability to inject concrete implementations of some functions, based on the needs of the programmer or user. The program should work correctly regardless of the specific implementation. You can read more about it on Wiki. My choice of DI library is [Boost].DI, as it has all that I need and is extremely fast. You can access their docs here, but keep in mind that some links may be broken.

In this post I use DI to inject specific controllers for some of my classes, context and window to be specific. I am doing this in case I change my mind about some library that I am using. For example, if I switch from GLFW to SDL at this stage of development, I will only have to rewrite the window controller and the context controller. All other code will be left unchanged.

Naming

There are a lot of ways to name classes when using DI. Here are a few examples:
  • class, class_controller and concrete_class_controller (my choice)
  • class_controller, class_service and concrete_class_service (closer to service programming)
  • class, class_interface and concrete_class_interface (appears most in simple online examples)
  • class, class_impl and concrete_class_impl (resembles the PIMPL idiom)
I chose the first one because "the concrete controller controls the execution of different calls to window functions", but I am not in any way against other approaches.

Window Creation

As I mentioned before, I am using GLFW for window and context handling. This library was proven to be great for desktop programming (which is the aim, although I am keeping track of library compatibilities with different OS types), so I didn't think long before using it. There are several tutorials on the net about basic window creation with GLFW and all of them use more or less the same functions (which makes sense). Since I will focus on Vulkan in this engine, I followed the vulkan-tutorial example of window creation. I didn't add anything from myself yet, so you can just use code from there or from my GitHub.

GLFW stores window information in a GLFWwindow pointer and it is used a lot for event handling and so on. Unfortunately, it is implementation-specific, so I had to toy around with converting it back and forth to a void pointer. Although it is clumsy, AFAIK other libraries use pointer handles for windows as well, so it is fairly reusable. That is, as long as I don't forget to convert it when needed.

Project structure

The project structure changed a bit since the last post. I may have forgotten to write about some parts of the structure, but now I've added controller folders for interfaces and implementations. I've also added a folder for memory management, although it is mostly empty for now. I can promise, that it will be filled in the future.

Here is the basic outline of the engine:
  • [app]
    • [context controllers]
    • [window controllers]
    • app
    • context
    • window
  • [memory]
    • [allocators]
    • memory tracker
  • [utility]
    • dependencies
    • engine info
    • errors
    • macros
I understand that storing the application class deep inside the engine removes the point of separating the engine and the project on top, but it is easier to develop for now. Later, I will leave a partially implemented and commented class in the engine, preventing the user from creating an instance of it. The user will be able to use this info to create their own application class.

Code

I am not sure of what to put here, as most of the code was taken straight from the tutorials and guides, that I have linked. This is why I have decided to show the application class. If you want to check out the systems, that are being used, you can browse the source code (link at the beginning of the post).

application.h


#pragma once

#include <memory>
#include "spdlog/spdlog.h"

#include "vivid_core/app/context.h"
#include "vivid_core/app/window.h"

namespace vivid_core
{
 namespace app
 {
  class application {
  private:
   std::shared_ptr<spdlog::logger> _logger;
   std::shared_ptr<context> _context;
   std::shared_ptr<window> _window;
  public:
   int run();
  private:
   int init();
   int main_loop();
   int term();

   int init_logger();
   int init_context();
   int init_window();

   int term_logger();
   int term_context();
   int term_window();
  };
 }
}

application.cpp


#include "vivid_core/app/application.h"
#include "vivid_core/utility/error.h"

//Include for Vulkan support only
#define GLFW_INCLUDE_VULKAN
#include <GLFW/glfw3.h>

#include "spdlog/async.h"
#include "spdlog/sinks/stdout_color_sinks.h" // or "../stdout_sinks.h" if no colors needed
#include "spdlog/sinks/basic_file_sink.h"

#include "vivid_core/utility/dependencies.h"

using namespace vivid_core::app;
using namespace vivid_core::utility;

int application::run()
{
 init();
 main_loop();
 term();
 return (int)error::SUCCESS;
}

int application::init()
{
 int res = (int)error::SUCCESS;
 res = init_logger();
 if (res != (int)error::SUCCESS)
 {
  term_logger();
  return res;
 }
 res = init_context();
 if (res != (int)error::SUCCESS)
 {
  term_context();
  term_logger();
  return res;
 }
 res = init_window();
 if (res != (int)error::SUCCESS)
 {
  term_window();
  term_context();
  term_logger();
  return res;
 }

 spdlog::info("Init successful");
 return (int)error::SUCCESS;
}

int application::term()
{
 term_window();
 term_context();
 term_logger();

 return (int)error::SUCCESS;
}

int application::main_loop()
{
 while (!glfwWindowShouldClose((GLFWwindow*)_window->expose_handle())) {
  glfwPollEvents();
 }
 return (int)error::SUCCESS;
}

int application::init_context()
{
 _context = dependencies::get_injector().create<std::shared_ptr<context>>();
 return _context->init();
}

int application::init_window()
{
 _window = dependencies::get_injector().create<std::shared_ptr<window>>();
 return _window->init(640, 480, "Vivid Engine");
}

int application::init_logger()
{
 //TODO - replace allocations with a custom allocator
 auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 console_sink->set_level(spdlog::level::warn);

 auto file_sink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("log.txt", true);
 file_sink->set_level(spdlog::level::trace);

 spdlog::sinks_init_list sink_list = { file_sink, console_sink };
 spdlog::logger *logger = new spdlog::logger("global log", sink_list.begin(), sink_list.end());
 logger->set_level(spdlog::level::debug);

 _logger = std::shared_ptr<spdlog::logger>(logger);
 spdlog::set_default_logger(_logger);

 return (int)error::SUCCESS;
}

int application::term_logger()
{
 spdlog::drop_all();
 spdlog::shutdown();
 return (int)error::SUCCESS;
}

int application::term_context()
{
 int result = 0;
 if (_context) return _context->term();
 return (int)error::SUCCESS;
}

int application::term_window()
{
 return (int)error::SUCCESS;
}

Notes


Take a look at how initialization and termination is handled. The systems are started one by one. If one fails, all other are shut down and the process ends. Logger is independent of other parts of the app, so it is initialized first and terminated last.

dependenies.h

This is the file where the dependency injector resides. It is a singleton, because I think it will be used for more stuff later, but the injector is accessed separately. I have used macros for namespaces not to mess with my indentations.
#pragma once

#include "vivid_core/utility/macros.h"
#include <boost/di.hpp>

#include "vivid_core/app/context_controllers/context_controller.h"
#include "vivid_core/app/window_controllers/window_controller.h"

#include "vivid_core/app/context_controllers/glfw_context_controller.h"
#include "vivid_core/app/window_controllers/glfw_window_controller.h"

START_ENGINE
START_NAME(utility)

class dependencies
{
private:
 dependencies();
public:
 dependencies(dependencies const&) = delete;
 void operator=(dependencies const&) = delete;

 static dependencies& get_instance();
 static auto& get_injector() noexcept
 {
  const auto injector = boost::di::make_injector(
   boost::di::bind<app::context_controllers::context_controller>
   .to<app::context_controllers::glfw_context_controller>()
  , boost::di::bind<app::window_controllers::window_controller>
   .to<app::window_controllers::glfw_window_controller>()
  );
  return injector;
 }
};

END_NAME(utility)
END_ENGINE

Conclusion

I can understand, that this was a shallow post without much interesting info. It shows a bit of everything, but doesn't cover any specific topic. I felt that it was necessary to make it in order to document the whole development, but I hope that someone will find it useful regardless of the quality.

The next post will probably continue the Vulkan path, starting with multithreading. You can check it out right now!

Comments