As a newly graduated MS student, I have been eagerly sending my resume to
potential employers. However, I have barely heard a peep in terms of interest. I
have seen many many rejections, and from what I hear this is normal.
The upside of this job hunt is that I have had ample opportunity to explore my
interests and improve my skills in pursuit of a decent job. The project that has
held my interest is the project I started with a fellow classmate, Ben
(@montymxb on github). The project is called FDSSL
(https://github.com/FDSSL/FDSSL), and it is a programming language for writing
shaders.
The goal of the language is to make it easy for people to get into Shader
programming. We designed the language to abstract away the existing ergonomic
issues with shader programming (e.g. needing to use different code files for
each shader) and to make code reuse a bit easier (Not having to redefine
functions across shaders). One issue that we see occur in shader programming is
sending a variable from the vertex shader to the fragment shader without
actually setting it. This happens more frequently than you would think, so our
language ensures that no variables are uninitialized between shaders. We cannot
ensure that the user passes variables INTO the pipeline as that exists within
the OpenGL/Vulkan environment.
The language as it stands now is implemented in Haskell, which is a fun and
interesting language to implement a translator in, but lacks the ability to grow
in the direction we want due to lack of community support. We aim to create a
web frontend where the user can write a shader in our language to manipulate an
object of their choice, and support for GUIs and web is not strong within
Haskell. This among a few other practical reasons is why we are moving to Rust.
If you want to see the full detail on our reasoning, it’s located in the issues
section of the repo or you can go to it directly here:
https://github.com/FDSSL/FDSSL/issues/14
Originally we had intended for the language to be purely functional, but quickly
ran into the notion that attempting to do the sort of iterative design that is
common in shader development is less ergonomic when writing in a functional
language. We also kept moving towards an imperative design when designing the
language, perhaps because imperative is embedded within our psyche from our
first languages.
vert v2 : (Vec3 aVertPos) -> (Vec3 vXYZ) = {
mut Float x = 0.1 mut Float y = 0.5
mut Vec4 pos = vec4 aVertPos[x] aVertPos[y] aVertPos[z] 1.0
set gl_Position (uProjectionMatrix * uModelViewMatrix * pos)
out vXYZ aVertPos
}
Above is a snippet from the FDSSL language. You can see we keep standard ideas
of indexing and assignment, but we also introduce ideas from other languages.
Notice first that we use the equals sign as an initialization, rather than
assignment which you can see at the end of the shader. We also introduce the
‘mut’ keyword which allows the variable to be modified. This comes from the Rust
language and we felt that this adds more guard rails to prevent the user from
accidentally modifying a value that would inadvertantly alter the shader. After
all, the only feedback your get is an image, which might not tell you very much.
We originally wanted to be able to compose shaders much the same way that
functions can compose, but we were unable to achieve this in the time alloted
for our class. We imagine that with this you can create simple shaders and use
them as building blocks to create interesting effects. We intend to implement
this in the future after our migration to another language.
The ‘vec4’ is the constructor for the ‘Vec4’ type, and each element of a vector
is the same type. This coresponds to the GLSL notion of ‘vec4’ which has the
same requirement. The syntax for a function is similar to that of a fragment
shader. The only difference is that fragment shaders have a special keyword that
denotes the type of shader they are. Below you can see an example of a function
within our language.
fact : (Int x) -> Int = {
if x <= 1 then {
1
}
else {
mut Int accum = 1
for x do {
// set is 'update'
set accum (accum * x)
}
accum
}
}
An if statement is an expression and evaluates to a specific value, and each
block of the branch treats the last expression as a return value for that block.
We also intend to have function composition, but might stop short at partial
function evaluation. The underlying GLSL does not have this feature, so it would
not be a simple task to implement.
Another difference you may have noticed between functions and shaders is that
functions only return types, whereas shaders return named variables. This mimics
passing `varying` variables in GLSL, but couples it with the shader block. This
way the user can define multiple shaders of the same type without having the
`varying` values pile up or clash.
Finally, to make a program you simply use the mkProg constructor and pass the
vertex and fragment shaders to it. In this example, this will create the files
`e1.vert` and `e1.frag` to be used in the program.
e1 : Prog = mkProg v2 f2
There are a lot of features that are not implemented for this language, and a
long way to go to make something useable as we intended. The move to Rust will
take some time just to reach feature parity with the Haskell implementation. We
do intend to implement function and shader composition, and we are considering
moving the base type of variables to be array types (the root of the type tree
being array). Within a GPU context it seems natural to work in a vectorized
context, but for a simple accumulation variable it might make the user
experience a bit difficult.
This lanugage is going to change with time, so this blog post is merely a
snapshot of development. If you are interested in following our project, feel
free to follow the github links above and send us some feedback! Who knows, by
the time you read this the name might have changed!