Structs in RSL

Structs in RSL

June, 2009

Introduction

The RenderMan shading language now supports struct types. A C-style syntax is used for struct definitions, except that a default value is specified for each struct member:

struct Float2 {
    float x=0, y=0;
}

The arrow operator (e.g. a->x) is used to access struct members. For example, here is a function that operates on two structs:

float AddX(Float2 a; Float2 b) {
    return a->x + b->x;
}

Structs can contain uniform and varying data of any type, including arrays, nested structs, etc. Struct members are operated upon by reference, so adopting structs has little performance impact.

The remainder of this document describes the syntax and semantics of RSL structs in more detail. The shader plugin API for structs is also discussed. Several limitations in the current release are then described, followed by a discussion of possible future improvements.

Struct definitions

A struct definition specifies the struct type name and a list of member declarations inside curly braces (a trailing semicolon is optional). Struct members are uniform by default. For example:

struct Plastic {
    float Ks=.5, Kd=.5, Ka=1, roughness=.1;
color specularcolor=(1,1,1);
}

Each member declaration must specify a constant default value (except for a nested struct, whose own struct definition specifies its default value). Simple constant expressions like MAX_VALUES+1 can be used as default values.

A struct definition can be local to a class or function; the usual scoping rules apply. A struct definition can appear anywhere a function definition is permitted. Duplicate struct definitions are ignored, provided they are identical.

Structs can contain values of any type, including arrays and nested structs.

Detail

Structs can contain a mix of uniform and varying data, but structs themselves have no detail. It is not possible to declare a varying struct variable. Rather than defining a varying struct containing uniform members, such as:

struct Float2 { float x=0, y=0; }   // uniform members
varying Float2 coord;       // varying struct is ILLEGAL

the struct members should instead be varying:

struct Float2 { varying float x=0, y=0; }   // varying members
Float2 coord;               // the struct itself has no detail

Initialization

The members of a struct are always initialized. A struct variable definition can use the default member values, or it can override them using a constructor call:

Plastic basicPlastic;
Plastic shinyPlastic = Plastic("Ks", .8, "roughness", 0);

A constructor call takes any number of name/value pairs (in any order), each of which specifies the name of a member variable and its value. If a constructor call does not specify a value for a member, the default value specified in the struct definition is used.

Initialization of struct arrays is similar. If no array initializer is specified, the structs are initialized using the default values specified in the struct definition. Or the array initializer can use a constructor to override the default member values. Note that an entire array can be initialized using a single element, which is copied to fill the remaining elements. For example:

Float2 coords[3];   // initialized using default values
Float2 coords2[3] = { Float2("x", 1, "y", 1) };  // first element copied to fill array

The member names in a constructor call must be string constants, which allows the arguments to be typechecked at compile time. Constructor calls are compiled into code that employs integer offsets, not strings, at runtime.

Note that a struct definition implicitly defines a constructor. The type name cannot be used as a function name. Nothing prevents the type name from being used as a variable name, however. We recommend the use of a naming convention that clearly distinguishes between types, functions, and variables to avoid confusion.

Selection

A struct member is selected using the arrow operator. As in C, member selection has higher precedence than any other infix operator. For example:

float y = foo->y + bar->y;

Unfortunately the dot operator cannot be used with structs in RSL because it is reserved for dot product. (It cannot be overloaded because dot product has lower precedence than member selection.)

A struct member can be used as the target of an assignment or as an output parameter in a function call. For example:

brdf->Kd = 0;
surface("Kd", brdf->Kd);

Call by reference

Structs are passed by reference, which allows functions like the following to operate efficiently:

float getX(Foo foo) {
    return foo->x;
}

Output struct parameters are also passed by reference, allowing results to be computed in place, without unnecessary copying:

void getBrdf(output Brdf brdf) {
    surface("Ka", brdf->Ka);
    surface("Kd", brdf->Kd);
surface("Ks", brdf->Ks);
}

Returning a struct from a function often involves a copy unless the return statement directly calls the constructor, so passing a struct as as an output parameter is sometimes preferable to returning a struct.

Other operations on structs

As in C, assigning one struct to another copies the entire struct. Structs can also be compared for equality using the "==" operator.

Separate compilation

A given struct might be used by multiple shaders. For example, a struct can be passed as a method parameter, or a struct stored in a public class member can be accessed by a another shader. In such cases it's important that all the shaders agree on the struct definition.

Runtime type checking is based on the layout of a struct, not its name. A runtime type error is reported if shaders share struct values but were compiled with different struct definitions. We recommend keeping struct definitions (and related code) in header files, and using make (or a similar system) to ensure that shaders are recompiled whenever the header files they depend upon are modified.

RslStruct plugin API

Structs can be passed to shader plugins as input or output parameters. Like other types of parameters, a plugin function receives a struct as an RslArg object. For example, here is a typical plugin function declaration:

int myfunction(RslContext* ctx, int argc, const RslArg** argv) {
   ...
}

An RslStruct can be constructed from an RslArg to gain access to the struct members. For example:

RslStruct structure(argv[0]);

An RslStruct can be used like an array of RslArg, using an index operator to obtain the RslArg for each struct member. Each of those can in turn be used to construct an iterator to access the data. For example, a struct containing a float and a color can be used as follows:

RslStruct structure(argv[0]);
const RslArg* floatArg = structure[0];
const RslArg* colorArg = structure[1];
RslFloatIter f(floatArg);
RslカラーIter c(colorArg);

or more concisely:

RslStruct structure(argv[0]);
RslFloatIter f(structure[0]);
RslカラーIter c(structure[1]);

Struct arrays in plugins

An RslStructArray represents an array of structs. It provides an indexing operator that returns an RslStruct. For example, here is some code that operates on an array of structs that contain a varying float:

RslStructArray array(argv[1]);
int arrayLength = array.GetLength();
for (int i = 0; i < arrayLength; ++i) {
    RslStruct structure(array[i]);
    const RslArg* floatMember = structure[0];
    RslFloatIter f(floatMember);
    int n = floatMember->NumValues();
    for (int j = 0; j < n; ++j) {
        ...
    }
}

Struct types in plugin function prototypes

Struct type names can be used in plugin function prototypes. Those prototypes are listed in a plugin's RslFunctionTable. For example:

static RslFunction myfunctions[] = {
    { "Float2 getCoords()", getCoords },
    NULL
};
RSLEXPORT RslFunctionTable RslPublicFunctions(myfunctions);

The shader compiler loads these function prototypes when it encounters a plugin declaration in a shader. Any struct types that are mentioned in the prototypes must be defined before the plugin declaration:

struct Float2 { varying float x=0, y=0; };

plugin "myplugin.so";   // must follow struct definition

Encapsulating structs in plugins

If several plugin functions operate on a particular type of struct, it might be worth encapsulating the representation of the struct, making it easier to update the plugin when the struct definition changes. This can be accomplished by defining a C++ struct (or class) whose constructor obtains an iterator for each varying struct member. For example:

// C++ struct corresponding to an RSL struct with varying members.
struct Float2 {
    RslFloatIter x, y;

    // Construct C++ struct from an RslStruct.
    Float2(const RslStruct& structure) :
        x(structure[0]),
        y(structure[1])
    {
    }
};

int sum(RslContext* ctx, int argc, const RslArg** argv) {
    RslFloatIter result(argv[0]);
    RslStruct structure(argv[1]);
    Float2 f(structure);
int n = argv[0]->NumValues();
for (int i = 0; i < n; ++i) {
    *result = *f.x + *f.y;
    ++result;
    ++f.x;
    ++f.y;
    }
}

RslStruct type introspection

Unless it is overloaded, a plugin function usually knows the types of its arguments. The shader compiler's typechecker ensures that its arguments always match its function prototype.

Nevertheless, it is sometimes useful to introspect a struct type. This is especially helpful for detecting version skew, which might arise if a member is added to a struct definition without making corresponding modifications to plugins that depend upon it.

An RslStruct object provides a GetName method that returns the struct type name. The GetNumMembers method returns the number of members. Information about the struct members is obtained from their RslArg objects using the usual type and detail queries (e.g. IsFloat, IsStruct, IsArray, GetArrayLength, IsVarying). In addition, RslArg now provides a GetName method that returns the name of a struct member.

Here is an example that demonstrates how to bulletproof a plugin function against incompatible changes to a struct definition.

assert(argv[0]->IsStruct());
RslStruct structure(argv[0]);
assert(!strcmp(structure.GetName(), "Float2"));
assert(structure.GetNumMembers() == 2);

const RslArg* x(structure[0]);
assert(x->IsFloat() && x->IsVarying());
assert(!strcmp(x->GetName(), "x"));
RslFloatIter xval(x);

... // and similarly for remaining struct members

Much of this code is overkill, however. Unless the plugin function is overloaded, the compiler guarantees that its argument matches the function prototype, so checking that the argument is a struct and verifying the struct name is unnecessary. Checking the member names is probably also unnecessary, since checking the number of members and their types is probably sufficient to detect version skew.

Limitations

  • In the current release, structs can be stored in local variables and class member variables and they can be used as function and method parameters. However, they cannot be used as shader parameters.

  • The default value of a struct is determined by the default values specified in its definition. It is not possible to override these defaults when a nested struct is used as a member of a class or struct definition. For example:

    struct Float2 { varying float x=0, y=0; }
    struct Bounds2 {
       Float2 upperLeft, lowerRight;  // Cannot specify defaults here.
    }
    
    class MyShader(...) {
       Float2 coords;         // Cannot specify initial value here
    
       // Instead, initialize nested structs in construct or begin method.
       public void construct() {
          coords = Float2("x", 1, "y", 1);
       }
    }
    

    This limitation is due to the fact that the default value of a struct or class member must be a constant, but the SLO file format does not yet support struct constants. We hope to address this in a future release.

  • Structs cannot be used as optional method output parameters. (Optional method parameters need constant default values, but struct constants are not yet supported.)

  • The shader compiler currently does a poor job of common subexpression elimination, so repeatedly referencing struct members incurs some overhead:

    if (params->Kd > 0)
        Ci += params->Kd * diffuse(N);  // repeated reference to params->Kd
    

    This is usually not worth manually optimizing, particularly if the data is varying, since the overhead of copying the data can exceed the cost of repeating the member reference operation. We hope to incorporate improved compiler optimizations in the future.

Struct member functions and inheritance

RPS 15 introduces struct member functions and inheritance for structs. These two features provide useful mechanisms to aid code factoring and code maintainance. They also provide a namespace encapsulation for functions that are relevant to a struct.

Unlike shader methods, struct member functions are statically typechecked and can be inlined in shader code.

Struct member functions

Struct member functions may be declared inside the struct like so:

struct MyStruct{
   uniform float aUniformVal = 0;
   varying float aVaringVal = 0;

   float computeScaledValue() {
       return aUniformVal*aVaryingVal;
   }

   void setSomeValue(float f,float range) {
       aVaryingVal = min(max(0,f),range);
   }
}

They have access to the member variables of the struct. Inside a struct member function, the 'this' keyword has a type matching the struct, i.e. this is output MyStruct in the example above, which allows you to pass the current struct to functions requiring it as an argument.

The functions above may be called using the arrow (->) operator:

MyStruct ms;

ms->setSomeValue(sin(xcomp(P)),1);
float scaledValue = ms->computeScaledValue();

Struct inheritance

Additionally, a struct may now inherit from other structs. This allows both methods and member variables to be composed together:

struct BaseStruct{
   uniform float baseUniform = 0;
   uniform float commonName = 0;

   float foo() {
       //...
   }
}

struct DerivedStruct : BaseStruct {
   uniform float derivedUniform = 0;
   uniform float commonName = 0;

   float bar() {
       //...
   }
   float foo() {
       BaseStruct::foo();
       //...
       printf("%f %f\n",baseUniform,derivedUniform);
       printf("%f\n",commonName);              // DerivedStruct's commonName
       printf("%f\n",BaseStruct::commonName);  // BaseStructs's commonName
   }
}

The struct DerivedStruct inherits both members and member functions from BaseStruct. Note that inside the DerivedStruct's functions, the base's members may be accessed without additional decoration. In the case where this is ambiguous, a full path to the member may be specified with scope-resolution in the name (BaseStruct::commonName). Similarly, the parent's function may be called, even if overridden using scope resolution (BaseStruct::foo() will call the parent's foo() function).

Outside of member functions, access to the struct members works similarly:

DerivedStruct ds;

ds->foo();                 // Calls DerivedStruct::foo()
ds->BaseStruct::foo();     // Calls BaseStruct::foo()

uniform float f = ds->baseUniform;
uniform float f = ds->BaseStruct::commonName;  // avoid DerivedStruct's version

Global functions may be disambiguated using ::globalname(). Supposing there was a global function foo(), it could be called from within a struct method thusly:

float foo() {
   //...
}
struct DerivedStruct{
   //...
   float foo() {
       //...
   }
   float bar() {
       ::foo();    // call the global foo()
   }
}

A struct's parent may be accessed in the following way:

DerivedStruct ds;
BaseStruct base = ds->BaseStruct;

Finally a struct may inherit from more than one parent struct by listing the base structs in a comma separated list; Finally, a struct may inherit from more than one parent struct by listing the base structs in a comma separated list. Construction of a derived struct can take one of three forms

struct StructWithMembers{
   varying float m1=0;
   varying float m2=0;
   varying float m3=0;
}

struct MoreDerivedStruct : StructWithMembers {
   varying float expfac=0;
}

// simple
MoreDerivedStruct mds1 = MoreDerivedStruct("m1",1,
                                           "m2",2,
                                           "m3",3,
                                           "expfac",1.5);


// disambiguate each member
MoreDerivedStruct mds2 = MoreDerivedStruct("StructWithMembers::m1",1,
                                           "StructWithMembers::m2",2,
                                           "StructWithMembers::m3",3,
                                           "expfac",1.5);

// explicitly initialize the parent
MoreDerivedstruct mds3 = MoreDerivedStruct("StructWithMembers",
                                           StructWithMembers(
                                                       "m1",1,
                                                       "m2",2,
                                                       "m3",3),
                                           "expfac",1.5);

In any of these cases, individual members may be left with their default (struct definition specified) value.

Finally a struct may inherit from more than one parent struct by listing the base structs in a comma separated list;

It should be noted that struct member functions are not virtual in the C++ sense. Struct member functions are as fast as normal functions in RSL and are statically typechecked (unlike shader methods). Structs with inheritance have no performance penalty, but provide a useful code factoring and code maintainance tool.

Future directions

Unlike in C++, there are significant differences between structs and classes in RSL. Class/shader methods are virtual, which provides a certain amount of flexibility that is not possible with structs. On the other hand, structs are lightweight and statically typed, whereas objects are heavyweight and require runtime type checking when fetching member variables and calling methods. As the language evolves it is likely that some of these distinctions will persist. Improving the performance and generality of classes and objects (perhaps incorporating inheritance and static typechecking) might provide an avenue for addressing some of the limitations of structs.

The ability to declare a struct member function as virtual, along with the declaration of an interface or protocol in a base class would permit statically typechecked functions whose implementation could be overridden. This would be most useful when combined with heterogenous containers of struct. As such, an array of the base class could be filled with derived classes (for example an array of BRDFLobe could be filled with concrete brdf lobe implementations) and 'protocols' that the base class guarantees might then be invoked on each item. This can be more efficient than method calls on shaders because we are able to statically typecheck the call to the functions.