Defining the layout

Defining the layout of a draw pipeline is fairly similar to defining the inputs and outputs of our nodes:

struct layout
{
  // Indicate that this is a layout for a graphics pipeline
  enum { graphics };

  struct vertex_input 
  {
    struct my_first_attribute { 
      // ...
    } my_first_attribute;

    struct my_second_attribute { 
      // ...
    } my_second_attribute;

    // etc...
  };

  struct vertex_output 
  {
    // ...
  };

  struct fragment_input 
  {
    // ...
  };
  struct fragment_output 
  {
    // ...
  };
  
  struct bindings 
  {
    struct my_ubo 
    {
      // ..
    } my_ubo;

    struct my_sampler 
    {
      // ..
    } my_sampler;
  };
};

Defining attributes

An attribute is defined by the matching C++ data type.

For instance:

layout(location = 1) in vec3 v_pos;

is defined through the following C++ code:

struct {
  // Name of the variable in the shader
  static constexpr auto name() { return "v_pos"; }

  // Location
  static constexpr int location() { return 1; }
 
  // Optional standardized semantic usage for compatibility with usual engines
  enum { position };

  // Corresponding data type
  float data[3];
} my_first_attribute;

An helper macro is provided to reduce typing:

// location, name, type, meaning
gpp_attribute(1, v_pos, float[3], position) my_first_attribute;

Defining samplers

Samplers are locations to which textures are bound during the execution of a pipeline. They are defined in the bindings section of the layout struct.

For instance:

layout(binding = 2) uniform sampler2D my_tex;

is defined through:

struct bindings {
  struct { 
    // Name of the variable in the shader
    static constexpr auto name() { return "my_tex"; }
  
    // Location
    static constexpr int binding() { return 2; }
   
    // Type flag
    enum { sampler2D };  
  } my_sampler;
};

or the helper version:

struct bindings {
  gpp::sampler<"my_tex", 2>  my_sampler;
};

Defining uniform buffers

layout(std140, binding = 2) uniform my_params {
  vec2 coords;
  float foo;
};

is defined as follows:

struct bindings {
  struct custom_ubo {
    static constexpr auto name() { return "my_params"; }
    static constexpr int binding() { return 2; }
    enum { std140, ubo };
  
    struct {
      static constexpr auto name() { return "coords"; }
      float value[2];
    } coords;
  
    struct
    {
      static constexpr auto name() { return "foo"; }
      float value;
    } foo;
  } ubo;
};

And can be refactored a bit to:

struct bindings {
  struct {
    halp_meta(name, "my_params");
    halp_meta(binding, 2);
    halp_flags(std140, ubo);
  
    gpp::uniform<"coords", float[2]> coords;
    gpp::uniform<"foo", float> foo;
  } ubo;
};

Note that this is only used as a way to enable us to synthesize the UBO layout in the shader. In particular, depending on the types used, the GLSL variable packing rules are sometimes different than the C++ ones, thus we cannot just send this struct as-is to GPU memory. Maybe when metaclasses happen one will be able to write something akin to:

std140_struct { ... } ubo;

Note that not everything GLSL feature is supported yet, but is on the roadmap: arrays, ssbos, sampler2DShadow, etc... Contributions welcome !