Low-latency scripting for game engines
fwsgonzo.medium.com
fwsgonzo.medium.com
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
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).
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.
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.
> 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.
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 ..
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:
...
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.
>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.
I disagree with you, there very definitely IS a clear definition of what is scripting and what is not - one must not call it 'scripting' unless runtime loading, validation (or not), interpreting, and subsequent execution (whether on an interim bytecode or otherwise) is happening - and that is not what is happening here in fwsgonzo's project. He is compiling binary blobs, and his eventual execution environment has nothing to do with the validation of the scripted code. The C compiler he uses in a prior step does, however.
Scripting is a development workflow where compilation/linking/building is not required - results are immediate, the program reports validity of the loaded script, or otherwise, and executes directly upon command by the developer.
Additionally, fwsgonzo has also, already confirmed that this distinction is correct (see elsewhere in the thread) - that in fact interpreting of a script is NOT happening in any sense other than the RISCV VM is interpreting pre-validated machine code (not bytecode!) in the form of RISCV instructions.
That machine code is not a human-readable script, and it is not bytecode! It is machine code, produced by a compiler.
This distinction is important because that same machine code could, theoretically, be executed directly, unmodified, on a RISCV machine - that is not the case with Lua bytecode, ever, which always requires interpretation by a VM.
fwsgonzo's project is entirely based around pre-compilation of native binary resources, not interpretation. A human-readable language is not being interpreted by his runtime.
There is no opportunity (at runtime) with his project to have the runtime report on errors in the human-readable script - this has all been done with a prior compilation step.
>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.
I disagree with you. I think you are taking an aberration of the gaming industry, which is rife with such things, and applying it to computing as a whole - and this is where the protest over your position lays.
It is absolutely important to make this useful distinction, because in case a) INTERPRETATION: an interim representation is being generated after validating program correctness/conformity to a language spec, and in case b) COMPILATION: no such validation is occurring once the compiler produces runnable machine code.
> Yet it's commonly called "scripting" in the industry.
I have 40 years of experience with this subject, including working on game engines and realtime (scientific) use of Lua in high-performance environments, and the phrase "scripting" has always referred to the process of writing code that is immediately interpreted by some program before resulting in actual runtime execution if it is found to be valid code.
The interpretation may not even work as desired, but the host environment/program will still continue to be executed in a runtime context.
The fact that compilers "interpret" code before producing an interim representation that is then used to produce static machine code, is entirely why the distinction must be made: because there is a point where code can be incorrect and thus not runnable!
In the game world, "scripting" means 'writing some human-readable script and having it interpreted at runtime, without involving a native compilation/linking/building/packaging step" - and as this is not what is happening (key word: without) in fwsgonzos' project, it is incorrect use of the phrase.
He is compiling resources which can be loaded at runtime, bypassing the native loading/linking phase, and it is therefore more appropriate to refer to what he is doing as "runtime resource management" which, incidentally, feeds into a VM. This is not scripting, although scripting often necessitates the same activity - errors in the code are not reported at runtime, by the process doing something with the resource.
Please just use the right term and stop trying to justify an incorrect usage - don't re-define these terms based on a misunderstanding of the mechanics involved. There are literally decades of examples which are entirely counter to your misunderstanding out there, in the state of the art.
(tl;dr - Is /bin/bash a compiler? Is /bin/python a compiler? Why isn't gcc exclusively referred to as an interpreter? Is clang an interpreter? Is ld a scripting environment?)
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.