Sunday, April 28, 2013

Design of Mesa 3D Part 7: Shader Assembly Emission

The last stage in glCompileShader is to actually emit assembly commands that can be executed by the shader VM. We've previously created an intermediate representation of nodes that represent the shader; now the task is to serialize this tree into something similar to an object file. I believe that the actual assembly language is the ARB assembly language, which can then be translated by a driver into platform-specific instructions. This architecture is similar to HLSL's "assembly" language. This takes place in emit(), defined in src/mesa/shader/slang/slang_emit.c. You pass a slang_ir_node to the function; the initial node is the root of the IR tree.

This function is a big switch statement switching over the IR opcode. All of the math operators (abs, sin, min, add, less-than, etc.) fall through to the same case, which calls emit_arith(). This function is actually really straightforward; it calls emit() on all of its children, allocates a node to store the result by calling alloc_node_storage(), then calls emit_instruction() on the operation itself.

alloc_node_storage() is also fairly straightforward; it's used to allocate temporaries that don't have the Store parameter in the slang_ir_node struct set. This is a code block from the beginning of the function:


   if (!n->Store) {
      assert(defaultSize > 0);
      n->Store = _slang_new_ir_storage(PROGRAM_TEMPORARY, -1, defaultSize);
   }

Therefore, an invariant is that n->Store should always be set for an IR node after this function is called on it. _slang_new_ir_storage() is a simple constructor that just copies the register file, index, and size into a newly allocated slang_ir_storage struct. I've already copied and pasted the definition of slang_ir_storage_ in this post. One of the interesting things is that the index parameter in slang_ir_storage_ is allowed to be -1, which means that the actual location doesn't matter; just put it anywhere (as long as it's in the correct register file). Because of this, alloc_node_storage() must choose a real index for all the -1 indexes. It does this by calling _slang_alloc_temp(), defined in src/mesa/shader/slang/slang_vartable.c. This function calls alloc_reg() to actually do the allocation, then sets up the slang_ir_store to the appropriate values regarding the newly allocated register. alloc_rec() uses the struct table defined at the top of the same file. This struct represents meta information about which parts of which register files are free. Here's the definition:


typedef enum {
   FREE,
   VAR,
   TEMP
} TempState;

/**
 * Variable/register info for one variable scope.
 */
struct table
{
   int Level;
   int NumVars;
   slang_variable **Vars;  /* array [NumVars] */
   TempState Temps[MAX_PROGRAM_TEMPS * 4];  /* per-component state */
   int ValSize[MAX_PROGRAM_TEMPS * 4];     /**< For debug only */
   struct table *Parent;  /** Parent scope table */
};

The algorithm that alloc_rec() uses is quite straightforward; It simply walks Temps trying to find 4 successive components that are marked as FREE. Once it's found one, it marks them all as TEMP. So that's pretty simple.

Back to emitting code. Emitting a single instruction is handled with the emit_instruction() function. This function takes an opcode and 4 slang_ir_storage nodes: one for the destination and 3 for the inputs. You would think that this function would be trivial; however, because of indirect register inputs + outputs, it isn't. If the output or any of the inputs are indirect, this function has to deal with it. I'll skip over how we deal with this for now, but once we have our input and output registers, the code just looks like this:

   inst = new_instruction(emitInfo, opcode);
   if (!inst)
      return NULL;

   if (dst)
      storage_to_dst_reg(&inst->DstReg, dst);

   for (i = 0; i < 3; i++) {
      if (src[i])
         storage_to_src_reg(&inst->SrcReg[i], src[i]);
   }

new_instruction() is the trivial function: If we're at the end of our output array, grow the buffer, then just get a pointer to the next available instruction in the array, and initialize it. The instruction stream is attached to the gl_program object stored in emitInfo->prog(); this will become important when we call functions. storage_to_dst_reg() and storage_to_src_reg() are also rather simple: They simply fill in the register file and index, as well as a swizzle. Here are the prog_dst_register and prog_src_register structs, defined in src/mesa/shader/prog_instruction.h.

struct prog_src_register
{
   GLuint File:4; /**< One of the PROGRAM_* register file values. */
   GLint Index:(INST_INDEX_BITS+1); /**< Extra bit here for sign bit.
                                     * May be negative for relative addressing.
                                     */
   GLuint Swizzle:12;
   GLuint RelAddr:1;
   /** Take the component-wise absolute value */
   GLuint Abs:1;
   /**
    * Post-Abs negation.
    * This will either be NEGATE_NONE or NEGATE_XYZW, except for the SWZ
    * instruction which allows per-component negation.
    */
   GLuint Negate:4;
};

/**
 * Instruction destination register.
 */
struct prog_dst_register
{
   GLuint File:4;      /**< One of the PROGRAM_* register file values */
   GLuint Index:INST_INDEX_BITS;  /**< Unsigned, never negative */
   GLuint WriteMask:4;
   GLuint RelAddr:1;
   /**
    * \name Conditional destination update control.
    *
    * \since
    * NV_fragment_program, NV_fragment_program_option, NV_vertex_program2,
    * NV_vertex_program2_option.
    */
   /*@{*/
   /**
    * Takes one of the 9 possible condition values (EQ, FL, GT, GE, LE, LT,
    * NE, TR, or UN).  Dest reg is only written to if the matching
    * (swizzled) condition code value passes.  When a conditional update mask
    * is not specified, this will be \c COND_TR.
    */
   GLuint CondMask:4;
   /**
    * Condition code swizzle value.
    */
   GLuint CondSwizzle:12;
   /**
    * Selects the condition code register to use for conditional destination
    * update masking.  In NV_fragmnet_program or NV_vertex_program2 mode, only
    * condition code register 0 is available.  In NV_vertex_program3 mode,
    * condition code registers 0 and 1 are available.
    */
   GLuint CondSrc:1;
   /*@}*/
   GLuint pad:28;
};

As you can see, the instruction is optimized for size by using bitfields.

Alright, let's talk about indirect registers. This indirection is done using the ARL instruction, or Address Register Load. The spec (Section 2.14.5.3) states that it simply performs a load into the address register, which is then used for future loads and stores. This is used for doing array accesses where the index is a variable; that requires loading the value of the variable into the address register, then doing an operation using the address register as an offset into the array. However, we only have one address register (only the x component is actually used). What happens if we want to say something like x[i] + y[j]? The add instruction uses the address register explicitly, but the two operands should have different offsets. This means that we have to first load x[i] into a temporary, then run temp + y[j]. Allocating this temporary register uses the same call that it did above. It then emits a MOV instruction using the address register. A similar codepath occurs for an indirect destination register; however, if the destination is relative, all of the relative sources will be put into temporaries, so we can use an indirect destination here. The RelAddr bit in the prog_dst_register and prog_src_register structs determines if we should use the address register. After we emit the actual instruction that we're trying to perform, we have to then free the temporary registers that we've allocated.

Cool; that's how we do math. Register loads and stores work the same way. IR_SEQ instructions work exactly the way you would expect. A variable declaration tries to call _slang_alloc_var(), which works similarly to _slang_alloc_temp(). The IR_NOT operator is implemented as v = v == 0, which is cool.

All right, what about comparisons? Because performing less-than and greater-than operations doesn't makes sense on structs and vectors, it is handled by emit_arith(). However, equality comparisons work almost exactly the same way, except that we have to be able to compare structs and vectors, etc. Comparing two floats is straightforward; just call emit_instruction(). Comparing two vectors is a little more complicated, because the comparison instruction returns a vector of outputs, for each component. We can solve this by computing the dot product of the output with itself, and looking at the output. This requires allocating a temporary. Now, what about structs? This just allocates an accumulator, and walks through the size of the object, adding the output of the comparisons to the accumulator. Then, we can use the dot product trick again. Note that this won't work with arrays with padding; this is kind of an interesting problem (which doesn't look like is solved in this version of Mesa).

Alright, how about loops? There is an IR_LOOP instruction, which triggers a call to emit_loop(). There is a flag in the slang_emit_info structure which determines if we should emit so-called "high level" instructions. If so, we can simply emit a OPCODE_BGNLOOP instruction, which is pretty cool. Before we do that, we save the number of previously-emitted instructions to use for a label to jump to, should we need to. Then we can just emit the body of the loop (the 0th child of the IR loop), and then possibly emit OPCODE_ENDLOOP. Otherwise, we emit a OPCODE_BRA (branch) instruction, and set the target to the beginning of the loop. Once we've done that, we have to walk through the instructions in the loop, looking for IR_BREAK and IR_CONT nodes, and replacing them with OPCODE_BRA nodes. Now we're done!

Sampling from a texture is simply an instruction, so that doesn't add much complexity.

The last piece I'd like to get into is function calls. Because setting up all the arguments and return value was done when creating the IR (as well as as much inlining as possible), calling functions is actually fairly simple. Because instruction streams are attached to gl_program objects, we save the current gl_program object (originally in emitInfo->prog) and create a new program by calling new_subroutine() which delegates to ctx->Driver.NewProgram(). Then, we can emit a label for the new function, call emit() on the function body, and a return instruction just in case. We also might surround the function with OPCODE_BGNSUB and OPCODE_ENDSUB instructions, if the emitInfo->EmitBeginEndSub is set. Once we've emitted the new function, we set the active program to the original saved value and emit the OPCODE_CAL instruction to that stream.

Cool! Now we've got a stream of instructions that our VM can execute. Before getting into VM execution, the OpenGL pipeline, or linking shaders, I'd like to show the life of an example function, with all its intermediate forms along the way of compilation. I think that'll make the shader compilation steps clearer.

Sunday, April 21, 2013

Design of Mesa 3D Part 6: Intermediate Representation Translation of Shaders

Previously, I had covered lexing and parsing of shaders. At this point, we have a large data structure describing the structure of a shader. Now, we want to convert this data structure (specifically, the main function) into a stream of commands that our VM can execute during shader execution. We do that by translating the slang_operation_ tree that we created earlier into a similar but simpler tree, which is called an intermediate representation. Then, we can actually emit instructions from this intermediate representation.

Because a slang_operation_ is already a tree of operations, this translation is fairly straightforward. It happens in _slang_codegen_function(), defined in src/mesa/shader/slang/slang_codegen.c.  Interestingly enough, this function makes sure that it's only called on the "main" function, because most other functions should get inlined. The functions that can't get inlined will get codegen'ed upon an actual call to the function. I'll talk more about inlining later.

The first thing that function does is it calls _slang_simplify(), defined in src/mesa/shader/slang/slang_simplify.c, to do some trivial simplifications. The simplifications are:
  • Replacing references to constant variables with the literal form of those variables
  • Performing addition, subtraction, multiplication, division, negation, logical and, logical or, and logical xor on literal values, and replacing the operation with the result of the computation. This is done bottom-up, so the large constant expressions can be simplified properly
  • Replacing calls to vector constructors with literal arguments to a literal vector
Alright, back to _slang_codegen_function(), which calls _slang_gen_operation().  This function has a giant switch statement, where it switches over all of the operation types. For each one, it calls a relevant _slang_gen_*() function. Interestingly enough, addition, multiplication, etc., as well as operators like the post increment operator, etc., are translated directly into a function call to a function named "+", which is defined in the builtin .gc files. This means that most of the work that we're about to do is just nested function calls. Ultimately, there are some functions (such ass adding two floats) that can't delegate to other functions; these functions are defined with the "__asm" keyword in the .gc files. We also care about assigning things to variables, and sequencing these assignments. "if" statements and loops are also interesting. Because shaders can't really do any IO, there's not much else that shaders can do.

Alright, let's take these one at a time, starting with assembly instructions. The relevant node is SLANG_OPER_ASM, which just calls _slang_gen_asm(). Let's look at an example, taken from src/mesa/shader/slang/library/slang_core.gc:

int __operator + (const int a, const int b)
{
   __asm vec4_add __retVal, a, b;
}

You may notice a couple things about this function. First of all, even for adding floats, the command adds vec4s. This is because Mesa assumes that all registers are vec4s, which is true on many graphics cards. Secondly, the function outputs into a variable called "__retVal", which Mesa uses as a hidden return value. Each assembly statement gets marked with its own "__asm" keyword, and each assembly command takes at most 3 arguments. This can also be verified by looking at the slang_ir_node struct in src/mesa/shader/slang/slang_ir.h:

typedef struct slang_ir_node_
{
   slang_ir_opcode Opcode;
   struct slang_ir_node_ *Children[3];
   slang_ir_storage *Store;  /**< location of result of this operation */
   GLint InstLocation;  /**< Location of instruction emitted for this node */

   /** special fields depending on Opcode: */
   const char *Field;  /**< If Opcode == IR_FIELD */
   GLfloat Value[4];    /**< If Opcode == IR_FLOAT */
   slang_variable *Var;  /**< If Opcode == IR_VAR or IR_VAR_DECL */
   struct slang_ir_node_ *List;  /**< For various linked lists */
   struct slang_ir_node_ *Parent;  /**< Pointer to logical parent (ie. loop) */
   slang_label *Label;  /**< Used for branches */
   const char *Comment; /**< If Opcode == IR_COMMENT */
} slang_ir_node;

slang_ir_opcode is an enum with all the different kinds of nodes. For example, there's IR_ADD, IR_CALL, IR_COPY, IR_IF, IR_LABEL, IR_CROSS, among others. I've described the types of the rest of the fields in my previous parsing post.

Alright, let's get back to _slang_gen_asm(). This function does some sanity checks on the input, and calls slang_find_asm_info(id), which just returns a mapping from the string "vec4_add" used in the source to the actual IR_ADD operator. It then constructs a node using this operator, and calls _slang_gen_operation() for each of the children of the operation, assigning the IR node's children accordingly. The actual IR node creation function is new_node3(), which takes an opcode and three slang_ir_nodes to set as the newly created node's children. There is also a new_node2(), new_node1(), and new_node0() which call new_node3() with NULL as the extra arguments.

There's one more thing that _slang_gen_asm() does. It checks to see if the number of arguments specified in the source is the same as the number of arguments that the assembly command expects (gotten from slang_find_asm_info()). If it isn't, that means that the source doesn't specify the storage for the result of the operation. If the storage isn't specified, a temporary will be allocated later. On the other hand, if storage is specified, we have to set up the Store field of the new IR node. It does this by calling _slang_gen_operation() on the result argument, then taking its Store value and copying it into the new node's Store member.

Alright, let's talk about function calls now. The relevant function is _slang_gen_function_call_name(). The first thing this function does is call _slang_function_locate() with the function name string to try to find the actual slang_function that's being called. _slang_function_locate(), defined in src/mesa/shader/slang/slang_compile_function.c, walks the slang_function_scope, iterating through all the functions in that scope. It matches a function that has the correct name, but also matches argument types by iterating through them and calling slang_type_specifier_compatible()., defined in src/mesa/shader/slang/slang_typeinfo.c. This function has a special case for comparing ints and floats (they are compatible), but then just makes sure that the types are equal. If the types are structs, it calls slang_struct_equal(), which works similarly. If the two types are arrays, it recurses with the inner array type. If no functions are found, _slang_gen_function_call_name() tries to find an appropriate function by looking for a constructor and trying to cast/unroll constructors.

Now, once we've found a function, if the function doesn't have a body, we need to set a flag telling the linker that it needs to link the function body to this call. At this point, we can finally call _slang_gen_function_call()., which tries to inline the function. It calls slang_inline_function_call() which generates a slang_operation representing the function, and then proceeds to try to inline that operation. I'll describe slang_inline_function_call() in a second, but for now we have to know that it generates "SLANG_OPER_RETURN_INLINED" instructions instead of "SLANG_OPER_RETURN" instructions. These have different runtime semantics.

Inlining is tricky. If the only return statement is at the very end of the function, we can just replace the return_inlined statement with a noop and return the operation. However, if execution hits a return in the middle of the function, execution has to bypass the rest of the function. There are two ways to deal with this; we can either use a return flag and wrap the rest of the function in a giant if statement, or we can simply not inline the function. The reason that we try to inline all these functions is that many graphics cards don't have a runtime stack, and so can't properly call and return from functions. If that's the case, we have to use a return flag. The driver can specify at context creation time if we should be using a return flag or not. If we're told not to use a return flag, then we replace the "SLANG_OPER_RETURN_INLINED" nodes to "SLANG_OPER_RETURN" nodes, and change the top-level operation's type to "SLANG_OPER_NON_INLINED_CALL". Otherwise, if we're using a return flag, we call declare_return_flag() to add a new child to the operation, and create a boolean variable called "__notRetFlag".  Then we change the top-level operation's type to "SLANG_OPER_NON_INLINED_CALL" just like we would have before. At the very end, we recursively call slang_gen_operation on the body of the function. This means that we don't translate functions that can't be reached from main().

Alright, now let's take a look at slang_inline_function_call(). The last argument to this function, returnOper, is a pointer to a operation that the return value should fill in. This is so, if we have something like "x = f(a, b)" we can avoid a copy from a temporary into x. If returnOper is NULL, we have to allocate a temporary called __resultTmp, but only if the function returns a value. This is done by creating a new operation with three children: one to declare __resultTmp, one which actually runs the body of the function, and one to specify the output is __resultTmp. That first child has a type of SLANG_OPER_VARIABLE_DECL. The last child has a type of SLANG_OPER_IDENTIFIER.

The next thing we've got to do is deal with function arguments. In particular, values are passed by value, so we have to copy the values into the function's local scope, but only if the parameter isn't const.  We also have to copy output values from their temporaries to their actual storage. We do this by creating an array of substitution information. Each element in the array specifies a variable name to substitute and an operation to use for the substitution, as well as an enum to determine if the argument is an IN or and OUT variable. (It's actually three arrays; the code uses a struct-of-arrays style instead of an array-of-structs style). We can then copy the body of the function into the slang_function->body member. Then we call slang_substitute do run the actual substitution with those three arrays we just built up, which walks the operation tree, making copies of nodes and modifying them to substitute the specified variables for operations.

Now, we generate the copy instructions that are necessary for the input parameters. For each input parameter, we call slang_operation_insert() to insert a dummy operation into the beginning of the stream. We then fill in this operation with SLANG_OPER_VARIABLE_DECL, and create a single child, and call slang_operation_copy() on it, which emits the copy instruction. We also then have to add the variable to the local scope by calling slang_variable_scope_grow() and filling in the new slang_variable. Once we're done with this, we have to add the function's explicit local variables to the local scope, which is done similarly.

Now we deal with the epilogue. We create a label with slang_operation_insert() of type SLANG_OPER_LABEL so that return has somewhere to jump to. Then, similar to the prologue, we go through the COPY_OUT arguments, and insert SLANG_OPER_ASSIGN operations with slang_operation_insert(). The last thing we do is call slang_replace_scope(), defined in src/mesa/shader/slang/slang_compile_operation.c, which walks the operation tree finding operations which target the old scope, and updates them to use the new scope.

Phew! That was a lot to deal with. There's only a little bit more that's relevant: assignments and sequences. Sequences are really straightforward: An input node of type SLANG_OPER_BLOCK_NO_NEW_SCOPE specifies a sequence of instructions. Each of these gets translated, then is passed to new_seq(tree, n). This function creates a binary node of type IR_SEQ, which means that the block gets turned into a linked list of sequenced operations. An input node of type SLANG_OPER_BLOCK_NEW_SCOPE simply creates a new scope, then delegates to SLANG_OPER_BLOCK_NO_NEW_SCOPE.

That leaves assignments, which are not super complicated either. The relevant function is _slang_gen_assignment(). If the destination is a variable, we look up the variable with _slang_variable_locate() and see if its writable. Then, we need to see if our assignment is predicated on the __notRetFlag that I described earlier. If it is, we create a new predication operation for use later. Now, we see if the rvalue of the assignment is a function call; if so, we can use the function return copy optimization I referred to earlier. Otherwise, we check to see if the types are compatible in assignment with a call to _slang_assignment_compatible(), which checks to see if the size of the types match, and if so, checks some special failure cases (assigning from bool to float or int, or assigning between two different structs of different names, etc). Otherwise, it just returns true. Then, _slang_gen_assignment() calls _slang_gen_operation() for the destination of the assignment, and checks to see that the operation has the Store value set, and that it's writable. We then call _slang_gen_operation() for the right side, convert the destination's Store's swizzle to a writemask if possible, then call new_node2(IR_COPY, lhs, rhs). Now, if the predication operation that we created before exists, we create a new_if() operation instead, and use that. Otherwise we just return the copy node.

Phew, that was a whole lot. Alright, now we've got a representation of the program that's fairly close to the assembly that we want to generate in the end. Next will be converting the IR into a stream of instructions that we can actually execute at runtime.

Saturday, April 20, 2013

Design of Mesa 3D Part 5: Parsing Shaders

Last time I described how GLSL source gets lexed and turned into a stream of tokens, isomorphic with the source of the input shader. However, a stream of tokens representing the input isn't really anywhere near what we want, which is a sequence of assembly commands that we can execute. The next step, once you have a stream of tokens, is to create a data structure representing the program, which is isomorphic with the token stream (called an abstract syntax tree). The actual conversion here isn't really the most interesting piece (though I'll mention a couple interesting parts of the conversion function); instead, I'd like to describe the data structure that represents a program. Once we understand the relevant data structure, filling it in from a sequence of tokens is straightforward.

Most parsing functions take just two arguments: an input slang_parse_ctx and a slang_output_ctx. The slang_parse_ctx is straightforward, it essentially wraps the output of the lexer with a few metadata options. Here's its definition, from src/mesa/shader/slang/slang_compile.c.


typedef struct slang_parse_ctx_
{
   const byte *I;
   slang_info_log *L;
   int parsing_builtin;
   GLboolean global_scope;   /**< Is object being declared a global? */
   slang_atom_pool *atoms;
   slang_unit_type type;     /**< Vertex vs. Fragment */
   GLuint version;           /**< user-specified (or default) #version */
} slang_parse_ctx;

I is the input stream of symbols that the lexer produced, and everything else is fairly straightforward. The output of the parser is a slang_output_ctx; here's the definition of that struct:


typedef struct slang_output_ctx_
{
   slang_variable_scope *vars;
   slang_function_scope *funs;
   slang_struct_scope *structs;
   struct gl_program *program;
   struct gl_sl_pragmas *pragmas;
   slang_var_table *vartable;
   GLuint default_precision[TYPE_SPECIFIER_COUNT];
   GLboolean allow_precision;
   GLboolean allow_invariant;
   GLboolean allow_centroid;
   GLboolean allow_array_types;  /* float[] syntax */
} slang_output_ctx;

As you can see, this is the representation of the program. The lines that parse the GLboolean fields are straightforward as you would imagine; they just check a single token in the lexer's output. The scope variables are defined as a linked list from one scope to an outer scope; here's an example from src/mesa/shader/slang/slang_compile_variable.h:

typedef struct slang_variable_scope_
{
   slang_variable **variables;  /**< Array [num_variables] of ptrs to vars */
   GLuint num_variables;
   struct slang_variable_scope_ *outer_scope;
} slang_variable_scope;

This means that if you want to locate a variable, you just have to walk the linked list of scopes to find it, similar to walking a prototype chain.

Each individual variable is defined like this in the same file:

typedef struct slang_variable_
{
   slang_fully_specified_type type; /**< Variable's data type */
   slang_atom a_name;               /**< The variable's name (char *) */
   GLuint array_len;                /**< only if type == SLANG_SPEC_ARRAy */
   struct slang_operation_ *initializer; /**< Optional initializer code */
   GLuint size;                     /**< Variable's size in bytes */
   GLboolean is_global;
   GLboolean isTemp;                /**< a named temporary (__resultTmp) */
   GLboolean declared;              /**< for debug */
   struct slang_ir_storage_ *store; /**< Storage for this var */
} slang_variable;

A slang_atom is simply typedef'ed to a void*. Let's look at how types are represented, in src/mesa/shader/slang/slang_typeinfo.h:

typedef struct slang_fully_specified_type_
{
   slang_type_qualifier qualifier;
   slang_type_specifier specifier;
   slang_type_precision precision;
   slang_type_variant variant;
   slang_type_centroid centroid;
   GLint array_len;           /**< -1 if not an array type */
} slang_fully_specified_type;

The qualifier is an enum with items "const", "attribute", "varying", "uniform", "out", and "inout". The specifier is the following struct:

typedef struct slang_type_specifier_
{
   slang_type_specifier_type type;
   struct slang_struct_ *_struct;         /**< if type == SLANG_SPEC_STRUCT */
   struct slang_type_specifier_ *_array;  /**< if type == SLANG_SPEC_ARRAY */
} slang_type_specifier;

This is a recursive data structure used to define user-created types in terms of builtin types. The builtin types (slang_type_specifier_type) is an enum with an item for each builtin type: "bool", "int", "vec2", "mat32", etc. Looking back at slang_fully_specified_type, the precision is an enum with "high", "medium", and "low" entries. The variant member is an enum with just "variant" and "invariant". The centroid member is an enum with just "center" and "centroid".

Let's look at the last field in the slang_variable struct, slang_ir_storage_, in src/mesa/shader/slang/slang_ir.h:

struct slang_ir_storage_
{
   gl_register_file File;  /**< PROGRAM_TEMPORARY, PROGRAM_INPUT, etc */
   GLint Index;    /**< -1 means unallocated */
   GLint Size;     /**< number of floats or ints */
   GLuint Swizzle; /**< Swizzle AND writemask info */
   GLint RefCount; /**< Used during IR tree delete */
   GLboolean RelAddr; /* we'll remove this eventually */
   GLboolean IsIndirect;
   gl_register_file IndirectFile;
   GLint IndirectIndex;
   GLuint IndirectSwizzle;
   GLuint TexTarget;  /**< If File==PROGRAM_SAMPLER, one of TEXTURE_x_INDEX */
   /** If Parent is non-null, Index is relative to parent.
    * The other fields are ignored.
    */
   struct slang_ir_storage_ *Parent;
};

This represents an offset into a gl_register_file, with some allowances for indirect offsets. The register file, defined in src/mesa/main/mtypes.h, is an enum that specifies which of the various register file to use inside the virtual machine that will eventually perform shader execution.

Cool. Now we understand how variables are represented. What about the other members in the slang_output_context, for example, slang_function_scope? Well, the "scope" piece is set up the same way as for variables (though it's defined in src/mesa/shader/slang/slang_compile_function.h), so I'll skip right to the slang_function struct, defined in the same file. Here it is:


typedef struct slang_function_
{
   slang_function_kind kind;
   slang_variable header;      /**< The function's name and return type */
   slang_variable_scope *parameters; /**< formal parameters AND local vars */
   unsigned int param_count;   /**< number of formal params (no locals) */
   slang_operation *body;      /**< The instruction tree */
} slang_function;

We've already looked at slang_variable and slang_variable_scope; I think it's neat that they're re-using these structs. The slang_function_kind type is an enum with items for "ordinary", "constructor", and "operator". I'm assuming that the constructors are used for the builtin code that initializes vec3s and mat4s and such. Let's now look at slang_operation, defined in mesa/shader/slang/slang_compile_operation.h:


typedef struct slang_operation_
{
   slang_operation_type type;
   struct slang_operation_ *children;
   GLuint num_children;
   GLfloat literal[4];           /**< Used for float, int and bool values */
   GLuint literal_size;          /**< 1, 2, 3, or 4 */
   slang_atom a_id;              /**< type: asm, identifier, call, field */
   slang_atom a_obj;             /**< object in a method call */
   slang_variable_scope *locals; /**< local vars for scope */
   struct slang_function_ *fun;  /**< If type == SLANG_OPER_CALL */
   struct slang_variable_ *var;  /**< If type == slang_oper_identier */
   struct slang_label_ *label;   /**< If type == SLANG_OPER_LABEL */
   /** If type==SLANG_OPER_CALL and we're calling an array constructor,
    * for which there's no real function, we need to have a flag to
    * indicate such.  num_children indicates number of elements.
    */
   GLboolean array_constructor;
} slang_operation;

slang_operation_type is a giant enum with, for example, entries meaning "do", "for", "assign", "add", "less", "equal", "minus", "call", etc. You can see that it's a recursive data type, creating a tree of computation. The comments give a pretty good explanation of the usage of each member.

slang_struct_scope is set up in much the same way. Here's the relevant definition, from src/mesa/shader/slang/slang_compile_struct.h:


typedef struct slang_struct_
{
   slang_atom a_name;
   struct slang_variable_scope_ *fields;
   slang_struct_scope *structs;
   struct slang_function_ *constructor;
} slang_struct;

Most of the parsing functions, in src/mesa/shader/slang/slang_compile*.c, are straightforward: they create relevant data structures and fill in their members based on the output of the lexer. I did want to mention one place, however, that I thought was interesting: parse_statement() in src/mesa/shader/slang/slang_compile.c. This is the bulk of what people usually mean when they say "parse". This function has a switch statement with elements for "new scope", "no new scope", "declare", "if", "while", "for", etc. Each of these cases delegates to parse_child_operation(), passing a "true" or a "false" to its "statement" argument. If "statement" is true, parse_child_operation() recursively calls parse_statement(), but if it's false, it calls parse_expression().  The "for" case, for example, calls parse_child_operation() 4 times, and 3 of those 4 specify "true" and the other one specifies "false".

parse_expression() consumes characters until it receives an OP_END, and, each iteration, reallocs an array of the operations that it constructs. Then, there's a giant switch statement, switching over each type of operation that a shader can perform. If the operation is one that can have sub-operations (for example, "add" takes two sub-operations), the function fills in the type of the operation and then delegates to handle_nary_expression(..., n). This function simply sets up the n child pointers inside the expression objects. I also noticed something: the code that actually sets up these child pointers looks like this:


   for (i = 0; i < n; i++) {
      slang_operation_destruct(&op->children[i]);
      op->children[i] = (*ops)[*total_ops - (n + 1 - i)];
   }

This means that the operation occurs in postorder, because 'total_ops' is the index of the newly-added op, and n is the number of child ops. This is also the case with the statement operators: the first two parse_child_operations()s specify that they should be parsing statements, then comes an expression, then another statement (which is backwards). This means that the lexer outputs these items in reverse order.

Alright, cool: now we've got a big data structure defining the structure of the GLSL shader. Now we've got to actually generate instructions which will execute the shader!



Wednesday, April 17, 2013

Design of Mesa 3D Part 4: Lexing Shaders

I'm going to skip over some of the pieces that I've mentioned already (namely creating a shader and attaching source to the shader) since those work in much the same way as I've already described. Now I'd like to jump in to the fun part: GLSL lexing and parsing! The relevant entry point is _slang_compile(), found in src/mesa/shader/slang/slang_compile.c. This function delegates to compile_shader(), which delegates to compile_object(), which is where the fun really starts. The first thing this function calls is grammar_load_from_text((const byte *) (slang_shader_syn)). slang_shader_syn is defined like this:

LONGSTRING static const char *slang_shader_syn =
#include "library/slang_shader_syn.h"
;

Alright, well let's take a look at that file. Opening it up, we see stuff like this:

/* DO NOT EDIT - THIS FILE IS AUTOMATICALLY GENERATED FROM THE .syn FILE */
".syntax translation_unit;\n"
".emtcode REVISION 5\n"
".emtcode EXTERNAL_NULL 0\n"
".emtcode EXTERNAL_FUNCTION_DEFINITION 1\n"
".emtcode EXTERNAL_DECLARATION 2\n"
".emtcode DEFAULT_PRECISION 3\n"
...

Clearly this is a long string, but I don't recognize the language that it's writing in. Let's take a look at the .syn file (src/mesa/shader/slang/library/slang_shader.syn):

.syntax translation_unit;
/* revision number - increment after each change affecting emitted output */
.emtcode REVISION                                   5
/* external declaration (or precision or invariant stmt) */
.emtcode EXTERNAL_NULL                              0
.emtcode EXTERNAL_FUNCTION_DEFINITION               1
.emtcode EXTERNAL_DECLARATION                       2
.emtcode DEFAULT_PRECISION                          3
...

This actually looks almost exactly the same as the ".h" file, with some comments at the top. The comments describe that the translation from the .syn to the .h file is done with src/mesa/shader/slang/library/syn_to_c.c, so let's take a look at the converter. The entire source isn't that long, so it's easy to see that the script simply removes comments and adds escape characters, allowing the source to be #included as a string. Straightforward enough.

However, we still don't understand the meaning of the source of the .syn file. Let's see if we can glean any information from how it's used, so look at grammar_load_from_text(), defined in src/mesa/shader/grammar/grammar.c. That file actually has a very helpful (and long) comment at the top of it explaining exactly what the language is and what kind of thing it describes. I won't copy and paste the entire thing here, but I will give an executive summary:

The file is a collection of declarations, which are essentially rules in a formal language. Each definition, however, is very simplified: a literal character in the body of a declaration means that the next character in the input stream should be that particular character. If the next character doesn't match, the rule fails. The definition is defined as a list of specifiers joined by either the ".and" or the ".or" keyword. The language also allows for, if a particular specifier matches successfully, to emit a character. Therefore, this language defines a transformation from an input string to an output string. The comment also gives a little justification for why this language exists: it claims that describing GLSL in C code itself is error-prone, so instead, the description of GLSL should be in some other language. The contents of grammar_load_from_text() essentially parse a description of a language (which is itself described in the language that I've just talked about, called "Synek"), and constructs a function from a stream of characters to a stream of characters. Alright.

So now, let's get back to compile_object(). The next thing this function does is run a few invocations of compile_binary(), each of which is run on something like "slang_core_gc" or "slang_120_core_gc". These symbols are defined similarly to slang_shader_syn:

static const byte slang_core_gc[] = {
#include "library/slang_core_gc.h"
};

Let's take a look at this file:

5,1,90,95,0,0,5,0,1,1,1,0,0,9,0,102,0,0,0,1,4,118,101,99,52,95,116,111,95,105,118,101,99,52,0,18,
95,95,114,101,116,86,97,108,0,0,18,102,0,0,0,0,1,90,95,0,0,5,0,1,1,1,0,0,1,0,98,0,0,0,1,9,18,95,95,
114,101,116,86,97,108,0,18,98,0,20,0,0,1,90,95,0,0,5,0,1,1,1,0,0,5,0,105,0,0,0,1,9,18,95,95,114,
101,116,86,97,108,0,18,105,0,20,0,0,1,90,95,0,0,1,0,1,1,1,0,0,5,0,105,0,0,0,1,4,118,101,99,52,95,
115,110,101,0,18,95,95,114,101,116,86,97,108,0,0,18,105,0,0,17,48,0,48,0,0,0,0,0,1,90,95,0,0,1,0,1,
...

Well that's unhelpful. A comment at the top of the file says that the file was generated from slang_core.gc, let's take a look at that:

int __constructor(const float f)
{
   __asm vec4_to_ivec4 __retVal, f;
}
int __constructor(const bool b)
{
   __retVal = b;
}
int __constructor(const int i)
{
   __retVal = i;
}
...

Interesting. This looks like GLSL! Except, it looks like it's definitions of symbols that are built into the language of GLSL. It's straightforward enough to look at the script that generated slang_core_gc.h from slang_core.gc (src/mesa/shader/slang/library/gc_to_bin.c). I won't copy the source here, but the file is quite simple. That file essentially just opens up the input file, calls grammar_fast_check() on the source of the input file, then outputs the string that that function produces. grammar_fast_check(), defined in src/mesa/shader/grammar/grammar.c, is just the function that "runs" the grammar, outputting the sequence of characters that the Synek describes. In total, this means that the builtin functions are pre-lexed, so libGL doesn't have to do this at runtime. Smart! We can also see, back in , that each invocation of compile_binary puts its output into object->builtin[x]. Cool!

The last thing that compile_object() does is run compile_with_grammar(), which runs grammar_fast_check(), the same function that gc_to_bin.c ran. So, we're running all our GLSL code through the same lexer, the only difference is that the builtin functions get run through the lexer at compile time, and the user-specified functions get run through the lexer at runtime. Cool! One difference, however, is that the user-specified shader has to have a preprocess pass, because it might have preprocessor macros. The builtin code, however, doesn't have any preprocessor macros, so it's unnecessary.

Alright, that doesn't actually solve our problem, however. Synek simply tokenizes the input; it doesn't parse the input. At this point, we don't have a sequence of instructions to run; we only have a sequence of token that represent the input. The missing piece is at the end of compile_with_grammar(), namely, a call to compile_binary(). Note that this call contains all the lexed source, including the builtins. We still, however, have to run a translation to create instructions that our Mesa virtual machine can execute. I'll save that for next time!

Friday, April 12, 2013

Design of Mesa 3D Part 3: Dispatch

Previously, I have written about how Mesa 3D's implementation of GLX works; Now, I'd like to transition to OpenGL calls.

First, however, we've got to take a step back and look at one detail that I had glossed over before. We learned in part 2 that one of the important calls that glXMakeCurrent() makes is a call to _mesa_make_current(), defined in src/mesa/glapi/glapi.c. One of the thing that this function does is make a call to _glapi_set_dispatch(newCtx->CurrentDispatch). The argument to this function is a pointer to a _glapi_table struct, which contains one function pointer for each of the OpenGL calls. In particular, the file where this struct is defined, src/mesa/glapi/glapitable.h, is automatically generated from the src/mesa/glapi/gl_table.py script. This script generates uses, as input, src/mesa/glapi/gl_API.xml, which is a hand-constructed XML file containing information about each OpenGL call. There's also an accompanying src/mesa/glapi/glX_API.xml file, as well as a src/mesa/glapi/gl_and_glX_API.xml that includes from both of the previous two.

Anyway, _glapi_set_dispatch() once again, has three implementations: one for TLS, one for threads, and one for no-threads. The threaded implementation calls _glthread_SetTSD() with a key of _gl_DispatchTSD, declared in src/mesa/glapi/glapi.c, which is of type _glthread_TSD. The _glthread_SetTSD() function was described in a previous post, so I'll just post the struct definition here.


typedef struct {
   pthread_key_t  key;
   int initMagic;
} _glthread_TSD;


You can see that the initMagic variable is used to see if the key has been initialized, and the key can be used as an argument to pthread_getspecific(). So now, someone calls a GL function. The actual implementation of all the GL functions is in as assembly file, src/mesa/x86-64/glapi_x86-64.S. For each function, the file has a stanza that looks like this:


.p2align 4,,15
.globl GL_PREFIX(Viewport)
.type GL_PREFIX(Viewport), @function
GL_PREFIX(Viewport):
#if defined(GLX_USE_TLS)
...
#elif defined(PTHREADS)
pushq %rdi
pushq %rsi
pushq %rdx
pushq %rcx
pushq %rbp
call _x86_64_get_dispatch@PLT
popq %rbp
popq %rcx
popq %rdx
popq %rsi
popq %rdi
movq 2440(%rax), %r11
jmp *%r11
#else
...
#endif /* defined(GLX_USE_TLS) */
.size GL_PREFIX(Viewport), .-GL_PREFIX(Viewport)


As you can see, this particular function has three different implementations as well. The posix threaded implementation pushes some registers onto the stack, then calls x86_86_get_dispatch from the PLT. Then, pops those registers back off the stack, moves a function pointer that is at a constant offset of the return value of the previous call into a register, and executes that function. It's important to realize here that the register + memory state at the 'jmp' command is the same as it was when the function being called. Therefore, this is a way to do argument forwarding (which is otherwise impossible to do in C). This makes the argument forwarding code easier to write (it can even be written by a script) and faster at runtime. This function calls another function with the same arguments that it's given. That other function is looked up with the x86_64_get_dispatch function. That leads us to the implementation of that function:


#ifdef GLX_USE_TLS
...
#elif defined(PTHREADS)
.extern _glapi_Dispatch
.extern _gl_DispatchTSD
.extern pthread_getspecific
.p2align 4,,15
_x86_64_get_dispatch:
movq _gl_DispatchTSD(%rip), %rdi
jmp pthread_getspecific@PLT
#elif defined(THREADS)
...
#endif


This shows that this function simply another implementation of _glthread_GetTSD(), where the key here is _gl_DispatchTSD. "But wait," you may be interjecting, "the type of gl_DispatchTSD can't be used as an argument to pthread_getspecific()!" However, because the argument is a pointer, and the first element of a _glthread_TSD is a pthread_key_t, the pointers will be identical. The assembly is just using shorthand for a zero offset. (This code will also break if someone rearranges the elements in the struct.)

The last item in the implementation of the assembly dispatch code is the offset that you jump to from the pointer returned by _x86_64_get_dispatch. This, however, is the offset of the function pointer inside the _glapi_table struct. This can be verified because the function pointers are numbered starting at 1 in src/mesa/glapi/glapitable.h. Each function pointer is 8 bytes (since it's a 64-bit machine), so that means that glViewport's index, 305, times 8 bytes per pointer, equals 2440, which is exactly the offset that is specified by the jmp command. It's also worth noting that because the function doesn't set up a stack at all, it doesn't have to ret.

One more thing: Drivers have to fill in the CurrentDispatch member of the GLcontext struct with a function table before calling _mesa_make_current(). Most drivers get a baseline table by calling _mesa_init_exec_table(), defined in src/mesa/main/api_exec.c. This function initializes all the variables with the default mesa implementations. However, the driver is then able to modify the resulting vtable before calling _mesa_make_current(). This allows drivers to swap out whole functions of the OpenGL API.

So now, we've finally traced to the meat of a default GL function, _mesa_Viewport(), defined in src/mesa/main/viewport.c. This function delegates to a helper, _mesa_set_viewport(), which simply changes some parameters in ctx->Viewport. It then sets the _NEW_VIEWPORT bit in ctx->NewState, so that future (more substantive) functions will be able to react to the change. The last thing it does is interesting: it calls if (ctx->Driver.Viewport) { ctx->Driver.Viewport(...) }. This ctx->Driver is a similar vtable, of type dd_function_table, defined in src/mesa/main/dd.h, which exports a different, mesa-specific API. This class's API seems to be almost exactly the same as OpenGL, except these functions are executed 'in addition to' Mesa's default OpenGL implementation, not 'instead of.' This struct member is usually set by a call to _mesa_init_driver_functions(), defined in mesa/drivers/common/driverfuncs.c, inside the implementation of the driver's context creation function. The device driver is then free to modify the vtable as it pleases. For many of these smaller, simpler functions, the implementation of these driver functions is just NULL.

Here are notes on some other small functions (which all share the dispatch code and all call their relevant driver function after execution):

  • _mesa_GetString(), in src/mesa/main/getstring.c, switches on its argument, and looks at ctx->Extensions to compute its version string.
  • _mesa_ClearColor(), in src/mesa/main/clear.c, simply modifies ctx->Color.ClearColor.
  • _mesa_Enable() and _mesa_EnableClientState(), both in src/mesa/main/enable.c, delegate to _mesa_set_enable() and switch on its argument and sets flags inside ctx.
  • _mesa_BlendFunc(), in mesa/main/blend.c, does a large amount of error checking, and then modifies ctx->Color->Blend*
Alright, that's a good time to stop for now. Next time I'll write about how shaders get compiled.