Skip to content

Instantly share code, notes, and snippets.

@reduz
Last active May 1, 2024 12:06
Show Gist options
  • Star 123 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save reduz/cb05fe96079e46785f08a79ec3b0ef21 to your computer and use it in GitHub Desktop.
Save reduz/cb05fe96079e46785f08a79ec3b0ef21 to your computer and use it in GitHub Desktop.

During the past days, this great article by Sam Pruden has been making the rounds around the gamedev community. While the article provides an in-depth analysis, its a bit easy to miss the point and exert the wrong conclusions from it. As such, and in many cases, users unfamiliar with Godot internals have used it points such as following:

  • Godot C# support is inefficient
  • Godot API and binding system is designed around GDScript
  • Godot is not production ready

In this brief article, I will shed a bit more light about how the Godot binding system works and some detail on the Godot architecture. This should hopefully help understand many of the technical decisions behind it.

Built-in Types

Compared to other game engines, Godot is designed with a relatively high level data model in mind. At the heart, it uses several datatypes across the whole engine. These datatypes are:

  • Nil: To indicate an empty value.
  • Bool, Int64 and Float64: For scalar math.
  • String: For String and Unicode handling.
  • Vector2, Vector2i, Rect2, Rect2i, Transform2D: For 2D Vector math.
  • Vector3, Vector4, Quaternion, AABB, Plane, Projection, Basis, Transform3D: For 3D Vector math.
  • Color: For color space math.
  • StringName: For fast processing of Unique IDs (internally a unique pointer).
  • NodePath: For referencing paths between nodes in the Scene Tree.
  • RID: Resource ID for referencing a resource inside a server.
  • Object: An instance of a class.
  • Callable: A generic function pointer.
  • Signal: A signal (see Godot docs).
  • Dictionary: A generic dictionary (can contain any of these datatypes as either key or value).
  • Array: A generic array (can contain any of these datatypes).
  • PackedByteArray, PackedInt32Array, PackedInt64Array, PackedFloatArray, PackedDoubleArray: Scalar packed arrays.
  • PackedVector2Array, PackedVector3Array, PackedColorarray: Vector packed arrays.
  • PackedStringArray: Packed string array.

Does this mean that anything you do in Godot has to use these datatypes? Absolutely not. These datatypes have several roles in Godot:

  • Storage: Any of these datatypes can be saved to disk and loaded back very efficiently.
  • Transfer: These datatypes can be very efficiently marshalled and compressed for transfer over a network.
  • Introspection: Objects in Godot can only expose their properties as any of those datatypes.
  • Editing: When editing any object in Godot, it is done via any of these datatypes (of course, different editors can exist for the same datatype, depending on the context).
  • Languge API: Godot exposes its API to all languages it binds via those datatypes.

Of course, if you are absolutely unfamliar to Godot, the first questions that come to mind are:

  • How do you expose more complex datatypes?
  • What about other datatypes such as int16?

In general, you can expose more complex datatypes via Object API, so this is not much of an issue. Additionally, modern processors all have at minimum 64 bit buses, so exposing anything other than 64 bit scalar types makes no sense.

If you are unfamliar to Godot, I can totally understand the disbelief. But in truth, it works fine and it makes everything far simpler at the time of developing the engine. This data model is one of the main reasons why Godot is such a tiny, efficient and yet feature packed engine compared to the large mainstream mamooths. As you get more familiar with the source code, you will start to see why.

Language Binding System

Now that we have our data model, Godot imposes a strict requirement that almost any function exposed to the engine API must be done via those datatypes. Any function parameters, return types or properties exposed must be via them too.

This makes the job of the binder much simpler. As such, Godot has what we call an universal binder. How does this binder work, then?

Godot registers any C++ function to the binder like this:

Vector3 MyClass::my_function(const Vector3& p_argname) {
   //..//
}

// Then, on a special function, Godot does:

// Describe the method as having a name and the name of the argument, the pass the method pointer
ClassDB::bind_method(D_METHOD("my_function","my_argname"), &MyClass::my_function);

Internally, my_function and my_argument are converted to a StringName (described above), so from now onwards they are treated just as a unique pointer by the binding API. In fact, when compiling on release, the argument name is ignored by the template and no code is generated, since it serves no purpose.

So, what does ClassDB::bind_method do? If you want to dive into the depths of insanity and try to understand the incredibly complex and optimized C++17 variadic templates black magic, feel free to go ahead.

But In short, it creates a static function like this, which Godot calls "ptrcall" form.:

// Not really done like this, but simplifying as much as possible so you get an idea:

static void my_function_ptrcall(void *instance, void **arguments, void *ret_value) {
    MyClass *c = (MyClass*)instance;
    Vector3 *ret = (Vector3*)ret_value;
    *ret = c->my_method( *(Vector3*)arguments[0] );
}

This wrapper is basically as efficient as it can be. In fact, for critical functions, inline is forced into the class method, resulting in a C function pointer to the actual function code.

Then Language API works by allowing the request of any engine function in "ptrcall" format. To call this format, the language must:

  • Allocate a bit of stack (basically just adjusting the stack pointer of the CPU)
  • set a pointer to the arguments (which already exist in native form in this language 1:1, be it GodotCPP, C#, Rust, etc).
  • call.

And that's it. This is an incredibly efficient generic glue API that you can use to expose any language to Godot efficiently.

So, as you can imagine, the C# API in Godot basically uses a C function pointer via unsafe API to call after assigning pointers to native C# types. It is very, very efficient.

Godot is not the new Unity - The anatomy of a Godot API call

I want to insist that the article written by Sam Pruden is fantastic, but if you are not familiar with how Godot is intended to work under the hood it can be very misleading. I will proceed to explain a bit more in detail what is easy to misunderstand.

Only a pathological use case is shown, the rest of the API is fine.

The use case shown in the article, the ray_cast function, is a pathological one in the Godot API. Cases like this are most likely less 0.01% of the API exposed by Godot. It looks like the author found this by coincidence when trying to profile raycasting, but it is not representative of the rest of the bindings.

The problem is that, at the C++ level, this function takes a struct pointer for performance. But at the language binding API this is difficult to expose properly. This is very old code (dating to the opensourcing of Godot) and a Dictionary was hacked-in to use temporarily until something better is found. Of course, other stuff was more prioritary and very few games need thousands of raycasts, so pretty much nobody complained. Still, there is a recently open proposal to discuss more efficient binding of these types of functions.

Additionally, to add to how unfortunate this choice of function is, the Godot language binding system does support struct pointers like this. GodotCPP and Rust bindings can use pointers to structs without any issue. The problem is that C# support in Godot predates the extension system and it was not converted to it yet. Eventually, C# will be moved to the universal extension system and this will allow the unifying of the default and .net editors, it is just not the case yet, but its top in the list of priorities.

The workaround is even more pathological

Although this time, due to a limitation of C#. If you bind C++ to C#, you need to create a C# version of a C++ instance as an adapter. This is not an unique problem to Godot, any other engine or application doing this will require the same.

Why is it troublesome? because C# has a garbage collector and C++ does not. This forces the C++ instance to keep a link to the C# instance to avoid it from being collected.

Due to this, the C# binder must do extra work when calling Godot functions that take class instances. You can see this code in Sam's article:

public static GodotObject UnmanagedGetManaged(IntPtr unmanaged)
{
    if (unmanaged == IntPtr.Zero) return null;

    IntPtr intPtr = NativeFuncs.godotsharp_internal_unmanaged_get_script_instance_managed(unmanaged, out var r_has_cs_script_instance);
    if (intPtr != IntPtr.Zero) return (GodotObject)GCHandle.FromIntPtr(intPtr).Target;
    if (r_has_cs_script_instance.ToBool()) return null;

    intPtr = NativeFuncs.godotsharp_internal_unmanaged_get_instance_binding_managed(unmanaged);
    object obj = ((intPtr != IntPtr.Zero) ? GCHandle.FromIntPtr(intPtr).Target : null);
    if (obj != null) return (GodotObject)obj;

    intPtr = NativeFuncs.godotsharp_internal_unmanaged_instance_binding_create_managed(unmanaged, intPtr);
    if (!(intPtr != IntPtr.Zero)) return null;

    return (GodotObject)GCHandle.FromIntPtr(intPtr).Target;
}

While very efficient, it's still not ideal for hot paths so the Godot API exposed is considerate and does not expose anything critical this way. The workaround used, however, is quite complex and hits this path due to not using the actual function intended for it.

The question of cherry picking

I firmly believe the author did not cherry pick this API on purpose. In fact, he himself writes that he checked other places of API usages and did not find anything with this level of pathology either.

To clarify further, he mentions:

Let’s also remember that Dictionary is only part of the problem. If we look a little wider for things returning 
Godot.Collections.Array<T> (remember: heap allocated, contents as Variant) we find lots from physics, 
mesh & geometry manipulation, navigation, tilemaps, rendering, and more.

From my side and contributors side, none of those usages are hot paths or pathological. Remember that, as I mentioned above, Godot uses the Godot types mainly for serialization and API communication. While it is true that they do heap allocation, this only happens once when the data is created.

I think what may have confused Sam and a few others in this area (which is normal if you are not familiar with the Godot codebase) is that Godot containers don't work like STL containers. Because they are used mainly to pass data around, they are allocated once and then kept via reference counting.

This means, the function that reads your mesh data from disk is the only one doing the allocation, then this pointer gets passed through many layers via reference counting until arrives Vulkan and is uploaded to the GPU. Zero copies happen along the way.

Likewise, when these containers are exposed to C# via the Godot collections, they are also reference counted internally. If you create one of those arrays to pass the the Godot API, the allocation only happens once. Then no further copies happen and the data arrives intact to the consumer.

Of course, intenally, Godot uses far more optimized containers that are not directly exposed to the binder API.

Misleading conclusion

The article concludes like this:

Godot has made a philosophical decision to be slow. The only practical way to interact with the engine is via this binding layer, and its core design prevents it from ever being fast. No amount of optimising the implementation of Dictionary or speeding up the physics engine is going to get around the fact we’re passing large heap allocated values around when we should be dealing with tiny structs. While C# and GDScript APIs remain synchronised, this will always hold the engine back.

As you have read in the above points, the binding layer is absolutely not slow. What can be slow is an extremely limited amount of use cases that can be pathological. For those cases, a dedicated solution is found. This is a general philosophy behind Godot development that helps keep the codebase small, tidy, maintainable and easy to understand.

In other words, this principle:

image

The current binder serves its purpose and works well and efficiently for over 99.99% of use cases. For the exceptional ones, as mentioned before, the extension API supports structs already (which you can see here in this excerpt of the extension api dump):

		{
			"name": "PhysicsServer2DExtensionRayResult",
			"format": "Vector2 position;Vector2 normal;RID rid;ObjectID collider_id;Object *collider;int shape"
		},
		{
			"name": "PhysicsServer2DExtensionShapeRestInfo",
			"format": "Vector2 point;Vector2 normal;RID rid;ObjectID collider_id;int shape;Vector2 linear_velocity"
		},
		{
			"name": "PhysicsServer2DExtensionShapeResult",
			"format": "RID rid;ObjectID collider_id;Object *collider;int shape"
		},
		{
			"name": "PhysicsServer3DExtensionMotionCollision",
			"format": "Vector3 position;Vector3 normal;Vector3 collider_velocity;Vector3 collider_angular_velocity;real_t depth;int local_shape;ObjectID collider_id;RID collider;int collider_shape"
		},
		{
			"name": "PhysicsServer3DExtensionMotionResult",
			"format": "Vector3 travel;Vector3 remainder;real_t collision_depth;real_t collision_safe_fraction;real_t collision_unsafe_fraction;PhysicsServer3DExtensionMotionCollision collisions[32];int collision_count"
		},

So, ultimately, I believe that the conclusion that "Godot is slow by design" is a bit rushed. What is currently missing is the move of the C# language to the GDExtension system in order to be able to take advantage of these. This is currently a work in progress.

To sum up

I hope that this short article is used to dispell a few misconceptions that unintentionally arised from Sam's excellent article:

  • Godot C# API is inefficient: This is absolutely not the case, but very few pathological cases remain to be solved and were already being in discussion before last week. In practice, very very few games may run into them and, by next year, hopefully none.
  • Godot API is designed around GDScript: This is also not true. In fact, until Godot 4.1, typed GDScript did calls via "ptrcall" syntax, and the argument encoding was a bottleneck. As a result, we created a special path for GDScript to call more efficiently.

Thanks for reading and remember that Godot is not commercial software developed behind closed doors. All of us who make it are available online in the same communities as you. If you have any doubt, feel free to ask us directly.

Bonus: As a side note, and contrary to popular belief, the Godot data model was not created for GDScript. Originaly, the engine used other languages such as Lua or Squirrel, with several published games while an in-house engine. GDScript was developed afterwards.

@Nifflas
Copy link

Nifflas commented Sep 24, 2023

Yeah, that's fair! Maybe I overreacted. If I know I can use the C# stuff in the future and feel safe that there will be 0 bytes of garbage from all features I use on a per-frame basis, I won't worry at all! It needs to be a guiding principle, same requirement as what I expect from Unity or things from its asset store basically.

@markdibarry
Copy link

Is it not better to just use C++, which is well supported in Godot, almost as easy to use since it has has the same API and you don't have to care about the GC at all?

Maybe? If you're a seasoned C++ developer, it may make more sense to use C++ than anything else for workflow, memory management, etc. Obviously C# and GDScript are completely separate languages with tons of differences that appeal to different users, but I think the appeal of C# on the front of memory management from a user perspective is the same one as users of JavaScript, Python, and GDScript which you designed to have the same appeal: You shouldn't have to think about it. It just works!

I know you said you're not as familiar with C#, but I'm more a longtime Godot C# user than Unity, so I'm pretty used to discussing tricks the community has come up with to mitigate some of these issues. I imagine the concern comes from C# users coming from other game engines or a web backgrounds. In those scenarios, they don't need to be aware of the GC in their normal game code. For other game engines, it's just "don't do anything crazy in methods being used every frame", and web dev obviously wouldn't care about frame stuttering at all. It just works. To them, the burden is on the API side, not the user's. I mainly work on tools for others to use, so performance is more important to me because I have no idea what the needs are of the person who is using it. All I know is I don't want my code to be their bottleneck.

There's always going to be some culture shock when moving to a different country and learning the ways of the locals and whether or not you'll be accepted. Similarly, there will be concern from locals that outsiders will make them lose their culture. I can't speak on all the scenarios discussed, but I can at least hopefully shed some light on where some of the concern comes from. Honestly, I'm not too familiar with a lot of this since my day-to-day with C# is only Godot and my job, but as far back as 3.0 the Godot C# community's main rules are "never ever use Godot.Collections if you can avoid it" and "avoid doing anything that communicates with the engine whenever possible" (which actually encompasses the first rule). Which, there's been some improvements on that front since then, but it's still significant enough that you need to be aware of it.

If there's a solution where that wouldn't be the case anymore, that'd be nice?

@reduz
Copy link
Author

reduz commented Sep 24, 2023

Alright, I want to thank everyone hugely for all the feedback. I believe I now understand well what the problem is with C# and the garbage collector (which arised from the discussion here, not necessarily related to the original article).

While I do my best to give my word that we do our best to ensure hot paths don't do memory allocation, and that the general consensus that the GC in CoreCLR is better than Unity's GC regarding to this, I can totally understand that if anyone wants to make full commitment into using Godot, they should be given full assurance that they can avoid GC interaction at all.

As such, I opened this proposal regarding to no allocation and no copies versions for functions exposed in the C# API, so they can be used anytime you are concerned about performance / GC spikes:

godotengine/godot-proposals#7842

@perkele1989
Copy link

I think the concern regarding all of this drama isn't necessarily the implementation details. It's the philosophy Godot has embraced during its development over the years. Godot has chosen a path where flexibility and ease of use/quick implementations have taken precedence over maximizing performance. This has led to a situation where, if Godot wishes to compete with hyperoptimized game engines, they would basically have to rewrite everything from the ground up (which is practically quite unfeasible at this point).

Personally, I don't think it really needs to be a concern. Godot is clearly still an engine that's used for many smaller games, where performance isn't necessarily a key deciding factor. Clearly these devs have chosen Godot for its benefits and pros, not its limitations. And for them, clearly this hasn't been an issue.

The real issue, if there is one, is made visible once you start to consider Godot a serious alternative for AA or AAA level games. At this level, using Godot can easily become unfeasible due to its philosophical decisions. I'm not sure if Godot should or even wants to compete at this level though, so I'm not sure it's even an issue to begin with.

The engine we are building (e2) is built on an entirely different philosophy, but then again, we have an entirely different userbase that we wish to target. In fact, for a lot of teams/types of games, we would instead recommend something like Godot or Unity, due to the higher level of prerequisite experience that comes with using a more advanced and optimized engine.

Be glad that you aren't making Unreal, an engine that is supposedly "highperforming" and "hyperoptimized", however in reality it's extremely bloated and comes with very high performance overheads and tons of technical debt.

I definitely think Godot has a place for certain types of games, where it's the best alternative out there. Just as we are aiming to be the best alternative for a whole other types of games. Compared that to Unreal, which is an engine that is half-shitty for every type of game, even if it of course also has its benefits too.

My point is, don't worry too much about performance, it's not a key concern of Godot and honestly neither should it.

/end rant

@reduz
Copy link
Author

reduz commented Sep 26, 2023

@haikarainen

This has led to a situation where, if Godot wishes to compete with hyperoptimized game engines, they would basically have to rewrite everything from the ground up (which is practically quite unfeasible at this point).

You are basically describing all the optimization effort that hundreds of contributors put into Godot 4 these past three years. Thanks for the informed rant, regardless.

@perkele1989
Copy link

You are basically describing all the optimization effort that hundreds of contributors put into Godot 4 these past three years

To be very frank, if I were actually describing that effort, we wouldn't be having this discussion right now. The "practically unfeasible" point is quite important for you to either understand and accept, or if you actually believe Godot can compete at this level, prove wrong. The fact that you've put 3 years into optimizing this engine, and still today do things like what Sam brings up in his article, and argue about gameloop allocations being negligible, is probably the biggest evidence for it being actually unfeasible.

My recommendation here would be to pivot and embrace what Godot is good at. If you truly wish to compete with high performing engines, I'm convinced your best path forward would be to build an entirely new engine from scratch, as it will likely be faster to do than what you're attempting to do now.

Best,

@reduz
Copy link
Author

reduz commented Sep 26, 2023

@haikarainen

To be very frank, if I were actually describing that effort, we wouldn't be having this discussion right now

"This discussion" is about an isolated problem in the language binder layer. To divert the discussion towards general engine optimization is entirely missing the point.

@perkele1989
Copy link

"This discussion" is about an isolated problem in the language binder layer. To divert the discussion towards general engine optimization is entirely missing the point.

Your original gist was about an isolated problem. My comment, and your response to said comment, and thus "this discussion" we are having right now, while off topic, is not.

@reduz
Copy link
Author

reduz commented Sep 26, 2023

@haikarainen

Your original gist was about an isolated problem. My comment, and your response to said comment, and thus "this discussion" we are having right now, while off topic, is not.

Thanks for using ambiguity to make your point. Now that we agree that your original post is off-topic, what are you trying to accomplish with it?

@jams3223
Copy link

@haikarainen Godot 4 is not the same as Godot 3, it has already been rebuilt from the ground for 3D; in fact, it took 1 year, and next year is going to make it 2 years since Godot 4 dropped, and it's still in the stabilizing faze. Godot 3 sure isn't highly performant, but Godot 4 is and is getting better day by day.

@reduz
Copy link
Author

reduz commented Sep 26, 2023

Folks, I want to thank everyone for this super insightful and productive discussion. I gained a lot of understanding on how C# developers expect APIs to be exposed and should hopefully help improve this aspect of Godot.

To avoid spamming everyone else with notifications, and because locking is not possible on Gists, I ask to please not continue posting on this thread.

@TranscendentThots
Copy link

Since these 'pathological' use-cases are so vanishingly rare, can we please get a list of them in the documentation? I'd settle for something non-comprehensive, or even community maintained. Anything to help users switching from Unity to decide which "standard" video game features we should avoid planning a Godot game around, or which API calls to avoid like the plague during implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment