Auto-derive uniform interfaces in luminance-0.25.5

on 2018-03-01 02:18:00 UTC

luminance, uniform, interface, derive, macro

luminance was released in version 0.25.5 two weeks ago and I realized I haven’t blogged about it while it’s an important release!

Actually, it’s from 0.25.2, but I’ll just talk about the latest patched.

That new release received several patches:

I’ll introce that uniform_interface! macro in this blog entry.

Auto derive free almost free

That macro was written to provide a smoother and better experience when writing types that will act as uniform interfaces in shader. For the record, when you create a Program<_, _, _> with luminance, the third type variable is called the uniform interface. You’ll be handed back a value of that type when your shader is being used. That will give you the possibility to send data to the sader prior to making any rendering.

The main idea is to have a type like this:

struct Common {
  resolution: Uniform<[f32; 2]>,
  time: Uniform<f32>,
  jitter: Uniform<f32>

If you build a program which type is Program<_, _, Common>, whenever you ask a pipeline to use that program, you’ll be handed a Common object so that you can access back the uniforms.

In order for that to happen, though, there’s a constraint: your type must implement a trait. That trait is UniformInterface. There’s no magic behind that – though, it can be a bit harsh to implement that trait. If you’re using luminance or plan to, I strongly advise you to try an impl that trait by hand first.

Anyway, after the fifth or sixth uniform interface you’ll have written, you’ll start to get bored of writing the same code every now and then. That’s the exact same feeling as for serializing and deserializing with serde: it’s interesting to do it by hand the first time, but it gets really, really annoying after a while.

So it’d be great to be able to automatically derive UniformInterface for your own types. There’re two solutions here:

  1. Use procedural macros so that we can write something like #[derive(UniformInterface)].
  2. Use standard macro_rules!.
  1. is great because it seems seamless when you use it – you already use #[derive(Clone, Debug, Eq, PartialEq, Etc)]. However, even though it’s doable to learn syn in order to parse the Rust token stream, it’ll require some time and patience while I wanted something quick to mockup the idea. For that, (2) is perfect, because regular macros are easy and if correctly written, shouldn’t add too much weirdness over the type.

The idea is to move to (1) if I find (2) to be a success and really useful.

The macro

So… we’re talking about the uniform_interface! macro. This macro has already a pretty decent documentation, so feel free to visit it, but I’ll explain more here.

A macro is generally used to introduce an EDSL. For instance, nom’s do_parse macro uses a funny EDSL (you use operators like >> and return result with (…), which is not normal Rust code).

The uniform_interface! macro uses an EDSL that you know very well: it’s plain Rust code! Hot news: you’ll have nothing more to learn!

Note: it’s not really any Rust code, since you can only define structs. Then, see that as a subset of Rust.

The syntax is very simple. Let’s take back our sample from above with the Common interface but now let’s the macro do all the magic for us!

extern crate luminance;

uniform_interface! {
  struct Common {
    resolution: [f32; 2],
    time: f32,
    jitter: f32

You’ll notice two interesting properties:

The second point is the most interesting one, since it’s akin to writing #[derive(UniformInterface)]. As you can see, the syntax overhead is not really a problem.

The cool part of that macro is that it also supports Rust annotations on its fields. You have two possible annotations available:

The as(…) annotation is very simple and straight-forward to get, since it lets you change the binding name of the field. The unbound annotation is a bit trickier and you need to understand how UniformInterface expects the value to be constructed.

By default, when you try to map a field to something on the GPU side (shader program), you use the UniformBuilder::ask function. If you have a closer look at function, you can see that it returns Result<Uniform<T>, UniformWarning>. In order to get the Uniform<T> out, you’re left with the choice of how you should handle errors. And that will depend on how you want your value to be built. For instance, some fields might be completely mandatory for your shader to work and others optional. unbound tags a field as optional.

You can mix both annotations if you want to remap an optional field!

extern crate luminance;

uniform_interface! {
  // we want to export that, so we also make it pub
  pub struct Common {
    resolution: [f32; 2], // this is really required
    time: f32, // will be referenced with "t" in the shader and is mandatory as well
    #[unbound, as("bias")]
    jitter: f32 // will be referenced with "bias" in the shader and is optional

That’s all for today! I hope that feature will be useful for whomever uses luminance – I use it extensively and I’ve been using uniform_interface! for several days now and it’s really appreciated! :D

I plan to add another macro to create buffer types, since they must meet GPU-specific alignment rules, who are boring to maintain without code generation.

Feel free to provide feedback on Reddit or GitHub and have fun!