I don't have an answer either, but I've seen people who want to use bytecode/a VM using RISC-V just because it's well defined and tooling is already there. This has less to do with RISC-V hardware, or even really the fact that it's RISC-V, and more with using a simple bytecode and VM - e.g. JVM could also work there, but the Java ecosystem comes with what some people consider considerable pain.
Many game developers have been successfully using XNA/FNA with C# on all kinds of platforms (and, of course, Unity existed for a long time, and now we have Godot and Stride too). You get to access all the low-level bits when you need to, and you don't have to sacrifice productivity or performance for scripting otherwise.
It doesn't need emulation of hardware per se, but most scripting environments are already virtual machines of some variety or another - and RISC-V just happens to be the latest in a long line of virtual machine architectures available for the purpose.
The author is cleverly using libriscv for this purpose - and it is indeed a neat use of the library - but I fail to see the 'scripting' part, personally - isn't it scripting when you can just load a text file off the disk, containing program instructions, have it interpreted into some (hopefully optimized) interim representation, and then pushed through a virtual machine? I don't see the "text files" part here - but perhaps it gets more relevant in parts 2/3/4, which I haven't read yet, either...
EDIT: I heard you like ABI, so I put a foreign ABI within your ABI so you can ABI with your ABI .. actually, this started off as a joke but its kind of painful to think about. This isn't really "scripting" .. its more "embed a foreign ABI into your application so you can throw a foreign binary at it, any time, and modify program behaviour extensively "without needing to recompile" .. maybe the author should rename the article "how to embed a virtual machine into your app so you never have to write (your app code) again .."
I can see why they call it "scripting" since, this is what you usually do when embedding the Lua VM into an app - but the fact that I have to use a compiler disqualifies the term 'scripting' from being applicable here, imho ..
> but the fact that I have to use a compiler disqualifies the term 'scripting' from being applicable here, imho ..
I'm not sure if this is a useful distinction to make.
Lua, perhaps the most used scripting language out there, runs through a compiler too and then runs a bytecode interpreter on the resulting code. Most scripting languages work this way. The compiler is still there even if it gets invoked at runtime. Most games ship the compiled bytecode files, not the script source.
The key part here is loading the "script" bytecode at runtime and then executing it in the host, allowing reloading and restarting the script without restarting the "host" process (often a game engine with long load times and lots of resident assets like textures, shaders, models etc that don't need reloading).
The host process could also include a mechanism to invoke the compiler at runtime, pass in the script as text, grab the output and then run it as it does currently. This would be identical to how Lua et al work, but quite a bit of work to set up for limited benefit.
Hey - the distinction is important but I don't think you've got it right.
Out-of-the-box, Lua runs through an interpreter - not a compiler.
A compiler produces native machine code from a human-readable programming language for the target platform - whereas an interpreter (in some cases) produces an interim representation (bytecode) for a virtual machine, not necessarily native - and in other cases, simply executes script code line-by-line, without the interim step.
In the case of Lua, this interim representation is in the form of bytecode which is then further interpreted by a virtual machine, pretending that it is a register-based machine vastly different to the actual hardware it is running on. This particular aspect is the only feature that the authors' libriscv usage and the Lua VM have in common - but that doesn't make it scripting.
And scripting means (to my jaded 40-years of development brain) to load text files from a resource (disk or otherwise), interpret it in some interim form, and then push through a secondary mechanism for final execution in the native environment.
>The key part here is loading the "script" bytecode at runtime and then executing it in the host, allowing reloading and restarting the script without restarting the "host" process (often a game engine with long load times and lots of resident assets like textures, shaders, models etc that don't need reloading).
So this is where our definitions conflict and go awry, because to me all you've described is runtime loading of a binary resource. Is loading and parsing a .PNG resource that appears in your executive bundle somewhere, also considered "Scripting" in your opinion? Because I don't think that is an accurate use of the term, personally. It would be scripting if the file (or memory blob) was somehow modifiable by the user, and then interpreted without further interaction required on the part of the developer - but in this case (the article) the developer (not the end user) still has to compile a binary blob, integrate it into their application, and then 'run it'.
This is more appropriately referred to as runtime resource modification and execution - for which the action of "scripting" is a superset - but the fact that there are two compilers involved in this project (native to build the .exe, and risc-v cross-compiling to build the binary blob resource for integration) means that we are far, far away from the typical scripting mechanic.
Its important to make this distinction. This isn't about scripting. It is about runtime resource modification and processing - but the different paths taken to the same fork in the road between scripting and compiling are vastly different and should not be conflated.
This isn't to say that integrating libriscv as a virtual machine in ones application execution environment is not a brilliant idea with a great deal of merit - just that the author is incorrectly using terms, and this must simply not be allowed. There is no interpreting happening here until runtime - when the binary risc-v blob is loaded and parsed by a virtual machine. If there were some mechanism to load code intended for the risc-v component at runtime, compile it, and load it for execution/report errors in the script/etc. - then I would say that the scripting workflow has been completed for this project - and it would be appropriate to use the term - but that has not happened here. It still requires a separate compilation (risc-v machine code) step, and there are still many, many aspects of the scripting workflow that are not implemented in this project ..
You're right! I actually did try to integrate a compiler, but I quickly realized that it wasn't worth it for me, personally. Iterating on the script is very quick right now. It's mostly a matter of compiler flags and ccache, believe it or not. And I can use mold for linking RISC-V!
Its great work, and you've inspired me to put libriscv in places where I usually put the LuaVM, but I wish you'd update your article to s/Scripting/Dynamic modification of resources at runtime/ .. or something.
We can see, already here in this thread, the dangers of people mis-interpreting your technology.
Now, if you get the riscv compiler integrated into your projects such that we can indeed just load a text file containing C code, dynamically at runtime, (a la shaders), I'll revisit all of this and go along with your program. ;)
Anyway, thanks for the great series of articles - really gave me an interesting read on the way to work this morning, and I've put libriscv (as well as your sample projects) in my stack of Lab Todo's for the week .. now if someone produces LuaRISCV that targets the riscv VM instead of Lua Bytecode, that's gonna break a few molds, in and of itself .. ;) (In the context of your work so far, this seems like a low-hanging fruit, actually..)
> Out-of-the-box, Lua runs through an interpreter - not a compiler.
The first thing the "interpreter" does is run the Lua code through `luac`, the Lua compiler and then feed the bytecode to the Lua VM. This may be transparent to the user, but it's still there.
Almost all interpreted languages have a compiler and a bytecode interpreter as distinct steps.
> Is loading and parsing a .PNG resource that appears in your executive bundle somewhere, also considered "Scripting" in your opinion?
Of course it's not, an image file does not contain any code that could be interpreted or executed in a host environment.
There is a distinction here on how code is executed, whether by compiling, interpreting, JITting or a combination of the above. But if you draw the line for "scripting" at consuming textual source code, that would exclude most use of Lua in gaming engines, as it is typically distributed as bytecode and may not even link luac compiler to the host executable. Yet it's commonly called "scripting" in the industry.
And conversely, if the code in this article included a mechanism to compile the source to risc-v would it meet your definition of "scripting"?
There isn't a clear definition of what's scripting and what's not, but drawing the line at consuming textual source code is not, in my opinion, a useful distinction (it's certainly a distinction) because it hardly matches what's the state of the art out there.
For decades, compiling has referred to the process of turning human-readable language into native machine code. That is what is happening here.
Interpreting has, also for decades, referred to the run-time interpretation of human-readable language into some mechanism which either a) immediately results in native execution at runtime, or b) produces errors reported to the developer for fixing, precluding the generation of immutable machine code being executed natively by the CPU (except of course with JIT, which is a form of compilation at runtime, since the interim bytecode is translated into real machine code instructions...)
Scripting has, for decades - not just in the gaming industry, which is a subset of computing enterprises - meant "loading a human readable text file containing a programming language and directly executing it if it passes validation - reporting errors, otherwise, preventing further execution. This is why we have BASH scripts and Python scripts and Lua scripts - they are not machine code, they are interpreted and either eventually produce machine code, or run through a state device pretending to be a non-native machine.
fwsgonzo is literally compiling C code into machine code to be executed on a pretend CPU (in the form of an embedded libriscv).
The code is not interpreted (from one language to another) - it is executed in a virtual machine environment. Bytecode is, on the other hand, always interpreted from one form to another prior to execution (except JIT, it is only interpreted once, compiled, and then exists as machine code always)
fwsgonzo's binary blob is executed by a 'fake' CPU, it is not interpreted.
If he had the execution environment set up, he could literally run that very same machine code on real RISCV hardware, unchanged, without translation or interpretation.
It is not, therefore, interpreted in any sense other than by his embedded RISCV emulator - and thus it is not scripting!
This is the point where the distinction is important.
>The first thing the "interpreter" does is run the Lua code through `luac`,
The first thing the Lua interpreter does is validate the inbound, human-readable script to make sure the program is correct, syntactically and otherwise - giving errors to the developer if errors are encountered. Meaning, at runtime, errors can be caught and fixed by the human developer.
Only then, once it has been validated, is it processed into an interim representation as bytecode. Sure, Lua can be told to produce bytecode in order to speed up runtime loading and execution, but its still an interpreted bytecode. It hasn't been compiled into a native form for direct execution - it still requires a secondary native program (a VM host) in order to provide any functionality.
From the Lua 5.1 manual, Section 2.4.1:
...
Chunks can also be pre-compiled into binary form; see program luac for details. Programs in source and compiled forms are interchangeable; Lua automatically detects the file type and acts accordingly.
Lua does have a compiler for the case where you want to create Lua bytecode (.luac), as a loadable-at-runtime resource to be further interpreted and also, the bytecode is subsequently interpreted according to the needs of the target (native) environment on which it is being executed, whether through a JIT or otherwise. That same bytecode can be executed on vastly different native CPU architectures - through interpretation.
The difference is, prior to Lua's internal compilation of the script into bytecode, validation of the programs correctness is performed, preventing the developer from producing incorrect bytecode if there are errors, and this is the process of scripting - distinct from compilation - because it is a more direct human/computer interaction regarding the correctness of the script language as it is being used by the developer.
> This particular aspect is the only feature that the authors' libriscv usage and the Lua VM have in common - but that doesn't make it scripting.
Maybe I'm nitpicking, but to me scripting is just programming/extending a certain fixed piece of software to execute instructions not already programmed into it without having to modify that software itself. Usually using an API. Not unlike computer programming, which aims to make a piece of hardware execute some instructions without needing to alter the said hardware. The main difference is conceptual (the platform targeted: hardware or software). If the script happens to be run by a VM emulating a real processor because your software includes such a thing, I think the distinction becomes purely conceptual.
Now, the technology is obviously super cool, but what I don't quite understand yet is what is the best use case for this? Is it really game scripting? Or compiling C++ on the fly?
It's not exactly a simple drop in replacement for a Lua interpreter.
I would argue that “scripting” has to do with what you, as the programmer, do to deploy your code, and not what happens under the hood. If you can deploy by shipping a single source code file, that’s a script. If there’s a build step, that’s not a script. Whether the underlying system compiles it, interprets it, sends it to MTurk for a human to evaluate by hand, or whatever doesn’t matter.
I was kind of wondering that too. Perhaps for sandboxing, but then i wonder why not something like webassembly or even eBPF. I kind of wish the author went more into the why of it.
Nothing wrong with using WebAssembly, if you wish to. eBPF I'm not so sure is a good fit. Most people are just fine with Lua, LuaJIT or Luau though. This is really just for especially interested, or if you really need low latency.
> ... but Lua still creates issues now and then. Like if you forget return, the return value is that of the last expression.
Is this true? Or was it true for some Lua 2.X versions? Because it does not seem to be the case for Lua 3.0 and on: [1]
> If control reaches the end of a function without a return instruction, then the function returns with no results.
As to the idea of using an emulator for scripting, it seems a bit similar to approaches using WebAssembly. But looking into the benchmarks [2] provided by libriscv, it is quite interesting to see it outperforms luajit and wasm3 (in its "hand-picked" tests, at the very least).
Maybe I'm old now. I'll remove it from the article! Reading back on it, it seems like I'm writing negatively about Lua, despite using it for many years. I think highly of Lua, and especially think it has made so many things easier for people over the years. I actually chose Nelua as one of the examples (Part 3) because of it!
I spent a lot of time on those benchmarks. I think my conclusion is just that because WASM isn't a register-machine architecture, you basically have to implement a register allocator to be really fast. wasmtime uses a complex allocator to great success. wasm3 has done it too, but it's just a basic one and it won't be able to compete with GCC or Clang. I could be wrong here, but what else is there? During my testing, it was never in question if libriscv was faster.
I used my usual benchmarks: STREAM (memory) and CoreMark (CPU).
I saw that. Keep in mind that libriscv is 35% faster than wasm3, and it looks like wasmi is not as fast as wasm3 yet. But they are making good progress.
This is seriously cool but I really think we are losing our way in game development.
We used to create DSLs to make scripting of games simpler and less error prone... so that coders/designers could write Scripts/AI in a language better suited to the problems they face. It was sort of a side issue that is was a sandbox although that was really useful for lots of reasons.
Now we seem to be creating more and more complex toolchains while complaining that game development is getting more and more expensive. We're not solving the correct problems at all.
Even if it's c++ to c++, the goal of scripting at the end of the day is quick iteration. What is "quick iteration" for scripting will depend on the scriptor.
This article is clearly targeted at engineers rolling their own engine, so that audience may find it beneficial keep everything in one language if they are already comfortable with it. As long as the core engine doesn't need to re-compile every little change, it's a win.
Yeah, I can confirm that the iteration speed is fast. I consider the scripting portion a solved problem right now, having used it so long. Gamedev is so many other things though, and practically every corner of the engine has to have convenience functionality in order to reduce the overall workload. It's rough for gamedevs right now, I feel like.
IMO the big tools are convinent enough to use. But the rampup time to really understand how the engine ticks is dreadful, because documentation drops off a cliff once you want to do more than animate a few dozen actors in a level (and tbf, sometimes that is indeed enough to ship).
The other half of the problem is asset production time. But no game engine is really solving that problem. As you'll discover in that domain, the art community is a lot more protective of their techniques and pipelines than programming. Even more so if looking for game ready asset production. Same issue, different cause.
There is one benefit to jumping through these hoops to use C++ as a scripting language (how much of a benefit it is, well, that's your judgment): you get to reuse libraries, as the author notes.
Modern AAA game development is suffering a chronic mismanagement problem. Like fimmaking. Poor creative vision, not communicated clearly to those executing it; endless rewrites and rework; lack of originality; poor treatment of staff leading to poor morale; and churning away in the background, the culture war.
Technically the only interesting challenge is "how do we get off Unity?"
> a chronic mismanagement problem. Like fimmaking.
the mismanagement stems from the fact that it is costing too much from the onset, and "management" wants to ensure that the investment makes a good enough return to justify the high cost.
Therefore, they dictate all of the risks have to be removed - such as not going for a risky new brand, but make a sequel. They want realistic graphics because it's more commercially viable - going for a different or uncommon aesthetic means taking risks.
I would argue all of the problems in AAA development stems from this fact.
it is also why indie games can be so cheap yet so successful. Not because indies are better (tho they are- at least more passionate if nothing else), but because indies are capable of taking risks to push the edge and make something new and untested.
It's the same as films - creative endeavours cannot have their risks managed away. It is inherent, and there's a large chance of failure even if the team has done their best.
The risk aversion in product is not a new phenomenon, though. Bigger producers have been following that track since at least the 90s. Adherence to brand, and even more so to basic game genres and mechanics, has been an issue complained about by devs at any big enough studio for decades.
What changed, I think, is all of the development studios being bought up by large producers. But, then again, I see steam as being a great counterforce on this, and the ease of releasing on web or mobile has been a boon for indie devs too. So maybe that hasn't changed that much.
The technology has advanced to where you can spend blockbuster budgets and get nearly film-quality results (not nearly possible in the cartridge and CD days). Meanwhile the price per AAA title has stayed relatively fixed at 50-60 USD for that same range of time, so production houses have had to try being appealing to ever-larger audiences. Fortunately, many people continue playing these games into their 30s and 40s so the market grows on its own too.
I'm trying to say you're right about the risk aversion being a big part of the industry's problems but that's been an issue for a while, it has gotten terribly enhanced with the growth of the industry.
Now I wonder which is worse.. extracting money one quarter a time with punishingly difficult timing tests, or extracting money via cosmetics that have no actual game impact but are marketed very compellingly.
>it is also why indie games can be so cheap yet so successful. Not because indies are better (tho they are- at least more passionate if nothing else), but because indies are capable of taking risks to push the edge and make something new and untested.
The cheapness also means indies simply get more "shots on goal." The barrier to entry is relatively low, so failing is lower-stakes. You have way more indies trying (and mostly failing), which means you have a greater raw number of hits. That's compared to a big company only having a handful of big-budget games in development.
This also creates a better environment for learning the craft. You learn more about how to create good games by launching 3 small indie games in 3 years than 1 large game. Especially when you’re a small cog in that large game project.
This certainly seems to be true, at least for one modern example, according to a recent post that linked to a YouTube video whose creator evidently has insider information about the 'failure' of Kerbal Space Program 2 (https://news.ycombinator.com/item?id=40485788).
As someone in an otherwise healthy position at my job, fuck yes, I want to get away from Unity, but there's just no better option for the platforms we're targeting and how quickly we want to ship things. Something about the devil you know, ya know?
As someone who's worked quite a bit with both, no. I have hopes for Godot but in terms of actually shipping commercially-viable games, it's not even in the same league as Unity. This comes from many things, but some of the big ones are:
* Ability to do first-party console ports
* 3D rendering fidelity and speed
* Asset store quality & quantity
* Robustness & stability - they both have tons of bugs but with Unity there is almost always a way around the bug, due to the decades of commercial games that paved the path before you.
For one quick example, Godot's physics engine is well-known to be poor, both in terms of performance and in terms of accuracy/stability. It has tons of bugs, tunneling, jittering, etc. Both the 2D and 3D physics engines in Godot are currently slated to be ripped out & replaced with external ones of higher quality (Box2D and Jolt). But currently, that hasn't happened, so one of the foundational parts of most games - physics - is guaranteed to be broken to some degree in a Godot game.
I adore Godot and have been learning it on my own time, but it's just not there for XR stuff yet, our current project has the Vision Pro as a target, for instance, and Unity is really the only option there. Apple's native tools (Reality Composer Pro) are just abysmal.
But yes, all of my personal projects are in Godot now, and I'm planning to use it for some tooling at work, just because it's UI system is nicer than Unity's as well as it having better support for re-using editor UI components in an application.
Getting off Unity, or Unreal, is the same as asking "how do we get off Office?".
Before their ascension there were already plenty to middleware to chose from, Ogre3D, SFML, SDL, Gamemaker, RenderWare, jMonkeyEngine,...
They aren't used as much, even the free ones, because of supported languages, tooling, asset store, platform owners support, and everything else that keeps them relevant in the industry despite their flaws.
Shameless plug, but some time ago I've created a language where the main design goal was for it to be easy to use and embed: https://github.com/kgabis/ape.
Something I want is a sandboxed place to run USER scripts in game. I trust the game's creators to not hack my machine with their DSL (else I wouldn't be running their game). I don't trust random drive by game mod to the same level. I'd love not to have to trust user additions, but currently I do have to trust them or go without.
I run a few games that takes C# plugins. The games have a big warning that they aren't doing anything to prevent my machine getting hacked, personal files uploaded to random servers, etc....
Lua is a great language for embedding - the difference between lua and luau is that luau gives you a restricted sandbox for your scripts, meaning you can have user generated scripts - like Roblox, UEFN, that sort of thing
I think wasm would work pretty well given that sandboxing is essential for browser security. However, the hard part is having a functional permissions system that isn’t so strict that actually malicious plugins can slip through due to every plugin needing a ton of permissions for basic functionality.
AAA game development has become more expensive because production values are incredibly high and there's a shitton of content. Compare a modern AAA game to a high quality game from the SNES era, it's obvious that the AAA game just has a shitton more stuff (though that doesn't necessarily mean it's a better game obviously).
Well... Of course on Linux it would be straight forward
I'd love for games to start having any/all system languages as the embedded scripting language.
But with Linux being around ~2% of the market share (with "~" in a hand-wavey way), I was kinda looking for the Windows steps to see if how much of a pain this would actually be if somebody wanted to set it up.
Ah yeah, I have installed RISC-V with a oneliner in WSL. So it's not too far off on Windows, but I mostly program on Linux these days. You can also build on a Linux VM and share it to Windows, but what kind of iteration loop is that?
I don't know. Perhaps clang-cl could be used on Windows? It depends on how much of a run-time environment one wants.
Is there anything Linux-specific about ELF? I was under the impression it's a pretty common format for executables, used by a whole bunch of systems. It certainly predates Linux, as it was (according to Wikipedia) introduced in 1988 with SVR4.
This is wonderful! No need for a second language for plugins/extensions if your software is written in C++. Hell, it is mind-blowing - using a RISC-V emulator to run C++ scripts. Lol - this is not something that immediately strikes one's brain. Looking forward to fully digging into this next weekend.
If I understand correctly, the idea here is to provide safe scripting by running a RISC V emulator. The "scripts" would be written in C/C++ compiled to RISC V. And then these compiled scripts would be run inside the game engine's emulator. The emulator's library then will allow to share some memory between the emulated program and the host game engine by sharing some memory.
All this sounds like a poor re-invention of Web Assembly 1.0.
I think Wasm is a better option for most cases because:
- There are more advanced Wasm runtimes that can do JIT compilation and be very fast.
- There are probably more languages compiled to Wasm than to RISC V. Especially for higher level languages, which is attractive for scripting.
- There is better tooling for debugging Wasm.
- Interoperability is better specified in Wasm, it was designed from the ground up for that. And with Wasm components it's even better. This is specially important for different host architectures.
Risc-v is a hardware ISA optimised for education. Wasm is a sandboxed IR optimised for compilers. It would be phenomenally unlikely for risc-v to be better than wasm in the exact domain wasm was built to excel at.
Both are targets for a general purpose programming language. As OP commented below, wasm is a stack machine while riscv is a register machine. It would surprise me if there is any wasm program for which there is not a semantically equivalent and faster riscv implementation.
In no way I intended to be dismissive of you work or the RISC-V architecture. I apologize if my words could be interpreted in that way. I actually think that RISCV is a really cool architecture, mostly because it is open. I would love to see it getting more traction in the hardware world. I also think that your work on libriscv is impressive!
My criticism was at the idea of using a RISC-V emulator as a scripting platform. And I'm not saying that it doesn't work, only that I personally think that Web assembly seems a better option for that particular use case.
I've been at it for a few years now and my experience with game engine scripting is that the functions are small, and that it's helpful or pleasant to have the ability to make many calls back into the game engine to ask for things, even simple things. My measurements have shown time and again that libriscv spends 3ns entering and leaving the emulator dispatch, and 2ns to execute a system call or even a more complex host function scheme (using custom instructions).
So now it's a race. For example, wasmtime needs 48ns just to enter and leave, and 24ns to make a call into the host. That's around 100-150 instructions libriscv can execute before wasmtime has even overcome the fixed call latencies. And then add 50 each time we make a call back into the engine.
Now combine this with my ~200 host functions in my game, and I don't know how many in-game events. Which function would wasmtime be faster at? I don't know, probably not a single one. And that's really it. There's no fibonacci computations in game engine scripting. It's just logic.
As far as using complex run-times for scripting: I've already been doing this dance for years now. For example, TinyKVM is a native performance KVM userspace emulator that I among other things ran v8 inside. You can find my research paper about it. So it's not like I couldn't throw v8 in there and slideware some JS solution. But, I really do prefer writing C++.
Also related is attack surface. libriscv is 10k LOC. wasmtime is what, 350k LOC?
wasmtime spent 107ns and libriscv 57ns. Those kinds of functions is your average script function. And, even if we were exactly matching in latency, I would still call that a win!
EDIT: To add, you did make a good point about tooling, components etc.
It's a classic fallacy of thinking "interpreter would be fast enough". Except here it's worse by not even getting the productivity advantage of automatic memory management!
No, use something actually good for gamescript, like C#, which is industry proven and has no issues interpreted languages suffer from.
I'm always baffled this even has to be said, yet enthusiast circles keep sabotaging themselves in assuming they can do better with techniques known to do worse.
I'm so glad this was mentioned every time I see performance related metrics I really wish there was more documentation on how quakec/quake vm worked. Mind you https://fabiensanglard.net/quake3/qvm.php is very good on the technical breakdown which is far more valuable.
> There are probably more languages compiled to Wasm than to RISC V. Especially for higher level languages, which is attractive for scripting.
To expand on this, this is especially useful for modding. The original developers should probably standardize on a single scripting language for practical reasons, but individual modders or groups or modders may benefit from being able to choose their scripting language independent of the original developers'.
I agree; wasm would be a better choice, as it's specifically designed for this use case. RISC-V is designed for hardware implementation, not to be an IR, which means that things are more verbose than they need to be. For example, accessing an arbitrary 64-bit address requires 6 instructions [1] in RISC-V, whereas in Wasm you can just do it. Even more important than ISA differences, though, is that with Wasm you get WASI, which saves you a whole lot of time creating a sandboxed system interface.
There was an interesting paper I read the other day about a scheme for guaranteed constant time dynamic allocation and deallocation. The idea was you make all objects identical in size (they used 32 bytes). Then, on deallocation of a 32 byte node, you just put the node on a free list. Then, when a new allocation request comes in decrement the ref count of any *direct* reference(s) and move those objects to the free list if their ref count goes to 0. Therefore, even if you have some huge list that goes out of scope, only the 1st node goes on the freelist, but the rest of the list will get freed eventually assuming you continue to allocate new objects.
I thought it was a really cool idea and it seems totally viable for things like a game engine. All the pointer chasing would be expensive, but being able to freely allocate memory without much lag would be a really nice property, particularly for games with plugins like Roblox, Factorio, etc. where code quality is often out of your control.
I think this practice is generally good for "most fast things" I noticed that https://github.com/kparc/ksimple/blob/31370a2c799a2a0e491d52... uses this approach too (this is the core of the k apl which is used in finance and engineering industries so very fast for data processing applications. Not sure how it fares in a more general setting but wouldn't be surprised if it was also competitive.
71 comments
[ 3.0 ms ] story [ 196 ms ] threadThe author is cleverly using libriscv for this purpose - and it is indeed a neat use of the library - but I fail to see the 'scripting' part, personally - isn't it scripting when you can just load a text file off the disk, containing program instructions, have it interpreted into some (hopefully optimized) interim representation, and then pushed through a virtual machine? I don't see the "text files" part here - but perhaps it gets more relevant in parts 2/3/4, which I haven't read yet, either...
EDIT: I heard you like ABI, so I put a foreign ABI within your ABI so you can ABI with your ABI .. actually, this started off as a joke but its kind of painful to think about. This isn't really "scripting" .. its more "embed a foreign ABI into your application so you can throw a foreign binary at it, any time, and modify program behaviour extensively "without needing to recompile" .. maybe the author should rename the article "how to embed a virtual machine into your app so you never have to write (your app code) again .."
I can see why they call it "scripting" since, this is what you usually do when embedding the Lua VM into an app - but the fact that I have to use a compiler disqualifies the term 'scripting' from being applicable here, imho ..
I'm not sure if this is a useful distinction to make.
Lua, perhaps the most used scripting language out there, runs through a compiler too and then runs a bytecode interpreter on the resulting code. Most scripting languages work this way. The compiler is still there even if it gets invoked at runtime. Most games ship the compiled bytecode files, not the script source.
The key part here is loading the "script" bytecode at runtime and then executing it in the host, allowing reloading and restarting the script without restarting the "host" process (often a game engine with long load times and lots of resident assets like textures, shaders, models etc that don't need reloading).
The host process could also include a mechanism to invoke the compiler at runtime, pass in the script as text, grab the output and then run it as it does currently. This would be identical to how Lua et al work, but quite a bit of work to set up for limited benefit.
Out-of-the-box, Lua runs through an interpreter - not a compiler.
A compiler produces native machine code from a human-readable programming language for the target platform - whereas an interpreter (in some cases) produces an interim representation (bytecode) for a virtual machine, not necessarily native - and in other cases, simply executes script code line-by-line, without the interim step.
In the case of Lua, this interim representation is in the form of bytecode which is then further interpreted by a virtual machine, pretending that it is a register-based machine vastly different to the actual hardware it is running on. This particular aspect is the only feature that the authors' libriscv usage and the Lua VM have in common - but that doesn't make it scripting.
And scripting means (to my jaded 40-years of development brain) to load text files from a resource (disk or otherwise), interpret it in some interim form, and then push through a secondary mechanism for final execution in the native environment.
>The key part here is loading the "script" bytecode at runtime and then executing it in the host, allowing reloading and restarting the script without restarting the "host" process (often a game engine with long load times and lots of resident assets like textures, shaders, models etc that don't need reloading).
So this is where our definitions conflict and go awry, because to me all you've described is runtime loading of a binary resource. Is loading and parsing a .PNG resource that appears in your executive bundle somewhere, also considered "Scripting" in your opinion? Because I don't think that is an accurate use of the term, personally. It would be scripting if the file (or memory blob) was somehow modifiable by the user, and then interpreted without further interaction required on the part of the developer - but in this case (the article) the developer (not the end user) still has to compile a binary blob, integrate it into their application, and then 'run it'.
This is more appropriately referred to as runtime resource modification and execution - for which the action of "scripting" is a superset - but the fact that there are two compilers involved in this project (native to build the .exe, and risc-v cross-compiling to build the binary blob resource for integration) means that we are far, far away from the typical scripting mechanic.
Its important to make this distinction. This isn't about scripting. It is about runtime resource modification and processing - but the different paths taken to the same fork in the road between scripting and compiling are vastly different and should not be conflated.
This isn't to say that integrating libriscv as a virtual machine in ones application execution environment is not a brilliant idea with a great deal of merit - just that the author is incorrectly using terms, and this must simply not be allowed. There is no interpreting happening here until runtime - when the binary risc-v blob is loaded and parsed by a virtual machine. If there were some mechanism to load code intended for the risc-v component at runtime, compile it, and load it for execution/report errors in the script/etc. - then I would say that the scripting workflow has been completed for this project - and it would be appropriate to use the term - but that has not happened here. It still requires a separate compilation (risc-v machine code) step, and there are still many, many aspects of the scripting workflow that are not implemented in this project ..
We can see, already here in this thread, the dangers of people mis-interpreting your technology.
Now, if you get the riscv compiler integrated into your projects such that we can indeed just load a text file containing C code, dynamically at runtime, (a la shaders), I'll revisit all of this and go along with your program. ;)
Anyway, thanks for the great series of articles - really gave me an interesting read on the way to work this morning, and I've put libriscv (as well as your sample projects) in my stack of Lab Todo's for the week .. now if someone produces LuaRISCV that targets the riscv VM instead of Lua Bytecode, that's gonna break a few molds, in and of itself .. ;) (In the context of your work so far, this seems like a low-hanging fruit, actually..)
The first thing the "interpreter" does is run the Lua code through `luac`, the Lua compiler and then feed the bytecode to the Lua VM. This may be transparent to the user, but it's still there.
Almost all interpreted languages have a compiler and a bytecode interpreter as distinct steps.
> Is loading and parsing a .PNG resource that appears in your executive bundle somewhere, also considered "Scripting" in your opinion?
Of course it's not, an image file does not contain any code that could be interpreted or executed in a host environment.
There is a distinction here on how code is executed, whether by compiling, interpreting, JITting or a combination of the above. But if you draw the line for "scripting" at consuming textual source code, that would exclude most use of Lua in gaming engines, as it is typically distributed as bytecode and may not even link luac compiler to the host executable. Yet it's commonly called "scripting" in the industry.
And conversely, if the code in this article included a mechanism to compile the source to risc-v would it meet your definition of "scripting"?
There isn't a clear definition of what's scripting and what's not, but drawing the line at consuming textual source code is not, in my opinion, a useful distinction (it's certainly a distinction) because it hardly matches what's the state of the art out there.
Interpreting has, also for decades, referred to the run-time interpretation of human-readable language into some mechanism which either a) immediately results in native execution at runtime, or b) produces errors reported to the developer for fixing, precluding the generation of immutable machine code being executed natively by the CPU (except of course with JIT, which is a form of compilation at runtime, since the interim bytecode is translated into real machine code instructions...)
Scripting has, for decades - not just in the gaming industry, which is a subset of computing enterprises - meant "loading a human readable text file containing a programming language and directly executing it if it passes validation - reporting errors, otherwise, preventing further execution. This is why we have BASH scripts and Python scripts and Lua scripts - they are not machine code, they are interpreted and either eventually produce machine code, or run through a state device pretending to be a non-native machine.
fwsgonzo is literally compiling C code into machine code to be executed on a pretend CPU (in the form of an embedded libriscv).
The code is not interpreted (from one language to another) - it is executed in a virtual machine environment. Bytecode is, on the other hand, always interpreted from one form to another prior to execution (except JIT, it is only interpreted once, compiled, and then exists as machine code always)
fwsgonzo's binary blob is executed by a 'fake' CPU, it is not interpreted.
If he had the execution environment set up, he could literally run that very same machine code on real RISCV hardware, unchanged, without translation or interpretation.
It is not, therefore, interpreted in any sense other than by his embedded RISCV emulator - and thus it is not scripting!
This is the point where the distinction is important.
>The first thing the "interpreter" does is run the Lua code through `luac`,
The first thing the Lua interpreter does is validate the inbound, human-readable script to make sure the program is correct, syntactically and otherwise - giving errors to the developer if errors are encountered. Meaning, at runtime, errors can be caught and fixed by the human developer.
Only then, once it has been validated, is it processed into an interim representation as bytecode. Sure, Lua can be told to produce bytecode in order to speed up runtime loading and execution, but its still an interpreted bytecode. It hasn't been compiled into a native form for direct execution - it still requires a secondary native program (a VM host) in order to provide any functionality.
From the Lua 5.1 manual, Section 2.4.1:
Lua does have a compiler for the case where you want to create Lua bytecode (.luac), as a loadable-at-runtime resource to be further interpreted and also, the bytecode is subsequently interpreted according to the needs of the target (native) environment on which it is being executed, whether through a JIT or otherwise. That same bytecode can be executed on vastly different native CPU architectures - through interpretation.The difference is, prior to Lua's internal compilation of the script into bytecode, validation of the programs correctness is performed, preventing the developer from producing incorrect bytecode if there are errors, and this is the process of scripting - distinct from compilation - because it is a more direct human/computer interaction regarding the correctness of the script language as it is being used by the developer.
>There isn't a clear d...
Maybe I'm nitpicking, but to me scripting is just programming/extending a certain fixed piece of software to execute instructions not already programmed into it without having to modify that software itself. Usually using an API. Not unlike computer programming, which aims to make a piece of hardware execute some instructions without needing to alter the said hardware. The main difference is conceptual (the platform targeted: hardware or software). If the script happens to be run by a VM emulating a real processor because your software includes such a thing, I think the distinction becomes purely conceptual.
Now, the technology is obviously super cool, but what I don't quite understand yet is what is the best use case for this? Is it really game scripting? Or compiling C++ on the fly?
It's not exactly a simple drop in replacement for a Lua interpreter.
Is this true? Or was it true for some Lua 2.X versions? Because it does not seem to be the case for Lua 3.0 and on: [1]
> If control reaches the end of a function without a return instruction, then the function returns with no results.
As to the idea of using an emulator for scripting, it seems a bit similar to approaches using WebAssembly. But looking into the benchmarks [2] provided by libriscv, it is quite interesting to see it outperforms luajit and wasm3 (in its "hand-picked" tests, at the very least).
[1] https://www.lua.org/manual/3.0/manual.html [2] https://github.com/fwsGonzo/libriscv?tab=readme-ov-file#benc...
Maybe he's talking about something else than pure lua? Something with the game's bindings?
I used my usual benchmarks: STREAM (memory) and CoreMark (CPU).
We used to create DSLs to make scripting of games simpler and less error prone... so that coders/designers could write Scripts/AI in a language better suited to the problems they face. It was sort of a side issue that is was a sandbox although that was really useful for lots of reasons.
Now we seem to be creating more and more complex toolchains while complaining that game development is getting more and more expensive. We're not solving the correct problems at all.
Oh well.
This article is clearly targeted at engineers rolling their own engine, so that audience may find it beneficial keep everything in one language if they are already comfortable with it. As long as the core engine doesn't need to re-compile every little change, it's a win.
The other half of the problem is asset production time. But no game engine is really solving that problem. As you'll discover in that domain, the art community is a lot more protective of their techniques and pipelines than programming. Even more so if looking for game ready asset production. Same issue, different cause.
Technically the only interesting challenge is "how do we get off Unity?"
the mismanagement stems from the fact that it is costing too much from the onset, and "management" wants to ensure that the investment makes a good enough return to justify the high cost.
Therefore, they dictate all of the risks have to be removed - such as not going for a risky new brand, but make a sequel. They want realistic graphics because it's more commercially viable - going for a different or uncommon aesthetic means taking risks.
I would argue all of the problems in AAA development stems from this fact.
it is also why indie games can be so cheap yet so successful. Not because indies are better (tho they are- at least more passionate if nothing else), but because indies are capable of taking risks to push the edge and make something new and untested.
It's the same as films - creative endeavours cannot have their risks managed away. It is inherent, and there's a large chance of failure even if the team has done their best.
What changed, I think, is all of the development studios being bought up by large producers. But, then again, I see steam as being a great counterforce on this, and the ease of releasing on web or mobile has been a boon for indie devs too. So maybe that hasn't changed that much.
The technology has advanced to where you can spend blockbuster budgets and get nearly film-quality results (not nearly possible in the cartridge and CD days). Meanwhile the price per AAA title has stayed relatively fixed at 50-60 USD for that same range of time, so production houses have had to try being appealing to ever-larger audiences. Fortunately, many people continue playing these games into their 30s and 40s so the market grows on its own too.
I'm trying to say you're right about the risk aversion being a big part of the industry's problems but that's been an issue for a while, it has gotten terribly enhanced with the growth of the industry.
Now I wonder which is worse.. extracting money one quarter a time with punishingly difficult timing tests, or extracting money via cosmetics that have no actual game impact but are marketed very compellingly.
The cheapness also means indies simply get more "shots on goal." The barrier to entry is relatively low, so failing is lower-stakes. You have way more indies trying (and mostly failing), which means you have a greater raw number of hits. That's compared to a big company only having a handful of big-budget games in development.
* Ability to do first-party console ports
* 3D rendering fidelity and speed
* Asset store quality & quantity
* Robustness & stability - they both have tons of bugs but with Unity there is almost always a way around the bug, due to the decades of commercial games that paved the path before you.
For one quick example, Godot's physics engine is well-known to be poor, both in terms of performance and in terms of accuracy/stability. It has tons of bugs, tunneling, jittering, etc. Both the 2D and 3D physics engines in Godot are currently slated to be ripped out & replaced with external ones of higher quality (Box2D and Jolt). But currently, that hasn't happened, so one of the foundational parts of most games - physics - is guaranteed to be broken to some degree in a Godot game.
Replacing the 3d engine with Jolt is on the official roadmap. If it were really a drop in replacement today, that would have already shipped.
But yes, all of my personal projects are in Godot now, and I'm planning to use it for some tooling at work, just because it's UI system is nicer than Unity's as well as it having better support for re-using editor UI components in an application.
Before their ascension there were already plenty to middleware to chose from, Ogre3D, SFML, SDL, Gamemaker, RenderWare, jMonkeyEngine,...
They aren't used as much, even the free ones, because of supported languages, tooling, asset store, platform owners support, and everything else that keeps them relevant in the industry despite their flaws.
I run a few games that takes C# plugins. The games have a big warning that they aren't doing anything to prevent my machine getting hacked, personal files uploaded to random servers, etc....
It’s what Roblox uses
> On Linux this is fairly straight-forward.
Well... Of course on Linux it would be straight forward
I'd love for games to start having any/all system languages as the embedded scripting language.
But with Linux being around ~2% of the market share (with "~" in a hand-wavey way), I was kinda looking for the Windows steps to see if how much of a pain this would actually be if somebody wanted to set it up.
I don't know. Perhaps clang-cl could be used on Windows? It depends on how much of a run-time environment one wants.
All this sounds like a poor re-invention of Web Assembly 1.0.
I think Wasm is a better option for most cases because:
- There are more advanced Wasm runtimes that can do JIT compilation and be very fast.
- There are probably more languages compiled to Wasm than to RISC V. Especially for higher level languages, which is attractive for scripting.
- There is better tooling for debugging Wasm.
- Interoperability is better specified in Wasm, it was designed from the ground up for that. And with Wasm components it's even better. This is specially important for different host architectures.
I have a lot of blog posts that you can read to get up to speed. Or maybe you already made up your mind.
My criticism was at the idea of using a RISC-V emulator as a scripting platform. And I'm not saying that it doesn't work, only that I personally think that Web assembly seems a better option for that particular use case.
So now it's a race. For example, wasmtime needs 48ns just to enter and leave, and 24ns to make a call into the host. That's around 100-150 instructions libriscv can execute before wasmtime has even overcome the fixed call latencies. And then add 50 each time we make a call back into the engine.
Now combine this with my ~200 host functions in my game, and I don't know how many in-game events. Which function would wasmtime be faster at? I don't know, probably not a single one. And that's really it. There's no fibonacci computations in game engine scripting. It's just logic.
As far as using complex run-times for scripting: I've already been doing this dance for years now. For example, TinyKVM is a native performance KVM userspace emulator that I among other things ran v8 inside. You can find my research paper about it. So it's not like I couldn't throw v8 in there and slideware some JS solution. But, I really do prefer writing C++.
Also related is attack surface. libriscv is 10k LOC. wasmtime is what, 350k LOC?
Here is a random function I benchmarked: https://fwsgonzo.medium.com/a-sandboxed-rainbow-function-b42...
wasmtime spent 107ns and libriscv 57ns. Those kinds of functions is your average script function. And, even if we were exactly matching in latency, I would still call that a win!
EDIT: To add, you did make a good point about tooling, components etc.
No, use something actually good for gamescript, like C#, which is industry proven and has no issues interpreted languages suffer from.
I'm always baffled this even has to be said, yet enthusiast circles keep sabotaging themselves in assuming they can do better with techniques known to do worse.
This is more like Quake 3 which used a general purpose VM that you could compile regular C to.
To expand on this, this is especially useful for modding. The original developers should probably standardize on a single scripting language for practical reasons, but individual modders or groups or modders may benefit from being able to choose their scripting language independent of the original developers'.
[1]: https://github.com/riscv-non-isa/riscv-elf-psabi-doc/pull/38...
Considering RISC-V is inevitable, this will prove a good decision.
I thought it was a really cool idea and it seems totally viable for things like a game engine. All the pointer chasing would be expensive, but being able to freely allocate memory without much lag would be a really nice property, particularly for games with plugins like Roblox, Factorio, etc. where code quality is often out of your control.
https://lptk.github.io/files/ctrc-2024-05-09.pdf