Closed rsaccon closed 2 years ago
I have to say that each part of it (node editor, half edge operations, polyasm compiler) is mind blowing brilliant and inspirational for lots of other use cases beside of 3d modeling
Thanks! :smile: I really don't deserve such high praise, but I'm really happy you enjoyed reading the source and found it inspirational. As always, don't hesitate to ask questions!
Now, onto the main topic. I wasn't going to make an announcement about this yet, but since you noticed the commits I'm going to ellaborate a bit on my rationale behind all this.
The realization that blackjack needs a full-blown scripting language came after I considered the best way to implement several upcoming features. Basically, I have been thinking a lot about how to bring blackjack's feature set to the next level, and I'm not sure how well something like PolyASM will scale.
Without ellaborating much on the new features just yet, I can say most of them rely on the idea of executing some custom behaviour for every element of the mesh (face, edge, vertex... but for simplicity I'll just call these per-vertex operations).
This new kind of customization poses two new challenges:
Users need a way to express this 'per vertex' logic. How will this representation look like? Taking a hint from other similar tools, the key seems to be in finding a balance between nodes and a textual language, managing to get the best of both worlds.
Regardless of the representation, this custom logic is user code. And as much as I'd like self-modifying Rust code, it is not a thing, so blackjack is going to need some sort of interpreter to accept code modifications at runtime. Moreover, this interpreter is now going to be running some expressions for every vertex of a mesh, so it needs to be somewhat fast.
The thing is, my current polyasm implementation fails on both departmens: It doesn't have a textual representation and it's definitely not fast enough as an interpreter to handle per-vertex expressions. Fixing these two problems essentially boils down to making my own programming language, and as fun as that may sound, it is a considerable detour! My current goal is to make blackjack into something usable as soon as possible.
Finally, another important aspect is user-generated content. The current version of Blackjack has too many missing features and most of them are missing nodes. The problem with the current approach is that every node needs to be added manually by the developers. This goes against the spirit of it being a tool for tinkerers: I want users to be able to make their own tools without having to fill an issue and wait for the devs to do it.
The solution I see here is to move from nodes being the API, to having a lower level API nodes are made with. And this starts to look a lot like a scripting language.
So, from two different angles, I have reached the same conclusion: Blackjack needs a scripting language. And in that scenario, keeping a separate interpreter around (i.e. polyasm) is just extra maintenance burden. Instead, the graphs can be compiled to scripts, using the same API available to users.
I have spent a lot of time evaluating various options. I didn't start with Lua in mind, and I was a bit hesitant about choosing Lua for a Rust project. But after having looked everywhere, I believe it's the solution that gets me closer to my vision of what scripting / user code should look like for Blackjack, both in terms of UX and API. I won't say Lua fits every one of my needs, but it's by far the best combination of tradeoffs I could find.
I have compared Lua to several other alternatives. Here's a summary:
My first go-to solution for this was webassembly. Being fast, sandboxed and having several (but not that many I, as came to learn) languages supporting it, it seemed to be a perfect fit. The truth, though, is that it's still a bit early to build a plugin system with wasm. The moment you want to share anything non-trivial between host and guest (that is, something other than an integer), you're mostly in completely uncharted territory and having to invent your own ABI.
Proposals like Interface Types are exactly what I'm looking for and are going to completely eliminate this issue once implemented. But implementations are still far away, with no clear roadmap (could be months, could be years :shrug:). I'm aware of tools like wit-bindgen trying to bridge this gap, but after trying to build some minimal MVP with them, I realized they don't yet support blackjack's use case well.
Overall, my feeling for wasm is that it's too early, so I will wait until interface types are stable, then re-evaluate my choice.
You mentioned Rhai, and indeed I took a very good look at Rhai! Its progress looks absolutely impressive, and scores 10/10 on ease of integration with Rust.
On the not-so-bright side, I benchmarked Rhai vs Lua for what I believe was a representative use case for blackjack. When it comes to host (i.e. Rust) interoperability, both are pretty much tied. But when doing some non-trivial computations on the script side, Lua outperformed rhai by a quite significant margin. I mainly attribute this to Lua(JIT) having a JIT compiler, which Rhai doesn't, so it wasn't too surprising.
My other issue with Rhai is adoption. I know it's a circular argument (it'll never get adoption if people don't adopt it!). But being pragmatic, Lua has lots of documentation, tutorials, and is well-known on the gamedev/modding/scripting community. Moreover, it has tooling: A language server, documentation generators, a (not great, but also not terrible) library ecosystem. There is a great chance blackjack users already know Lua, whereas learning Rhai is going to be an extra requirement for pretty much anyone (even for me!).
I took a quick look at Python, and PyO3 seems to be quite nice. But I had trouble finding documentation and I don't need to benchmark Python to know it's going to be a bit too slow for my taste unless I spend a lot of time building "native" abstractions similar to pandas / numpy.
Overall, it looked like it can be done and it may even be a great fit, but it also seems a bit too complex for what I'm capable to pull off by working alone on this in my spare time. I'd rather have something where the integration effort is not so high.
Javascript looked promising. V8 is known to be a very fast interpreter, as fast as an interpreted dynamic language can be. But unfortunately, the point I made for Python is doubly true here: I wouldn't even know where to start taming that V8 beast. I also want to stress that one of blackjack's main use cases is game engine integration and people are definitely not going to appreciate their games pulling all of V8 with them just for a few procedural meshes.
There are other more lightweight alternatives to V8, both in terms of ease-of-integration and memory footprint. But in that case I suspect JS would loose most of its competitive advantage to other runtimes. I haven't benchmarked this though, so I may be wrong there.
Finally, my last problem with a possible JS integration is the lack of operator overloading. This makes vector math in Javascript look terrible. And I expect users to do quite a bit of vector math in scripts.
In a similar fashion to Rhai, Mun looked quite promising on a surface level. But after delving into it, I'm not even sure Mun is meant to be embedded into Rust projects. Even if it is, it seems to be too far from their focus. As it is today, it seems Mun's goal is to become a standalone programming language where Rust is an implementation detail. Someone more knowledgeable about Mun may want to correct me about that though, it's just my impression.
In a similar vein to JS above, mun also leverages a heavy runtime for compilation: LLVM. Blackjack needs to compile code on-the-fly, and for Mun that means shipping LLVM with it. Just like V8, people are not going to want to ship LLVM with their games!
And last, but not least, we reach Lua. After having seen all the alternatives, Lua too has its pros and cons:
When compared to wasm, it's slow. There's LuaJit, but the Jit seems to be unable to kick in when there's interaction with the host (Rust). So opportunities for running fast are limited to "pure" lua code. Less than ideal, but not a deal breaker: Most other runtimes I explored are also slower than wasm, and I can't use wasm until it's ready.
When compared to Rhai, integration with Rust is slightly more cumbersome. But not by far, and it's nothing a few macros can't fix. So far I've found mlua to be a great crate.
When compared to statically typed languages, there's several drawbacks in Lua. Not having the compiler help you debug basic code before it runs is certainly going to be an issue when people start developing non-trivial extensions. That's why I'm still planning to include some sort of mechanism for native Rust extensions, even if that requires having to manually compile Blackjack. The scripting language should be kept for simple stuff where the rigidness of the Rust compiler is not needed.
When compared to Python and Javascript, it's slightly less well known and has substantially smaller ecosystem. Tooling is also inferior in many aspects.
But at the end of the day, I am quite convinced with Lua because it feels it brings the right set of tradeoffs to the table:
(odd)
and hurting adoption 🙂So I think this is all! I've been thinking about this for quite some time now, so it's nice to have someone ask and finally put it in writing :) Please feel free to share your opinions on this and, at some point, I may use this very long (sorry!) message as a basis for a design document.
Thanks a lot for clarification. Sad that Webassembly it not ready yet for a use case like this.
Sad that Webassembly it not ready yet for a use case like this.
Indeed! But I learned a lot about wasm by looking into this and I'll keep a close look on the new interface types proposal :) I am sure it will become a viable solution eventually.
I tidied up a bit my explanation and moved it into a document: https://github.com/setzer22/blackjack/blob/main/doc/why_lua.md
I will link to it in the README once some upcoming features lang that will expose Lua to the users. Right now it's completely transparent so I will wait to make a proper announcement.
I have been playing with blackjack and trying to understand its code base and I have to say that each part of it (node editor, half edge operations, polyasm compiler) is mind blowing brilliant and inspirational for lots of other use cases beside of 3d modeling. So I noticed your latest commit is a lua scripting engine. I played myself with with rhai and thought that would be the natural first choice when there are no special requirements. So why are you experimenting with lua ? Is it faster or otherwise better than your polyasm compiler ? Or do you consider to expose the lua source script AND the node graph in editable form to the user ?