Skip to main content

Plugins

In this guide you will learn, how to write your own plugin for the FakeEngine. Plugins are one of the core techniques of the engine, enabling the engine to be up-to-date with any industry standard for a long time. Any plugin that gets outdated will be much less work to rewrite/refactor than static modules, which are part of the core engine.

Getting started

The build system of the engine is very advanced and will pick up any plugin automatically, that wants to get built and distributed. The plugins are compiled into DLLs on windows or dylib files on MacOS, or .so files on Linux. All this is already handled, all you need to provide to the build system are the instructions, on how to build your own plugin, also enabling you to use third-party libraries, you have the full freedom.

Directory structure

This is, how a typical plugin folder structure for the FakeEngine would look like:

FakePlugins/
├── Your-Plugin-Directory/
├── src/ // The directory containing your source files.
├── vendor/ // Optional directory, containing your third-party dependencies.
├── build.bat // Build file for windows, that describes the build steps for just your plugin.
├── build.sh // Build file for MacOS/Linux, that describes the build steps for just your plugin.

Building your plugin

The following section will focus on just the build.bat and build.sh files.

Windows

For Windows, you need to provide a simple batch script, that just has to invoke our own makefiles with the parameters specific to your plugin. This is an example implementation:

@ECHO OFF

make -f "FakeEngine.library.makefile" %ACTION% TARGET=%TARGET% ASSEMBLY=<YourPluginName> VER_MAJOR=0 VER_MINOR=1 DO_VERSION=no ADDL_INC_FLAGS="-IFakeEngine\src -IFakePlugins\<YourPluginDir>\src" ADDL_LINK_FLAGS="-lFakeEngine" ADDL_FLAGS="MODULE_IS_PLUGIN"
if %ERRORLEVEL% neq 0 exit /b %ERRORLEVEL%

The paths have to be relative from the root directory of the engine repository, let's dive in into the parameters even more:

ParameterExplanation
make -f "FakeEngine.library.makefile"This has to say exactly like this, as the current cwd is always the root repository of the engine.
%ACTION%Is set by parent scripts, you just have to forward this. It tells the makefile, whether to build or clean the build output.
%TARGET%Is set by parent scripts, you just have to forward this. It tells the makefile the platform, which should be build for.
ASSEMBLYThe name of your plugin. This is how the output binary is going to be named.
VER_MAJORThe major version of your plugin, only used if DO_VERSION is set to yes.
VER_MINORThe minor version of your plugin, only used if DO_VERSION is set to yes.
DO_VERSIONTells the makefile, if you want your plugin to be versioned. Valid options are no or yes.
ADDL_INC_FLAGSAdditional include flags, specific to your plugin. In this example it is set to your own source directoy and to the source directory of the engine. If you want to use the engine, the last include flag is necessary and the syntax is the same as in gcc/clang.
ADDL_LINK_FLAGSAdditional linker flags, specific to your plugin. In this example it is set to link into the engine, if you want to use the engine, this is necessary and the syntax is the same as in gcc/clang.
ADDL_FLAGSAdditional compiler flags, as this project is a plugin, it is absolutely required to be set to MODULE_IS_PLUGIN.

MacOS/Linux

The MacOS and Linux version is just a bit longer, because the parameters from the parent scripts have to be received first. But the core principle is exactly the same, as on windows.

This is, how your plugin build script for Linux and MacOS will have to look like:

#!/bin/bash

ACTION=$1
TARGET=$2
DO_VERSION=$3

make -f FakeEngine.library.makefile $ACTION TARGET=$TARGET ASSEMBLY=<YourPluginName> VER_MAJOR=0 VER_MINOR=1 DO_VERSION=no ADDL_INC_FLAGS="-I./FakeEngine/src -I./FakePlugins/<YourPluginDir>/src" ADDL_LINK_FLAGS="-lFakeEngine" ADDL_FLAGS="MODULE_IS_PLUGIN"

ERRORLEVEL=$?
if [ $ERRORLEVEL -ne 0 ]
then
echo "Error: "$ERRORLEVEL && exit
fi

Starting the first lines of code of your plugin

Now you know, how your plugin will be built. You also have to provide some source files, in order for the build system to compile your plugin successfully.

In this section, you will learn how to start with the bare minimum, so that your plugin can be successfully compiled and successfully loaded by the engine.

The engine requires specific entry points, that the engine assumes are present inside the dynamic library, so you will need to make sure to also provide these functions.

This is how the source folder will typically look like:

src/
├── PluginSpecificFolder/ // In this folder, you can place as many source files as you like. The folder is not required but it helps seeing the entry point quicker.
├── PluginMain.c // This is the main entry point of your plugin. It only implements the functions defined in the header file.
├── PluginMain.h // This is the main entry point of your plugin. It defines the required functions and also exports them into a dynamic library.

The PluginMain.h file would look like this:

#pragma once

// If you used the build script examples above, the include and the types will build successfully.
#include <Plugin/Plugin.h>
#include <Core/FrameData.h>

FAKE_API b8 fake_plugin_create(fake_plugin *out_plugin);
FAKE_API void fake_plugin_destroy(fake_plugin *plugin);
FAKE_API b8 fake_plugin_update(fake_plugin *plugin, const fake_frame_data *p_frame_data);

The FAKE_API macro exports the function into a shared library, therefore it is absolutely necessary to be present here.

And this is, how the implementation of these functions will look like:

#include "PluginMain.h"

#include <Containers/DynamicArray.h>

#include <Core/Assert.h>
#include <Core/Logger.h>
#include <Core/Memory.h>
#include <Core/String.h>

// Called, when the plugin is loaded by the engine.
b8 fake_plugin_create(fake_plugin *out_plugin)
{
// These 4 lines are the only ones required, see below for more details.
out_plugin->name = "<Your Plugin name>";
out_plugin->type = FAKE_PLUGIN_TYPE_; // See below for the options.

// Further information for these will follow in the next section.
out_plugin->interface_size = 0;
out_plugin->interface = 0;

return true;
}

// Called, when the plugin is destroyed by the engine.
void fake_plugin_destroy(fake_plugin *plugin)
{
if (plugin)
{
// Your destruction code for the plugin.
// But only resources you actively allocated in the creation function,
// the plugin structure itself is cleaned by the engine.
}
}

// Called on each frame from the engine.
b8 fake_plugin_update(fake_plugin *plugin, const fake_frame_data *p_frame_data)
{
// If your plugin wants to use the rendering frontend, you can do it in this function.
// As for plugins, the typical update() function is also a render() function.
return true;
}

The plugin type is an enumeration value, and the engine has the knowledge of these for specific core plugins, which are covered later in this guide.

These are the valid options for core plugins:

Type nameExplanation
FAKE_PLUGIN_TYPE_NO_RENDERER@Deprecated. Used, to provide a dummy implementation for the renderer backend.
FAKE_PLUGIN_TYPE_VULKAN_RENDERERUsed, to let the engine know about a new vulkan implementation. You would probably only have one vulkan plugin, but if you want to provide your own, you need to provide this plugin type.
FAKE_PLUGIN_TYPE_SHADER_COMPILERUsed, to let the engine know about a new shader compiler. Shader compiler can be present multiple times.
FAKE_PLUGIN_TYPE_SSLUsed, to let the engine know about a new SSL implementation, the engine itself uses a OpenSSL plugin, that uses OpenSSL as a third-party dependency, but only inside the plugin itself, the engine itself knows nothing about OpenSSL, it just provides a frontend to interact with.
FAKE_PLUGIN_TYPE_CLOUDUsed, to let the engine know about a new cloud system, currently we support Google drive and Microsoft OneDrive. If you want to provide more cloud system, this plugin type is required to be used.

If you plan to not write a core plugin, you will not need those types, but you will still need a type. For this purpose, you need to provide a value between 255 and 512 (excluding).

danger

TODO: Write a better system, maybe a function for the plugin system, that knows about already registered user plugins and provides a number automatically, that was not used before.

Exploring the plugin structure further

The plugin structure looks like this:

typedef struct fake_plugin
{
/// @brief The name of the plugin.
char *name;

/// @brief The plugin type.
fake_plugin_type type;

/// @brief The interface pointer contains a plugin specific structure, known by the plugin and the engine.
void *interface;

/// @brief the size of the plugin specific structure in bytes.
u32 interface_size;
} fake_plugin;

A user plugin, never has to provide anything for the interface or interface_size. Those are used by core plugins, if the engine needs knowledge about plugin specific types. But if you want to share a struct through the functions and have it kept alive during runtime, you can misuse these fields, as user plugins won't be ever checked for the validity of these fields. It enables your plugin to hold states and data.

danger

TODO: Rename the interface and interface_size fields to user_data and user_data_size, as those names would make more sense for both usages, in core and user plugins.