Skip to content
Jonathan Hoffstadt edited this page Oct 4, 2024 · 7 revisions

Welcome to the Pilot Light wiki!

Table of Contents

How Do I Contribute?

For those wishing to contribute, first find an issue you would like to work on that is listed in the issue list and leave a comment so we can assign you to the issue. Next make sure you checkout the coding style guide. Once the work is complete, checkout the contributing page for information on commit message format and branch naming. Finally, just open a pull request!

What is Pilot Light?

I’m still not quite sure what label to put on Pilot Light. Calling it a “game engine” is way too generous at this stage. Maybe the best way to understand it is by describing the individual components that comprise it, goals I wish to accomplish with it, and the problems it attempts to solve.

Primary Goal

As the name suggests (Pilot Light), the project is meant to be a starting point for other projects. Whether those projects are games, tools, or prototypes, the goal is to be a useful starting point. In addition to this, I want to keep Pilot Light lightweight, performant, and (nearly) dependency free. I do not want to be too restrictive or force users to work at any specific layer of abstraction. I also want things to be as modular as possible.

In the ideal situation, everything would STB style libraries which are about as decoupled as things can be. In reality, this obviously isn't possible for everything, but the effort is still made when possible, and those libraries end up in the libs folder. These are standalone libraries that can easily be dropped into any project. Everything thing else becomes an extension.

Architecture

The overall architecture takes inspiration from the now nonexistent The Machinery game engine. This architecture is very plugin (what I call extension) based. If something can be an extension, then it should be! Functionality is provided by APIs which are structs of function pointers.

An example of one of these APIs look like this:

typedef struct _plJobI
{
    // setup/shutdown
    void (*initialize)(uint32_t uThreadCount); // set thread count to 0 to get optimal thread count
    void (*cleanup)(void);

    // typical usage
    //   - submit an array of job descriptions and receive an atomic counter pointer
    //   - pass NULL for the atomic counter pointer if you don't need to wait (fire & forget)
    //   - use "wait_for_counter" to wait on jobs to complete and return counter for reuse
    void (*dispatch_jobs)(uint32_t uJobCount, plJobDesc*, plAtomicCounter**);

    // batch usage
    //   Follows more of a compute shader design. All jobs use the same data which can be indexed
    //   using the job index. If the jobs are small, consider increasing the group size.
    //   - uJobCount  : how many jobs to generate
    //   - uGroupSize : how many jobs to execute per thread serially (set 0 for optimal group size)
    //   - pass NULL for the atomic counter pointer if you don't need to wait (fire & forget)
    void (*dispatch_batch)(uint32_t uJobCount, uint32_t uGroupSize, plJobDesc, plAtomicCounter**);
    
    // waits for counter to reach 0 and returns the counter for reuse but subsequent dispatches
    void (*wait_for_counter)(plAtomicCounter*);
} plJobI;

The APIs are accessed through the API registry which has the following API:

typedef struct _plApiRegistryI
{
    const void* (*add)   (const char* pcName, const void* pInterface);
    void        (*remove)(const void* pInterface);
    const void* (*first) (const char* pcName);
    const void* (*next)  (const void* pPrevInterface);
} plApiRegistryI;

The APIs are provided by the core runtime and by extensions. Extensions are just dynamic libraries that provide a load and unload function. You can see an example of these functions for the extension that provides the job system API shown above:

static void
pl_load_job_ext(plApiRegistryI* ptApiRegistry, bool bReload)
{
    ptApiRegistry->add(PL_API_JOB, pl_load_job_api());
    if(bReload)
    {
        gptJobCtx = gptDataRegistry->get_data("plJobContext");
    }
    else
    {
        static plJobContext gtJobCtx = {0};
        gptJobCtx = &gtJobCtx;
        gptDataRegistry->set_data("plJobContext", gptJobCtx);
    }
}

static void
pl_unload_job_ext(plApiRegistryI* ptApiRegistry, bool bReload)
{
    ptApiRegistry->remove(pl_load_job_api());
}

Core

The core of the engine is very small and entirely contained in the src folder. The core is just an executable that provides the following systems:

  • API Registry
  • Data Registry
  • Extension Registry

It also provides APIs for those systems in addition to APIs for general memory allocation, keyboard/mouse input, etc. These APIs are required by all platform backends and are found in pl.h. Backends also provide optional OS APIs for windows, libraries, files, networking, threads, atomics, and virtual memory (found in pl_os.h).

In addition to the above APIs and systems, the core is responsible for handling loading, unloading, and hot reloading of applications and extensions.

Application

An application is just a dynamic library that exports the following functions:

PL_EXPORT void* pl_app_load    (plApiRegistryI*, void* pAppData);
PL_EXPORT void  pl_app_shutdown(void* pAppData);
PL_EXPORT void  pl_app_resize  (void* pAppData);
PL_EXPORT void  pl_app_update  (void* pAppData);
PL_EXPORT bool  pl_app_info    (plApiRegistryI*); // optional

Applications are hot reloadable. A very minimal app can be seen in example_1.c.

Extensions

An extension is just a dynamic library that exports the following functions:

PL_EXPORT void pl_load_ext  (plApiRegistryI* ptApiRegistry, bool bReload);
PL_EXPORT void pl_unload_ext(plApiRegistryI* ptApiRegistry, bool bReload);

Build System

The project does not have a typical build system. We prefer to just use plain batch/bash scripts that directly call the compiler. However, this starts to be hard to maintain when supporting multiple platforms and target binaries. It can also be error prone. So I built our own little build system (called pl-build). It is entirely contained in the pl_build folder. It is standalone and can also be found on pypi. It is python based. It simply outputs the final build scripts for each platform. You can see how we use it in the scripts folder.

Notes

Some of the following things may be interesting to some:

  • header files are not allowed to include other header files (with the exception of system headers)
  • everything is hot reloadable
  • we utilize unity builds often

Current Status

With a very modular project like this, different components are following their own versioning. There are individual versions for the core runtime, individual libs, build system, and extensions. It is also worth noting that I take semantic versions very serious and do not like increasing the major number often. So "1.0" is a big deal for me! The statuses currently:

  • 1.0 core (everything in src folder)
  • 1.0 libs (everything in libs folder)
  • 1.0 pl_build

I'm currently working on 1.0 statuses for all the extensions (everything in extensions folder. The status of those can be found on the API & Extension Review page.

Obviously, I also need to get documentation going. Now that I'm getting some 1.0 statuses, its time.

The renderer extension is a ways off but that one in particular is where I want to spend the most time and what I enjoy working on most. Unfortunately, it's been neglected in favor of building up all the surrounding systems. But that is why I'm pushing to 1.0 everything else so that I can finally focus on rendering!

Clone this wiki locally