utopia-rise / godot-kotlin-jvm

Godot Kotlin JVM Module
MIT License
620 stars 45 forks source link

GDExtension use-case #709

Open danielperano opened 5 days ago

danielperano commented 5 days ago

I'm investigating whether Godot (along with this extension) would be a good fit to migrate an existing Java game project too. I'm very impressed with what I've seen so far with both - getting started with vanilla Godot and with this project was trivially easy. The biggest downside I've seen so far with this project is the requirement that it be built as a module rather than a GDExtension. I see in #311 that porting to an extension is a matter of interest, and I wanted to suggest another use-case that might be useful for.

One of my biggest wishlist items with modern Java is a good SIMD library in the JDK. The only up-and-coming option (Vector API) is still an incubating feature, and with its dependence on Valhalla that doesn't look likely to change soon. In any case, this still does not give nearly the same level of control as, say, C#'s System.Runtime.Intrinsics APIs, which I've tinkered with and confirmed are available in Godot. I view SIMD as a non-critical but very nice to have feature for game development, and it occurs to me that one could create a node in C# that implements some algorithm in SIMD code and call it from Java/Kotlin via cross-language scripting functionality.

I guess the above could be accomplished by building this project against Godot + C# instead of just Godot rather than porting to GDExtension, but GDExtension seems like it would be the better option long term from what I've been reading so far and it looks like that's already your intention. Not sure what your roadmap is for GDExtension or how high a priority it is, but if there's anything I can do to help make that happen please let me know - unfortunately I'm not in a great position to do it and offer a PR right now, but perhaps there are other things I could do to help out.

CedNaru commented 5 days ago

Hi,

It's a complex matter, and I think there are feature here that are related to different concepts. First regarding GDExtension. It's indeed planned to move to a GDExtension, but it won't be a complete conversion, we are going to make it so the C++ code can compile both as a module and as a gdextension. The reason for that is that a module will always be more efficient. Right now the cost of interacting with a Kotlin script is justC++ <==> JVM, but moving to an extension changes it to C++ <==> Extension Dynamic Library <==> JVM. A level of indirection is never free. We only want this project to run as a GDExtension for user convenience, so they can use Kotlin without the requirement of downloading a non-official Godot build.

Writing efficient and low level code, that the JVM can't recreate, in a different language, doesn't require us to be a GDExtension, it only requires the possibility to call a script from a difference language, which is quite inefficient because those calls can only happen using Godot reflection system. But that efficient code doesn't have to be a script itself. You could perfectly right that efficient code using C++, make it its own extension and call it from Kotlin. One of the main difference between a script and an extension is that extensions are registered in Godot's ClassDB, meaning we can get a pointer to their function that can be called directly from Kotlin instead of reflection. Basically, we don't need to be a GDExtension, but the optimized code needs to be one. Right now, the Kotlin bindings are only generated from the standard Godot API, but we plan at some point to give people access to our API generator that will let them create Kotlin bindings to even extensions. That way, you will be able to use them the same way as any other Godot types.

Even if you were to do a Godot + JVM+ C# trick, you would still need to go through reflection to call the C# script, which defeat the purpose of an efficient script. On top of that, I would strongly discourage using Kotlin and C# together. That would make 2 rather big VMs run in Godot which is overkill. Last time I checked how the C# module was designed, it didn't do well with other GC-based languages. I leave the technical aspect aside, but the tldr is that if C# and the JVM interact with the same native instance, a memory leak is going to happen because the 2 VMs are going to wait each other's GC (specifically RefCounted instances and its children classes).

So the road ahead for what you are suggesting doesn't depend on us switching to GDExtension, but on adding the api generator as a tool for users and writing your low level code as a GDExtension, preferably with a language that doesn't use a GC (C++ and Rust being the best candidates).

danielperano commented 4 days ago

It's a complex matter, and I think there are feature here that are related to different concepts.

For sure - really the only thing relevant to GDExtension in my musings about using C# SIMD from Java/Kotlin is the convenience provided by extensions as opposed to custom/unofficial engine builds.

Right now the cost of interacting with a Kotlin script is justC++ <==> JVM, but moving to an extension changes it to C++ <==> Extension Dynamic Library <==> JVM. A level of indirection is never free. We only want this project to run as a GDExtension for user convenience, so they can use Kotlin without the requirement of downloading a non-official Godot build.

Thank you for elaborating on this. Assuming that the Godot -> Extension Library is reasonably efficient I'm not personally terribly concerned until I see something like this prove itself troublesome in a profiler, but I wasn't aware that there would be a meaningful difference between calling into a module versus an extension.

Writing efficient and low level code, that the JVM can't recreate, in a different language, doesn't require us to be a GDExtension, it only requires the possibility to call a script from a difference language, which is quite inefficient because those calls can only happen using Godot reflection system.

Yes, this is a limitation that would make this approach viable only for workloads where the performance gain from moving the task to a different language runtime exceeds the cost of performing the dispatch - very much like the tradeoffs involved in delegating tasks to the GPU.

You could perfectly right that efficient code using C++, make it its own extension and call it from Kotlin. One of the main difference between a script and an extension is that extensions are registered in Godot's ClassDB, meaning we can get a pointer to their function that can be called directly from Kotlin instead of reflection.

That is quite powerful! I'm aware that falling back to C++ in some form (module or extension) is always an option, but was hoping that there might be a way to write/call expensive algorithms cross-language in managed languages, depending on which was best suited for the task at hand. I'm primarily a Java programmer (with some years of C# usage from the .Net Core 2.x days), and while I'm reasonably proficient with C++ I prefer to avoid it when reasonable due to the comparative pain of dealing with native code, the toolchain/build requirements for multiple platforms, etc.

Last time I checked how the C# module was designed, it didn't do well with other GC-based languages. I leave the technical aspect aside, but the tldr is that if C# and the JVM interact with the same native instance, a memory leak is going to happen because the 2 VMs are going to wait each other's GC (specifically RefCounted instances and its children classes).

Bringing in two VMs is a lot, yes, and probably not practically worthwhile in most scenarios. It's a bummer (albeit not surprising) that the CLR & JVM don't play well together - usually it's quite painful enough getting native code to interact properly with one GC'd runtime.

So the road ahead for what you are suggesting doesn't depend on us switching to GDExtension, but on adding the api generator as a tool for users and writing your low level code as a GDExtension, preferably with a language that doesn't use a GC (C++ and Rust being the best candidates).

That's exciting! Bindings are usually painful (particularly for C++), so it would be great to have an automated tool to ease that. I have some experience with jextract & similar header -> binding generators, but if memory serves me correctly Godot produces some API definition files in JSON for generating bindings - which should eliminate many of the pitfalls common to header parsing. I'll have a look at your API generator as I have time. Are you open to PRs (if well-written and aligned with your project goals), or do you prefer to keep development within your team?

CedNaru commented 4 days ago

We are open to PRs. It's just that in practice, it's hard for most people to dive into our code. This project became quite big by now, even us maintainers can't keep up with each other at times. And yes, our API generator is using the Godot generated Json to create the bindings. It's an operation we do ourselves and can only be done on the official Godot API when it comes to what we put into our JVM library. The idea is that users should be able to query a new json from Godot, one that will also contain their extensions, so they can generate their own bindings and replace the default one we provide.

Thank you for elaborating on this. Assuming that the Godot -> Extension Library is reasonably efficient I'm not personally terribly concerned until I see something like this prove itself troublesome in a profiler, but I wasn't aware that there would be a meaningful difference between calling into a module versus an extension.

The extra cost is not that much, your code won't suddenly run 2x slower because of that. Even if minor, it's still a downgrade, so we don't feel like ditching the current approach for a (even if just a bit) slower one. But we acknowledge that the convenience a gdextension provide can't easily be beaten. So we are greedy and want to try having both.