aNIRtomy

NIR passes

I’ve spent a lot of time talking about NIR and writing passes, so let’s take a shallow dive into what exactly that means.

To start, there’s this idea of a “lowering” pass, where “lowering” means reducing or changing some part of the shader representation. These passes are run for various reasons, ranging from handling compatibility (e.g., the gl_FragColor -> gl_FragData[n] pass I discussed previously) to optimizing the shader by removing unused variables and instructions.

Basics

The most basic type of NIR pass, which is what I’ll be covering quickly today since time got away from me, is the type that runs through a shader’s instructions in order to rewrite a specific type of instruction.

Usually this starts out with this block:

   nir_foreach_function(function, shader) {
      if (function->impl) {
         nir_builder builder;
         nir_builder_init(&builder, function->impl);
         nir_foreach_block(block, function->impl) {
            nir_foreach_instr_safe(instr, block) {
               progress = lower_impl(instr, &builder);
            }
         }
     }
  }

In this, the pass iterates over the shader’s functions, then creates a nir_builder (an object used for altering shader internals) while it iterates the blocks within the function. A function block is a group of instructions contained within a given scope, e.g., a conditional. The pass iterates into each block, looping over all the instructions and passing them to the lower_impl internal function for the pass, which is where all the work happens.

A pass I wrote earlier today searches fragment shaders for writes to gl_SampleMask and deletes them in order to mimic OpenGL behavior of ignoring that variable if a render target’s sample count is zero:

static bool
lower_samplemask_instr(nir_intrinsic_instr *instr, nir_builder *b)
{
   nir_variable *out;
   if (instr->intrinsic != nir_intrinsic_store_deref)
      return false;

   out = nir_deref_instr_get_variable(nir_src_as_deref(instr->src[0]));
   if (out->data.location != FRAG_RESULT_SAMPLE_MASK || out->data.mode != nir_var_shader_out)
      return false;
   b->cursor = nir_after_instr(&instr->instr);
   nir_instr_remove(&instr->instr);
   return true;
}

This filters for intrinsic instructions, which are those defined as intrinsics in mesa/src/compiler/nir/nir_intrinsics.py, and then skips over all instructions which aren’t store_deref. A store_deref is the instruction used when writing a value to a shader output. More filtering is done based on the variable used as the source of the operation (instr->src[0]) until the pass has located a gl_SampleMask write, and then the instruction is removed.

Important here is that true and false are returned by the implementation here to indicate progress. NIR has a feature where setting the NIR_PRINT environment variable causes the current NIR output to be printed any time a lowering pass changes the shader, so setting this correctly is important.

More on NIR passes in a future post.

Written on July 13, 2020