Jump to content
  • Advertisement
Sign in to follow this  
Netzapper

OpenGL Parasol: a pragmatic, functional shader language. Invitation for comments and assistance.

This topic is 1032 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Advertisement

Always nice to see new shading languages. I haven't seen too many functional shader languages smile.png
Any plans to support HLSL code-generation for compatibility with the non-GL platforms?
 
I like this ability to write code that crosses multiple shader stages:

  f[outColor] = 
    texture(
      u[tex: sampler2D], 
      v[v_texCoord] = 
        a[v_texCoord1: vec2]
    )

I'm tired of writing shaders in GLSL. It requires a lot of repetition, a lot of boilerplate. And there's no reasonable way to build modular code: uber shaders, conditional compilation, shader graphs, glsw-style string glue, shader templates... all suck for various reasons; I've tried them all. I've still written that same damn vertex multiply a thousand times.

What about #includes and function libraries, a'la procedural C code? That's the approach that I've most commonly seen (and uber shaders / conditional compilation is added orthogonally).

Share this post


Link to post
Share on other sites

1. HLSL - there's no reason Parasol can't include an HLSL codegen, except that I don't know DirectX or HLSL. This would be a place where I definitely need some help.

 

2. Cross-stage expressions - I believe this is a killer feature in Parasol. The ultimate in don't-repeat-yourself.

 

2 #include - I guess I missed that on my list. And if you've got that preprocessor, it's not a bad way to go. The problem it doesn't solve for me is generic programming. For instance, switching from a 2D texture to a 2D array texture is only a change of two types: `sampler2D` -> `sampler2DArray`, and `vec2` -> `vec3`. But, with a GLSL function library, now I need two versions of the function, despite the texture expressions being identical.

 

Frankly, it's *always* felt weird to me to program the GPU in C when so many of the expected constructs are poorly supported. `if`, `switch`, and early return are ways of improving performance in C; but they either decimate performance or have no effect on GPU. Dynamic loops cost no more than static loops in C; but they're a totally different beast on GPU. Basically, while C is an *excellent* model of low-level CPU computation, it does a bad job of mapping to the GPU. I find lots of shader newbies want to write tree code or other recursive structures; they want pointers; they want function pointers and indirection. The C-like language encourages them to program as if they're still on the CPU.

Share this post


Link to post
Share on other sites

I am confused. Simply, I have extreme difficulty understand even the single statements, I just cannot put the things together.

vertex_pos {
  v[v_pos] = u[viewMatrix: mat4] *
             u[modelMatrix: mat4] *
             vec4(a[v_inPosition: vec3], 1)
           
  v[gl_Position] = u[projMatrix: mat4] * v_pos
}
vertex_surface_normals {
  v[v_normal] = u[u_normalMat: mat3] * a[v_inNormal: vec3]
}


struct point_light {
  position: vec3
  color: vec4
}

Ok so, what's the point? How is this better than imperative?

Why did you choose to go with that peculiar syntax for declarations?

u[point_lights: point_light@16]    ; declares an array of size 16

This in particular is quite odd to me. Ok, I see in this example we're doing a 16 lights. Interesting syntax for the map/reduce shorthand.

What are the advantages of setting whole pipelines in the same source? I assume you can do it from other sources yet in my language I did separate pipeline definition from code.

Edited by Krohm

Share this post


Link to post
Share on other sites


Simply, I have extreme difficulty understand even the single statements, I just cannot put the things together.

 

That sums up functional languages from my perspective.

Share this post


Link to post
Share on other sites

One of my primary goals is reducing repetition. The worst offender, at least in GLSL, is the constant redefinition of `in` and `out` variables. They are *supposed* to form a linking surface, allowing you to interchange different fragment shaders on the same vertex shader. And it works reasonably well for that if you're mostly just putting different materials on realistic meshes. But it breaks down really quickly when you're working on special effects, 2D compositing, or pretty much anything but skinning triangle meshes. You wind up writing custom vertex shaders just to pass through data.

 

So in Parasol, one goal is permit interface variables to be declared inline to the expressions that use them. Perhaps that's confusing you? The `v[`, `u[`, etc. introduce what I'm calling a "stage scope". The names are short to reduce RSI and clutter, but they could be called `vertex[`, `uniform[` and `fragment[`, etc. I use the `varName: type index` format so that you can introduce a type-inferred variable, a type-defined variable, or an indexed variable (for attribute input and fragment output) optionally. Any other syntax requires that you specify something you may not know yet (specifically, type or index).

vertex_pos {
  v[v_pos] =    ; declare `out vec4 v_pos` in the vertex stage, and set its value
    u[viewMatrix: mat4] *    ; declare `uniform mat4 viewMatrix`
    u[modelMatrix: mat4] *    ; declare `uniform mat4 modelMatrix`
    vec4(a[v_inPosition: vec3], 1)    ; declare `in vec3 v_inPosition`, but leave its index undefined
}

vertex_normal {
  v[v_normal] = 
    u[normalMatrix: mat3] *
    a[v_inNormal: vec3]
}

The key component here is that you can now include `vertex_pos` and `vertex_normal` into another pipeline and your fragment shader in that pipeline could reference `v_pos` and `v_normal` without needing to respec them. Type inference travels across stages, so if you change a type anywhere, the whole expression will update or give an error (for instance, switching from 3d to 4d normals).

 

Additionally, your vertex position attribute and vertex normal attribute are now handled modularly, in a way that allows you to pull in just one or the other if the material you're working on only requires one or the other. Parasol actually allows you to write all of these "abstract" pipelines without spec'ing the attribute indices or fragment output indices (as above)... you can then provide that final index spec in the final "concrete" pipeline that you synthesize as GLSL or SPIR-V code.

normal_debug {
  include vertex_pos
  a[v_inPosition: 0]    ; spec attribute index
  v[gl_Position] = u[projMat: mat4] * v_pos

  include vertex_normal
  a[v_inNormal: 1]
  f[outColor: vec3 0] = v_normal.xyz
}

Ok so, what's the point? How is this better than imperative?

 

I don't know that it's better. I'm not a language purist. I do argue that Parasol code is briefer and more modular than GLSL code. And most shaders I write are *already* in closed, branch-free form, which a functional language encourages and supports. I want function templates, and they're super easy to implement in a functional language. Plus a functional language permits aggressive static evaluation with trivially-understood semantics.

 

But, the main reason that I ditched the imperative model is so that statement order isn't important. This allows you to reason about how a particular attribute or texture is used in your shader program independently of any other usage in your program. That's what lets you pull in `vertex_pos` without worrying about `vertex_normal` stepping on something you're doing. Re-declared variables are merged (or an error occurs if they're incompatibly spec'd), meaning that you can have multiple "overlapping" pipelines reusing the same stage outputs, attributes, and uniforms.

 

Not that you *can't* achieve that in an imperative language, but you have to be a lot more careful. Just look at the GPU execution model! Look how many C features are missing from GLSL and SPIR-V. No function pointers? No goto? No recursion? The `main()` function executed by any particular shader unit is imperative because that's how we build silicon these days and it's better than the busted-ass VS/FS assembly we had 10 years ago, but the *flow* of the shader as a whole isn't imperative.

 

I would say there's little reason to use Parasol if all of your shaders are in the fragment stage or if you have very few different shaders in your application. If you've got triangle meshes, and everything has the same attributes, then you're probably just writing fragment shaders, who cares what language they're in. But, I write a lot of non-photorealistic special effects, and right now I'm doing a lot of n-channel 2D compositing and instancing... and I'm tired of re-writing the vertex shader again just because I added (or removed) an attribute or changed the number of channels in a blend operation.

 

I'm trying to tame the combinatorial explosion at least at the source level, permitting the compiler to generate the necessary combinations for me. And that's most easily done in a functional language.

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

We are the game development community.

Whether you are an indie, hobbyist, AAA developer, or just trying to learn, GameDev.net is the place for you to learn, share, and connect with the games industry. Learn more About Us or sign up!

Sign me up!