libsdl-org / SDL_shader_tools

Shader compiler and tools for SDLSL (Simple Directmedia Layer Shader Language)
https://libsdl.org/
zlib License
135 stars 2 forks source link

Vertex Shader Syntax Megathread #3

Open icculus opened 2 years ago

icculus commented 2 years ago

I NEED FEEDBACK. IF YOU LOVE OR HATE THIS, PLEASE SPEAK LOUDLY.

IF THIS WILL ABSOLUTELY CAUSE PAIN AND MISERY, SPEAK TWICE AS LOUDLY.

THANK YOU.

Here's a first example of where I'm heading towards. The syntax and the heavy commenting explaining the syntax are what's important; the code itself is not and just meant to be an example.

/*
 * FIRST DRAFT OF SDL SHADER LANGUAGE (or whatever we're calling it) SYNTAX.
 *
 * This was a random HLSL shader generated by Unreal Engine 3, that I trimmed up
 * to remove unused code, etc.
 *
 * Don't worry about what this shader _does_, as it's just one of thousands of
 * variations UE3 generated from whatever and I grabbed one at random.
 *
 *
 * I don't know if this program is correct yet; not only is the grammar still
 * evolving and the compiler still in development, it's possible I deleted
 * a needed function by accident or fat-fingered a simple edit. Just get the
 * idea of what I'm trying to do here.
 *
 * Anything in here might change. Also, if something looks like a
 * short-sighted, dumbass mistake, it probably is. Come talk to me about it.
 *
 * --ryan.
 *
 * icculus@icculus.org
 * https://twitter.com/icculus
 */

// These are constants (uniforms). We put them in a struct, because we'll be
// setting them up in a buffer on the CPU and sending them to GPU memory all
// at once, where the shader will _also_ be treating them as fields in a block
// of memory instead of individual variables. In that sense, there's no value
// in considering them individually.
//
// Since this piece happens to be compatible with C/C++ syntax, and you
// can #include files from there _and_ in these shaders, it's not unreasonable
// that this would go in a header by itself that the shader and native code
// can share (or the shader can have some #ifdefs so you can include it and
// only see this part of the file from C)...this can help make sure they're
// definitely looking at the same data layout.
//
// Alignment and packing on these will be well-defined.
struct FVertexConstants
{
    float4x4 ViewProjectionMatrix;
    float4x4 LocalToWorld;
    float3x3 WorldToLocal;
    float4 CameraPosition;
    float4 ScreenPositionScaleBias;
};

// This is _also_ a constant, but it's one that changes at a different
// rate than the others (one lives per-mesh, one lives per-frame), so
// it will live in a seperate buffer.
struct BoneMatricesConstants
{
    float4x3 BoneMatrices[75];
};

// These are your vertex attributes. You set up your arrays however you like
//  (interleaved in a single buffer, or totally separate buffers, or a little
//  of both), and fill in the details (format, offset, stride, buffer index)
//  when creating a pipeline object. Then assign the buffers with the data to
//  the appropriate indices before drawing, and when the vertex shader
//  runs, it assembles this struct from those sources. You specify
//  this struct as an argument to your main function.
//
// "@attribute(0)" means "this value is retrieved with the details in the
// first entry of SDL_GpuPipelineDescription::vertices array." There are
// several things with "@keyword" in the current language. I call them
// ATtributes. I'm _funny_!
struct FVertexInput
{
    float4 Position @attribute(0);
    half3 TangentX @attribute(1);
    half4 TangentZ @attribute(2);
    int4 BlendIndices @attribute(3);
    float4 BlendWeights @attribute(4);
    float3 DeltaPosition @attribute(5);
    half3 DeltaTangentZ @attribute(6);
};

// This struct holds all the _outputs_ from this shader.
// This struct is passed to the next stage of the pipeline (the fragment
// shader, at this moment). That next shader will need to know what these
// attributes mean and match up, so putting this in a common header
// (or having the matching fragment shader function _in the same source
// file_) is highly recommended.
//
// Other shader languages let you specify what these outputs mean, so
// the fragment shader can pick out the parts it cares about, but I'm
// hoping to avoid that and just pass it all through. If this turns out
// to be a terrible idea, we have @attributes at the ready!
//
// The "@position" means "this is the vertex position set by this shader"
// The system needs to know this beyond your shader pipeline, so there's
// an attribute to let it know where to find it in the output. Everything
// else here is just data that the fragment shader acts on.
struct FVertexOutput
{
    float4 ScreenPosition;
    float4 CameraVector_FogA;
    float4 Position @position;
};

// Heretical to C, functions start with the word "function" ...I am
// gambling that when people say "C-like language" they mean some
// set of characteristics that may include: curly braces, a preprocessor,
// almost no rules about whitespace, the distinctive for-loop syntax,
// and calculate-then-assign operators like +=, *=, etc. But not that it
// exactly resembles C in all ways.
//
// Notably, PHP and Javascript both use the "function" keyword and
// everyone thinks they are "C enough," so I hope this is okay.
//
// The C grammar just expects you to drop right into a datatype and figure
// out what you're talking about several tokens down the line...was it a
// function declaration? A function definition? A variable? A struct that
// might be defining a variable or a new type...?! An identifier, which
// might be a user-declared typedef, making parsing REALLY HARD...?!
//
// Sprinkling some "function" and "var" prefixes on these things helps me,
// the parser writer, immensely, and if we're being honest I bet it helps
// you, the program writer, too.
function float3 MorphPosition(FVertexInput Input)
{
    return Input.Position.xyz + Input.DeltaPosition;
}

// This function uses intrinsic functions, like "normalize" and "cross". Right now
// these match naming conventions in Direct3D, but that may change (GLSL provides
// many of the same functions but disagrees on what to call some of them).
//
// You can also see that matrices can be dereferenced like arrays.
//
// local variables are defined with a "var" keyword, which is also heretical
// to C users, but I couldn't help but notice WebGPU did this too, probably
// because parsing C is hard.
function float3x3 MorphTangents(FVertexInput Input)
{
    var float3x3 Result = 0;
    var float3 TangentX = ((Input.TangentX / 127.5) - 1);
    var float4 TangentZ = ((Input.TangentZ / 127.5) - 1);
    var float3 DeltaTangentZ = ((Input.DeltaTangentZ / 127.5) - 1);
    Result[2] = normalize(float3(TangentZ.x, TangentZ.y, TangentZ.z) + DeltaTangentZ);
    Result[0] = normalize(TangentX - (dot(TangentX, Result[2]) * Result[2]));
    Result[1] = normalize(cross(Result[2], Result[0]) * TangentZ.w);
    return Result;
}

// Note that this calls CalcBoneMatrix which has not been predeclared (it appears
// later in the file). There is no predeclaration; as long as the function is
// in the same compilation unit, the compiler will accept it (parsing accepts all
// function calls, then during semantic analysis, it parses functions definitions
// first to build a full list of predeclarations).
function float3 SkinPosition(FVertexInput Input, BoneMatricesConstants BoneBuffer)
{
    var float4 Position = float4(MorphPosition(Input), 1);
    var float4x3 BoneMatrix = CalcBoneMatrix(Input, BoneBuffer);
    return Position * BoneMatrix;
}

// Notable: all structs are passed by reference (which means if you change
// a field, it will be changed for the caller too). There are no pointers,
// references, or copies made. We should probably offer a way to mark it "const" though...
function float4x3 CalcBoneMatrix(FVertexInput Input, BoneMatricesConstants BoneBuffer)
{
    var float4x3 BoneMatrix = Input.BlendWeights.x * BoneBuffer.BoneMatrices[Input.BlendIndices.x];
    BoneMatrix += Input.BlendWeights.y * BoneBuffer.BoneMatrices[Input.BlendIndices.y];
    BoneMatrix += Input.BlendWeights.z * BoneBuffer.BoneMatrices[Input.BlendIndices.z];
    BoneMatrix += Input.BlendWeights.w * BoneBuffer.BoneMatrices[Input.BlendIndices.w];
    return BoneMatrix;
}

// Vector constructors work like you'd expect. Also, if you haven't noticed, these are
// floatX instead of vecX.
// Matrix and vector multiplication is built-in using standard C operators (even if this
// dithers down to an intrinsic "mul" function in Direct3D, etc).
function float3x3 SkinTangents(FVertexInput Input, BoneMatricesConstants BoneBuffer)
{
    var float3x3 Tangents = MorphTangents(Input);
    var float3x3 TangentBasis;
    var float4x3 BoneMatrix = CalcBoneMatrix(Input, BoneBuffer);
    TangentBasis[0] = float4(Tangents[0], 0) * BoneMatrix;
    TangentBasis[1] = float4(Tangents[1], 0) * BoneMatrix;
    TangentBasis[2] = float4(Tangents[2], 0) * BoneMatrix;
    return TangentBasis;
}

// The main event, literally. This is called once for each vertex. A vertex shader!
//
// You'll note the "@vertex" attribute. General functions can be shared between different shader types, but
// you have to flag the entry point as @vertex so certain magic can occur. Likewise for @fragment entry points.
// One parameter is marked "@inputs," which tells the compiler that this is the magic struct that will be
// assembled from various buffers based on the pipeline details. It changes values for every vertex. "@buffer(1)" means
// "whatever was bound to buffer index 1 with SDL_GpuSetRenderPassVertexBuffer() is available here." It stays
// the same for every vertex until you change it.
// The outputs are returned from the function in a struct.
// Note that you can have more than one shader in a single source file, and can also have vertex _and_
// fragment shaders in the same file; when loading shaders for use, you tell it the name of the entry point
// you want.
// Note that local variable declarations can be interspersed with code.
// Note that assignment is NOT an expression; the `Output.ScreenPosition = Output.Position = X;` works because
// assignment statements allow this as syntactic sugar, but something like `if (X = Y)` or even `if ((X = Y) != 0)` would be illegal.
function @vertex FVertexOutput VertexMain(FVertexInput Input @inputs, FVertexConstants Constants @buffer(0), BoneMatricesConstants BoneBuffer @buffer(1))
{
    var FVertexOutput Output;
    var float4 WorldPosition = Constants.LocalToWorld * float4(SkinPosition(Input, BoneBuffer), 1);
    Output.ScreenPosition = Output.Position = Constants.ViewProjectionMatrix * WorldPosition;
    var float3x3 TangentBasis = SkinTangents(Input, BoneBuffer);
    var float3 WorldVector = Constants.CameraPosition.xyz - WorldPosition.xyz * Constants.CameraPosition.w;
    Output.CameraVector_FogA.xyz = TangentBasis * (Constants.WorldToLocal * WorldVector);
    Output.CameraVector_FogA.w = 0;
    return Output;
}

// end of example_sdlsl_vertex_shader_ue3.sdlsl ...
Akaricchi commented 1 year ago

I also strongly dislike the idea of having two syntaxes for variable declarations. I don't really care about how it looks like in the end, but please stick with one. The C-like style probably makes more sense given how C-like the rest of the language is, and out of practical concerns such as GLSL/HLSL compatibility for people who enjoy pain.

I'm not a fan of the var either; personally I think it should be removed if it doesn't complicate the parsing, but I can live with it.

icculus commented 1 year ago

I can remove one of them, but why is this such a terrible thing that there are two ways to do this? Are people that are capable of developing shaders going to see an unexpected "var mydata : int;" and not be able to piece together its cryptic meaning?

I think part of the struggle is not just creating a better language, but creating something that is still close enough to C that people won't rebel--which drives me nuts, but feels crucial. Keeping the C-like version of variable declaration as an option is part of that.

But if I had to drop one, the C-like version would be the one I'd like to drop.

The good news is that, since this is currently just syntactic sugar, it's easy to leave it in place now, and yank either out at the last moment, so decisions don't need to be made now.

Akaricchi commented 1 year ago

I can remove one of them, but why is this such a terrible thing that there are two ways to do this? Are people that are capable of developing shaders going to see an unexpected "var mydata : int;" and not be able to piece together its cryptic meaning?

No that's not the problem. As I said above, the actual syntax doesn't matter all that much. But when (not if, when) I eventually see both of those syntaxes used in the same project I'm working on, or god forbid in the same file, I'm probably punching my monitor in OCD fueled rage :)

Are there actually any examples of other languages with two different, functionally identical syntaxes for variable declaration?

I think part of the struggle is not just creating a better language, but creating something that is still close enough to C that people won't rebel--which drives me nuts, but feels crucial. Keeping the C-like version of variable declaration as an option is part of that. But if I had to drop one, the C-like version would be the one I'd like to drop.

I'm personally not very invested in keeping the language very C-like. I'm just saying that if that's the direction you're going with, the "rusty" declaration style makes little sense, in my opinion. I just don't see it adding anything other than needless inconsistency and mild confusion. For one, you can't use it in structs you share with your C/C++ code. And at that point, why would you go out of your way to use it anywhere else? Is there any reason for the alternative syntax to exist other than that it looks nicer (which is debatable in itself)?

On that note, I think the compiler needs some sort of a "reflection" API to probe at the layout of types for non-C-like languages and people who don't want or can't share headers between C/C++ and the shaders.

Akaricchi commented 1 year ago

By the way, I have to say one departure from C that I really love is requiring braces for if/else/for/while/etc. clauses. Sometimes I wish actual C compilers had warning options to enforce this style.

One question though: how does else if work with this syntax? In C it emerges naturally, but I guess it's a special case syntactic sugar here?

icculus commented 1 year ago

By the way, I have to say one departure from C that I really love is requiring braces for if/else/for/while/etc. clauses. Sometimes I wish actual C compilers had warning options to enforce this style.

One question though: how does else if work with this syntax? In C it emerges naturally, but I guess it's a special case syntactic sugar here?

I think I probably forgot to update the grammar to deal with this, but yeah, I'd like "else if" to be legal. But it also makes me nervous that so many popular programming languages went with a separate elseif or elsif keyword here, like they discovered that this was a giant pain to parse. I guess I'll report back when I try this and find out its a disaster.

icculus commented 1 year ago

Does anyone have strong feelings about using array dereferencing with vectors?

So you have a float4 named x, and it's legal to do y = x[2]; and this is exactly identical to y = x.z; ... referencing x[4] would be a compile error, and possibly we disallow variables here, too, so it's literally syntactic sugar that needs an integer constant.

Or maybe we just forbid array syntax at all with vectors?

Opinions?

hw-claudio commented 1 year ago

I can remove one of them, but why is this such a terrible thing that there are two ways to do this? Are people that are capable of developing shaders going to see an unexpected "var mydata : int;" and not be able to piece together its cryptic meaning?

No that's not the problem. As I said above, the actual syntax doesn't matter all that much. But when (not if, when) I eventually see both of those syntaxes used in the same project I'm working on, or god forbid in the same file, I'm probably punching my monitor in OCD fueled rage :)

I'd prefer a single syntax for the language too, no matter which one is chosen in the end.

darksylinc commented 1 year ago

Does anyone have strong feelings about using array dereferencing with vectors?

There has been various times where I would've wanted to do that.

However bear in mind such cases were usually of the sort:

vec3 myVec = ...;
for( int i = 0; i < 3; ++i )
   myVec[i] = ...;

Rather than using literals. Which adds a few issues on the compiler side:

FXC HLSL supports this (D3D11), but it can result in weird compiler errors once you nest a few loops or use ThreadGroup barriers, or divergence is involved; and it can lead to long compile times as FXC goes the extra mile to unroll the loop and try to make it work.

I don't know if all Vulkan/D3D12 HW would support dynamic addressing just fine though.

kevin-rogovin commented 1 year ago

So, for a large number of GPU's there is a world of difference between dynamic and static array access. The usual story is that all variables (except UBO's, SSBO's and samplers/textures) in a shader are realized as a register. An array access determined at compile time is easy, but an array access of run time is a proper pain. For Intel GPU's the shader driver then stores these arrays in a scratch buffer (with enough buffer to cover all threads of all EU's). Reading and writing to that array then becomes a memory read and write with the gamble that the read and write are heavily cached. However, this is much, much slower than straight up register access.

The upshot is that allowing for the index of myVec4[] to be dynamic means a proper nightmare when it finally runs on a GPU. With that in mind, I suggest to avoid the issue completely and to not have array syntax to access elements of a [iu]vec2, [iu]vec3, [iu]vec4 unless the indices are static constants. Ditto for matrix element access too.

cos1110 commented 1 year ago

How the parser will be implemented?

(a) Let's say that is to write it from scratch, but the catch here is to see the future and prevent common mistakes and pitfalls (or workarounds) that other parsers had to do historically. Say for example a project reference like this can give some proper insight on how things can possible turn out. https://github.com/Thekla/hlslparser/blob/master/src/HLSLParser.cpp

(b) Another case is that the parser will be generated with some generator (ie: Antlr or something), so in this case there is not so much to mention about since the process is automated and algorithmic. More or less it means that we could possible get exactly identical to GLSL syntax but also if needed throw an even more additions onto the mix (like other specialized syntax or more API additions). https://github.com/AcademySoftwareFoundation/OpenShadingLanguage/blob/main/src/liboslcomp/osllex.l https://github.com/google/graphicsfuzz/blob/master/ast/src/main/antlr4/com/graphicsfuzz/parser/GLSL.g4

Another question is what is the fundamental syntax used as a reference? From what I understand is a GLSL approach but with a few alterations (eg: var/function and attributes). One thing to note is that if automated parser generator is used then the var/function might seem obsolete since the generated parser will figure out the details internally. Another point is that if the parser is auto-generated, then there is nothing to loose by conforming to a standard syntax. Say for example you keep the best parts of GLSL (which is important in terms of compability and reusability of existing shaders), and making it streamlined and neat as OSL.

Saying that since GLSL and HLSL have been into a constant evolution for many decades they had their ups-and-downs in terms of evolution and improvements. Now there are lots of messy things, lots of workarounds, huge specs etc...

Starting from scratch, is a best bet to go with the OSL route instead, since by focus scope OSL was very dedicated and precise on what it tried to do and still does, it never tried to become too robust or advanced, but provide just exactly what is important. Thus having great API and very stable and formal procedures, without any monkeypatches an workarounds. https://github.com/sambler/osl-shaders/blob/master/patterns/ChHalftone/ChHalftone.osl

These were only two of my comments, on the parser implementation, and then on the possible final goal of the SYNTAX. What must be done must be pragmatic, most possible is that by project scope it will implement the most common and typical shaders of this generation, like 2DGUI, Vertex Skinning, HDPR, etc... So if it covers these use cases then there would be nothing other to think about in terms of upfront design.

icculus commented 1 year ago

We have a parser, it's built with Lemon. Here it is:

https://github.com/libsdl-org/SDL_shader_tools/blob/main/SDL_shader_parser.lemon

(This is still work in progress, so things are changing.)

An overview of the language syntax is here: https://github.com/libsdl-org/SDL_shader_tools/blob/main/docs/README-shader-language-quickstart.md

creichen commented 1 year ago

Nice to see shader work aimed at the SDL community! Can you elaborate a little on the design goals, please? I've been looking around a bit for GLSL alternatives, and there seem to be a bunch of other shader languages out there (slang, rust-gpu, shady, ...), and I am a bit lost as to what the different advantages of the different languages are-- why would I use SDLSL instead of GLSL or one of the other shader languages?

You list "fast and cheap" and "reasonable to ship at runtime or embed in another project" as goals for the compiler, but it's not clear to me that this requires a new language (as opposed to a new compiler for an existing language).

I do agree that language design informs the necessary complexity of the compiler, but, if I may play devil's advocate for a second, making a compiler "fast and cheap" through a new language design is probably easier if you drop complex syntax and go for LISP-like S-expressions; parsing is still a nontrivial part of compile-time overhead. Also, removing the preprocessor would avoid costly token/string manipulation; for meta-programming / modularisation, a scheme/rust-like macro mechanism that operates directly on the AST (or perhaps just better options for linking) would likely be faster.

Thus, my guess is that SDLSL has additional design goals, on top of the ones that are explicitly listed?

icculus commented 1 year ago

The upshot is that allowing for the index of myVec4[] to be dynamic means a proper nightmare when it finally runs on a GPU. With that in mind, I suggest to avoid the issue completely and to not have array syntax to access elements of a [iu]vec2, [iu]vec3, [iu]vec4 unless the indices are static constants. Ditto for matrix element access too.

Right now (in my working copy), my_vector[2] will dither down to the same code as my_vector.z ...if one were to specify my_vector[i] then it becomes a VECTORDEREF instruction in the bytecode instead, which I fully expect might turn into something at runtime as atrocious as this psuedo code:

float val;
if (i == 0) {
    val = my_vector.x;
} else if (i == 1) {
    val = my_vector.y;
} else if (i == 2) {
    val = my_vector.z;
} else if (i == 3) {
    val = my_vector.w;
} else {
    val = 0.0f;
}

I don't intend to bog down into loop unrolling to attempt to turn that back into a simple swizzle operation, because there will certainly be times where one can't obviously unroll the loop, and I don't want to have people trying to solve compiler error messages by making their code arbitrarily less complex.

I haven't decided if the luxury of this working slowly is worth it...if people are bothered that there's two ways to declare variables, surely they'll also be bothered by both vec[0] and vec.x working, and the unexpectedly bad fallback to that psuedocode when you don't have a constant index is probably not worth it.

icculus commented 1 year ago

why would I use SDLSL instead of GLSL or one of the other shader languages?

My personal hopes are that it's enjoyable to use, can be jumped into with an absolute minimum of hassle, and it Just Works everywhere you want your games to be. If other languages offer that, too, that's fantastic.

Akaricchi commented 1 year ago

if people are bothered that there's two ways to declare variables, surely they'll also be bothered by both vec[0] and vec.x working, and the unexpectedly bad fallback to that psuedocode when you don't have a constant index is probably not worth it.

IMO this is not as bad as the two ways to declare a variable, because vec[i] and vec.x suggest different purposes: the former allows dynamic access and the later allows swizzling (and is arguably easier to read and write).

Dynamic access being silently transformed into horrible if-ladders is a nasty surprise, however. If the vec[i] syntax stays in the language, I would like to have a compiler option to make any non-constant access an error and/or a warning (regardless of whether the backend supports it or not). That'll be useful for people who wish to target hardware that does not support actual dynamic vector access.

rygo6 commented 9 months ago

I'd prefer a language completely different from GLSL over a language that is GLSL-like with a handful of differences. It's easier for my brain to compartmentalize it.

For instance, jumping between C11 and Python I can do very easily on the fly. Jumping between C# and Swift, also very easy. They inhabit separate parts on my brain.

However, jumping between GLSL and HLSL? Bit more cumbersome. I keep wanting to write something like GLSL in HLSL and vice versa. Jumping between JavaScript and Typescript, also cumbersome. My brain doesn't fully switch modes.

Makes me think of back when unity had javascript-like "UnityScript" and python-like "Boo" which were .NET CLR langs they made to look like javascript and python but worked different under the hood in many ways. Absolutely hated it. Programming in a language that looks like a certain language you have much muscle memory in, but behaves differently, is very annoying. I don't think I was alone in this, so few people used UnityScript and Boo they eventually removed both. Community in mass glommed onto C# even though it was technically more complex than either one and had "fluff" syntax for unity that you didn't really need. Knowing a more complex language with some things you don't need depending on context is less mental overhead than knowing two different languages that look the same but behave differently.

So, I'd expect if it is 95% like GLSL with a few differences, those few differences are going to be really obnoxious to me. Especially under-the-hood behavior changes where I have to go read docs to figure out what's happening. That is the type of thing that I can imagine for years, over and over, I will just keep forgetting on the periodic times I might jump out of GLSL and into this other lang. I'm inclined to say, if you're going to take GLSL style, take it all, even what you might not like. If it looks like GLSL it should behave like GLSL.

Or make it behave like C. Since SDL is so C-friendly making it so C can compile to shaders would probably fit quite well.

Changing the way buffers and images are declared is fine, probably preferable. Or changing/adding some attributes, that's probably fine. But the syntax and behavior of the general ops that do the logic? Something "GLSL-Like with little differences" will be far more annoying to me than just GLSL with things I have to omit or some methods I have to change. I'd rather copy over a chunk of GLSL and get a list of methods it doesn't support when trying to compile rather than rewriting it with a bunch of little nuances here and there.

slouken commented 9 months ago

What about adopting ReShade FX? One advantage there is that there's a wealth of examples and shaders that you can potentially just drop into your project.

rygo6 commented 9 months ago

Also want to point out this to be thorough: https://github.com/heroseh/hcc Although I don't seriously consider anyone would get on board with making C11 the shader lang at this point :P But it's sitting right there, working, and is being actively worked on by its author. It would fit in with the overtly C-Style aesthetic of everything else in SDL well. Also the author heroseh might find SDL3 adopting this pretty cool and be into helping make changes.

hw-claudio commented 9 months ago

something like https://github.com/heroseh/hcc seems perfect for me doing my indy stuff, as my engine and games are in C anyway, using C also for the shader lang would be the simplest thing. But I'll use what is there as long as it fairly simple to get going, and can be made to work on all relevant platforms (pc, android etc).