Scripting: What Do We Need to Get There

(Zicklag) #1

Hey @Moxinilian, as a part of my work on Arsenal, a big thing that we are going to need is the scripting support in Amethyst. I think you’ve got a really good idea with the RFC that you wrote, but I can’t find any further progress documented officially anywhere so I wanted to get an idea of what work/planning needs to be done. I’m planning on putting some work into scripting, whether it be code, ideas, or collaboration, but I’m going to need some guidance on what we need to do.

As far as I understand we should create a tracking issue for the RFC now that it has been merged, does that sound good?

(Théo Degioanni) #2

Yes! Thank you. The project has been on a little bit of pause because of a lack in availability for the people involved.

We noticed that the proposed solution in the RFC could be derived from a traditional LuaJIT integration: the typical way to do it in LuaJIT is entirely compatible with the overal design, and the more complicated aspects could be derived from it. Therefore we think we could start by having a typical integration first.

I thought we could divide the integration plan into the following steps:

Step 1 - Basic scripted systems

Step 2 - Securing the LuaJIT runtime

Step 3 - Advanced type interfaces

Step 4 - Scripted states

Step 5 - Hot reloading scripts

Step 6 - Externally declared ECS data types

Step 7 - Dynamic ECS resources

Step 8 - Asset pipeline integration

Step 9 - Third party language drivers

The actual content of those steps is difficult to describe shortly however. Maybe we could have a call to discuss it more in details? I assume Arsenal would require step 5 to be implemented, but I am not sure.

1 Like
(Zicklag) #3

OK, cool. I have a few things that I need to finish up first, but I’ll likely going to be digging into this more soon. Do you want to be the one to create the tracking issue?


I don’t know that it is particularly necessary. We’re just going to start as basic as possible and build on it as we go, so hot reload won’t be the first thing that gets implemented. The most important and minimal requirement is that we be able to create components and systems with the scripting language, whether it be Rust or Lua, without having to re-compile the amethyst core.

1 Like
(Zicklag) #4

So, as far as I see it, and I would like to get feedback on this, there are a couple of “realms” that need to be worked on to get scripting setup:

  1. We need have a way to create new components and systems in the ECS at runtime
  2. We need to create bindings to a scripting language so that you can create those components and systems from outside of the Amethyst game

As far as I see it, we first have to be able achieve #1 or bindings to a scripting language are useless. I found this issue about scripting support for Specs, but it was closed in the interest of stabilizing the Specs API and work for scripting would be directed more towards Nitric. My question is, do we have any idea how we are going to work in dynamic systems and components for scripting in Amethyst?

This is the next big thing that I need for Arsenal, so I should have some time to help with it, but I need to know where to start.

@torkleyy @Moxinilian @fletcher I’ve seen all of you around the subject of scripting in one way or another and would like initiate some discussion on what we need to do next.

To be more clear on what I am trying to achieve specifically, I don’t care a whole lot what language we integrate yet, whether that be Rust, WASM, Lua, or anything else because any of those languages could be integrated as soon as there is a way to dynamically integrate with the ECS. Once that is possible, then we will be able to add a language integration to Amethyst or I can experiment with my own separate language integrations for Arsenal.

(Fletcher) #5

@zicklag

First thing I’m doing is a simple experimental integration with wasmer this weekend, which you can view here if you want: https://github.com/amethyst/amethyst/tree/feature/wasmer. By integration, I mean Amethyst can create a runtime and execute a pre-compiled WASM application.

After that, I’m going to work on exposing the ECS functionality, most likely via opaque pointers. This will require some thought. If you want to ping me on Discord, I’ll make a shared doc we can hack on together for that aspect.

2 Likes
(Zicklag) #6

@fletcher, unfortunately I’m not going to be able to get on Discord. I’m free to discuss on GitHub or the forum, though.

I did a little bit of looking in to WASM and it seems like it will be perfect for mods. For Arsenal, though, right now I’m thinking that I first want to provide a zero cost scripting API that is meant for building the game itself. I might do some experimentation on an Amethyst C API to use for that.

Regardless of the API used, though, I’m still not understanding where the ability to create components at runtime will come from. It isn’t currently possible with Specs, so unless we end up switching to Nitric after it gets mature enough, we still won’t be able to script Amethyst games without writing Rust to create the components. Does that make sense or am I misunderstanding something?

For Arsenal we need to be able to program an Amethyst game completely through the scripting API. We need to be able to create states, components, and systems, all without re-compiling Amethyst, and, from what I’ve read, it doesn’t seem like this is within the roadmap for Specs. If it isn’t in the roadmap for Specs, then to support scripting in Amethyst we would have to move to Nitric. Now I’m not telling anybody that that is what we should do yet, I’m just trying to understand what is actually going to be required to get full scripting support.

Am I getting this right?

(Théo Degioanni) #7

It is currently “possible” to create components at runtime in specs. You can create dynamically accessed resources, I believe there even is an example doing that on the specs repository. At least now you can do was is proposed in the scripting RFC.

There is no blocker on specs side to implement a complete scripting system.

2 Likes
(Zicklag) #8

OK, that’s great. Earlier I wasn’t able to find anything along those lines.

I did find the example that you mentioned in the shred repository; that will help a lot.

Is there a performance hit when using runtime defined resource types, or is the performance entirely dependent on how you access the data? Like in the example, the data was stored in a HashMap, but that could be stored however you like and the performance of the implementation is dependant on that right?

(Zicklag) #9

So I think that I will start experimenting with a scripting setup for Arsenal using a C API and if I get that working then I can show you guys what I’ve got and we can figure out how/if we want to work it into the Amethyst codebase.

The first POC would be being able to script a game using Rust through the C API. After that, the next-most important language to Arsenal will be Python, and I already have some experience with PyO3 for creating Rust python bindings, so I will try to create a Rust module that will use the scripting C API and bind that to Python.

2 Likes
(Théo Degioanni) #10

I’m not 100% sure there is, we would need benchmarks to make sure of that. Nonetheless, we ought to build scripting support in a way that it is achievable to compile “dynamically created” things into static specs constructs. I don’t quite remember if I go into details about this in the RFC, but basically if you for example have a common schema file format for your components, then it’s quite straight forward to turn them into real Rust specs component at a deployment step.

1 Like
(Zicklag) #11

I just realized, the data for scripted components needs to be stored in a data container where the data cannot be known at compile-time, like a hash map or something else, but that means that it has to be allocated on the heap, whereas normal components can be stored on the stack. Isn’t that going to be a lot less efficient? Is there a way to store the scripted components on the stack somehow?

The only way I could think to do that would be to use chunks of fixed-sized byte arrays for component storage and then use pointers into those arrays to access the component data for specific entities. I’m still picking up on this stuff, though, so I’m not sure if that makes sense.

(Théo Degioanni) #12

At a point in time, you know the fixed size of each component. For example if you want to implement an equivalent of VecStorage, you can make a custom Vec that allocates arbitrary buffers of elements of size n, then on a change create a new Vec of the new size and move the components there.

Then, accessing the actual data can be done with a typical C header. Generally, treat components as buffers of size n that you “unsafely” access with C header declarations.

1 Like
(Belhorma Bendebiche) #13

Even if you could store it on the stack, chances are interacting with a dynamic language would require you to copy it to/from a heap location anyway.

(Zicklag) #14

That is true, but I want to preserve the ability to have a zero cost API so that you can script in Rust when you need higher performance or the ability to interact with native APIs. The goal is to allow you to script in Rust with no performance cost and to script in other easier languages as well when you don’t need the maximum achievable performance.

(Zicklag) #15

So I was thinking about component storages:

  • Scripted components’ type structure will be documented in a schema file
    • The RON file format would probably be a good candidate for the schema file
  • Component data will be stored as bytes inside of Specs
  • Component byte data needs to be interpreted as native types inside of the scripting language
    • Rust, C++, Python, etc. all have to use the structure outlined in the schema file to deserialize the component’s byte data to types native to the language.
    • In order to prevent scripted components from having lower read/write performance than non-scripted components, the deserialization of the component’s byte representation should be zero-copy.

So in the interest of a language-agnostic zero-copy memory representation of the component structure, I was looking at Arrow:

Arrow has libraries for a lot of languages including C/C++, C#, JavaScript, Java, Python, and Rust. It looks like Arrow could help us provide an efficient means by which each scripting language can modify the component data while it stays in a format universal to all languages, without having to be deserialized and reserialized upon every modification.

They also have a Rust example of creating a schema at runtime:

In reality it Arrow might not be directly used for Python or other dynamic scripting languages because the language driver that would be interfacing directly with the component data would be written in Rust.

This looks pretty promising to me, @Moxinilian what do you think?

(Théo Degioanni) #16

I think I had seen Arrow before, and what I noted from all sorts of pre-made schemas is that they don’t seem to support generics.
In my opinion, maintaining a simple schema language that fits our specific rusty needs would not be too much work, but otherwise it still seems like a fine choice too.

On the rest, this is exactly what I had in mind!

1 Like
(Zicklag) #17

OK cool, a lot of that was just voicing what you had already pointed out in your RFC, but it helped me organize my thoughts to put it down. :slight_smile:

I didn’t think about generics, though. Generics would be nice. Without that we can’t do stuff like Option<T>. :thinking:

The motivation behind Arrow was the fact that without it we would have to design our own method of serializing and deserializing types that would work across C/C++ and Rust, and Arrow has already defined a memory representation that makes the deserialization as efficient as possible. I’ll try it out and if it looks like it will work I think the tradoff is worth it. I would rather have it perform as fast as possible without generics than make everything slower with generics.

(Théo Degioanni) #18

You can keep it very simple: C primitives, arbitrary Rust types, generic Rust types, and other structures declared in the schema language. This is not much and really would cover all we need. You probably don’t even need to do pointers/references.

(Zicklag) #19

Do you think we could interpret the byte representation without copying memory easily enough?

(Théo Degioanni) #20

I’m not sure I understand. If you are talking about setting up the interface, this is up to the language, it does not need the data to interpret the structure. For example in LuaJIT it just works™.