Bringing C++ to Godot with GDExtensions

22/Feb/2024

So I’ve been fumbling around with Godot a fair amount recently. From making little microgames as exercises to poking around and trying to get a more standard application up and see how the engine behaves and, perhaps considering it as a standard for regular applications

So far, Godot’s been impressing me in many areas. With Qt letting me down for the deployment of web assembly applications, there certainly is a niche to be filled. When compared to Godot, however, one of the big advantages Qt has is C++ itself. It’s really easy to develop and integrate other code with C++, especially if that code is already C++. With Qt being a well established framework built on a well established language, not only there’s a lot of C++ code around, there is also a lot of bindings for different things and, sometimes, the trappings of something like Godot just won’t quite give you what you need

But it’s not like this is where the road ends for Godot, you absolutely CAN get your own external code to work with and integrate into your Godot projects. It is finally time to get a look at Godot’s GDExtensions and once again, as tradition, we’re going to do that by bringing in toyBrot

Godot Mini Monthly - Jan 24

As part of my ongoing studies with Godot and in the interest of keeping contact with the engine and avoid falling too far apart as I juggle all of my other projects, I’ve decided to try and make sure I do one “mini project” using Godot each month.

 

The rules are:

  • It HAS to use Godot in some way
  • I HAVE to finish it in one or two days
  • I NEED to have something to show for it at the end
  • It SHOULD give me the excuse to try something new with the engine, as a treat

So while this may rule out bigger projects, the goal here is make sure I’m active, even if most of my attention is trapped in a different thing. Managing the scope for these is a good exercise in itself and finding new bits of the engine to poke is always a satisfying experience

As for what each mini project actually IS, whatever strikes my fancy at the time will do. Though this means I’m likely to try and look for things that will build towards larger projects, it could just be a free-form minigame, or demo or what have you. Fun is definitely a goal here

January ended up being a bit messy on my end and this actually gave me more trouble than I expected so I didn’t quite make it in time. I’m still on the fence if February’s actual project is going to build on this or be something entirely new, guess you’ll have to wait and see

C++ in Godot: GDExtensions and Modules

So, GDExtensions are a new feature of Godot 4 but integrating C++ into your projects isn’t, there is actually an alternative, older way of doing it: C++ Modules

They don’t quite work the same way and there is some give and take. Broadly speaking, with Modules you build them into Godot itself. You start by making sure you can build Godot from source and plug more stuff in from there. This means you can get your hands in, basically all of the pie. A lot of what you would see as core Godot functionality is built with this system, like support for GDScript itself

But having to go this far in and building the engine itself is a bigger hassle than most people would like to deal with and can mean your code is more tied into the thing. For example, you then HAVE to go with C++ (and C, I guess) as that’s what the engine itself is built with. For a bit more of a “healthy distance”, you can use GDExtension. Instead of having to build the entire engine and putting your modules in it, through the use of bindings (which can be for languages other than C++) you can build this additional functionality as a shared library which can then get imported in your project

This means your code doesn’t need to be as deeply integrated with Godot itself (I mean, they indent with tabs? Do YOU want to indent YOUR CODE with tabs?) but it does mean that you get more limited to the avenues the bindings provide you. It also means there’s a bit more work in getting your library imported into your project as an extension. However, because you don’t need to compile the entire engine, this is also much easier on your build

For my case, this is well worth it. Easier iteration, looser ties and, thinking a bit more broadly: just as I wouldn’t expect someone to have to build Qt from source in order to work on some of my code, I also wouldn’t want to inflict on someone having to build the whole of Godot without very good reason

GDExtensions are still new and are being improved on, so let’s see how they fare

Getting my feet wet

So, Godot’s docs have a hello world type example that is easy to follow and pretty helpful. Going through it was largely a positive experience and, at the end you get a cute little wobbly sprite that you can drop into your project within the Godot editor. Reading through and following it was massively helpful in getting a feel for what I would need. And these start with some pre-requisites:

  • a Godot 4 executable,
  • a C++ compiler,
  • SCons as a build tool,
  • a copy of the godot-cpp repository.

So you need Godot itself, yeah, of course…. A C++ compiler, duh… a clone of the repository with the bindings, sure and… “Scons as a build tool”. Okay, what’s this about?

“SCons is an Open Source software construction tool that replaces the classic Make utility with Python scripts and integrated functionality.”

I’ll pretend to be sorry and all, but I will NOT bring Python into my build unless I am absolutely forced to do so. My previous experience has been builds immediately turning into card castles that topple over at the most random and minute breeze. Python HAS its uses but build systems is just not one of them. So this immediately threatened to massively cut my enthusiasm for GDExtensions. Lucky for me, while I was going through Godot’s example, I noticed something:

A screenshot of a code editor showing a CMakeLists.txt file in the godot-cpp directory
That’s a CMakeLists.txt right there!

And checking it out, it looked very much like I could just use that instead! I decided to take all of the demo code, drop all of the Scons and try putting it together with CMake instead, which basically worked with no issues. False alarm, the dream is very much still alive!

A screenshot of a code editor with a small godot extensions project. There's a CMakeLists file but no SConstruct
No SConstruct in sight. Nature is healing

But that’s not to say that using godot-cpp with CMake doesn’t have its own details and considerations. It’s enough of an interesting topic that I could very easily write a post just about that… and I will do so next time!

For now, I’m just going to glance over it: toyBrot is all CMake already and, as such, I decided to bring in godot-cpp and write my extensions as part of the same build tree

The Building Blocks

If instead of looking at these snippets you’d rather just get the full code itself, it’s all up on Gitlab. Everything I’m showing here is on the Godot/Extensions/cpp-threads directory

A GDExtension, from a Godot project’s point of view is two things: A shared library that’s going to get loaded by Godot (either your application or the editor when you’re working on it) and an extension definition file that tells you what the extension is called, how to initialise it and what libraries to look for in different platforms

Godot provides an example of this file which other than giving you a glimpse of just how many different platforms you end up being able to target with Godot, is pretty simple

[configuration]

entry_symbol = "tb_cpp_threads_library_init"
compatibility_minimum = "4.2"

[libraries]

macos.debug = "res://bin/libgdex-cppthreads.macos.template_debug.framework"
macos.release = "res://bin/libgdex-cppthreads.macos.template_release.framework"
windows.debug.x86_32 = "res://bin/libgdex-cppthreads.windows.template_debug.x86_32.dll"
windows.release.x86_32 = "res://bin/libgdex-cppthreads.windows.template_release.x86_32.dll"
windows.debug.x86_64 = "res://bin/libgdex-cppthreads.windows.template_debug.x86_64.dll"
windows.release.x86_64 = "res://bin/libgdex-cppthreads.windows.template_release.x86_64.dll"
linux.debug.x86_64 = "res://bin/libgdex-cppthreads.linux.template_debug.x86_64.so"
linux.release.x86_64 = "res://bin/libgdex-cppthreads.linux.template_release.x86_64.so"
linux.debug.arm64 = "res://bin/libgdex-cppthreads.linux.template_debug.arm64.so"
linux.release.arm64 = "res://bin/libgdex-cppthreads.linux.template_release.arm64.so"
linux.debug.rv64 = "res://bin/libgdex-cppthreads.linux.template_debug.rv64.so"
linux.release.rv64 = "res://bin/libgdex-cppthreads.linux.template_release.rv64.so"
android.debug.x86_64 = "res://bin/libgdex-cppthreads.android.template_debug.x86_64.so"
android.release.x86_64 = "res://bin/libgdex-cppthreads.android.template_release.x86_64.so"
android.debug.arm64 = "res://bin/libgdex-cppthreads.android.template_debug.arm64.so"
android.release.arm64 = "res://bin/libgdex-cppthreads.android.template_release.arm64.so"

On the C++ side, there are basically two things you need. The first is some housekeeping boilerplate. Your extension definition file tells Godot what symbol to look for when initialising your library. On this boilerplate, you need to register your custom types in Godot’s ClassDB. This is also where you’d handle any extraneous init/teardown shenanigans. If you’re not doing anything special, the example code is pretty sufficient. Godot has a template project you can use to get started. I copied their register_types files but otherwise didn’t really need the rest of the template (submodule, Sconstruct, etc…)

// register_types.hpp

#ifndef CPP_THREADS_REGISTER_TYPES_H
#define CPP_THREADS_REGISTER_TYPES_H

#include <godot_cpp/godot.hpp>


void initialize_tb_cpp_threads_types(godot::ModuleInitializationLevel p_level);
void uninitialize_tb_cpp_threads_types(godot::ModuleInitializationLevel p_level);

#endif // CPP_THREADS_REGISTER_TYPES_H
// register_types.cpp

#include "register_types.hpp"

#include <gdextension_interface.h>
#include <godot_cpp/core/class_db.hpp>
#include <godot_cpp/core/defs.hpp>

#include "toybrotcppthreads.hpp"

void initialize_tb_cpp_threads_types(godot::ModuleInitializationLevel p_level)
{
    if (p_level != godot::MODULE_INITIALIZATION_LEVEL_SCENE) {
		return;
	}
    godot::ClassDB::register_class<ToybrotCppThreads>();
}

void uninitialize_tb_cpp_threads_types(godot::ModuleInitializationLevel p_level) {
    if (p_level != godot::MODULE_INITIALIZATION_LEVEL_SCENE) {
		return;
	}
}

extern "C"
{
	// Initialization
    GDExtensionBool GDE_EXPORT tb_cpp_threads_library_init(GDExtensionInterfaceGetProcAddress p_get_proc_address, GDExtensionClassLibraryPtr p_library, GDExtensionInitialization *r_initialization)
	{
        godot::GDExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
        init_obj.register_initializer(initialize_tb_cpp_threads_types);
        init_obj.register_terminator(uninitialize_tb_cpp_threads_types);
        init_obj.set_minimum_library_initialization_level(godot::MODULE_INITIALIZATION_LEVEL_SCENE);

		return init_obj.init();
	}
}

With that boilerplate out of the way, you need to define the types in your extension. For me, at this point, I only need one, the toyBrot generator itself. Again, some of how this all looks should be familiar to people who’ve used Qt. You inherit from godot::Node (or variant/resource if that’s what you’re writing) and begin your declaration with a GDCLASS macro. This is the meat of our extension, we’re going to be looking at this class from now on

// toybrotcppthreads.hpp

class ToybrotCppThreads : public godot::Node
{
    GDCLASS(ToybrotCppThreads, Node)

public:
    ToybrotCppThreads();
    ~ToybrotCppThreads() override;
    godot::Ref<godot::Image> get_fractal();
    void generate();

protected:
    static void _bind_methods();

private:

    void _generation();

    bool exit;

    std::unique_ptr<FracGen> generator;

    std::thread fractalThread;
    std::condition_variable cv;
    std::mutex mtx;

};

About the rest of this section

In here I will go over in a bit of a handholdy level of detail all the things that are happening in toybrotcppthreads.cpp. If that sounds like a good time, that’s a good opportunity to go step by step. If this instead is like “oh, yeah, yeah, okay” for you, then here’s a TL;DR and you can then eyeball the code and skip over to the next section if you want. It’s not a lot of code and pretty straightforward

I extended Godot::Node into a wrapper for preexisting C++ code. The node itself fires a worker C++ thread that listens on a condition variable to call FracGen::Generate() which does the actual work. This wrapper also provides a function that memcpys toyBrot’s own generated pixel data and creates a Godot::Image from it, which it returns to the caller

This workflow is controlled by two signals emitted by that helper worker thread. Whatever happens within the generator is opaque to the Node and beyond. The Node itself also has hard-coded parameters for the generator that it passes on during construction. The whole code (aside from the constants) is less than a hundred lines long

Instead of just bringing over the C++ code or what have you, I decided to wrap the preexisting code. My godot::Node is really just a translation layer of sorts. I’ll call functions from the original “FracGen” objects from regular toyBrot and just have the Node passing stuff between both sides. IN FACT, the actual source and headers being used are the ones from the regular std::threads project. So I’m not going over the ins and outs of how that works

# extensions/cpp-threads/CMakeLists.txt
# The only new files are the ones for the extension registration and the Wrapper Node

add_library(${PROJECT_NAME} SHARED
            "${CMAKE_SOURCE_DIR}/raymarched/STDTHREADS/FracGen.hpp"
            "register_types.hpp"
            "toybrotcppthreads.hpp"
            "${TB_COMMON_SRC_DIR}/util.hpp"

            "${CMAKE_SOURCE_DIR}/raymarched/STDTHREADS/FracGen.cpp"
            "register_types.cpp"
            "toybrotcppthreads.cpp"
            "${TB_COMMON_SRC_DIR}/util.cpp" )

What matters here is this:

When the object is created, it takes parameters for where the “camera” is and where it’s pointing as well as how to generate and colour the fractal. For the GDExtension node, these are all hard-coded constants. The generator then has two functions of interest: generate actually calculates the fractal. For CPU-based generators it does so (optionally) in slices 10 pixels tall, just to have some progressive rendering instead of the user waiting forever to see any results. The return of that function indicates whether the full image is complete. getBuffer() is the other function of interest and it returns a shared_ptr to the generator’s internal buffer, where it writes the pixel data in RGBAF format. Other than that, it just has some helpers for information on the buffer itself

// STDTHREADS/FracGen.hpp

class FracGen
{
public:

    FracGen(bool bench = false, CameraPtr c = nullptr, ParamPtr p = nullptr, const size_t factor = 0);
    ~FracGen();
    FracPtr getBuffer() noexcept { return outBuffer;}
    size_t outLength() const noexcept { return outBuffer->size();}
    size_t outSize() const noexcept { return sizeof(RGBA) * outBuffer->size();}

    bool Generate();


private:

    bool bench;
    CameraPtr cam;
    ParamPtr parameters;
    FracPtr outBuffer;
    size_t lastHeight;

};
On the Node side, I have a function that tells the generator to run and a function that gets a godot::Image from whatever is on the generator’s buffer, these relate to the FracGen functions mentioned above

Before we move on to the implementation of these, there are couple things to note in the not quite public face of the class. There is a static _bind_methods function and a couple of private ones. And I think we can start looking at our source through this static function

// toybrotcppthreads.cpp

void ToybrotCppThreads::_bind_methods()
{

    godot::ClassDB::bind_method(godot::D_METHOD("_generation"), &ToybrotCppThreads::_generation);
    godot::ClassDB::bind_method(godot::D_METHOD("generate"), &ToybrotCppThreads::generate);
    godot::ClassDB::bind_method(godot::D_METHOD("get_fractal"), &ToybrotCppThreads::get_fractal);

    ADD_SIGNAL( godot::MethodInfo("update_available", godot::PropertyInfo(godot::Variant::OBJECT, "node")) );
    ADD_SIGNAL( godot::MethodInfo("generation_finished", godot::PropertyInfo(godot::Variant::OBJECT, "node")) );
}

This function is what you need in order to tell the Godot engine what this class actually does. If you want your functions and properties to be accessible from, say, your GDScripts, this is where you need to expose them. I’m only exposing a handful of methods here but this is also where you can expose properties with getters and setters and even group them for editor convenience

More curiously, perhaps, this is also where you declare your signals. While a lot of things in Godot have reminded me of Qt, this is one aspect where it differs. In Qt, you’d declare them as functions in your class declaration. This way MOC can pick them up and do the whole callback function pointer shenanigans we all love to never have to think about. In Godot they’re registered directly in ClassDB during the method binding process

These signals are how we are going to communicate with the outlying Godot application and they’re both emitted in the _generation function. ToyBrot’s actual generate function blocks until that round of calculation is done. In the context of an interactive application we don’t really want that. Not only that’d halt Godot’s own event loops and whatnot but also mean that we couldn’t interact with the application at all until the generation was complete, not even to quit it

// toybrotcppthreads.cpp

ToybrotCppThreads::ToybrotCppThreads()
    :fractalThread{&ToybrotCppThreads::_generation, this}
{
    if(godot::Engine::get_singleton()->is_editor_hint())
    {
        //Skip initialising at all within the editor
        return;
    }
    <...>
}

void ToybrotCppThreads::_generation()
{
    std::unique_lock<std::mutex> lock(mtx);
    while(true)
    {
        // Every second we check if we're not just leaving altogether
        auto status = cv.wait_for(lock, std::chrono::seconds(1));
        if(exit || !generator || godot::Engine::get_singleton()->is_editor_hint())
        {
            return;
        }
        if(status == std::cv_status::timeout)
        {
            continue;
        }
        const bool finished = generator->Generate();
        if(finished)
        {
            call_deferred("emit_signal", "generation_finished", this);
        }
        call_deferred("emit_signal", "update_available", this);
    }
}

This is where this _generation function comes in. It runs on its own C++ thread and watches a little condition variable for action. Every second it checks if it’s not actually time to pack it up and clean up the application. Otherwise, if that condition variable is triggered, it fires up the generator. One thing of note is that it can’t just emit the signals normally. In Godot, signals need to be fired within the main even processing processing thread. So we need to ask IT to do so. This is what call_deferred is about, it’s essentially a “can you please call this function with these arguments from the main thread at your earliest convenience? Plzkthx”. It’s a similar sort of issue as the one I had to overcome with OpenGL in Warp Drive. I haven’t looked at Godot’s own code but I imagine their code can afford to be slightly less involved due to their own ClassDB system

Notifying this condition variable is all the public facing ToybrotCppThreads::generate function does. It shares a mutex with the internal _generation just to make sure they don’t overlap and tells the condition variable to fire one round. Nothing special here

// toybrotcppthreads.cpp

void ToybrotCppThreads::generate()
{
    std::unique_lock<std::mutex> lock(mtx);
    cv.notify_one();
}

The get_fractal function is how we give the image data from toyBrot back to the overarching application. Luckily, Godot provides us with the means of handling raw image data such as the one we get from toyBrot. I do need to copy over the data from the internal buffer to a godot::PackedByteArray. Other than having some funny syntax to actually get the pointer to the first element, everything here is straightforward. Once we get this data in the Godot format, we can create a godot::Image from it, by telling it what are the dimensions and what the pixel data in the buffer actually is. We return a godot::Ref<godot::Image>. Godot has its own memory management system. Most built-in variant types are passed by value, but everything else is passed as counted references (like shared_ptrs). So if we’re sending our image out into the world, this is what they expect

// toybrotcppthreads.cpp

godot::Ref<godot::Image> ToybrotCppThreads::get_fractal()
{
    if(!generator)
    {
        return {};
    }
    godot::PackedByteArray data;
    data.resize(screenWidth * screenHeight * 4 * sizeof(tbFPType));

    memcpy(data.begin().operator->(), generator->getBuffer().get()->data(), data.size());
    return godot::Image::create_from_data(screenWidth, screenHeight, false, godot::Image::FORMAT_RGBAF, data);
}

And this is all there is to the C++ side, really… for the most part. For now, we can have a look at the Godot side

Enough C++ for now, let’s look at some GDScript

Straight off the bat there’s a tiny bit of chicken and the egg issue with GDExtensions. The Godot project will look for them in its own directory so, you need the directory to exist in order to deploy it there, even if it’s just to play with the extension. It’s the price you pay for living in two environments, I guess. They work together but each plays by their own rules for the most part. If the editor finds a .gdextension file in your project tree, it’ll use it to look for the libraries. As it stands, this project follows the convention in the demo, which places them in the bin folder but that’s more a lib or extensions to me if I’m actually thinking about it. Good enough for a first demo, though

If the extensions get properly loaded, they show up in the editor as regular nodes of the general type you inherited from. For toyBrot the node is strictly the code interface to a calculation backend, so it’s just a plain Node, but you can extend anything in the engine. The engine also picks up the signals you registered with the ClassDB and you can use them as if they were any regular old built-ins

A screenshot of the GodotEditor showing custom nodes from GDExtension both in the scene tree as well as in the Add New Node dialogue
Our new nodes show in dialogues and can be added like regular engine nodes

Here you can also see that I already have more than one toyBrot node in. Each is its own extension and, yeah, no reason why you can’t have several in your project. I’ll get back to the other node in a bit. Something that I did not do but is equally possible is to have one extension register several different types in. A more proper implementation of toyBrot would, at the very least, also register a params Resource such as the one I wrote for the interactive fragment shader project

Back to this application, though, there’s a button for each generator and, once they’re clicked, they lock all buttons up and tell that specific generator to fire up. It also makes a little “now working” indicator visible for some additional user feedback. I’m also keeping internal track as to whether there’s a fractal being calculated in the background

After that, the main script is just listening on those two signals from the extension node. Once the generation is finished, it hides the indicator and re-enables the buttons if you want to go again

A screenshot of the Godot Editor showing signals from GDExtensions working like regular signals in the editor
This is all of the GDScript, really

Every time a round of generation is finished the update_available signal from either generator is emitted. We get the image from that generator and pass it on to the texture on the screen. With a little more thought and making use of inheritance this could all be statically typed too, but for the demo we can just rely on Godot’s dynamic typing and avoid duplicating some code. Once the image is set, we just check if there’s still more fractal to calculate and, if so, fire another round automatically

And that’s basically it, really. There’s almost no work on the Godot side of things. Once you Export your project, Godot automatically copies the libraries for the extensions to your output folder so it’s pretty much ready to go

Taking Godot to new heights!

With all of this in place, really you can just go crazy. Once I had this C++ threads working, I then went back and “what if I just bring OpenCL into Godot?”. I copied the C++ threads project, did a little find and replace and it mostly just worked. You want to have OpenCL in your Godot application? Sure, you’ve got it!

I mentioned a while back that support for GDScript itself is implemented as a Godot Module and, if you were so inclined, you could even do something like a more involved system to offload some work, as long as it’s all runtime work. Suddenly you don’t need to, say, limit yourself to Godot’s own compute shaders to offload work, and can even bring in some heavy hitters. No reason why you can’t just have all the working bits in your extension written with ISPC, for example and suddenly your Godot project can leverage AVX under the hood

But what’s with that “MOSTLY” up there a couple paragraphs above?

Every rose has its thorns

So, like everything else, GDExtensions has its own set of rough edges and pitfalls. Some of the problems I ran into were of my own doing but in others I’m innocent, I swear

First and foremost, toyBrot’s original code DID need some tweaks. Historically toyBrot has always been a one-shot thing. Even in benchmarks the generator object gets destroyed and rebuilt between runs. This is something that I thought would give me some interesting insight in setup and teardown of different compute backends but it didn’t really pan out that way. While the big old refactor that’s going to change this doesn’t come, I went and just added an initial cleanup of the fractal image to allow multiple iterations in an environment where the generator is just kept around

// I had to add this to every FracGen.cpp

if(lastHeight == 0)
{
    outBuffer->assign(outBuffer->size(), RGBA{0,0,0,1});
}

A second thing that gave me trouble was that Godot didn’t like me outputting to std::cout. To overcome that I wrote my own stream buffer that, instead, flushes to Godot’s own print(). You can look at some of that code here but this post is already WAY too long without me getting into the details of that, but I’m happy to do so in another time

// common/util.cpp

// Instead of directly using std::cout, everyone now outputs to this toyBrot::out
// On debug GDExtensions it outputs to godot::UtilityFunctions::print()
// this happens further in the file and is not shown here

std::streambuf* toyBrot::messageBuffer::get()
{
#ifndef TB_NOSTDOUT
    return std::cout.rdbuf();
#else

    static constexpr const size_t bufferSize = 512;
    static toyBrot::messageBuffer msgBuf(bufferSize);

    return &msgBuf;
#endif
}

std::ostream toyBrot::out(toyBrot::messageBuffer::get());

Still in code I had ONE actually bad issue. Keen-eyed readers may notice that there were three gdextensions in those editor screenshots, not two. I started by using std::async instead of std::threads but, for whatever reason, when I tried to get() any of the futures I got, I would get a segfault as if I had double-got them. I fumbled around a bunch, tried switching compilers and all but just couldn’t figure this one out. Also couldn’t search online. Now that this initial work is done, I want to revisit that and get in contact with the Godot dev community at some point about it

Finally, using OpenCL as my GPU implementation (I initially considered CUDA since I’m down to just an nVidia anyway(urgh), but wanted something easier for others to build and check if they wanted) exposed one tricky thing about the whole process: deployment of support files. ToyBrot wants to read a FracGen.cl from whatever directory it’s running in. This is hard-coded and it just crashes out if it doesn’t find it. But this presents a lot of additional difficulties with Godot. I can’t “Export” my cl file because Godot doesn’t understand it as a resource and, as such, just ignores it even if it is on the project folder. The engine just pretends it doesn’t see it anywhere in the file system. I’m also not entirely sure what is the active directory when you run your project from the editor. I assume the project root but, honestly, haven’t checked

A screenshot showing GDExtensions loaded in the Godot editor and their respective shared libraries in a directory, alongside a .cl file that is not listed in the editor at all
Only the .gdextension files are present in the editor's file explorer

All of those make it so that I can’t actually run my application from the editor. I need to export it, make sure I’ve manually copied the cl file to the export dir and run it from there. All of this can be overcome but will require more setup and… why does that sign say “please wrap it up”? One more for the pile, I guess

For the final thing I’ll mention today, initially I wanted to use a Godot::Thread for the Node’s _generation function but couldn’t get that to work. Eventually, since that is still internal to the node, I just went with regular C++ and that worked with zero hassle. This kind of pairs up with the fact that the whole documentation for Godot only really exists fro GDScript. Granted, these are all accessing the same underlying functions but it still means that it can be hard to grasp some details as you’re translating the docs back into C++ on the fly. For more involved projects I guess this gets old in a hurry

Those were quite a few pain points, is it even worth the hassle?

Short answer is: yes, very much so

Godot by itself brings a lot to the table but it also imposes a lot of limitations. By being able to suddenly plug in your library of choice, the gates are pretty much open. Bringing in OpenCL is an example of something Godot has no ambitions of doing (that I know of). You CAN use their own compute shaders and whatnot but what if you already have previous work?

This also allows you to go down a level lower and have tighter control of your program, something that can be very useful when it comes to wringing out as much performance as you can. One thing I have to go back is implementing toyBrot all within GDScript, using Godot’s own Threading system and whatnot. My initial attempt did not go well at all. By offloading this work to an external C++ implementations, it can live in its own bubble and does not need to play by Godot’s rules except for the few points of contact

If you need to implement some functionality that goes deeper than regular plain Godot would like you to or need to bring in some external code for whatever reason, there is a lot to like here. This is also true if your comfort zone is some other language. This is the case for me and C++, which is the officially supported set of GDExtension bindings but there are others available supported by the community, one other language mentioned in the docs is Rust. So you can rely on something more geared toward fiddly heavy lifting if you need

Now that I’m more familiar with this system, there is a good chance I’ll bring in Cartomancer as as a GDExtension in the future, for example. So yeah, despite all the pain, I had a good time and am excited with the whole thing

So what’s next?

This whole thing DID turn out to be more involved than I expected. For the next post I am going to go back to this very project and talk about the whole CMake side and general project setup, some things to consider and how you can make your life a bit easier for development and debugging

I did mention the whole thing with the stream buffer I implemented as well and that’s something that could be fun to talk about. It’s a thing that looks simple once you’ve seen an implementation but understanding the docs to figure out exactly what I needed to implement and how ended up quite the hassle

Finally, this whole journey has been leading me back to toyBrot in some peculiar ways. There’s a lot I like about toyBrot but it definitely has its own technical debt spread around and I think it’s time I made good on this blog’s name and went for some proper house cleaning. There’s a lot of details about both how toyBrot actually works as well as how the source itself is written and has been added over the years that is just not great and I want to make sure this is a project I’m proud to point to. I’m excited about it and, honestly, it took some effort to make sure I get this whole experiment with GDExtensions done before I dove into this refactor. We’ll see where this adventure takes me

Or follow this blog : @tgr@vilelasagna.ddns.net